github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/python/utilities.go (about)

     1  package python
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"regexp"
     7  	"strings"
     8  	"unicode"
     9  
    10  	"github.com/blang/semver"
    11  	"github.com/pulumi/pulumi/pkg/v3/codegen"
    12  	"github.com/pulumi/pulumi/pkg/v3/codegen/cgstrings"
    13  )
    14  
    15  // isLegalIdentifierStart returns true if it is legal for c to be the first character of a Python identifier as per
    16  // https://docs.python.org/3.7/reference/lexical_analysis.html#identifiers.
    17  func isLegalIdentifierStart(c rune) bool {
    18  	return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' ||
    19  		unicode.In(c, unicode.Lu, unicode.Ll, unicode.Lt, unicode.Lm, unicode.Lo, unicode.Nl)
    20  }
    21  
    22  // isLegalIdentifierPart returns true if it is legal for c to be part of a Python identifier (besides the first
    23  // character) as per https://docs.python.org/3.7/reference/lexical_analysis.html#identifiers.
    24  func isLegalIdentifierPart(c rune) bool {
    25  	return isLegalIdentifierStart(c) || c >= '0' && c <= '9' ||
    26  		unicode.In(c, unicode.Lu, unicode.Ll, unicode.Lt, unicode.Lm, unicode.Lo, unicode.Nl, unicode.Mn, unicode.Mc,
    27  			unicode.Nd, unicode.Pc)
    28  }
    29  
    30  // isLegalIdentifier returns true if s is a legal Python identifier as per
    31  // https://docs.python.org/3.7/reference/lexical_analysis.html#identifiers.
    32  func isLegalIdentifier(s string) bool {
    33  	reader := strings.NewReader(s)
    34  	c, _, _ := reader.ReadRune()
    35  	if !isLegalIdentifierStart(c) {
    36  		return false
    37  	}
    38  	for {
    39  		c, _, err := reader.ReadRune()
    40  		if err != nil {
    41  			return err == io.EOF
    42  		}
    43  		if !isLegalIdentifierPart(c) {
    44  			return false
    45  		}
    46  	}
    47  }
    48  
    49  // makeValidIdentifier replaces characters that are not allowed in Python identifiers with underscores. No attempt is
    50  // made to ensure that the result is unique.
    51  func makeValidIdentifier(name string) string {
    52  	var builder strings.Builder
    53  	for i, c := range name {
    54  		if !isLegalIdentifierPart(c) {
    55  			builder.WriteRune('_')
    56  		} else {
    57  			if i == 0 && !isLegalIdentifierStart(c) {
    58  				builder.WriteRune('_')
    59  			}
    60  			builder.WriteRune(c)
    61  		}
    62  	}
    63  	return builder.String()
    64  }
    65  
    66  func makeSafeEnumName(name, typeName string) (string, error) {
    67  	// Replace common single character enum names.
    68  	safeName := codegen.ExpandShortEnumName(name)
    69  
    70  	// If the name is one illegal character, return an error.
    71  	if len(safeName) == 1 && !isLegalIdentifierStart(rune(safeName[0])) {
    72  		return "", fmt.Errorf("enum name %s is not a valid identifier", safeName)
    73  	}
    74  
    75  	// If it's camelCase, change it to snake_case.
    76  	safeName = PyName(safeName)
    77  
    78  	// Change to uppercase and make a valid identifier.
    79  	safeName = makeValidIdentifier(strings.ToTitle(safeName))
    80  
    81  	// If the enum name starts with an underscore, add the type name as a prefix.
    82  	if strings.HasPrefix(safeName, "_") {
    83  		pyTypeName := strings.ToTitle(PyName(typeName))
    84  		safeName = pyTypeName + safeName
    85  	}
    86  
    87  	// If there are multiple underscores in a row, replace with one.
    88  	regex := regexp.MustCompile(`_+`)
    89  	safeName = regex.ReplaceAllString(safeName, "_")
    90  
    91  	return safeName, nil
    92  }
    93  
    94  var pypiReleaseTranslations = []struct {
    95  	prefix     string
    96  	replacment string
    97  }{
    98  	{"alpha", "a"},
    99  	{"beta", "b"},
   100  }
   101  
   102  // A valid release tag for pypi
   103  var pypiRelease = regexp.MustCompile("^(a|b|rc)[0-9]+$")
   104  
   105  // A valid dev tag for pypi
   106  var pypiDev = regexp.MustCompile("^dev[0-9]+$")
   107  
   108  // A valid post tag for pypi
   109  var pypiPost = regexp.MustCompile("^post[0-9]+$")
   110  
   111  // pypiVersion translates semver 2.0 into pypi's versioning scheme:
   112  // Details can be found here: https://www.python.org/dev/peps/pep-0440/#version-scheme
   113  // [N!]N(.N)*[{a|b|rc}N][.postN][.devN]
   114  func pypiVersion(v semver.Version) string {
   115  	var localList []string
   116  
   117  	getRelease := func(maybeRelease string) string {
   118  		for _, tup := range pypiReleaseTranslations {
   119  			if strings.HasPrefix(maybeRelease, tup.prefix) {
   120  				guess := tup.replacment + maybeRelease[len(tup.prefix):]
   121  				if pypiRelease.MatchString(guess) {
   122  					return guess
   123  				}
   124  			}
   125  		}
   126  		if pypiRelease.MatchString(maybeRelease) {
   127  			return maybeRelease
   128  		}
   129  		return ""
   130  	}
   131  	getDev := func(maybeDev string) string {
   132  		if pypiDev.MatchString(maybeDev) {
   133  			return "." + maybeDev
   134  		}
   135  		return ""
   136  	}
   137  
   138  	getPost := func(maybePost string) string {
   139  		if pypiPost.MatchString(maybePost) {
   140  			return "." + maybePost
   141  		}
   142  		return ""
   143  	}
   144  
   145  	var preListIndex int
   146  
   147  	var release string
   148  	var dev string
   149  	var post string
   150  	// We allow the first pre-release in `v` to indicate the release for the
   151  	// pypi version.
   152  	for _, special := range []struct {
   153  		getFunc  func(string) string
   154  		maybeSet *string
   155  	}{
   156  		{getRelease, &release},
   157  		{getDev, &dev},
   158  		{getPost, &post},
   159  	} {
   160  		if len(v.Pre) > preListIndex && special.getFunc(v.Pre[preListIndex].VersionStr) != "" {
   161  			*special.maybeSet = special.getFunc(v.Pre[preListIndex].VersionStr)
   162  			preListIndex++
   163  		}
   164  	}
   165  
   166  	// All other pre-release segments are added to the local identifier. If we
   167  	// didn't find a release, the first pre-release is also added to the local
   168  	// identifier.
   169  	if release != "" {
   170  		preListIndex = 1
   171  	}
   172  	for ; preListIndex < len(v.Pre); preListIndex++ {
   173  		// This can only contain [0-9a-zA-Z-] because semver enforces that set
   174  		// and '-' we need only replace '-' with a valid character: '.'
   175  		localList = append(localList, strings.ReplaceAll(v.Pre[preListIndex].VersionStr, "-", "."))
   176  	}
   177  	// All build flags are added to the local identifier list
   178  	for _, b := range v.Build {
   179  		// This can only contain [0-9a-zA-Z-] because semver enforces that set
   180  		// and '-' we need only replace '-' with a valid character: '.'
   181  		localList = append(localList, strings.ReplaceAll(b, "-", "."))
   182  	}
   183  	local := ""
   184  	if len(localList) > 0 {
   185  		local = "+" + strings.Join(localList, ".")
   186  	}
   187  	return fmt.Sprintf("%d.%d.%d%s%s%s%s", v.Major, v.Minor, v.Patch, release, dev, post, local)
   188  }
   189  
   190  // pythonCase converts s to PascalCase, ignoring underscores, e.g. __myWords -> __MyWords.
   191  func pythonCase(s string) string {
   192  	var underscores string
   193  	noUnderscores := strings.TrimLeftFunc(s, func(r rune) bool {
   194  		if r != '_' {
   195  			return false
   196  		}
   197  		underscores += "_"
   198  		return true
   199  	})
   200  	c := cgstrings.Unhyphenate(noUnderscores)
   201  	return underscores + cgstrings.UppercaseFirst(c)
   202  }