github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/python/gen.go (about) 1 // Copyright 2016-2021, Pulumi Corporation. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the 16 // goconst linter's warning. 17 // 18 // nolint: lll, goconst 19 package python 20 21 import ( 22 "bytes" 23 "fmt" 24 "io" 25 "path" 26 "path/filepath" 27 "reflect" 28 "regexp" 29 "sort" 30 "strconv" 31 "strings" 32 "unicode" 33 34 "github.com/blang/semver" 35 36 "github.com/pulumi/pulumi/pkg/v3/codegen" 37 "github.com/pulumi/pulumi/pkg/v3/codegen/schema" 38 "github.com/pulumi/pulumi/sdk/v3/go/common/diag" 39 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" 40 "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" 41 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 42 ) 43 44 type typeDetails struct { 45 outputType bool 46 inputType bool 47 resourceOutputType bool 48 plainType bool 49 } 50 51 type imports codegen.StringSet 52 53 func (imports imports) addType(mod *modContext, t *schema.ObjectType, input bool) { 54 imports.addTypeIf(mod, t, input, nil /*predicate*/) 55 } 56 57 func (imports imports) addTypeIf(mod *modContext, t *schema.ObjectType, input bool, predicate func(imp string) bool) { 58 if imp := mod.importObjectType(t, input); imp != "" && (predicate == nil || predicate(imp)) { 59 codegen.StringSet(imports).Add(imp) 60 } 61 } 62 63 func (imports imports) addEnum(mod *modContext, enum *schema.EnumType) { 64 if imp := mod.importEnumType(enum); imp != "" { 65 codegen.StringSet(imports).Add(imp) 66 } 67 } 68 69 func (imports imports) addResource(mod *modContext, r *schema.ResourceType) { 70 if imp := mod.importResourceType(r); imp != "" { 71 codegen.StringSet(imports).Add(imp) 72 } 73 } 74 75 func (imports imports) strings() []string { 76 result := make([]string, 0, len(imports)) 77 for imp := range imports { 78 result = append(result, imp) 79 } 80 sort.Strings(result) 81 return result 82 } 83 84 func title(s string) string { 85 if s == "" { 86 return "" 87 } 88 runes := []rune(s) 89 return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)) 90 } 91 92 type modLocator struct { 93 // Returns defining modlue for a given ObjectType. Returns nil 94 // for types that are not being generated in the current 95 // GeneratePacakge call. 96 objectTypeMod func(*schema.ObjectType) *modContext 97 } 98 99 type modContext struct { 100 pkg *schema.Package 101 modLocator *modLocator 102 mod string 103 pyPkgName string 104 types []*schema.ObjectType 105 enums []*schema.EnumType 106 resources []*schema.Resource 107 functions []*schema.Function 108 typeDetails map[*schema.ObjectType]*typeDetails 109 children []*modContext 110 parent *modContext 111 tool string 112 extraSourceFiles []string 113 isConfig bool 114 115 // Name overrides set in PackageInfo 116 modNameOverrides map[string]string // Optional overrides for Pulumi module names 117 compatibility string // Toggle compatibility mode for a specified target. 118 119 // Determine whether to lift single-value method return values 120 liftSingleValueMethodReturns bool 121 } 122 123 func (mod *modContext) isTopLevel() bool { 124 return mod.parent == nil 125 } 126 127 func (mod *modContext) walkSelfWithDescendants() []*modContext { 128 var found []*modContext 129 found = append(found, mod) 130 for _, childMod := range mod.children { 131 found = append(found, childMod.walkSelfWithDescendants()...) 132 } 133 return found 134 } 135 136 func (mod *modContext) addChild(child *modContext) { 137 mod.children = append(mod.children, child) 138 child.parent = mod 139 } 140 141 func (mod *modContext) details(t *schema.ObjectType) *typeDetails { 142 m := mod 143 144 if mod.modLocator != nil { 145 if actualMod := mod.modLocator.objectTypeMod(t); actualMod != nil { 146 m = actualMod 147 } 148 } 149 150 details, ok := m.typeDetails[t] 151 if !ok { 152 details = &typeDetails{} 153 if m.typeDetails == nil { 154 m.typeDetails = map[*schema.ObjectType]*typeDetails{} 155 } 156 m.typeDetails[t] = details 157 } 158 return details 159 } 160 161 func (mod *modContext) modNameAndName(pkg *schema.Package, t schema.Type, input bool) (modName string, name string) { 162 var info PackageInfo 163 contract.AssertNoError(pkg.ImportLanguages(map[string]schema.Language{"python": Importer})) 164 if v, ok := pkg.Language["python"].(PackageInfo); ok { 165 info = v 166 } 167 168 var token string 169 switch t := t.(type) { 170 case *schema.EnumType: 171 token, name = t.Token, tokenToName(t.Token) 172 case *schema.ObjectType: 173 namingCtx := &modContext{ 174 pkg: pkg, 175 modNameOverrides: info.ModuleNameOverrides, 176 compatibility: info.Compatibility, 177 } 178 token, name = t.Token, namingCtx.unqualifiedObjectTypeName(t, input) 179 case *schema.ResourceType: 180 token, name = t.Token, tokenToName(t.Token) 181 } 182 183 modName = tokenToModule(token, pkg, info.ModuleNameOverrides) 184 if modName != "" { 185 modName = strings.ReplaceAll(modName, "/", ".") + "." 186 } 187 return 188 } 189 190 func (mod *modContext) unqualifiedObjectTypeName(t *schema.ObjectType, input bool) string { 191 name := tokenToName(t.Token) 192 193 if mod.compatibility != tfbridge20 && mod.compatibility != kubernetes20 { 194 if t.IsInputShape() { 195 return name + "Args" 196 } 197 return name 198 } 199 200 switch { 201 case input: 202 return name + "Args" 203 case mod.details(t).plainType: 204 return name + "Result" 205 } 206 return name 207 } 208 209 func (mod *modContext) objectType(t *schema.ObjectType, input bool) string { 210 var prefix string 211 if !input { 212 prefix = "outputs." 213 } 214 215 // If it's an external type, reference it via fully qualified name. 216 if t.Package != mod.pkg { 217 modName, name := mod.modNameAndName(t.Package, t, input) 218 return fmt.Sprintf("'%s.%s%s%s'", pyPack(t.Package.Name), modName, prefix, name) 219 } 220 221 modName, name := mod.tokenToModule(t.Token), mod.unqualifiedObjectTypeName(t, input) 222 if modName == "" && modName != mod.mod { 223 rootModName := "_root_outputs." 224 if input { 225 rootModName = "_root_inputs." 226 } 227 return fmt.Sprintf("'%s%s'", rootModName, name) 228 } 229 230 if modName == mod.mod { 231 modName = "" 232 } 233 if modName != "" { 234 modName = "_" + strings.ReplaceAll(modName, "/", ".") + "." 235 } 236 237 return fmt.Sprintf("'%s%s%s'", modName, prefix, name) 238 } 239 240 func (mod *modContext) tokenToEnum(tok string) string { 241 modName, name := mod.tokenToModule(tok), tokenToName(tok) 242 243 if modName == "" && modName != mod.mod { 244 return fmt.Sprintf("'_root_enums.%s'", name) 245 } 246 247 if modName == mod.mod { 248 modName = "" 249 } 250 if modName != "" { 251 modName = "_" + strings.ReplaceAll(modName, "/", ".") + "." 252 } 253 254 return fmt.Sprintf("'%s%s'", modName, name) 255 } 256 257 func (mod *modContext) resourceType(r *schema.ResourceType) string { 258 if r.Resource == nil || r.Resource.Package == mod.pkg { 259 return mod.tokenToResource(r.Token) 260 } 261 262 // Is it a provider resource? 263 if strings.HasPrefix(r.Token, "pulumi:providers:") { 264 pkgName := strings.TrimPrefix(r.Token, "pulumi:providers:") 265 return fmt.Sprintf("pulumi_%s.Provider", pkgName) 266 } 267 268 pkg := r.Resource.Package 269 modName, name := mod.modNameAndName(pkg, r, false) 270 return fmt.Sprintf("%s.%s%s", pyPack(pkg.Name), modName, name) 271 } 272 273 func (mod *modContext) tokenToResource(tok string) string { 274 // token := pkg : module : member 275 // module := path/to/module 276 277 components := strings.Split(tok, ":") 278 contract.Assertf(len(components) == 3, "malformed token %v", tok) 279 280 // Is it a provider resource? 281 if components[0] == "pulumi" && components[1] == "providers" { 282 return fmt.Sprintf("pulumi_%s.Provider", components[2]) 283 } 284 285 modName, name := mod.tokenToModule(tok), tokenToName(tok) 286 287 if modName == mod.mod { 288 modName = "" 289 } 290 if modName != "" { 291 modName = "_" + strings.ReplaceAll(modName, "/", ".") + "." 292 } 293 294 return fmt.Sprintf("%s%s", modName, name) 295 } 296 297 func tokenToName(tok string) string { 298 // token := pkg : module : member 299 // module := path/to/module 300 301 components := strings.Split(tok, ":") 302 contract.Assertf(len(components) == 3, "malformed token %v", tok) 303 304 return title(components[2]) 305 } 306 307 func tokenToModule(tok string, pkg *schema.Package, moduleNameOverrides map[string]string) string { 308 // See if there's a manually-overridden module name. 309 canonicalModName := pkg.TokenToModule(tok) 310 if override, ok := moduleNameOverrides[canonicalModName]; ok { 311 return override 312 } 313 // A module can include fileparts, which we want to preserve. 314 var modName string 315 for i, part := range strings.Split(strings.ToLower(canonicalModName), "/") { 316 if i > 0 { 317 modName += "/" 318 } 319 modName += PyName(part) 320 } 321 return modName 322 } 323 324 func (mod *modContext) tokenToModule(tok string) string { 325 return tokenToModule(tok, mod.pkg, mod.modNameOverrides) 326 } 327 328 func printComment(w io.Writer, comment string, indent string) { 329 lines := strings.Split(comment, "\n") 330 for len(lines) > 0 && lines[len(lines)-1] == "" { 331 lines = lines[:len(lines)-1] 332 } 333 if len(lines) == 0 { 334 return 335 } 336 337 // Known special characters that need escaping. 338 replacer := strings.NewReplacer(`\`, `\\`, `"""`, `\"\"\"`) 339 fmt.Fprintf(w, "%s\"\"\"\n", indent) 340 for _, l := range lines { 341 if l == "" { 342 fmt.Fprintf(w, "\n") 343 } else { 344 escaped := replacer.Replace(l) 345 fmt.Fprintf(w, "%s%s\n", indent, escaped) 346 } 347 } 348 fmt.Fprintf(w, "%s\"\"\"\n", indent) 349 } 350 351 func genStandardHeader(w io.Writer, tool string) { 352 // Set the encoding to UTF-8, in case the comments contain non-ASCII characters. 353 fmt.Fprintf(w, "# coding=utf-8\n") 354 355 // Emit a standard warning header ("do not edit", etc). 356 fmt.Fprintf(w, "# *** WARNING: this file was generated by %v. ***\n", tool) 357 fmt.Fprintf(w, "# *** Do not edit by hand unless you're certain you know what you are doing! ***\n\n") 358 } 359 360 func (mod *modContext) genHeader(w io.Writer, needsSDK bool, imports imports) { 361 genStandardHeader(w, mod.tool) 362 363 // If needed, emit the standard Pulumi SDK import statement. 364 if needsSDK { 365 rel, err := filepath.Rel(mod.mod, "") 366 contract.Assert(err == nil) 367 relRoot := path.Dir(rel) 368 relImport := relPathToRelImport(relRoot) 369 370 fmt.Fprintf(w, "import copy\n") 371 fmt.Fprintf(w, "import warnings\n") 372 fmt.Fprintf(w, "import pulumi\n") 373 fmt.Fprintf(w, "import pulumi.runtime\n") 374 fmt.Fprintf(w, "from typing import Any, Mapping, Optional, Sequence, Union, overload\n") 375 fmt.Fprintf(w, "from %s import _utilities\n", relImport) 376 for _, imp := range imports.strings() { 377 fmt.Fprintf(w, "%s\n", imp) 378 } 379 fmt.Fprintf(w, "\n") 380 } 381 } 382 383 func relPathToRelImport(relPath string) string { 384 // Convert relative path to relative import e.g. "../.." -> "..." 385 // https://realpython.com/absolute-vs-relative-python-imports/#relative-imports 386 relImport := "." 387 if relPath == "." { 388 return relImport 389 } 390 for _, component := range strings.Split(relPath, "/") { 391 if component == ".." { 392 relImport += "." 393 } else { 394 relImport += component 395 } 396 } 397 return relImport 398 } 399 400 func (mod *modContext) genUtilitiesFile() []byte { 401 buffer := &bytes.Buffer{} 402 genStandardHeader(buffer, mod.tool) 403 fmt.Fprintf(buffer, utilitiesFile) 404 optionalURL := "None" 405 if url := mod.pkg.PluginDownloadURL; url != "" { 406 optionalURL = fmt.Sprintf("%q", url) 407 } 408 _, err := fmt.Fprintf(buffer, ` 409 def get_plugin_download_url(): 410 return %s 411 `, optionalURL) 412 contract.AssertNoError(err) 413 return buffer.Bytes() 414 } 415 416 func (mod *modContext) gen(fs codegen.Fs) error { 417 dir := path.Join(mod.pyPkgName, mod.mod) 418 419 var exports []string 420 for p := range fs { 421 d := path.Dir(p) 422 if d == "." { 423 d = "" 424 } 425 if d == dir { 426 exports = append(exports, strings.TrimSuffix(path.Base(p), ".py")) 427 } 428 } 429 430 addFile := func(name, contents string) { 431 p := path.Join(dir, name) 432 if !strings.HasSuffix(name, ".pyi") { 433 exports = append(exports, name[:len(name)-len(".py")]) 434 } 435 fs.Add(p, []byte(contents)) 436 } 437 438 // Utilities, config, readme 439 switch mod.mod { 440 case "": 441 fs.Add(filepath.Join(dir, "_utilities.py"), mod.genUtilitiesFile()) 442 fs.Add(filepath.Join(dir, "py.typed"), []byte{}) 443 444 // Ensure that the top-level (provider) module directory contains a README.md file. 445 446 var readme string 447 if pythonInfo, ok := mod.pkg.Language["python"]; ok { 448 if typedInfo, ok := pythonInfo.(PackageInfo); ok { 449 readme = typedInfo.Readme 450 } 451 } 452 453 if readme == "" { 454 readme = mod.pkg.Description 455 if readme != "" && readme[len(readme)-1] != '\n' { 456 readme += "\n" 457 } 458 if mod.pkg.Attribution != "" { 459 if len(readme) != 0 { 460 readme += "\n" 461 } 462 readme += mod.pkg.Attribution 463 } 464 if readme != "" && readme[len(readme)-1] != '\n' { 465 readme += "\n" 466 } 467 } 468 fs.Add(filepath.Join(dir, "README.md"), []byte(readme)) 469 470 case "config": 471 if len(mod.pkg.Config) > 0 { 472 vars, err := mod.genConfig(mod.pkg.Config) 473 if err != nil { 474 return err 475 } 476 addFile("vars.py", vars) 477 typeStubs, err := mod.genConfigStubs(mod.pkg.Config) 478 if err != nil { 479 return err 480 } 481 addFile("__init__.pyi", typeStubs) 482 } 483 } 484 485 // Resources 486 for _, r := range mod.resources { 487 if r.IsOverlay { 488 // This resource code is generated by the provider, so no further action is required. 489 continue 490 } 491 492 res, err := mod.genResource(r) 493 if err != nil { 494 return err 495 } 496 name := PyName(tokenToName(r.Token)) 497 if mod.compatibility == kubernetes20 { 498 // To maintain backward compatibility for kubernetes, the file names 499 // need to be CamelCase instead of the standard snake_case. 500 name = tokenToName(r.Token) 501 } 502 if r.IsProvider { 503 name = "provider" 504 } 505 addFile(name+".py", res) 506 } 507 508 // Functions 509 for _, f := range mod.functions { 510 if f.IsOverlay { 511 // This function code is generated by the provider, so no further action is required. 512 continue 513 } 514 515 fun, err := mod.genFunction(f) 516 if err != nil { 517 return err 518 } 519 addFile(PyName(tokenToName(f.Token))+".py", fun) 520 } 521 522 // Nested types 523 if len(mod.types) > 0 { 524 if err := mod.genTypes(dir, fs); err != nil { 525 return err 526 } 527 } 528 529 // Enums 530 if len(mod.enums) > 0 { 531 buffer := &bytes.Buffer{} 532 if err := mod.genEnums(buffer, mod.enums); err != nil { 533 return err 534 } 535 536 addFile("_enums.py", buffer.String()) 537 } 538 539 // Index 540 if !mod.isEmpty() { 541 fs.Add(path.Join(dir, "__init__.py"), []byte(mod.genInit(exports))) 542 } 543 544 return nil 545 } 546 547 func (mod *modContext) hasTypes(input bool) bool { 548 if allTypesAreOverlays(mod.types) { 549 return false 550 } 551 for _, t := range mod.types { 552 if input && mod.details(t).inputType { 553 return true 554 } 555 if !input && mod.details(t).outputType { 556 return true 557 } 558 } 559 return false 560 } 561 562 func (mod *modContext) isEmpty() bool { 563 if len(mod.extraSourceFiles) > 0 || len(mod.functions) > 0 || len(mod.resources) > 0 || len(mod.types) > 0 || 564 mod.isConfig { 565 return false 566 } 567 for _, child := range mod.children { 568 if !child.isEmpty() { 569 return false 570 } 571 } 572 return true 573 } 574 575 func (mod *modContext) submodulesExist() bool { 576 for _, submod := range mod.children { 577 if !submod.isEmpty() { 578 return true 579 } 580 } 581 return false 582 } 583 584 func (mod *modContext) unqualifiedImportName() string { 585 name := mod.mod 586 587 // Extract version suffix from child modules. Nested versions will have their own __init__.py file. 588 // Example: apps/v1beta1 -> v1beta1 589 parts := strings.Split(name, "/") 590 if len(parts) > 1 { 591 name = parts[len(parts)-1] 592 } 593 594 return PyName(name) 595 } 596 597 func (mod *modContext) fullyQualifiedImportName() string { 598 name := mod.unqualifiedImportName() 599 if mod.parent == nil && name == "" { 600 return mod.pyPkgName 601 } 602 if mod.parent == nil { 603 return fmt.Sprintf("%s.%s", pyPack(mod.pkg.Name), name) 604 } 605 return fmt.Sprintf("%s.%s", mod.parent.fullyQualifiedImportName(), name) 606 } 607 608 // genInit emits an __init__.py module, optionally re-exporting other members or submodules. 609 func (mod *modContext) genInit(exports []string) string { 610 w := &bytes.Buffer{} 611 mod.genHeader(w, false /*needsSDK*/, nil) 612 if mod.isConfig { 613 fmt.Fprintf(w, "import sys\n") 614 fmt.Fprintf(w, "from .vars import _ExportableConfig\n") 615 fmt.Fprintf(w, "\n") 616 fmt.Fprintf(w, "sys.modules[__name__].__class__ = _ExportableConfig\n") 617 return w.String() 618 } 619 fmt.Fprintf(w, "%s\n", mod.genUtilitiesImport()) 620 fmt.Fprintf(w, "import typing\n") 621 622 // Import anything to export flatly that is a direct export rather than sub-module. 623 if len(exports) > 0 { 624 sort.Slice(exports, func(i, j int) bool { 625 return PyName(exports[i]) < PyName(exports[j]) 626 }) 627 628 fmt.Fprintf(w, "# Export this package's modules as members:\n") 629 for _, exp := range exports { 630 name := PyName(exp) 631 if mod.compatibility == kubernetes20 { 632 // To maintain backward compatibility for kubernetes, the file names 633 // need to be CamelCase instead of the standard snake_case. 634 name = exp 635 } 636 fmt.Fprintf(w, "from .%s import *\n", name) 637 } 638 } 639 if mod.hasTypes(true /*input*/) { 640 fmt.Fprintf(w, "from ._inputs import *\n") 641 } 642 if mod.hasTypes(false /*input*/) { 643 fmt.Fprintf(w, "from . import outputs\n") 644 } 645 646 // If there are subpackages, import them with importlib. 647 if mod.submodulesExist() { 648 649 children := make([]*modContext, len(mod.children)) 650 copy(children, mod.children) 651 652 sort.Slice(children, func(i, j int) bool { 653 return PyName(children[i].mod) < PyName(children[j].mod) 654 }) 655 656 fmt.Fprintf(w, "\n# Make subpackages available:\n") 657 658 fmt.Fprintf(w, "if typing.TYPE_CHECKING:\n") 659 660 for _, submod := range children { 661 if !submod.isEmpty() { 662 unq := submod.unqualifiedImportName() 663 664 // The `__iam = iam` hack enables 665 // PyCharm and VSCode completion to do 666 // better. 667 // 668 // See https://github.com/pulumi/pulumi/issues/7367 669 fmt.Fprintf(w, " import %s as __%s\n %s = __%s\n", 670 submod.fullyQualifiedImportName(), 671 unq, 672 unq, 673 unq) 674 } 675 } 676 677 fmt.Fprintf(w, "else:\n") 678 679 for _, submod := range children { 680 if !submod.isEmpty() { 681 fmt.Fprintf(w, " %s = _utilities.lazy_import('%s')\n", 682 submod.unqualifiedImportName(), 683 submod.fullyQualifiedImportName()) 684 } 685 } 686 687 fmt.Fprintf(w, "\n") 688 } 689 690 // If there are resources in this module, register the module with the runtime. 691 if len(mod.resources) != 0 { 692 err := genResourceMappings(mod, w) 693 contract.Assert(err == nil) 694 } 695 696 return w.String() 697 } 698 699 func (mod *modContext) getRelImportFromRoot() string { 700 rel, err := filepath.Rel(mod.mod, "") 701 contract.Assert(err == nil) 702 relRoot := path.Dir(rel) 703 return relPathToRelImport(relRoot) 704 } 705 706 func (mod *modContext) genUtilitiesImport() string { 707 rel, err := filepath.Rel(mod.mod, "") 708 contract.Assert(err == nil) 709 relRoot := path.Dir(rel) 710 relImport := relPathToRelImport(relRoot) 711 return fmt.Sprintf("from %s import _utilities", relImport) 712 } 713 714 func (mod *modContext) importObjectType(t *schema.ObjectType, input bool) string { 715 if t.Package != mod.pkg { 716 return fmt.Sprintf("import %s", pyPack(t.Package.Name)) 717 } 718 719 tok := t.Token 720 parts := strings.Split(tok, ":") 721 contract.Assert(len(parts) == 3) 722 refPkgName := parts[0] 723 724 modName := mod.tokenToModule(tok) 725 if modName == mod.mod { 726 if input { 727 return "from ._inputs import *" 728 } 729 return "from . import outputs" 730 } 731 732 importPath := mod.getRelImportFromRoot() 733 if mod.pkg.Name != parts[0] { 734 importPath = fmt.Sprintf("pulumi_%s", refPkgName) 735 } 736 737 if modName == "" { 738 imp, as := "outputs", "_root_outputs" 739 if input { 740 imp, as = "_inputs", "_root_inputs" 741 } 742 return fmt.Sprintf("from %s import %s as %s", importPath, imp, as) 743 } 744 745 components := strings.Split(modName, "/") 746 return fmt.Sprintf("from %s import %[2]s as _%[2]s", importPath, components[0]) 747 } 748 749 func (mod *modContext) importEnumType(e *schema.EnumType) string { 750 if e.Package != mod.pkg { 751 return fmt.Sprintf("import %s", pyPack(e.Package.Name)) 752 } 753 754 modName := mod.tokenToModule(e.Token) 755 if modName == mod.mod { 756 return "from ._enums import *" 757 } 758 759 importPath := mod.getRelImportFromRoot() 760 761 if modName == "" { 762 return fmt.Sprintf("from %s import _enums as _root_enums", importPath) 763 } 764 765 components := strings.Split(modName, "/") 766 return fmt.Sprintf("from %s import %s", importPath, components[0]) 767 } 768 769 func (mod *modContext) importResourceType(r *schema.ResourceType) string { 770 if r.Resource != nil && r.Resource.Package != mod.pkg { 771 return fmt.Sprintf("import %s", pyPack(r.Resource.Package.Name)) 772 } 773 774 tok := r.Token 775 parts := strings.Split(tok, ":") 776 contract.Assert(len(parts) == 3) 777 778 // If it's a provider resource, import the top-level package. 779 if parts[0] == "pulumi" && parts[1] == "providers" { 780 return fmt.Sprintf("import pulumi_%s", parts[2]) 781 } 782 783 refPkgName := parts[0] 784 785 modName := mod.tokenToResource(tok) 786 787 importPath := mod.getRelImportFromRoot() 788 if mod.pkg.Name != parts[0] { 789 importPath = fmt.Sprintf("pulumi_%s", refPkgName) 790 } 791 792 name := PyName(tokenToName(r.Token)) 793 if mod.compatibility == kubernetes20 { 794 // To maintain backward compatibility for kubernetes, the file names 795 // need to be CamelCase instead of the standard snake_case. 796 name = tokenToName(r.Token) 797 } 798 if r.Resource != nil && r.Resource.IsProvider { 799 name = "provider" 800 } 801 802 components := strings.Split(modName, "/") 803 return fmt.Sprintf("from %s%s import %s", importPath, name, components[0]) 804 } 805 806 // genConfig emits all config variables in the given module, returning the resulting file. 807 func (mod *modContext) genConfig(variables []*schema.Property) (string, error) { 808 w := &bytes.Buffer{} 809 810 imports := imports{} 811 mod.collectImports(variables, imports, false /*input*/) 812 813 mod.genHeader(w, true /*needsSDK*/, imports) 814 fmt.Fprintf(w, "import types\n") 815 fmt.Fprintf(w, "\n") 816 817 // Create a config bag for the variables to pull from. 818 fmt.Fprintf(w, "__config__ = pulumi.Config('%s')\n", mod.pkg.Name) 819 fmt.Fprintf(w, "\n\n") 820 821 // To avoid a breaking change to the existing config getters, we define a class that extends 822 // the `ModuleType` type and implements property getters for each config key. We then overwrite 823 // the `__class__` attribute of the current module as described in the proposal for PEP-549. This allows 824 // us to maintain the existing interface for users but implement dynamic getters behind the scenes. 825 fmt.Fprintf(w, "class _ExportableConfig(types.ModuleType):\n") 826 indent := " " 827 828 // Emit an entry for all config variables. 829 for _, p := range variables { 830 configFetch, err := genConfigFetch(p) 831 if err != nil { 832 return "", err 833 } 834 835 typeString := genConfigVarType(p) 836 fmt.Fprintf(w, "%s@property\n", indent) 837 fmt.Fprintf(w, "%sdef %s(self) -> %s:\n", indent, PyName(p.Name), typeString) 838 dblIndent := strings.Repeat(indent, 2) 839 840 printComment(w, p.Comment, dblIndent) 841 fmt.Fprintf(w, "%sreturn %s\n", dblIndent, configFetch) 842 fmt.Fprintf(w, "\n") 843 } 844 845 return w.String(), nil 846 } 847 848 func genConfigFetch(configVar *schema.Property) (string, error) { 849 getFunc := "get" 850 unwrappedType := codegen.UnwrapType(configVar.Type) 851 switch unwrappedType { 852 case schema.BoolType: 853 getFunc = "get_bool" 854 case schema.IntType: 855 getFunc = "get_int" 856 case schema.NumberType: 857 getFunc = "get_float" 858 } 859 860 configFetch := fmt.Sprintf("__config__.%s('%s')", getFunc, configVar.Name) 861 if configVar.DefaultValue != nil { 862 v, err := getDefaultValue(configVar.DefaultValue, unwrappedType) 863 if err != nil { 864 return "", err 865 } 866 configFetch += " or " + v 867 } 868 return configFetch, nil 869 } 870 871 func genConfigVarType(configVar *schema.Property) string { 872 // For historical reasons and to maintain backwards compatibility, the config variables for python 873 // are typed as `Optional[str`] or `str` for complex objects since the getters only use config.get(). 874 // To return the rich objects would be a breaking change, tracked in https://github.com/pulumi/pulumi/issues/7493 875 typeString := "str" 876 switch codegen.UnwrapType(configVar.Type) { 877 case schema.BoolType: 878 typeString = "bool" 879 case schema.IntType: 880 typeString = "int" 881 case schema.NumberType: 882 typeString = "float" 883 } 884 885 if configVar.DefaultValue == nil || configVar.DefaultValue.Value == nil { 886 typeString = "Optional[" + typeString + "]" 887 } 888 return typeString 889 } 890 891 // genConfigStubs emits all type information for the config variables in the given module, returning the resulting file. 892 // We do this because we lose IDE autocomplete by implementing the dynamic config getters described in genConfig. 893 // Emitting these stubs allows us to maintain type hints and autocomplete for users. 894 func (mod *modContext) genConfigStubs(variables []*schema.Property) (string, error) { 895 w := &bytes.Buffer{} 896 897 imports := imports{} 898 mod.collectImports(variables, imports, false /*input*/) 899 900 mod.genHeader(w, true /*needsSDK*/, imports) 901 902 // Emit an entry for all config variables. 903 for _, p := range variables { 904 typeString := genConfigVarType(p) 905 fmt.Fprintf(w, "%s: %s\n", p.Name, typeString) 906 printComment(w, p.Comment, "") 907 fmt.Fprintf(w, "\n") 908 } 909 910 return w.String(), nil 911 } 912 913 func allTypesAreOverlays(types []*schema.ObjectType) bool { 914 for _, t := range types { 915 if !t.IsOverlay { 916 return false 917 } 918 } 919 return true 920 } 921 922 func (mod *modContext) genTypes(dir string, fs codegen.Fs) error { 923 genTypes := func(file string, input bool) error { 924 w := &bytes.Buffer{} 925 926 if allTypesAreOverlays(mod.types) { 927 // If all resources in this module are overlays, skip further code generation. 928 return nil 929 } 930 931 imports := imports{} 932 for _, t := range mod.types { 933 if t.IsOverlay { 934 // This type is generated by the provider, so no further action is required. 935 continue 936 } 937 938 if input && mod.details(t).inputType { 939 visitObjectTypes(t.Properties, func(t schema.Type) { 940 switch t := t.(type) { 941 case *schema.ObjectType: 942 imports.addTypeIf(mod, t, true /*input*/, func(imp string) bool { 943 // No need to import `._inputs` inside _inputs.py. 944 return imp != "from ._inputs import *" 945 }) 946 case *schema.EnumType: 947 imports.addEnum(mod, t) 948 case *schema.ResourceType: 949 imports.addResource(mod, t) 950 } 951 }) 952 } 953 if !input && mod.details(t).outputType { 954 mod.collectImports(t.Properties, imports, false /*input*/) 955 } 956 } 957 for _, e := range mod.enums { 958 imports.addEnum(mod, e) 959 } 960 961 mod.genHeader(w, true /*needsSDK*/, imports) 962 963 // Export only the symbols we want exported. 964 fmt.Fprintf(w, "__all__ = [\n") 965 for _, t := range mod.types { 966 if t.IsOverlay { 967 // This type is generated by the provider, so no further action is required. 968 continue 969 } 970 971 if input && mod.details(t).inputType || !input && mod.details(t).outputType { 972 fmt.Fprintf(w, " '%s',\n", mod.unqualifiedObjectTypeName(t, input)) 973 } 974 } 975 fmt.Fprintf(w, "]\n\n") 976 977 var hasTypes bool 978 for _, t := range mod.types { 979 if t.IsOverlay { 980 // This type is generated by the provider, so no further action is required. 981 continue 982 } 983 984 if input && mod.details(t).inputType { 985 if err := mod.genObjectType(w, t, true); err != nil { 986 return err 987 } 988 hasTypes = true 989 } 990 if !input && mod.details(t).outputType { 991 if err := mod.genObjectType(w, t, false); err != nil { 992 return err 993 } 994 hasTypes = true 995 } 996 } 997 if hasTypes { 998 fs.Add(path.Join(dir, file), w.Bytes()) 999 } 1000 return nil 1001 } 1002 if err := genTypes("_inputs.py", true); err != nil { 1003 return err 1004 } 1005 return genTypes("outputs.py", false) 1006 } 1007 1008 func awaitableTypeNames(tok string) (baseName, awaitableName string) { 1009 baseName = pyClassName(tokenToName(tok)) 1010 awaitableName = "Awaitable" + baseName 1011 return 1012 } 1013 1014 func (mod *modContext) genAwaitableType(w io.Writer, obj *schema.ObjectType) string { 1015 baseName, awaitableName := awaitableTypeNames(obj.Token) 1016 1017 // Produce a class definition with optional """ comment. 1018 fmt.Fprint(w, "@pulumi.output_type\n") 1019 fmt.Fprintf(w, "class %s:\n", baseName) 1020 printComment(w, obj.Comment, " ") 1021 1022 // Now generate an initializer with properties for all inputs. 1023 fmt.Fprintf(w, " def __init__(__self__") 1024 for _, prop := range obj.Properties { 1025 fmt.Fprintf(w, ", %s=None", PyName(prop.Name)) 1026 } 1027 fmt.Fprintf(w, "):\n") 1028 if len(obj.Properties) == 0 { 1029 fmt.Fprintf(w, " pass") 1030 } 1031 for _, prop := range obj.Properties { 1032 // Check that required arguments are present. Also check that types are as expected. 1033 pname := PyName(prop.Name) 1034 ptype := mod.pyType(prop.Type) 1035 fmt.Fprintf(w, " if %s and not isinstance(%s, %s):\n", pname, pname, ptype) 1036 fmt.Fprintf(w, " raise TypeError(\"Expected argument '%s' to be a %s\")\n", pname, ptype) 1037 1038 if prop.DeprecationMessage != "" { 1039 escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`) 1040 fmt.Fprintf(w, " if %s is not None:\n", pname) 1041 fmt.Fprintf(w, " warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n", escaped) 1042 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n\n", pname, escaped) 1043 } 1044 1045 // Now perform the assignment. 1046 fmt.Fprintf(w, " pulumi.set(__self__, \"%[1]s\", %[1]s)\n", pname) 1047 } 1048 fmt.Fprintf(w, "\n") 1049 1050 // Write out Python property getters for each property. 1051 mod.genProperties(w, obj.Properties, false /*setters*/, "", func(prop *schema.Property) string { 1052 return mod.typeString(prop.Type, false /*input*/, false /*acceptMapping*/) 1053 }) 1054 1055 // Produce an awaitable subclass. 1056 fmt.Fprint(w, "\n") 1057 fmt.Fprintf(w, "class %s(%s):\n", awaitableName, baseName) 1058 1059 // Emit __await__ and __iter__ in order to make this type awaitable. 1060 // 1061 // Note that we need __await__ to be an iterator, but we only want it to return one value. As such, we use 1062 // `if False: yield` to construct this. 1063 // 1064 // We also need the result of __await__ to be a plain, non-awaitable value. We achieve this by returning a new 1065 // instance of the base class. 1066 fmt.Fprintf(w, " # pylint: disable=using-constant-test\n") 1067 fmt.Fprintf(w, " def __await__(self):\n") 1068 fmt.Fprintf(w, " if False:\n") 1069 fmt.Fprintf(w, " yield self\n") 1070 fmt.Fprintf(w, " return %s(", baseName) 1071 for i, prop := range obj.Properties { 1072 if i > 0 { 1073 fmt.Fprintf(w, ",") 1074 } 1075 pname := PyName(prop.Name) 1076 fmt.Fprintf(w, "\n %s=self.%s", pname, pname) 1077 } 1078 fmt.Fprintf(w, ")\n") 1079 1080 return awaitableName 1081 } 1082 1083 func resourceName(res *schema.Resource) string { 1084 name := pyClassName(tokenToName(res.Token)) 1085 if res.IsProvider { 1086 name = "Provider" 1087 } 1088 return name 1089 } 1090 1091 func (mod *modContext) genResource(res *schema.Resource) (string, error) { 1092 w := &bytes.Buffer{} 1093 1094 imports := imports{} 1095 mod.collectImportsForResource(res.Properties, imports, false /*input*/, res) 1096 mod.collectImportsForResource(res.InputProperties, imports, true /*input*/, res) 1097 if res.StateInputs != nil { 1098 mod.collectImportsForResource(res.StateInputs.Properties, imports, true /*input*/, res) 1099 } 1100 for _, method := range res.Methods { 1101 if method.Function.Inputs != nil { 1102 mod.collectImportsForResource(method.Function.Inputs.Properties, imports, true /*input*/, res) 1103 } 1104 if method.Function.Outputs != nil { 1105 mod.collectImportsForResource(method.Function.Outputs.Properties, imports, false /*input*/, res) 1106 } 1107 } 1108 1109 mod.genHeader(w, true /*needsSDK*/, imports) 1110 1111 name := resourceName(res) 1112 1113 resourceArgsName := fmt.Sprintf("%sArgs", name) 1114 // Some providers (e.g. Kubernetes) have types with the same name as resources (e.g. StorageClass in Kubernetes). 1115 // We've already shipped the input type (e.g. StorageClassArgs) in the same module as the resource, so we can't use 1116 // the same name for the resource's args class. When an input type exists that would conflict with the name of the 1117 // resource args class, we'll use a different name: `<Resource>InitArgs` instead of `<Resource>Args`. 1118 const alternateSuffix = "InitArgs" 1119 for _, t := range mod.types { 1120 if mod.details(t).inputType { 1121 if mod.unqualifiedObjectTypeName(t, true) == resourceArgsName { 1122 resourceArgsName = name + alternateSuffix 1123 break 1124 } 1125 } 1126 } 1127 // If we're using the alternate name, ensure the alternate name doesn't conflict with an input type. 1128 if strings.HasSuffix(resourceArgsName, alternateSuffix) { 1129 for _, t := range mod.types { 1130 if mod.details(t).inputType { 1131 if mod.unqualifiedObjectTypeName(t, true) == resourceArgsName { 1132 return "", fmt.Errorf("resource args class named %s in %s conflicts with input type", resourceArgsName, mod.mod) 1133 } 1134 } 1135 } 1136 } 1137 1138 // Export only the symbols we want exported. 1139 fmt.Fprintf(w, "__all__ = ['%s', '%s']\n\n", resourceArgsName, name) 1140 1141 // Produce an args class. 1142 argsComment := fmt.Sprintf("The set of arguments for constructing a %s resource.", name) 1143 err := mod.genType(w, resourceArgsName, argsComment, res.InputProperties, true, false) 1144 if err != nil { 1145 return "", err 1146 } 1147 1148 // Produce an unexported state class. It's currently only used internally inside the `get` method to opt-in to 1149 // the type/name metadata based translation behavior. 1150 // We can consider making use of it publicly in the future: removing the underscore prefix, exporting it from 1151 // `__all__`, and adding a static `get` overload that accepts it as an argument. 1152 hasStateInputs := !res.IsProvider && !res.IsComponent && res.StateInputs != nil && 1153 len(res.StateInputs.Properties) > 0 1154 if hasStateInputs { 1155 stateComment := fmt.Sprintf("Input properties used for looking up and filtering %s resources.", name) 1156 err = mod.genType(w, fmt.Sprintf("_%sState", name), stateComment, res.StateInputs.Properties, true, false) 1157 if err != nil { 1158 return "", err 1159 } 1160 } 1161 1162 var baseType string 1163 switch { 1164 case res.IsProvider: 1165 baseType = "pulumi.ProviderResource" 1166 case res.IsComponent: 1167 baseType = "pulumi.ComponentResource" 1168 default: 1169 baseType = "pulumi.CustomResource" 1170 } 1171 1172 if !res.IsProvider && res.DeprecationMessage != "" && mod.compatibility != kubernetes20 { 1173 escaped := strings.ReplaceAll(res.DeprecationMessage, `"`, `\"`) 1174 fmt.Fprintf(w, "warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n\n\n", escaped) 1175 } 1176 1177 // Produce a class definition with optional """ comment. 1178 fmt.Fprintf(w, "class %s(%s):\n", name, baseType) 1179 if res.DeprecationMessage != "" && mod.compatibility != kubernetes20 { 1180 escaped := strings.ReplaceAll(res.DeprecationMessage, `"`, `\"`) 1181 fmt.Fprintf(w, " warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n\n", escaped) 1182 } 1183 1184 // Determine if all inputs are optional. 1185 allOptionalInputs := true 1186 for _, prop := range res.InputProperties { 1187 allOptionalInputs = allOptionalInputs && !prop.IsRequired() 1188 } 1189 1190 // Emit __init__ overloads and implementation... 1191 1192 // Helper for generating an init method with inputs as function arguments. 1193 emitInitMethodSignature := func(methodName string) { 1194 fmt.Fprintf(w, " def %s(__self__,\n", methodName) 1195 fmt.Fprintf(w, " resource_name: str,\n") 1196 fmt.Fprintf(w, " opts: Optional[pulumi.ResourceOptions] = None") 1197 1198 // If there's an argument type, emit it. 1199 for _, prop := range res.InputProperties { 1200 ty := mod.typeString(codegen.OptionalType(prop), true, true /*acceptMapping*/) 1201 fmt.Fprintf(w, ",\n %s: %s = None", InitParamName(prop.Name), ty) 1202 } 1203 1204 fmt.Fprintf(w, ",\n __props__=None):\n") 1205 } 1206 1207 // Emit an __init__ overload that accepts the resource's inputs as function arguments. 1208 fmt.Fprintf(w, " @overload\n") 1209 emitInitMethodSignature("__init__") 1210 mod.genInitDocstring(w, res, resourceArgsName, false /*argsOverload*/) 1211 fmt.Fprintf(w, " ...\n") 1212 1213 // Emit an __init__ overload that accepts the resource's inputs from the args class. 1214 fmt.Fprintf(w, " @overload\n") 1215 fmt.Fprintf(w, " def __init__(__self__,\n") 1216 fmt.Fprintf(w, " resource_name: str,\n") 1217 if allOptionalInputs { 1218 fmt.Fprintf(w, " args: Optional[%s] = None,\n", resourceArgsName) 1219 } else { 1220 fmt.Fprintf(w, " args: %s,\n", resourceArgsName) 1221 } 1222 fmt.Fprintf(w, " opts: Optional[pulumi.ResourceOptions] = None):\n") 1223 mod.genInitDocstring(w, res, resourceArgsName, true /*argsOverload*/) 1224 fmt.Fprintf(w, " ...\n") 1225 1226 // Emit the actual implementation of __init__, which does the appropriate thing based on which 1227 // overload was called. 1228 fmt.Fprintf(w, " def __init__(__self__, resource_name: str, *args, **kwargs):\n") 1229 fmt.Fprintf(w, " resource_args, opts = _utilities.get_resource_args_opts(%s, pulumi.ResourceOptions, *args, **kwargs)\n", resourceArgsName) 1230 fmt.Fprintf(w, " if resource_args is not None:\n") 1231 fmt.Fprintf(w, " __self__._internal_init(resource_name, opts, **resource_args.__dict__)\n") 1232 fmt.Fprintf(w, " else:\n") 1233 fmt.Fprintf(w, " __self__._internal_init(resource_name, *args, **kwargs)\n") 1234 fmt.Fprintf(w, "\n") 1235 1236 // Emit the _internal_init helper method which provides the bulk of the __init__ implementation. 1237 emitInitMethodSignature("_internal_init") 1238 if res.DeprecationMessage != "" && mod.compatibility != kubernetes20 { 1239 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n", name, res.DeprecationMessage) 1240 } 1241 fmt.Fprintf(w, " opts = pulumi.ResourceOptions.merge(_utilities.get_resource_opts_defaults(), opts)\n") 1242 fmt.Fprintf(w, " if not isinstance(opts, pulumi.ResourceOptions):\n") 1243 fmt.Fprintf(w, " raise TypeError('Expected resource options to be a ResourceOptions instance')\n") 1244 if res.IsComponent { 1245 fmt.Fprintf(w, " if opts.id is not None:\n") 1246 fmt.Fprintf(w, " raise ValueError('ComponentResource classes do not support opts.id')\n") 1247 fmt.Fprintf(w, " else:\n") 1248 } else { 1249 fmt.Fprintf(w, " if opts.id is None:\n") 1250 } 1251 fmt.Fprintf(w, " if __props__ is not None:\n") 1252 fmt.Fprintf(w, " raise TypeError(") 1253 fmt.Fprintf(w, "'__props__ is only valid when passed in combination with a valid opts.id to get an existing resource')\n") 1254 1255 // We use an instance of the `<Resource>Args` class for `__props__` to opt-in to the type/name metadata based 1256 // translation behavior. The instance is created using `__new__` to avoid any validation in the `__init__` method, 1257 // values are set directly on its `__dict__`, including any additional output properties. 1258 fmt.Fprintf(w, " __props__ = %[1]s.__new__(%[1]s)\n\n", resourceArgsName) 1259 fmt.Fprintf(w, "") 1260 1261 ins := codegen.NewStringSet() 1262 for _, prop := range res.InputProperties { 1263 pname := InitParamName(prop.Name) 1264 var arg interface{} 1265 var err error 1266 1267 // Fill in computed defaults for arguments. 1268 if prop.DefaultValue != nil { 1269 dv, err := getDefaultValue(prop.DefaultValue, codegen.UnwrapType(prop.Type)) 1270 if err != nil { 1271 return "", err 1272 } 1273 fmt.Fprintf(w, " if %s is None:\n", pname) 1274 fmt.Fprintf(w, " %s = %s\n", pname, dv) 1275 } 1276 1277 // Check that required arguments are present. 1278 if prop.IsRequired() { 1279 fmt.Fprintf(w, " if %s is None and not opts.urn:\n", pname) 1280 fmt.Fprintf(w, " raise TypeError(\"Missing required property '%s'\")\n", pname) 1281 } 1282 1283 // Check that the property isn't deprecated 1284 if prop.DeprecationMessage != "" { 1285 escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`) 1286 fmt.Fprintf(w, " if %s is not None and not opts.urn:\n", pname) 1287 fmt.Fprintf(w, " warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n", escaped) 1288 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n", pname, escaped) 1289 } 1290 1291 // And add it to the dictionary. 1292 arg = pname 1293 1294 if prop.ConstValue != nil { 1295 arg, err = getConstValue(prop.ConstValue) 1296 if err != nil { 1297 return "", err 1298 } 1299 } 1300 1301 // If this resource is a provider then, regardless of the schema of the underlying provider 1302 // type, we must project all properties as strings. For all properties that are not strings, 1303 // we'll marshal them to JSON and use the JSON string as a string input. 1304 if res.IsProvider && !isStringType(prop.Type) { 1305 arg = fmt.Sprintf("pulumi.Output.from_input(%s).apply(pulumi.runtime.to_json) if %s is not None else None", arg, arg) 1306 } 1307 name := PyName(prop.Name) 1308 if prop.Secret { 1309 fmt.Fprintf(w, " __props__.__dict__[%[1]q] = None if %[2]s is None else pulumi.Output.secret(%[2]s)\n", name, arg) 1310 } else { 1311 fmt.Fprintf(w, " __props__.__dict__[%q] = %s\n", name, arg) 1312 } 1313 1314 ins.Add(prop.Name) 1315 } 1316 1317 var secretProps []string 1318 for _, prop := range res.Properties { 1319 // Default any pure output properties to None. This ensures they are available as properties, even if 1320 // they don't ever get assigned a real value, and get documentation if available. 1321 if !ins.Has(prop.Name) { 1322 fmt.Fprintf(w, " __props__.__dict__[%q] = None\n", PyName(prop.Name)) 1323 } 1324 1325 if prop.Secret { 1326 secretProps = append(secretProps, prop.Name) 1327 } 1328 } 1329 1330 if len(res.Aliases) > 0 { 1331 fmt.Fprintf(w, ` alias_opts = pulumi.ResourceOptions(aliases=[`) 1332 1333 for i, alias := range res.Aliases { 1334 if i > 0 { 1335 fmt.Fprintf(w, ", ") 1336 } 1337 mod.writeAlias(w, alias) 1338 } 1339 1340 fmt.Fprintf(w, "])\n") 1341 fmt.Fprintf(w, " opts = pulumi.ResourceOptions.merge(opts, alias_opts)\n") 1342 } 1343 1344 if len(secretProps) > 0 { 1345 fmt.Fprintf(w, ` secret_opts = pulumi.ResourceOptions(additional_secret_outputs=["%s"])`, strings.Join(secretProps, `", "`)) 1346 fmt.Fprintf(w, "\n opts = pulumi.ResourceOptions.merge(opts, secret_opts)\n") 1347 } 1348 1349 replaceOnChangesProps, errList := res.ReplaceOnChanges() 1350 for _, err := range errList { 1351 cmdutil.Diag().Warningf(&diag.Diag{Message: err.Error()}) 1352 } 1353 if len(replaceOnChangesProps) > 0 { 1354 replaceOnChangesStrings := schema.PropertyListJoinToString(replaceOnChangesProps, PyName) 1355 fmt.Fprintf(w, ` replace_on_changes = pulumi.ResourceOptions(replace_on_changes=["%s"])`, strings.Join(replaceOnChangesStrings, `", "`)) 1356 fmt.Fprintf(w, "\n opts = pulumi.ResourceOptions.merge(opts, replace_on_changes)\n") 1357 } 1358 1359 // Finally, chain to the base constructor, which will actually register the resource. 1360 tok := res.Token 1361 if res.IsProvider { 1362 tok = mod.pkg.Name 1363 } 1364 fmt.Fprintf(w, " super(%s, __self__).__init__(\n", name) 1365 fmt.Fprintf(w, " '%s',\n", tok) 1366 fmt.Fprintf(w, " resource_name,\n") 1367 fmt.Fprintf(w, " __props__,\n") 1368 if res.IsComponent { 1369 fmt.Fprintf(w, " opts,\n") 1370 fmt.Fprintf(w, " remote=True)\n") 1371 } else { 1372 fmt.Fprintf(w, " opts)\n") 1373 } 1374 fmt.Fprintf(w, "\n") 1375 1376 if !res.IsProvider && !res.IsComponent { 1377 fmt.Fprintf(w, " @staticmethod\n") 1378 fmt.Fprintf(w, " def get(resource_name: str,\n") 1379 fmt.Fprintf(w, " id: pulumi.Input[str],\n") 1380 fmt.Fprintf(w, " opts: Optional[pulumi.ResourceOptions] = None") 1381 1382 if hasStateInputs { 1383 for _, prop := range res.StateInputs.Properties { 1384 pname := InitParamName(prop.Name) 1385 ty := mod.typeString(codegen.OptionalType(prop), true, true /*acceptMapping*/) 1386 fmt.Fprintf(w, ",\n %s: %s = None", pname, ty) 1387 } 1388 } 1389 fmt.Fprintf(w, ") -> '%s':\n", name) 1390 mod.genGetDocstring(w, res) 1391 fmt.Fprintf(w, 1392 " opts = pulumi.ResourceOptions.merge(opts, pulumi.ResourceOptions(id=id))\n") 1393 fmt.Fprintf(w, "\n") 1394 if hasStateInputs { 1395 fmt.Fprintf(w, " __props__ = _%[1]sState.__new__(_%[1]sState)\n\n", name) 1396 } else { 1397 // If we don't have any state inputs, we'll just instantiate the `<Resource>Args` class, 1398 // to opt-in to the improved translation behavior. 1399 fmt.Fprintf(w, " __props__ = %[1]s.__new__(%[1]s)\n\n", resourceArgsName) 1400 } 1401 1402 stateInputs := codegen.NewStringSet() 1403 if res.StateInputs != nil { 1404 for _, prop := range res.StateInputs.Properties { 1405 stateInputs.Add(prop.Name) 1406 fmt.Fprintf(w, " __props__.__dict__[%q] = %s\n", PyName(prop.Name), InitParamName(prop.Name)) 1407 } 1408 } 1409 for _, prop := range res.Properties { 1410 if !stateInputs.Has(prop.Name) { 1411 fmt.Fprintf(w, " __props__.__dict__[%q] = None\n", PyName(prop.Name)) 1412 } 1413 } 1414 1415 fmt.Fprintf(w, " return %s(resource_name, opts=opts, __props__=__props__)\n\n", name) 1416 } 1417 1418 // Write out Python property getters for each of the resource's properties. 1419 mod.genProperties(w, res.Properties, false /*setters*/, "", func(prop *schema.Property) string { 1420 ty := mod.typeString(prop.Type, false /*input*/, false /*acceptMapping*/) 1421 return fmt.Sprintf("pulumi.Output[%s]", ty) 1422 }) 1423 1424 // Write out methods. 1425 mod.genMethods(w, res) 1426 1427 return w.String(), nil 1428 } 1429 1430 func (mod *modContext) genProperties(w io.Writer, properties []*schema.Property, setters bool, indent string, 1431 propType func(prop *schema.Property) string) { 1432 // Write out Python properties for each property. If there is a property named "property", it will 1433 // be emitted last to avoid conflicting with the built-in `@property` decorator function. We do 1434 // this instead of importing `builtins` and fully qualifying the decorator as `@builtins.property` 1435 // because that wouldn't address the problem if there was a property named "builtins". 1436 emitProp := func(pname string, prop *schema.Property) { 1437 ty := propType(prop) 1438 fmt.Fprintf(w, "%s @property\n", indent) 1439 if pname == prop.Name { 1440 fmt.Fprintf(w, "%s @pulumi.getter\n", indent) 1441 } else { 1442 fmt.Fprintf(w, "%s @pulumi.getter(name=%q)\n", indent, prop.Name) 1443 } 1444 fmt.Fprintf(w, "%s def %s(self) -> %s:\n", indent, pname, ty) 1445 if prop.Comment != "" { 1446 printComment(w, prop.Comment, indent+" ") 1447 } 1448 fmt.Fprintf(w, "%s return pulumi.get(self, %q)\n\n", indent, pname) 1449 1450 if setters { 1451 fmt.Fprintf(w, "%s @%s.setter\n", indent, pname) 1452 fmt.Fprintf(w, "%s def %s(self, value: %s):\n", indent, pname, ty) 1453 fmt.Fprintf(w, "%s pulumi.set(self, %q, value)\n\n", indent, pname) 1454 } 1455 } 1456 var propNamedProperty *schema.Property 1457 for _, prop := range properties { 1458 pname := PyName(prop.Name) 1459 // If there is a property named "property", skip it, and emit it last. 1460 if pname == "property" { 1461 propNamedProperty = prop 1462 continue 1463 } 1464 emitProp(pname, prop) 1465 } 1466 if propNamedProperty != nil { 1467 emitProp("property", propNamedProperty) 1468 } 1469 } 1470 1471 func (mod *modContext) genMethods(w io.Writer, res *schema.Resource) { 1472 genReturnType := func(method *schema.Method) string { 1473 obj := method.Function.Outputs 1474 name := pyClassName(title(method.Name)) + "Result" 1475 1476 // Produce a class definition with optional """ comment. 1477 fmt.Fprintf(w, " @pulumi.output_type\n") 1478 fmt.Fprintf(w, " class %s:\n", name) 1479 printComment(w, obj.Comment, " ") 1480 1481 // Now generate an initializer with properties for all inputs. 1482 fmt.Fprintf(w, " def __init__(__self__") 1483 for _, prop := range obj.Properties { 1484 fmt.Fprintf(w, ", %s=None", PyName(prop.Name)) 1485 } 1486 fmt.Fprintf(w, "):\n") 1487 for _, prop := range obj.Properties { 1488 // Check that required arguments are present. Also check that types are as expected. 1489 pname := PyName(prop.Name) 1490 ptype := mod.pyType(prop.Type) 1491 fmt.Fprintf(w, " if %s and not isinstance(%s, %s):\n", pname, pname, ptype) 1492 fmt.Fprintf(w, " raise TypeError(\"Expected argument '%s' to be a %s\")\n", pname, ptype) 1493 1494 if prop.DeprecationMessage != "" { 1495 escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`) 1496 fmt.Fprintf(w, " if %s is not None:\n", pname) 1497 fmt.Fprintf(w, " warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n", escaped) 1498 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n\n", pname, escaped) 1499 } 1500 1501 // Now perform the assignment. 1502 fmt.Fprintf(w, " pulumi.set(__self__, \"%[1]s\", %[1]s)\n", pname) 1503 } 1504 fmt.Fprintf(w, "\n") 1505 1506 // Write out Python property getters for each property. 1507 mod.genProperties(w, obj.Properties, false /*setters*/, " ", func(prop *schema.Property) string { 1508 return mod.typeString(prop.Type, false /*input*/, false /*acceptMapping*/) 1509 }) 1510 1511 return name 1512 } 1513 1514 genMethod := func(method *schema.Method) { 1515 methodName := PyName(method.Name) 1516 fun := method.Function 1517 1518 shouldLiftReturn := mod.liftSingleValueMethodReturns && method.Function.Outputs != nil && len(method.Function.Outputs.Properties) == 1 1519 1520 // If there is a return type, emit it. 1521 var retTypeName, retTypeNameQualified, retTypeNameQualifiedOutput, methodRetType string 1522 if fun.Outputs != nil { 1523 retTypeName = genReturnType(method) 1524 retTypeNameQualified = fmt.Sprintf("%s.%s", resourceName(res), retTypeName) 1525 retTypeNameQualifiedOutput = fmt.Sprintf("pulumi.Output['%s']", retTypeNameQualified) 1526 1527 if shouldLiftReturn { 1528 methodRetType = fmt.Sprintf("pulumi.Output['%s']", mod.pyType(fun.Outputs.Properties[0].Type)) 1529 } else { 1530 methodRetType = retTypeNameQualifiedOutput 1531 } 1532 } 1533 1534 var args []*schema.Property 1535 if fun.Inputs != nil { 1536 // Filter out the __self__ argument from the inputs. 1537 args = make([]*schema.Property, 0, len(fun.Inputs.InputShape.Properties)-1) 1538 for _, arg := range fun.Inputs.InputShape.Properties { 1539 if arg.Name == "__self__" { 1540 continue 1541 } 1542 args = append(args, arg) 1543 } 1544 // Sort required args first. 1545 sort.Slice(args, func(i, j int) bool { 1546 pi, pj := args[i], args[j] 1547 switch { 1548 case pi.IsRequired() != pj.IsRequired(): 1549 return pi.IsRequired() && !pj.IsRequired() 1550 default: 1551 return pi.Name < pj.Name 1552 } 1553 }) 1554 } 1555 1556 // Write out the function signature. 1557 def := fmt.Sprintf(" def %s(", methodName) 1558 var indent string 1559 if len(args) > 0 { 1560 indent = strings.Repeat(" ", len(def)) 1561 } 1562 fmt.Fprintf(w, "%s__self__", def) 1563 // Bare `*` argument to force callers to use named arguments. 1564 if len(args) > 0 { 1565 fmt.Fprintf(w, ", *") 1566 } 1567 for _, arg := range args { 1568 pname := PyName(arg.Name) 1569 ty := mod.typeString(arg.Type, true, false /*acceptMapping*/) 1570 var defaultValue string 1571 if !arg.IsRequired() { 1572 defaultValue = " = None" 1573 } 1574 fmt.Fprintf(w, ",\n%s%s: %s%s", indent, pname, ty, defaultValue) 1575 } 1576 if retTypeNameQualifiedOutput != "" { 1577 fmt.Fprintf(w, ") -> %s:\n", methodRetType) 1578 } else { 1579 fmt.Fprintf(w, ") -> None:\n") 1580 } 1581 1582 // If this func has documentation, write it at the top of the docstring, otherwise use a generic comment. 1583 docs := &bytes.Buffer{} 1584 if fun.Comment != "" { 1585 fmt.Fprintln(docs, codegen.FilterExamples(fun.Comment, "python")) 1586 } 1587 if len(args) > 0 { 1588 fmt.Fprintln(docs, "") 1589 for _, arg := range args { 1590 mod.genPropDocstring(docs, PyName(arg.Name), arg, false /*acceptMapping*/) 1591 } 1592 } 1593 printComment(w, docs.String(), " ") 1594 1595 if fun.DeprecationMessage != "" { 1596 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n", methodName, 1597 fun.DeprecationMessage) 1598 } 1599 1600 // Copy the function arguments into a dictionary. 1601 fmt.Fprintf(w, " __args__ = dict()\n") 1602 fmt.Fprintf(w, " __args__['__self__'] = __self__\n") 1603 for _, arg := range args { 1604 pname := PyName(arg.Name) 1605 fmt.Fprintf(w, " __args__['%s'] = %s\n", arg.Name, pname) 1606 } 1607 1608 // Now simply call the function with the arguments. 1609 var typ string 1610 if retTypeNameQualified != "" { 1611 // Pass along the private output_type we generated, so any nested output classes are instantiated by 1612 // the call. 1613 typ = fmt.Sprintf(", typ=%s", retTypeNameQualified) 1614 } 1615 1616 if method.Function.Outputs == nil { 1617 fmt.Fprintf(w, " pulumi.runtime.call('%s', __args__, res=__self__%s)\n", fun.Token, typ) 1618 } else if shouldLiftReturn { 1619 // Store the return in a variable and return the property output 1620 fmt.Fprintf(w, " __result__ = pulumi.runtime.call('%s', __args__, res=__self__%s)\n", fun.Token, typ) 1621 fmt.Fprintf(w, " return __result__.%s\n", PyName(fun.Outputs.Properties[0].Name)) 1622 } else { 1623 // Otherwise return the call directly 1624 fmt.Fprintf(w, " return pulumi.runtime.call('%s', __args__, res=__self__%s)\n", fun.Token, typ) 1625 } 1626 1627 fmt.Fprintf(w, "\n") 1628 } 1629 1630 for _, method := range res.Methods { 1631 genMethod(method) 1632 } 1633 } 1634 1635 func (mod *modContext) writeAlias(w io.Writer, alias *schema.Alias) { 1636 fmt.Fprint(w, "pulumi.Alias(") 1637 parts := []string{} 1638 if alias.Name != nil { 1639 parts = append(parts, fmt.Sprintf("name=\"%v\"", *alias.Name)) 1640 } 1641 if alias.Project != nil { 1642 parts = append(parts, fmt.Sprintf("project=\"%v\"", *alias.Project)) 1643 } 1644 if alias.Type != nil { 1645 parts = append(parts, fmt.Sprintf("type_=\"%v\"", *alias.Type)) 1646 } 1647 1648 for i, part := range parts { 1649 if i > 0 { 1650 fmt.Fprint(w, ", ") 1651 } 1652 fmt.Fprint(w, part) 1653 } 1654 fmt.Fprint(w, ")") 1655 } 1656 1657 func (mod *modContext) genFunction(fun *schema.Function) (string, error) { 1658 w := &bytes.Buffer{} 1659 1660 imports := imports{} 1661 if fun.Inputs != nil { 1662 mod.collectImports(fun.Inputs.Properties, imports, true) 1663 } 1664 if fun.Outputs != nil { 1665 mod.collectImports(fun.Outputs.Properties, imports, false) 1666 } 1667 1668 mod.genHeader(w, true /*needsSDK*/, imports) 1669 1670 var baseName, awaitableName string 1671 if fun.Outputs != nil { 1672 baseName, awaitableName = awaitableTypeNames(fun.Outputs.Token) 1673 } 1674 name := PyName(tokenToName(fun.Token)) 1675 1676 // Export only the symbols we want exported. 1677 fmt.Fprintf(w, "__all__ = [\n") 1678 if fun.Outputs != nil { 1679 fmt.Fprintf(w, " '%s',\n", baseName) 1680 fmt.Fprintf(w, " '%s',\n", awaitableName) 1681 } 1682 fmt.Fprintf(w, " '%s',\n", name) 1683 if fun.NeedsOutputVersion() { 1684 fmt.Fprintf(w, " '%s_output',\n", name) 1685 } 1686 fmt.Fprintf(w, "]\n\n") 1687 1688 if fun.DeprecationMessage != "" { 1689 escaped := strings.ReplaceAll(fun.DeprecationMessage, `"`, `\"`) 1690 fmt.Fprintf(w, "warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n\n", escaped) 1691 } 1692 1693 // If there is a return type, emit it. 1694 retTypeName := "" 1695 var rets []*schema.Property 1696 if fun.Outputs != nil { 1697 retTypeName, rets = mod.genAwaitableType(w, fun.Outputs), fun.Outputs.Properties 1698 fmt.Fprintf(w, "\n\n") 1699 } 1700 1701 var args []*schema.Property 1702 if fun.Inputs != nil { 1703 args = fun.Inputs.Properties 1704 } 1705 1706 mod.genFunDef(w, name, retTypeName, args, false /* wrapInput */) 1707 mod.genFunDocstring(w, fun) 1708 mod.genFunDeprecationMessage(w, fun) 1709 1710 // Copy the function arguments into a dictionary. 1711 fmt.Fprintf(w, " __args__ = dict()\n") 1712 for _, arg := range args { 1713 // TODO: args validation. 1714 fmt.Fprintf(w, " __args__['%s'] = %s\n", arg.Name, PyName(arg.Name)) 1715 } 1716 1717 // If the caller explicitly specified a version, use it, otherwise inject this package's version. 1718 fmt.Fprintf(w, " opts = pulumi.InvokeOptions.merge(_utilities.get_invoke_opts_defaults(), opts)\n") 1719 1720 // Now simply invoke the runtime function with the arguments. 1721 var typ string 1722 if fun.Outputs != nil { 1723 // Pass along the private output_type we generated, so any nested outputs classes are instantiated by 1724 // the call to invoke. 1725 typ = fmt.Sprintf(", typ=%s", baseName) 1726 } 1727 fmt.Fprintf(w, " __ret__ = pulumi.runtime.invoke('%s', __args__, opts=opts%s).value\n", fun.Token, typ) 1728 fmt.Fprintf(w, "\n") 1729 1730 // And copy the results to an object, if there are indeed any expected returns. 1731 if fun.Outputs != nil { 1732 fmt.Fprintf(w, " return %s(", retTypeName) 1733 for i, ret := range rets { 1734 if i > 0 { 1735 fmt.Fprintf(w, ",") 1736 } 1737 // Use the get_dict_value utility instead of calling __ret__.get directly in case the __ret__ 1738 // object has a get property that masks the underlying dict subclass's get method. 1739 fmt.Fprintf(w, "\n %[1]s=__ret__.%[1]s", PyName(ret.Name)) 1740 } 1741 fmt.Fprintf(w, ")\n") 1742 } 1743 1744 mod.genFunctionOutputVersion(w, fun) 1745 return w.String(), nil 1746 } 1747 1748 func (mod *modContext) genFunDocstring(w io.Writer, fun *schema.Function) { 1749 var args []*schema.Property 1750 if fun.Inputs != nil { 1751 args = fun.Inputs.Properties 1752 } 1753 1754 // If this func has documentation, write it at the top of the docstring, otherwise use a generic comment. 1755 docs := &bytes.Buffer{} 1756 if fun.Comment != "" { 1757 fmt.Fprintln(docs, codegen.FilterExamples(fun.Comment, "python")) 1758 } else { 1759 fmt.Fprintln(docs, "Use this data source to access information about an existing resource.") 1760 } 1761 if len(args) > 0 { 1762 fmt.Fprintln(docs, "") 1763 for _, arg := range args { 1764 mod.genPropDocstring(docs, PyName(arg.Name), arg, true /*acceptMapping*/) 1765 } 1766 } 1767 printComment(w, docs.String(), " ") 1768 } 1769 1770 func (mod *modContext) genFunDeprecationMessage(w io.Writer, fun *schema.Function) { 1771 if fun.DeprecationMessage == "" { 1772 return 1773 } 1774 name := PyName(tokenToName(fun.Token)) 1775 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n", name, fun.DeprecationMessage) 1776 } 1777 1778 // Generates the function signature line `def fn(...):` without the body. 1779 func (mod *modContext) genFunDef(w io.Writer, name, retTypeName string, args []*schema.Property, wrapInput bool) { 1780 def := fmt.Sprintf("def %s(", name) 1781 var indent string 1782 if len(args) > 0 { 1783 indent = strings.Repeat(" ", len(def)) 1784 } 1785 fmt.Fprintf(w, def) 1786 for i, arg := range args { 1787 var ind string 1788 if i != 0 { 1789 ind = indent 1790 } 1791 pname := PyName(arg.Name) 1792 1793 var argType schema.Type 1794 if wrapInput { 1795 argType = &schema.OptionalType{ 1796 ElementType: &schema.InputType{ 1797 ElementType: arg.Type, 1798 }, 1799 } 1800 } else { 1801 argType = codegen.OptionalType(arg) 1802 } 1803 1804 ty := mod.typeString(argType, true /*input*/, true /*acceptMapping*/) 1805 fmt.Fprintf(w, "%s%s: %s = None,\n", ind, pname, ty) 1806 } 1807 fmt.Fprintf(w, "%sopts: Optional[pulumi.InvokeOptions] = None", indent) 1808 if retTypeName != "" { 1809 fmt.Fprintf(w, ") -> %s:\n", retTypeName) 1810 } else { 1811 fmt.Fprintf(w, "):\n") 1812 } 1813 } 1814 1815 // Generates `def ${fn}_output(..) version lifted to work on 1816 // `Input`-wrapped arguments and producing an `Output`-wrapped result. 1817 func (mod *modContext) genFunctionOutputVersion(w io.Writer, fun *schema.Function) { 1818 if !fun.NeedsOutputVersion() { 1819 return 1820 } 1821 1822 var retTypeName string 1823 if fun.Outputs != nil { 1824 originalOutputTypeName, _ := awaitableTypeNames(fun.Outputs.Token) 1825 retTypeName = fmt.Sprintf("pulumi.Output[%s]", originalOutputTypeName) 1826 } else { 1827 retTypeName = "pulumi.Output[void]" 1828 } 1829 1830 originalName := PyName(tokenToName(fun.Token)) 1831 outputSuffixedName := fmt.Sprintf("%s_output", originalName) 1832 1833 var args []*schema.Property 1834 if fun.Inputs != nil { 1835 args = fun.Inputs.Properties 1836 } 1837 1838 fmt.Fprintf(w, "\n\n@_utilities.lift_output_func(%s)\n", originalName) 1839 mod.genFunDef(w, outputSuffixedName, retTypeName, args, true /*wrapInput*/) 1840 mod.genFunDocstring(w, fun) 1841 mod.genFunDeprecationMessage(w, fun) 1842 fmt.Fprintf(w, " ...\n") 1843 } 1844 1845 func (mod *modContext) genEnums(w io.Writer, enums []*schema.EnumType) error { 1846 // Header 1847 mod.genHeader(w, false /*needsSDK*/, nil) 1848 1849 // Enum import 1850 fmt.Fprintf(w, "from enum import Enum\n\n") 1851 1852 // Export only the symbols we want exported. 1853 fmt.Fprintf(w, "__all__ = [\n") 1854 for _, enum := range enums { 1855 fmt.Fprintf(w, " '%s',\n", tokenToName(enum.Token)) 1856 1857 } 1858 fmt.Fprintf(w, "]\n\n\n") 1859 1860 for i, enum := range enums { 1861 if err := mod.genEnum(w, enum); err != nil { 1862 return err 1863 } 1864 if i != len(enums)-1 { 1865 fmt.Fprintf(w, "\n\n") 1866 } 1867 } 1868 return nil 1869 } 1870 1871 func (mod *modContext) genEnum(w io.Writer, enum *schema.EnumType) error { 1872 indent := " " 1873 enumName := tokenToName(enum.Token) 1874 underlyingType := mod.typeString(enum.ElementType, false, false) 1875 1876 switch enum.ElementType { 1877 case schema.StringType, schema.IntType, schema.NumberType: 1878 fmt.Fprintf(w, "class %s(%s, Enum):\n", enumName, underlyingType) 1879 printComment(w, enum.Comment, indent) 1880 for _, e := range enum.Elements { 1881 // If the enum doesn't have a name, set the value as the name. 1882 if e.Name == "" { 1883 e.Name = fmt.Sprintf("%v", e.Value) 1884 } 1885 1886 name, err := makeSafeEnumName(e.Name, enumName) 1887 if err != nil { 1888 return err 1889 } 1890 e.Name = name 1891 1892 fmt.Fprintf(w, "%s%s = ", indent, e.Name) 1893 if val, ok := e.Value.(string); ok { 1894 fmt.Fprintf(w, "%q\n", val) 1895 } else { 1896 fmt.Fprintf(w, "%v\n", e.Value) 1897 } 1898 if e.Comment != "" { 1899 printComment(w, e.Comment, indent) 1900 } 1901 } 1902 default: 1903 return fmt.Errorf("enums of type %s are not yet implemented for this language", enum.ElementType.String()) 1904 } 1905 1906 return nil 1907 } 1908 1909 func visitObjectTypes(properties []*schema.Property, visitor func(objectOrResource schema.Type)) { 1910 codegen.VisitTypeClosure(properties, func(t schema.Type) { 1911 switch st := t.(type) { 1912 case *schema.EnumType, *schema.ObjectType, *schema.ResourceType: 1913 visitor(st) 1914 } 1915 }) 1916 } 1917 1918 func (mod *modContext) collectImports(properties []*schema.Property, imports imports, input bool) { 1919 mod.collectImportsForResource(properties, imports, input, nil) 1920 } 1921 1922 func (mod *modContext) collectImportsForResource(properties []*schema.Property, imports imports, input bool, 1923 res *schema.Resource) { 1924 codegen.VisitTypeClosure(properties, func(t schema.Type) { 1925 switch t := t.(type) { 1926 case *schema.ObjectType: 1927 imports.addType(mod, t, input) 1928 case *schema.EnumType: 1929 imports.addEnum(mod, t) 1930 case *schema.ResourceType: 1931 // Don't import itself. 1932 if t.Resource != res { 1933 imports.addResource(mod, t) 1934 } 1935 } 1936 }) 1937 } 1938 1939 var requirementRegex = regexp.MustCompile(`^>=([^,]+),<[^,]+$`) 1940 var pep440AlphaRegex = regexp.MustCompile(`^(\d+\.\d+\.\d)+a(\d+)$`) 1941 var pep440BetaRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)b(\d+)$`) 1942 var pep440RCRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`) 1943 var pep440DevRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)\.dev(\d+)$`) 1944 1945 var oldestAllowedPulumi = semver.Version{ 1946 Major: 0, 1947 Minor: 17, 1948 Patch: 28, 1949 } 1950 1951 func sanitizePackageDescription(description string) string { 1952 lines := strings.SplitN(description, "\n", 2) 1953 if len(lines) > 0 { 1954 return lines[0] 1955 } 1956 return "" 1957 } 1958 1959 func genPulumiPluginFile(pkg *schema.Package) ([]byte, error) { 1960 plugin := &plugin.PulumiPluginJSON{ 1961 Resource: true, 1962 Name: pkg.Name, 1963 Server: pkg.PluginDownloadURL, 1964 } 1965 1966 if info, ok := pkg.Language["python"].(PackageInfo); pkg.Version != nil && ok && info.RespectSchemaVersion { 1967 plugin.Version = pkg.Version.String() 1968 } 1969 1970 return plugin.JSON() 1971 } 1972 1973 // genPackageMetadata generates all the non-code metadata required by a Pulumi package. 1974 func genPackageMetadata( 1975 tool string, pkg *schema.Package, pyPkgName string, requires map[string]string, pythonRequires string) (string, error) { 1976 1977 w := &bytes.Buffer{} 1978 (&modContext{tool: tool}).genHeader(w, false /*needsSDK*/, nil) 1979 1980 // Now create a standard Python package from the metadata. 1981 fmt.Fprintf(w, "import errno\n") 1982 fmt.Fprintf(w, "from setuptools import setup, find_packages\n") 1983 fmt.Fprintf(w, "from setuptools.command.install import install\n") 1984 fmt.Fprintf(w, "from subprocess import check_call\n") 1985 fmt.Fprintf(w, "\n\n") 1986 1987 // Create a constant for the version number to replace during build 1988 version := "0.0.0" 1989 pluginVersion := version 1990 info, ok := pkg.Language["python"].(PackageInfo) 1991 if pkg.Version != nil && ok && info.RespectSchemaVersion { 1992 version = pypiVersion(*pkg.Version) 1993 pluginVersion = pkg.Version.String() 1994 } 1995 fmt.Fprintf(w, "VERSION = \"%s\"\n", version) 1996 fmt.Fprintf(w, "PLUGIN_VERSION = \"%s\"\n\n", pluginVersion) 1997 1998 // Create a command that will install the Pulumi plugin for this resource provider. 1999 fmt.Fprintf(w, "class InstallPluginCommand(install):\n") 2000 fmt.Fprintf(w, " def run(self):\n") 2001 fmt.Fprintf(w, " install.run(self)\n") 2002 fmt.Fprintf(w, " try:\n") 2003 if pkg.PluginDownloadURL == "" { 2004 fmt.Fprintf(w, " check_call(['pulumi', 'plugin', 'install', 'resource', '%s', PLUGIN_VERSION])\n", pkg.Name) 2005 } else { 2006 fmt.Fprintf(w, " check_call(['pulumi', 'plugin', 'install', 'resource', '%s', PLUGIN_VERSION, '--server', '%s'])\n", pkg.Name, pkg.PluginDownloadURL) 2007 } 2008 fmt.Fprintf(w, " except OSError as error:\n") 2009 fmt.Fprintf(w, " if error.errno == errno.ENOENT:\n") 2010 fmt.Fprintf(w, " print(f\"\"\"\n") 2011 fmt.Fprintf(w, " There was an error installing the %s resource provider plugin.\n", pkg.Name) 2012 fmt.Fprintf(w, " It looks like `pulumi` is not installed on your system.\n") 2013 fmt.Fprintf(w, " Please visit https://pulumi.com/ to install the Pulumi CLI.\n") 2014 fmt.Fprintf(w, " You may try manually installing the plugin by running\n") 2015 fmt.Fprintf(w, " `pulumi plugin install resource %s {PLUGIN_VERSION}`\n", pkg.Name) 2016 fmt.Fprintf(w, " \"\"\")\n") 2017 fmt.Fprintf(w, " else:\n") 2018 fmt.Fprintf(w, " raise\n") 2019 fmt.Fprintf(w, "\n\n") 2020 2021 // Generate a readme method which will load README.rst, we use this to fill out the 2022 // long_description field in the setup call. 2023 fmt.Fprintf(w, "def readme():\n") 2024 fmt.Fprintf(w, " try:\n") 2025 fmt.Fprintf(w, " with open('README.md', encoding='utf-8') as f:\n") 2026 fmt.Fprintf(w, " return f.read()\n") 2027 fmt.Fprintf(w, " except FileNotFoundError:\n") 2028 fmt.Fprintf(w, " return \"%s Pulumi Package - Development Version\"\n", pkg.Name) 2029 fmt.Fprintf(w, "\n\n") 2030 2031 // Finally, the actual setup part. 2032 fmt.Fprintf(w, "setup(name='%s',\n", pyPkgName) 2033 if pythonRequires != "" { 2034 fmt.Fprintf(w, " python_requires='%s',\n", pythonRequires) 2035 } 2036 fmt.Fprintf(w, " version=VERSION,\n") 2037 if pkg.Description != "" { 2038 fmt.Fprintf(w, " description=%q,\n", sanitizePackageDescription(pkg.Description)) 2039 } 2040 fmt.Fprintf(w, " long_description=readme(),\n") 2041 fmt.Fprintf(w, " long_description_content_type='text/markdown',\n") 2042 fmt.Fprintf(w, " cmdclass={\n") 2043 fmt.Fprintf(w, " 'install': InstallPluginCommand,\n") 2044 fmt.Fprintf(w, " },\n") 2045 if pkg.Keywords != nil { 2046 fmt.Fprintf(w, " keywords='") 2047 for i, kw := range pkg.Keywords { 2048 if i > 0 { 2049 fmt.Fprint(w, " ") 2050 } 2051 fmt.Fprint(w, kw) 2052 } 2053 fmt.Fprintf(w, "',\n") 2054 } 2055 if pkg.Homepage != "" { 2056 fmt.Fprintf(w, " url='%s',\n", pkg.Homepage) 2057 } 2058 if pkg.Repository != "" { 2059 fmt.Fprintf(w, " project_urls={\n") 2060 fmt.Fprintf(w, " 'Repository': '%s'\n", pkg.Repository) 2061 fmt.Fprintf(w, " },\n") 2062 } 2063 if pkg.License != "" { 2064 fmt.Fprintf(w, " license='%s',\n", pkg.License) 2065 } 2066 fmt.Fprintf(w, " packages=find_packages(),\n") 2067 2068 // Publish type metadata: PEP 561 2069 fmt.Fprintf(w, " package_data={\n") 2070 fmt.Fprintf(w, " '%s': [\n", pyPkgName) 2071 fmt.Fprintf(w, " 'py.typed',\n") 2072 fmt.Fprintf(w, " 'pulumi-plugin.json',\n") 2073 2074 fmt.Fprintf(w, " ]\n") 2075 fmt.Fprintf(w, " },\n") 2076 2077 // Ensure that the Pulumi SDK has an entry if not specified. If the SDK _is_ specified, ensure 2078 // that it specifies an acceptable version range. 2079 if pulumiReq, ok := requires["pulumi"]; ok { 2080 // We expect a specific pattern of ">=version,<version" here. 2081 matches := requirementRegex.FindStringSubmatch(pulumiReq) 2082 if len(matches) != 2 { 2083 return "", fmt.Errorf("invalid requirement specifier \"%s\"; expected \">=version1,<version2\"", pulumiReq) 2084 } 2085 2086 lowerBound, err := pep440VersionToSemver(matches[1]) 2087 if err != nil { 2088 return "", fmt.Errorf("invalid version for lower bound: %v", err) 2089 } 2090 if lowerBound.LT(oldestAllowedPulumi) { 2091 return "", fmt.Errorf("lower version bound must be at least %v", oldestAllowedPulumi) 2092 } 2093 } else { 2094 if requires == nil { 2095 requires = map[string]string{} 2096 } 2097 requires["pulumi"] = "" 2098 } 2099 2100 // Sort the entries so they are deterministic. 2101 reqNames := []string{ 2102 "semver>=2.8.1", 2103 "parver>=0.2.1", 2104 } 2105 for req := range requires { 2106 reqNames = append(reqNames, req) 2107 } 2108 sort.Strings(reqNames) 2109 2110 fmt.Fprintf(w, " install_requires=[\n") 2111 for i, req := range reqNames { 2112 var comma string 2113 if i < len(reqNames)-1 { 2114 comma = "," 2115 } 2116 fmt.Fprintf(w, " '%s%s'%s\n", req, requires[req], comma) 2117 } 2118 fmt.Fprintf(w, " ],\n") 2119 2120 fmt.Fprintf(w, " zip_safe=False)\n") 2121 return w.String(), nil 2122 } 2123 2124 func pep440VersionToSemver(v string) (semver.Version, error) { 2125 switch { 2126 case pep440AlphaRegex.MatchString(v): 2127 parts := pep440AlphaRegex.FindStringSubmatch(v) 2128 v = parts[1] + "-alpha." + parts[2] 2129 case pep440BetaRegex.MatchString(v): 2130 parts := pep440BetaRegex.FindStringSubmatch(v) 2131 v = parts[1] + "-beta." + parts[2] 2132 case pep440RCRegex.MatchString(v): 2133 parts := pep440RCRegex.FindStringSubmatch(v) 2134 v = parts[1] + "-rc." + parts[2] 2135 case pep440DevRegex.MatchString(v): 2136 parts := pep440DevRegex.FindStringSubmatch(v) 2137 v = parts[1] + "-dev." + parts[2] 2138 } 2139 2140 return semver.ParseTolerant(v) 2141 } 2142 2143 // genInitDocstring emits the docstring for the __init__ method of the given resource type. 2144 // 2145 // Sphinx (the documentation generator that we use to generate Python docs) does not draw a 2146 // distinction between documentation comments on the class itself and documentation comments on the 2147 // __init__ method of a class. The docs repo instructs Sphinx to concatenate the two together, which 2148 // means that we don't need to emit docstrings on the class at all as long as the __init__ docstring 2149 // is good enough. 2150 // 2151 // The docstring we generate here describes both the class itself and the arguments to the class's 2152 // constructor. The format of the docstring is in "Sphinx form": 2153 // 1. Parameters are introduced using the syntax ":param <type> <name>: <comment>". Sphinx parses this and uses it 2154 // to populate the list of parameters for this function. 2155 // 2. The doc string of parameters is expected to be indented to the same indentation as the type of the parameter. 2156 // Sphinx will complain and make mistakes if this is not the case. 2157 // 3. The doc string can't have random newlines in it, or Sphinx will complain. 2158 // 2159 // This function does the best it can to navigate these constraints and produce a docstring that 2160 // Sphinx can make sense of. 2161 func (mod *modContext) genInitDocstring(w io.Writer, res *schema.Resource, resourceArgsName string, argOverload bool) { 2162 // b contains the full text of the docstring, without the leading and trailing triple quotes. 2163 b := &bytes.Buffer{} 2164 2165 // If this resource has documentation, write it at the top of the docstring, otherwise use a generic comment. 2166 if res.Comment != "" { 2167 fmt.Fprintln(b, codegen.FilterExamples(res.Comment, "python")) 2168 } else { 2169 fmt.Fprintf(b, "Create a %s resource with the given unique name, props, and options.\n", tokenToName(res.Token)) 2170 } 2171 2172 // All resources have a resource_name parameter and opts parameter. 2173 fmt.Fprintln(b, ":param str resource_name: The name of the resource.") 2174 if argOverload { 2175 fmt.Fprintf(b, ":param %s args: The arguments to use to populate this resource's properties.\n", 2176 resourceArgsName) 2177 } 2178 fmt.Fprintln(b, ":param pulumi.ResourceOptions opts: Options for the resource.") 2179 if !argOverload { 2180 for _, prop := range res.InputProperties { 2181 mod.genPropDocstring(b, InitParamName(prop.Name), prop, true /*acceptMapping*/) 2182 } 2183 } 2184 2185 // printComment handles the prefix and triple quotes. 2186 printComment(w, b.String(), " ") 2187 } 2188 2189 func (mod *modContext) genGetDocstring(w io.Writer, res *schema.Resource) { 2190 // "buf" contains the full text of the docstring, without the leading and trailing triple quotes. 2191 b := &bytes.Buffer{} 2192 2193 fmt.Fprintf(b, "Get an existing %s resource's state with the given name, id, and optional extra\n"+ 2194 "properties used to qualify the lookup.\n", tokenToName(res.Token)) 2195 fmt.Fprintln(b, "") 2196 2197 fmt.Fprintln(b, ":param str resource_name: The unique name of the resulting resource.") 2198 fmt.Fprintln(b, ":param pulumi.Input[str] id: The unique provider ID of the resource to lookup.") 2199 fmt.Fprintln(b, ":param pulumi.ResourceOptions opts: Options for the resource.") 2200 if res.StateInputs != nil { 2201 for _, prop := range res.StateInputs.Properties { 2202 mod.genPropDocstring(b, InitParamName(prop.Name), prop, true /*acceptMapping*/) 2203 } 2204 } 2205 2206 // printComment handles the prefix and triple quotes. 2207 printComment(w, b.String(), " ") 2208 } 2209 2210 func (mod *modContext) genTypeDocstring(w io.Writer, comment string, properties []*schema.Property) { 2211 // b contains the full text of the docstring, without the leading and trailing triple quotes. 2212 b := &bytes.Buffer{} 2213 2214 // If this type has documentation, write it at the top of the docstring. 2215 if comment != "" { 2216 fmt.Fprintln(b, comment) 2217 } 2218 2219 for _, prop := range properties { 2220 mod.genPropDocstring(b, PyName(prop.Name), prop, false /*acceptMapping*/) 2221 } 2222 2223 // printComment handles the prefix and triple quotes. 2224 printComment(w, b.String(), " ") 2225 } 2226 2227 func (mod *modContext) genPropDocstring(w io.Writer, name string, prop *schema.Property, acceptMapping bool) { 2228 if prop.Comment == "" { 2229 return 2230 } 2231 2232 ty := mod.typeString(codegen.RequiredType(prop), true, acceptMapping) 2233 2234 // If this property has some documentation associated with it, we need to split it so that it is indented 2235 // in a way that Sphinx can understand. 2236 lines := strings.Split(prop.Comment, "\n") 2237 for len(lines) > 0 && lines[len(lines)-1] == "" { 2238 lines = lines[:len(lines)-1] 2239 } 2240 for i, docLine := range lines { 2241 // If it's the first line, print the :param header. 2242 if i == 0 { 2243 fmt.Fprintf(w, ":param %s %s: %s\n", ty, name, docLine) 2244 } else { 2245 // Otherwise, print out enough padding to align with the first char of the type. 2246 fmt.Fprintf(w, " %s\n", docLine) 2247 } 2248 } 2249 } 2250 2251 func (mod *modContext) typeString(t schema.Type, input, acceptMapping bool) string { 2252 switch t := t.(type) { 2253 case *schema.OptionalType: 2254 return fmt.Sprintf("Optional[%s]", mod.typeString(t.ElementType, input, acceptMapping)) 2255 case *schema.InputType: 2256 typ := mod.typeString(codegen.SimplifyInputUnion(t.ElementType), input, acceptMapping) 2257 if typ == "Any" { 2258 return typ 2259 } 2260 return fmt.Sprintf("pulumi.Input[%s]", typ) 2261 case *schema.EnumType: 2262 return mod.tokenToEnum(t.Token) 2263 case *schema.ArrayType: 2264 return fmt.Sprintf("Sequence[%s]", mod.typeString(t.ElementType, input, acceptMapping)) 2265 case *schema.MapType: 2266 return fmt.Sprintf("Mapping[str, %s]", mod.typeString(t.ElementType, input, acceptMapping)) 2267 case *schema.ObjectType: 2268 typ := mod.objectType(t, input) 2269 if !acceptMapping { 2270 return typ 2271 } 2272 return fmt.Sprintf("pulumi.InputType[%s]", typ) 2273 case *schema.ResourceType: 2274 return fmt.Sprintf("'%s'", mod.resourceType(t)) 2275 case *schema.TokenType: 2276 // Use the underlying type for now. 2277 if t.UnderlyingType != nil { 2278 return mod.typeString(t.UnderlyingType, input, acceptMapping) 2279 } 2280 return "Any" 2281 case *schema.UnionType: 2282 if !input { 2283 for _, e := range t.ElementTypes { 2284 // If this is an output and a "relaxed" enum, emit the type as the underlying primitive type rather than the union. 2285 // Eg. Output[str] rather than Output[Any] 2286 if typ, ok := e.(*schema.EnumType); ok { 2287 return mod.typeString(typ.ElementType, input, acceptMapping) 2288 } 2289 } 2290 if t.DefaultType != nil { 2291 return mod.typeString(t.DefaultType, input, acceptMapping) 2292 } 2293 return "Any" 2294 } 2295 2296 elementTypeSet := codegen.NewStringSet() 2297 elements := make([]string, 0, len(t.ElementTypes)) 2298 for _, e := range t.ElementTypes { 2299 et := mod.typeString(e, input, acceptMapping) 2300 if !elementTypeSet.Has(et) { 2301 elementTypeSet.Add(et) 2302 elements = append(elements, et) 2303 } 2304 } 2305 2306 if len(elements) == 1 { 2307 return elements[0] 2308 } 2309 return fmt.Sprintf("Union[%s]", strings.Join(elements, ", ")) 2310 default: 2311 switch t { 2312 case schema.BoolType: 2313 return "bool" 2314 case schema.IntType: 2315 return "int" 2316 case schema.NumberType: 2317 return "float" 2318 case schema.StringType: 2319 return "str" 2320 case schema.ArchiveType: 2321 return "pulumi.Archive" 2322 case schema.AssetType: 2323 return "Union[pulumi.Asset, pulumi.Archive]" 2324 case schema.JSONType: 2325 fallthrough 2326 case schema.AnyType: 2327 return "Any" 2328 } 2329 } 2330 2331 panic(fmt.Errorf("unexpected type %T", t)) 2332 } 2333 2334 // pyType returns the expected runtime type for the given variable. Of course, being a dynamic language, this 2335 // check is not exhaustive, but it should be good enough to catch 80% of the cases early on. 2336 func (mod *modContext) pyType(typ schema.Type) string { 2337 switch typ := typ.(type) { 2338 case *schema.OptionalType: 2339 return mod.pyType(typ.ElementType) 2340 case *schema.EnumType: 2341 return mod.pyType(typ.ElementType) 2342 case *schema.ArrayType: 2343 return "list" 2344 case *schema.MapType, *schema.ObjectType, *schema.UnionType: 2345 return "dict" 2346 case *schema.ResourceType: 2347 return mod.resourceType(typ) 2348 case *schema.TokenType: 2349 if typ.UnderlyingType != nil { 2350 return mod.pyType(typ.UnderlyingType) 2351 } 2352 return "dict" 2353 default: 2354 switch typ { 2355 case schema.BoolType: 2356 return "bool" 2357 case schema.IntType: 2358 return "int" 2359 case schema.NumberType: 2360 return "float" 2361 case schema.StringType: 2362 return "str" 2363 case schema.ArchiveType: 2364 return "pulumi.Archive" 2365 case schema.AssetType: 2366 return "Union[pulumi.Asset, pulumi.Archive]" 2367 default: 2368 return "dict" 2369 } 2370 } 2371 } 2372 2373 func isStringType(t schema.Type) bool { 2374 t = codegen.UnwrapType(t) 2375 2376 for tt, ok := t.(*schema.TokenType); ok; tt, ok = t.(*schema.TokenType) { 2377 t = tt.UnderlyingType 2378 } 2379 2380 return t == schema.StringType 2381 } 2382 2383 // pyPack returns the suggested package name for the given string. 2384 func pyPack(s string) string { 2385 return "pulumi_" + strings.ReplaceAll(s, "-", "_") 2386 } 2387 2388 // pyClassName turns a raw name into one that is suitable as a Python class name. 2389 func pyClassName(name string) string { 2390 return EnsureKeywordSafe(name) 2391 } 2392 2393 // InitParamName returns a PyName-encoded name but also deduplicates the name against built-in parameters of resource __init__. 2394 func InitParamName(name string) string { 2395 result := PyName(name) 2396 switch result { 2397 case "resource_name", "opts": 2398 return result + "_" 2399 default: 2400 return result 2401 } 2402 } 2403 2404 func (mod *modContext) genObjectType(w io.Writer, obj *schema.ObjectType, input bool) error { 2405 name := mod.unqualifiedObjectTypeName(obj, input) 2406 resourceOutputType := !input && mod.details(obj).resourceOutputType 2407 return mod.genType(w, name, obj.Comment, obj.Properties, input, resourceOutputType) 2408 } 2409 2410 func (mod *modContext) genType(w io.Writer, name, comment string, properties []*schema.Property, input, resourceOutput bool) error { 2411 // Sort required props first. 2412 props := make([]*schema.Property, len(properties)) 2413 copy(props, properties) 2414 sort.Slice(props, func(i, j int) bool { 2415 pi, pj := props[i], props[j] 2416 switch { 2417 case pi.IsRequired() != pj.IsRequired(): 2418 return pi.IsRequired() && !pj.IsRequired() 2419 default: 2420 return pi.Name < pj.Name 2421 } 2422 }) 2423 2424 decorator := "@pulumi.output_type" 2425 if input { 2426 decorator = "@pulumi.input_type" 2427 } 2428 2429 var suffix string 2430 if !input { 2431 suffix = "(dict)" 2432 } 2433 2434 name = pythonCase(name) 2435 fmt.Fprintf(w, "%s\n", decorator) 2436 fmt.Fprintf(w, "class %s%s:\n", name, suffix) 2437 if !input && comment != "" { 2438 printComment(w, comment, " ") 2439 } 2440 2441 // To help users migrate to using the properly snake_cased property getters, emit warnings when camelCase keys are 2442 // accessed. We emit this at the top of the class in case we have a `get` property that will be redefined later. 2443 if resourceOutput { 2444 var needsCaseWarning bool 2445 for _, prop := range props { 2446 pname := PyName(prop.Name) 2447 if pname != prop.Name { 2448 needsCaseWarning = true 2449 break 2450 } 2451 } 2452 if needsCaseWarning { 2453 fmt.Fprintf(w, " @staticmethod\n") 2454 fmt.Fprintf(w, " def __key_warning(key: str):\n") 2455 fmt.Fprintf(w, " suggest = None\n") 2456 prefix := "if" 2457 for _, prop := range props { 2458 pname := PyName(prop.Name) 2459 if pname == prop.Name { 2460 continue 2461 } 2462 fmt.Fprintf(w, " %s key == %q:\n", prefix, prop.Name) 2463 fmt.Fprintf(w, " suggest = %q\n", pname) 2464 prefix = "elif" 2465 } 2466 fmt.Fprintf(w, "\n") 2467 fmt.Fprintf(w, " if suggest:\n") 2468 fmt.Fprintf(w, " pulumi.log.warn(f\"Key '{key}' not found in %s. Access the value via the '{suggest}' property getter instead.\")\n", name) 2469 fmt.Fprintf(w, "\n") 2470 fmt.Fprintf(w, " def __getitem__(self, key: str) -> Any:\n") 2471 fmt.Fprintf(w, " %s.__key_warning(key)\n", name) 2472 fmt.Fprintf(w, " return super().__getitem__(key)\n") 2473 fmt.Fprintf(w, "\n") 2474 fmt.Fprintf(w, " def get(self, key: str, default = None) -> Any:\n") 2475 fmt.Fprintf(w, " %s.__key_warning(key)\n", name) 2476 fmt.Fprintf(w, " return super().get(key, default)\n") 2477 fmt.Fprintf(w, "\n") 2478 } 2479 } 2480 2481 // Generate an __init__ method. 2482 fmt.Fprintf(w, " def __init__(__self__") 2483 // Bare `*` argument to force callers to use named arguments. 2484 if len(props) > 0 { 2485 fmt.Fprintf(w, ", *") 2486 } 2487 for _, prop := range props { 2488 pname := PyName(prop.Name) 2489 ty := mod.typeString(prop.Type, input, false /*acceptMapping*/) 2490 var defaultValue string 2491 if !prop.IsRequired() { 2492 defaultValue = " = None" 2493 } 2494 fmt.Fprintf(w, ",\n %s: %s%s", pname, ty, defaultValue) 2495 } 2496 fmt.Fprintf(w, "):\n") 2497 mod.genTypeDocstring(w, comment, props) 2498 if len(props) == 0 { 2499 fmt.Fprintf(w, " pass\n") 2500 } 2501 for _, prop := range props { 2502 pname := PyName(prop.Name) 2503 var arg interface{} 2504 var err error 2505 2506 // Fill in computed defaults for arguments. 2507 if prop.DefaultValue != nil { 2508 dv, err := getDefaultValue(prop.DefaultValue, codegen.UnwrapType(prop.Type)) 2509 if err != nil { 2510 return err 2511 } 2512 fmt.Fprintf(w, " if %s is None:\n", pname) 2513 fmt.Fprintf(w, " %s = %s\n", pname, dv) 2514 } 2515 2516 // Check that the property isn't deprecated. 2517 if input && prop.DeprecationMessage != "" { 2518 escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`) 2519 fmt.Fprintf(w, " if %s is not None:\n", pname) 2520 fmt.Fprintf(w, " warnings.warn(\"\"\"%s\"\"\", DeprecationWarning)\n", escaped) 2521 fmt.Fprintf(w, " pulumi.log.warn(\"\"\"%s is deprecated: %s\"\"\")\n", pname, escaped) 2522 } 2523 2524 // And add it to the dictionary. 2525 arg = pname 2526 2527 if prop.ConstValue != nil { 2528 arg, err = getConstValue(prop.ConstValue) 2529 if err != nil { 2530 return err 2531 } 2532 } 2533 2534 var indent string 2535 if !prop.IsRequired() { 2536 fmt.Fprintf(w, " if %s is not None:\n", pname) 2537 indent = " " 2538 } 2539 2540 fmt.Fprintf(w, "%s pulumi.set(__self__, \"%s\", %s)\n", indent, pname, arg) 2541 } 2542 fmt.Fprintf(w, "\n") 2543 2544 // Generate properties. Input types have getters and setters, output types only have getters. 2545 mod.genProperties(w, props, input /*setters*/, "", func(prop *schema.Property) string { 2546 return mod.typeString(prop.Type, input, false /*acceptMapping*/) 2547 }) 2548 2549 fmt.Fprintf(w, "\n") 2550 return nil 2551 } 2552 2553 func getPrimitiveValue(value interface{}) (string, error) { 2554 v := reflect.ValueOf(value) 2555 if v.Kind() == reflect.Interface { 2556 v = v.Elem() 2557 } 2558 2559 switch v.Kind() { 2560 case reflect.Bool: 2561 if v.Bool() { 2562 return "True", nil 2563 } 2564 return "False", nil 2565 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: 2566 return strconv.FormatInt(v.Int(), 10), nil 2567 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32: 2568 return strconv.FormatUint(v.Uint(), 10), nil 2569 case reflect.Float32, reflect.Float64: 2570 return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil 2571 case reflect.String: 2572 return fmt.Sprintf("'%s'", v.String()), nil 2573 default: 2574 return "", fmt.Errorf("unsupported default value of type %T", value) 2575 } 2576 } 2577 2578 func getConstValue(cv interface{}) (string, error) { 2579 if cv == nil { 2580 return "", nil 2581 } 2582 return getPrimitiveValue(cv) 2583 } 2584 2585 func getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) { 2586 defaultValue := "" 2587 if dv.Value != nil { 2588 v, err := getPrimitiveValue(dv.Value) 2589 if err != nil { 2590 return "", err 2591 } 2592 defaultValue = v 2593 } 2594 2595 if len(dv.Environment) > 0 { 2596 envFunc := "_utilities.get_env" 2597 switch t { 2598 case schema.BoolType: 2599 envFunc = "_utilities.get_env_bool" 2600 case schema.IntType: 2601 envFunc = "_utilities.get_env_int" 2602 case schema.NumberType: 2603 envFunc = "_utilities.get_env_float" 2604 } 2605 2606 envVars := fmt.Sprintf("'%s'", dv.Environment[0]) 2607 for _, e := range dv.Environment[1:] { 2608 envVars += fmt.Sprintf(", '%s'", e) 2609 } 2610 if defaultValue == "" { 2611 defaultValue = fmt.Sprintf("%s(%s)", envFunc, envVars) 2612 } else { 2613 defaultValue = fmt.Sprintf("(%s(%s) or %s)", envFunc, envVars, defaultValue) 2614 } 2615 } 2616 2617 return defaultValue, nil 2618 } 2619 2620 func generateModuleContextMap(tool string, pkg *schema.Package, info PackageInfo, extraFiles map[string][]byte) (map[string]*modContext, error) { 2621 // determine whether to use the default Python package name 2622 pyPkgName := info.PackageName 2623 if pyPkgName == "" { 2624 pyPkgName = fmt.Sprintf("pulumi_%s", strings.ReplaceAll(pkg.Name, "-", "_")) 2625 } 2626 2627 // group resources, types, and functions into modules 2628 // modules map will contain modContext entries for all modules in current package (pkg) 2629 modules := map[string]*modContext{} 2630 2631 var getMod func(modName string, p *schema.Package) *modContext 2632 getMod = func(modName string, p *schema.Package) *modContext { 2633 mod, ok := modules[modName] 2634 if !ok { 2635 mod = &modContext{ 2636 pkg: p, 2637 pyPkgName: pyPkgName, 2638 mod: modName, 2639 tool: tool, 2640 modNameOverrides: info.ModuleNameOverrides, 2641 compatibility: info.Compatibility, 2642 liftSingleValueMethodReturns: info.LiftSingleValueMethodReturns, 2643 } 2644 2645 if modName != "" && p == pkg { 2646 parentName := path.Dir(modName) 2647 if parentName == "." { 2648 parentName = "" 2649 } 2650 parent := getMod(parentName, p) 2651 parent.addChild(mod) 2652 } 2653 2654 // Save the module only if it's for the current package. 2655 // This way, modules for external packages are not saved. 2656 if p == pkg { 2657 modules[modName] = mod 2658 } 2659 } 2660 return mod 2661 } 2662 2663 getModFromToken := func(tok string, p *schema.Package) *modContext { 2664 modName := tokenToModule(tok, p, info.ModuleNameOverrides) 2665 return getMod(modName, p) 2666 } 2667 2668 // Create the config module if necessary. 2669 if len(pkg.Config) > 0 && 2670 info.Compatibility != kubernetes20 { // k8s SDK doesn't use config. 2671 configMod := getMod("config", pkg) 2672 configMod.isConfig = true 2673 } 2674 2675 visitObjectTypes(pkg.Config, func(t schema.Type) { 2676 if t, ok := t.(*schema.ObjectType); ok { 2677 getModFromToken(t.Token, t.Package).details(t).outputType = true 2678 } 2679 }) 2680 2681 // Find input and output types referenced by resources. 2682 scanResource := func(r *schema.Resource) { 2683 mod := getModFromToken(r.Token, pkg) 2684 mod.resources = append(mod.resources, r) 2685 visitObjectTypes(r.Properties, func(t schema.Type) { 2686 switch T := t.(type) { 2687 case *schema.ObjectType: 2688 getModFromToken(T.Token, T.Package).details(T).outputType = true 2689 getModFromToken(T.Token, T.Package).details(T).resourceOutputType = true 2690 } 2691 }) 2692 visitObjectTypes(r.InputProperties, func(t schema.Type) { 2693 switch T := t.(type) { 2694 case *schema.ObjectType: 2695 getModFromToken(T.Token, T.Package).details(T).inputType = true 2696 } 2697 }) 2698 if r.StateInputs != nil { 2699 visitObjectTypes(r.StateInputs.Properties, func(t schema.Type) { 2700 switch T := t.(type) { 2701 case *schema.ObjectType: 2702 getModFromToken(T.Token, T.Package).details(T).inputType = true 2703 case *schema.ResourceType: 2704 getModFromToken(T.Token, T.Resource.Package) 2705 } 2706 }) 2707 } 2708 } 2709 2710 scanResource(pkg.Provider) 2711 for _, r := range pkg.Resources { 2712 scanResource(r) 2713 } 2714 2715 // Find input and output types referenced by functions. 2716 for _, f := range pkg.Functions { 2717 mod := getModFromToken(f.Token, f.Package) 2718 if !f.IsMethod { 2719 mod.functions = append(mod.functions, f) 2720 } 2721 if f.Inputs != nil { 2722 visitObjectTypes(f.Inputs.Properties, func(t schema.Type) { 2723 switch T := t.(type) { 2724 case *schema.ObjectType: 2725 getModFromToken(T.Token, T.Package).details(T).inputType = true 2726 getModFromToken(T.Token, T.Package).details(T).plainType = true 2727 case *schema.ResourceType: 2728 getModFromToken(T.Token, T.Resource.Package) 2729 } 2730 }) 2731 } 2732 if f.Outputs != nil { 2733 visitObjectTypes(f.Outputs.Properties, func(t schema.Type) { 2734 switch T := t.(type) { 2735 case *schema.ObjectType: 2736 getModFromToken(T.Token, T.Package).details(T).outputType = true 2737 getModFromToken(T.Token, T.Package).details(T).plainType = true 2738 case *schema.ResourceType: 2739 getModFromToken(T.Token, T.Resource.Package) 2740 } 2741 }) 2742 } 2743 } 2744 2745 // Find nested types. 2746 for _, t := range pkg.Types { 2747 switch typ := t.(type) { 2748 case *schema.ObjectType: 2749 mod := getModFromToken(typ.Token, typ.Package) 2750 d := mod.details(typ) 2751 if d.inputType || d.outputType { 2752 mod.types = append(mod.types, typ) 2753 } 2754 case *schema.EnumType: 2755 if !typ.IsOverlay { 2756 mod := getModFromToken(typ.Token, pkg) 2757 mod.enums = append(mod.enums, typ) 2758 } 2759 default: 2760 continue 2761 } 2762 } 2763 2764 // Add python source files to the corresponding modules. Note that we only add the file names; the contents are 2765 // still laid out manually in GeneratePackage. 2766 for p := range extraFiles { 2767 if path.Ext(p) != ".py" { 2768 continue 2769 } 2770 2771 modName := path.Dir(p) 2772 if modName == "/" || modName == "." { 2773 modName = "" 2774 } 2775 mod := getMod(modName, pkg) 2776 mod.extraSourceFiles = append(mod.extraSourceFiles, p) 2777 } 2778 2779 // Setup modLocator so that mod.typeDetails finds the right 2780 // modContext for every ObjectType. 2781 modLocator := &modLocator{ 2782 objectTypeMod: func(t *schema.ObjectType) *modContext { 2783 if t.Package != pkg { 2784 return nil 2785 } 2786 2787 return getModFromToken(t.Token, t.Package) 2788 }, 2789 } 2790 2791 for _, mod := range modules { 2792 mod.modLocator = modLocator 2793 } 2794 2795 return modules, nil 2796 } 2797 2798 // LanguageResource is derived from the schema and can be used by downstream codegen. 2799 type LanguageResource struct { 2800 *schema.Resource 2801 2802 Name string // The resource name (e.g. Deployment) 2803 Package string // The package name (e.g. pulumi_kubernetes.apps.v1) 2804 } 2805 2806 // LanguageResources returns a map of resources that can be used by downstream codegen. The map 2807 // key is the resource schema token. 2808 func LanguageResources(tool string, pkg *schema.Package) (map[string]LanguageResource, error) { 2809 resources := map[string]LanguageResource{} 2810 2811 if err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil { 2812 return nil, err 2813 } 2814 info, _ := pkg.Language["python"].(PackageInfo) 2815 2816 modules, err := generateModuleContextMap(tool, pkg, info, nil) 2817 if err != nil { 2818 return nil, err 2819 } 2820 2821 for modName, mod := range modules { 2822 if modName == "" { 2823 continue 2824 } 2825 for _, r := range mod.resources { 2826 if r.IsOverlay { 2827 // This resource code is generated by the provider, so no further action is required. 2828 continue 2829 } 2830 2831 packagePath := strings.Replace(modName, "/", ".", -1) 2832 lr := LanguageResource{ 2833 Resource: r, 2834 Package: packagePath, 2835 Name: pyClassName(tokenToName(r.Token)), 2836 } 2837 resources[r.Token] = lr 2838 } 2839 } 2840 2841 return resources, nil 2842 } 2843 2844 func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) { 2845 // Decode python-specific info 2846 if err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil { 2847 return nil, err 2848 } 2849 info, _ := pkg.Language["python"].(PackageInfo) 2850 2851 modules, err := generateModuleContextMap(tool, pkg, info, extraFiles) 2852 if err != nil { 2853 return nil, err 2854 } 2855 2856 pkgName := info.PackageName 2857 if pkgName == "" { 2858 pkgName = pyPack(pkg.Name) 2859 } 2860 2861 files := codegen.Fs{} 2862 for p, f := range extraFiles { 2863 files.Add(filepath.Join(pkgName, p), f) 2864 } 2865 2866 for _, mod := range modules { 2867 if err := mod.gen(files); err != nil { 2868 return nil, err 2869 } 2870 } 2871 2872 // Generate pulumi-plugin.json 2873 plugin, err := genPulumiPluginFile(pkg) 2874 if err != nil { 2875 return nil, err 2876 } 2877 files.Add(filepath.Join(pkgName, "pulumi-plugin.json"), plugin) 2878 2879 // Finally emit the package metadata (setup.py). 2880 setup, err := genPackageMetadata(tool, pkg, pkgName, info.Requires, info.PythonRequires) 2881 if err != nil { 2882 return nil, err 2883 } 2884 files.Add("setup.py", []byte(setup)) 2885 2886 return files, nil 2887 } 2888 2889 const utilitiesFile = ` 2890 import importlib.util 2891 import inspect 2892 import json 2893 import os 2894 import pkg_resources 2895 import sys 2896 import typing 2897 2898 import pulumi 2899 import pulumi.runtime 2900 2901 from semver import VersionInfo as SemverVersion 2902 from parver import Version as PEP440Version 2903 2904 2905 def get_env(*args): 2906 for v in args: 2907 value = os.getenv(v) 2908 if value is not None: 2909 return value 2910 return None 2911 2912 2913 def get_env_bool(*args): 2914 str = get_env(*args) 2915 if str is not None: 2916 # NOTE: these values are taken from https://golang.org/src/strconv/atob.go?s=351:391#L1, which is what 2917 # Terraform uses internally when parsing boolean values. 2918 if str in ["1", "t", "T", "true", "TRUE", "True"]: 2919 return True 2920 if str in ["0", "f", "F", "false", "FALSE", "False"]: 2921 return False 2922 return None 2923 2924 2925 def get_env_int(*args): 2926 str = get_env(*args) 2927 if str is not None: 2928 try: 2929 return int(str) 2930 except: 2931 return None 2932 return None 2933 2934 2935 def get_env_float(*args): 2936 str = get_env(*args) 2937 if str is not None: 2938 try: 2939 return float(str) 2940 except: 2941 return None 2942 return None 2943 2944 2945 def _get_semver_version(): 2946 # __name__ is set to the fully-qualified name of the current module, In our case, it will be 2947 # <some module>._utilities. <some module> is the module we want to query the version for. 2948 root_package, *rest = __name__.split('.') 2949 2950 # pkg_resources uses setuptools to inspect the set of installed packages. We use it here to ask 2951 # for the currently installed version of the root package (i.e. us) and get its version. 2952 2953 # Unfortunately, PEP440 and semver differ slightly in incompatible ways. The Pulumi engine expects 2954 # to receive a valid semver string when receiving requests from the language host, so it's our 2955 # responsibility as the library to convert our own PEP440 version into a valid semver string. 2956 2957 pep440_version_string = pkg_resources.require(root_package)[0].version 2958 pep440_version = PEP440Version.parse(pep440_version_string) 2959 (major, minor, patch) = pep440_version.release 2960 prerelease = None 2961 if pep440_version.pre_tag == 'a': 2962 prerelease = f"alpha.{pep440_version.pre}" 2963 elif pep440_version.pre_tag == 'b': 2964 prerelease = f"beta.{pep440_version.pre}" 2965 elif pep440_version.pre_tag == 'rc': 2966 prerelease = f"rc.{pep440_version.pre}" 2967 elif pep440_version.dev is not None: 2968 prerelease = f"dev.{pep440_version.dev}" 2969 2970 # The only significant difference between PEP440 and semver as it pertains to us is that PEP440 has explicit support 2971 # for dev builds, while semver encodes them as "prerelease" versions. In order to bridge between the two, we convert 2972 # our dev build version into a prerelease tag. This matches what all of our other packages do when constructing 2973 # their own semver string. 2974 return SemverVersion(major=major, minor=minor, patch=patch, prerelease=prerelease) 2975 2976 2977 # Determine the version once and cache the value, which measurably improves program performance. 2978 _version = _get_semver_version() 2979 _version_str = str(_version) 2980 2981 2982 def get_version(): 2983 return _version_str 2984 2985 def get_resource_opts_defaults() -> pulumi.ResourceOptions: 2986 return pulumi.ResourceOptions( 2987 version=get_version(), 2988 plugin_download_url=get_plugin_download_url(), 2989 ) 2990 2991 def get_invoke_opts_defaults() -> pulumi.InvokeOptions: 2992 return pulumi.InvokeOptions( 2993 version=get_version(), 2994 plugin_download_url=get_plugin_download_url(), 2995 ) 2996 2997 def get_resource_args_opts(resource_args_type, resource_options_type, *args, **kwargs): 2998 """ 2999 Return the resource args and options given the *args and **kwargs of a resource's 3000 __init__ method. 3001 """ 3002 3003 resource_args, opts = None, None 3004 3005 # If the first item is the resource args type, save it and remove it from the args list. 3006 if args and isinstance(args[0], resource_args_type): 3007 resource_args, args = args[0], args[1:] 3008 3009 # Now look at the first item in the args list again. 3010 # If the first item is the resource options class, save it. 3011 if args and isinstance(args[0], resource_options_type): 3012 opts = args[0] 3013 3014 # If resource_args is None, see if "args" is in kwargs, and, if so, if it's typed as the 3015 # the resource args type. 3016 if resource_args is None: 3017 a = kwargs.get("args") 3018 if isinstance(a, resource_args_type): 3019 resource_args = a 3020 3021 # If opts is None, look it up in kwargs. 3022 if opts is None: 3023 opts = kwargs.get("opts") 3024 3025 return resource_args, opts 3026 3027 3028 # Temporary: just use pulumi._utils.lazy_import once everyone upgrades. 3029 def lazy_import(fullname): 3030 3031 import pulumi._utils as u 3032 f = getattr(u, 'lazy_import', None) 3033 if f is None: 3034 f = _lazy_import_temp 3035 3036 return f(fullname) 3037 3038 3039 # Copied from pulumi._utils.lazy_import, see comments there. 3040 def _lazy_import_temp(fullname): 3041 m = sys.modules.get(fullname, None) 3042 if m is not None: 3043 return m 3044 3045 spec = importlib.util.find_spec(fullname) 3046 3047 m = sys.modules.get(fullname, None) 3048 if m is not None: 3049 return m 3050 3051 loader = importlib.util.LazyLoader(spec.loader) 3052 spec.loader = loader 3053 module = importlib.util.module_from_spec(spec) 3054 3055 m = sys.modules.get(fullname, None) 3056 if m is not None: 3057 return m 3058 3059 sys.modules[fullname] = module 3060 loader.exec_module(module) 3061 return module 3062 3063 3064 class Package(pulumi.runtime.ResourcePackage): 3065 def __init__(self, pkg_info): 3066 super().__init__() 3067 self.pkg_info = pkg_info 3068 3069 def version(self): 3070 return _version 3071 3072 def construct_provider(self, name: str, typ: str, urn: str) -> pulumi.ProviderResource: 3073 if typ != self.pkg_info['token']: 3074 raise Exception(f"unknown provider type {typ}") 3075 Provider = getattr(lazy_import(self.pkg_info['fqn']), self.pkg_info['class']) 3076 return Provider(name, pulumi.ResourceOptions(urn=urn)) 3077 3078 3079 class Module(pulumi.runtime.ResourceModule): 3080 def __init__(self, mod_info): 3081 super().__init__() 3082 self.mod_info = mod_info 3083 3084 def version(self): 3085 return _version 3086 3087 def construct(self, name: str, typ: str, urn: str) -> pulumi.Resource: 3088 class_name = self.mod_info['classes'].get(typ, None) 3089 3090 if class_name is None: 3091 raise Exception(f"unknown resource type {typ}") 3092 3093 TheClass = getattr(lazy_import(self.mod_info['fqn']), class_name) 3094 return TheClass(name, pulumi.ResourceOptions(urn=urn)) 3095 3096 3097 def register(resource_modules, resource_packages): 3098 resource_modules = json.loads(resource_modules) 3099 resource_packages = json.loads(resource_packages) 3100 3101 for pkg_info in resource_packages: 3102 pulumi.runtime.register_resource_package(pkg_info['pkg'], Package(pkg_info)) 3103 3104 for mod_info in resource_modules: 3105 pulumi.runtime.register_resource_module( 3106 mod_info['pkg'], 3107 mod_info['mod'], 3108 Module(mod_info)) 3109 3110 3111 _F = typing.TypeVar('_F', bound=typing.Callable[..., typing.Any]) 3112 3113 3114 def lift_output_func(func: typing.Any) -> typing.Callable[[_F], _F]: 3115 """Decorator internally used on {fn}_output lifted function versions 3116 to implement them automatically from the un-lifted function.""" 3117 3118 func_sig = inspect.signature(func) 3119 3120 def lifted_func(*args, opts=None, **kwargs): 3121 bound_args = func_sig.bind(*args, **kwargs) 3122 # Convert tuple to list, see pulumi/pulumi#8172 3123 args_list = list(bound_args.args) 3124 return pulumi.Output.from_input({ 3125 'args': args_list, 3126 'kwargs': bound_args.kwargs 3127 }).apply(lambda resolved_args: func(*resolved_args['args'], 3128 opts=opts, 3129 **resolved_args['kwargs'])) 3130 3131 return (lambda _: lifted_func) 3132 `