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 }