pulumi/pkg/codegen/python/gen_program.go
Justin Van Patten 42a1896bf9
[codegen/python] Delete dead code around casing tables (#7647)
Coincident with the release of Pulumi 3.0, we updated the provider SDK codegen for Python to no longer use casing tables for translating Python snake_case names to Pulumi camelCase names (and vice versa). Instead, the mapping is encoded in decorators applied on class properties.

Some of the code that was used to generate and use the casing tables has persisted. This commits removes this code, as it's no longer necessary, and will improve the quality of our generated examples.
2021-07-27 08:37:49 -07:00

468 lines
13 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package python
import (
"bytes"
"fmt"
"io"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
type generator struct {
// The formatter to use when generating code.
*format.Formatter
program *hcl2.Program
diagnostics hcl.Diagnostics
configCreated bool
quotes map[model.Expression]string
}
func GenerateProgram(program *hcl2.Program) (map[string][]byte, hcl.Diagnostics, error) {
g, err := newGenerator(program)
if err != nil {
return nil, nil, err
}
// Linearize the nodes into an order appropriate for procedural code generation.
nodes := hcl2.Linearize(program)
var main bytes.Buffer
g.genPreamble(&main, program)
for _, n := range nodes {
g.genNode(&main, n)
}
files := map[string][]byte{
"__main__.py": main.Bytes(),
}
return files, g.diagnostics, nil
}
func newGenerator(program *hcl2.Program) (*generator, error) {
// Import Python-specific schema info.
for _, p := range program.Packages() {
if err := p.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil {
return nil, err
}
}
g := &generator{
program: program,
quotes: map[model.Expression]string{},
}
g.Formatter = format.NewFormatter(g)
return g, nil
}
// genLeadingTrivia generates the list of leading trivia associated with a given token.
func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) {
// TODO(pdg): whitespace
for _, t := range token.LeadingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
}
// genTrailingTrivia generates the list of trailing trivia associated with a given token.
func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) {
// TODO(pdg): whitespace
for _, t := range token.TrailingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
}
// genTrivia generates the list of trivia associated with a given token.
func (g *generator) genTrivia(w io.Writer, token syntax.Token) {
g.genLeadingTrivia(w, token)
g.genTrailingTrivia(w, token)
}
// genComment generates a comment into the output.
func (g *generator) genComment(w io.Writer, comment syntax.Comment) {
for _, l := range comment.Lines {
g.Fgenf(w, "%s#%s\n", g.Indent, l)
}
}
func (g *generator) genPreamble(w io.Writer, program *hcl2.Program) {
// Print the pulumi import at the top.
g.Fprintln(w, "import pulumi")
// Accumulate other imports for the various providers. Don't emit them yet, as we need to sort them later on.
importSet := codegen.NewStringSet("pulumi")
for _, n := range program.Nodes {
if r, isResource := n.(*hcl2.Resource); isResource {
pkg, _, _, _ := r.DecomposeToken()
importSet.Add("pulumi_" + makeValidIdentifier(pkg))
}
diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
if call, ok := n.(*model.FunctionCallExpression); ok {
if i := g.getFunctionImports(call); i != "" {
importSet.Add(i)
}
}
return n, nil
})
contract.Assert(len(diags) == 0)
}
var imports []string
for _, pkg := range importSet.SortedValues() {
if pkg == "pulumi" {
continue
}
if strings.HasPrefix(pkg, "pulumi_") {
imports = append(imports, fmt.Sprintf("import %s as %s", pkg, pkg[len("pulumi_"):]))
} else {
imports = append(imports, fmt.Sprintf("import %s", pkg))
}
}
// Now sort the imports and emit them.
sort.Strings(imports)
for _, i := range imports {
g.Fprintln(w, i)
}
g.Fprint(w, "\n")
}
func (g *generator) genNode(w io.Writer, n hcl2.Node) {
switch n := n.(type) {
case *hcl2.Resource:
g.genResource(w, n)
case *hcl2.ConfigVariable:
g.genConfigVariable(w, n)
case *hcl2.LocalVariable:
g.genLocalVariable(w, n)
case *hcl2.OutputVariable:
g.genOutputVariable(w, n)
}
}
// resourceTypeName computes the Python package, module, and type name for the given resource.
func resourceTypeName(r *hcl2.Resource) (string, string, string, hcl.Diagnostics) {
// Compute the resource type from the Pulumi type token.
pkg, module, member, diagnostics := r.DecomposeToken()
// Normalize module.
if r.Schema != nil {
pkg := r.Schema.Package
if lang, ok := pkg.Language["python"]; ok {
pkgInfo := lang.(PackageInfo)
if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
module = m
}
}
}
components := strings.Split(module, "/")
for i, component := range components {
components[i] = PyName(component)
}
return PyName(pkg), strings.Join(components, "."), title(member), diagnostics
}
// argumentTypeName computes the Python argument class name for the given expression and model type.
func (g *generator) argumentTypeName(expr model.Expression, destType model.Type) string {
schemaType, ok := hcl2.GetSchemaForType(destType.(model.Type))
if !ok {
return ""
}
schemaType = codegen.UnwrapType(schemaType)
objType, ok := schemaType.(*schema.ObjectType)
if !ok {
return ""
}
token := objType.Token
tokenRange := expr.SyntaxNode().Range()
// Example: aws, s3/BucketLogging, BucketLogging, []Diagnostics
pkgName, module, member, diagnostics := hcl2.DecomposeToken(token, tokenRange)
contract.Assert(len(diagnostics) == 0)
modName := objType.Package.TokenToModule(token)
// Normalize module.
pkg := objType.Package
if lang, ok := pkg.Language["python"]; ok {
pkgInfo := lang.(PackageInfo)
if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
modName = m
}
}
if modName != "" {
modName = "." + PyName(modName)
}
modName = strings.Replace(modName, "_", ".", -1)
member = member + "Args"
// Example: aws.s3.BucketLoggingArgs
return fmt.Sprintf("%s%s.%s", PyName(pkgName), modName, title(member))
}
// makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name
// and the count variable name, if any.
func (g *generator) makeResourceName(baseName, count string) string {
if count == "" {
return fmt.Sprintf(`"%s"`, baseName)
}
return fmt.Sprintf(`f"%s-{%s}"`, baseName, count)
}
func (g *generator) lowerResourceOptions(opts *hcl2.ResourceOptions) (*model.Block, []*quoteTemp) {
if opts == nil {
return nil, nil
}
var block *model.Block
var temps []*quoteTemp
appendOption := func(name string, value model.Expression) {
if block == nil {
block = &model.Block{
Type: "options",
Body: &model.Body{},
}
}
value, valueTemps := g.lowerExpression(value, value.Type())
temps = append(temps, valueTemps...)
block.Body.Items = append(block.Body.Items, &model.Attribute{
Tokens: syntax.NewAttributeTokens(name),
Name: name,
Value: value,
})
}
if opts.Parent != nil {
appendOption("parent", opts.Parent)
}
if opts.Provider != nil {
appendOption("provider", opts.Provider)
}
if opts.DependsOn != nil {
appendOption("depends_on", opts.DependsOn)
}
if opts.Protect != nil {
appendOption("protect", opts.Protect)
}
if opts.IgnoreChanges != nil {
appendOption("ignore_changes", opts.IgnoreChanges)
}
return block, temps
}
func (g *generator) genResourceOptions(w io.Writer, block *model.Block, hasInputs bool) {
if block == nil {
return
}
prefix := " "
if hasInputs {
prefix = "\n" + g.Indent
}
g.Fprintf(w, ",%sopts=pulumi.ResourceOptions(", prefix)
g.Indented(func() {
for i, item := range block.Body.Items {
if i > 0 {
g.Fprintf(w, ",\n%s", g.Indent)
}
attr := item.(*model.Attribute)
g.Fgenf(w, "%s=%v", attr.Name, attr.Value)
}
})
g.Fprint(w, ")")
}
// genResource handles the generation of instantiations of non-builtin resources.
func (g *generator) genResource(w io.Writer, r *hcl2.Resource) {
pkg, module, memberName, diagnostics := resourceTypeName(r)
g.diagnostics = append(g.diagnostics, diagnostics...)
if module != "" {
module = "." + module
}
qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
optionsBag, temps := g.lowerResourceOptions(r.Options)
name := PyName(r.Name())
g.genTrivia(w, r.Definition.Tokens.GetType(""))
for _, l := range r.Definition.Tokens.Labels {
g.genTrivia(w, l)
}
g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())
for _, input := range r.Inputs {
destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: input.Name})
g.diagnostics = append(g.diagnostics, diagnostics...)
value, valueTemps := g.lowerExpression(input.Value, destType.(model.Type))
temps = append(temps, valueTemps...)
input.Value = value
}
g.genTemps(w, temps)
instantiate := func(resName string) {
g.Fgenf(w, "%s(%s", qualifiedMemberName, resName)
indenter := func(f func()) { f() }
if len(r.Inputs) > 1 {
indenter = g.Indented
}
indenter(func() {
for _, attr := range r.Inputs {
propertyName := PyName(attr.Name)
if len(r.Inputs) == 1 {
g.Fgenf(w, ", %s=%.v", propertyName, attr.Value)
} else {
g.Fgenf(w, ",\n%s%s=%.v", g.Indent, propertyName, attr.Value)
}
}
g.genResourceOptions(w, optionsBag, len(r.Inputs) != 0)
})
g.Fprint(w, ")")
}
if r.Options != nil && r.Options.Range != nil {
rangeExpr := r.Options.Range
if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion {
g.Fgenf(w, "%s%s = None\n", g.Indent, name)
g.Fgenf(w, "%sif %.v:\n", g.Indent, rangeExpr)
g.Indented(func() {
g.Fprintf(w, "%s%s = ", g.Indent, name)
instantiate(g.makeResourceName(r.Name(), ""))
g.Fprint(w, "\n")
})
} else {
g.Fgenf(w, "%s%s = []\n", g.Indent, name)
resKey := "key"
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
g.Fgenf(w, "%sfor range in [{\"value\": i} for i in range(0, %.v)]:\n", g.Indent, rangeExpr)
resKey = "value"
} else {
g.Fgenf(w, "%sfor range in [{\"key\": k, \"value\": v} for [k, v] in enumerate(%.v)]:\n", g.Indent, rangeExpr)
}
resName := g.makeResourceName(r.Name(), fmt.Sprintf("range['%s']", resKey))
g.Indented(func() {
g.Fgenf(w, "%s%s.append(", g.Indent, name)
instantiate(resName)
g.Fprint(w, ")\n")
})
}
} else {
g.Fgenf(w, "%s%s = ", g.Indent, name)
instantiate(g.makeResourceName(r.Name(), ""))
g.Fprint(w, "\n")
}
g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
}
func (g *generator) genTemps(w io.Writer, temps []*quoteTemp) {
for _, t := range temps {
// TODO(pdg): trivia
g.Fgenf(w, "%s%s = %.v\n", g.Indent, t.Name, t.Value)
}
}
func (g *generator) genConfigVariable(w io.Writer, v *hcl2.ConfigVariable) {
// TODO(pdg): trivia
if !g.configCreated {
g.Fprintf(w, "%sconfig = pulumi.Config()\n", g.Indent)
g.configCreated = true
}
getType := "_object"
switch v.Type() {
case model.StringType:
getType = ""
case model.NumberType:
getType = "_float"
case model.IntType:
getType = "_int"
case model.BoolType:
getType = "_bool"
}
getOrRequire := "get"
if v.DefaultValue == nil {
getOrRequire = "require"
}
var defaultValue model.Expression
var temps []*quoteTemp
if v.DefaultValue != nil {
defaultValue, temps = g.lowerExpression(v.DefaultValue, v.DefaultValue.Type())
}
g.genTemps(w, temps)
name := PyName(v.Name())
g.Fgenf(w, "%s%s = config.%s%s(\"%s\")\n", g.Indent, name, getOrRequire, getType, v.Name())
if defaultValue != nil {
g.Fgenf(w, "%sif %s is None:\n", g.Indent, name)
g.Indented(func() {
g.Fgenf(w, "%s%s = %.v\n", g.Indent, name, defaultValue)
})
}
}
func (g *generator) genLocalVariable(w io.Writer, v *hcl2.LocalVariable) {
value, temps := g.lowerExpression(v.Definition.Value, v.Type())
g.genTemps(w, temps)
// TODO(pdg): trivia
g.Fgenf(w, "%s%s = %.v\n", g.Indent, PyName(v.Name()), value)
}
func (g *generator) genOutputVariable(w io.Writer, v *hcl2.OutputVariable) {
value, temps := g.lowerExpression(v.Value, v.Type())
g.genTemps(w, temps)
// TODO(pdg): trivia
g.Fgenf(w, "%spulumi.export(\"%s\", %.v)\n", g.Indent, v.Name(), value)
}
func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
message := fmt.Sprintf("not yet implemented: %s", fmt.Sprintf(reason, vs...))
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: message,
Detail: message,
})
g.Fgenf(w, "(lambda: raise Exception(%q))()", fmt.Sprintf(reason, vs...))
}