Skip to content

Commit ac5c077

Browse files
authored
main,bind: extended gopy:name control, auto PEP8 naming, and property docstring support
This commit provides an option to automatically rename functions, methods, and properties to PEP-style snake_case when generating code.
1 parent ddc41c9 commit ac5c077

File tree

14 files changed

+191
-22
lines changed

14 files changed

+191
-22
lines changed

_examples/rename/rename.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,37 @@
55
// package rename tests changing the names of methods and functions
66
package rename
77

8-
// gopy:name say_hi
8+
// gopy:name say_hi_fn
9+
// Comment follows the tag and function should be renamed
10+
// to say_hi_fn
911
func SayHi() string {
1012
return "hi"
1113
}
1214

15+
// I should be renamed to auto_renamed_func, when generated
16+
// with -rename flag
17+
func AutoRenamedFunc() {
18+
19+
}
20+
21+
// MyStruct has two fields
1322
type MyStruct struct {
23+
// I should be renamed to auto_renamed_property
24+
// when generated with -rename flag
25+
AutoRenamedProperty string
26+
27+
// I should be renamed to custom_name with the custom option
28+
AutoRenamedProperty2 string `gopy:"custom_name"`
1429
}
1530

31+
// A method that says something
1632
// gopy:name say_something
17-
func (s *MyStruct) SaySomething() (something string) {
33+
func (s *MyStruct) SaySomethingFunc() (something string) {
1834
return "something"
1935
}
36+
37+
// I should be renamed to auto_renamed_meth, when generated
38+
// with -rename flag
39+
func (s *MyStruct) AutoRenamedMeth() {
40+
41+
}

_examples/rename/test.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@
77

88
import rename
99

10-
print(rename.say_hi())
11-
print(rename.MyStruct().say_something())
10+
print("say_hi_fn():", rename.say_hi_fn())
11+
print("MyStruct().say_something():", rename.MyStruct().say_something())
12+
13+
# Just make sure the symbols exist
14+
rename.auto_renamed_func()
15+
struct = rename.MyStruct()
16+
struct.auto_renamed_meth()
17+
_ = struct.auto_renamed_property
18+
struct.auto_renamed_property = "foo"
19+
_ = struct.custom_name
20+
struct.custom_name = "foo"
21+
22+
print("MyStruct.auto_renamed_property.__doc__:", rename.MyStruct.auto_renamed_property.__doc__.strip())
23+
print("MyStruct.custom_name.__doc__:", rename.MyStruct.custom_name.__doc__.strip())
1224

1325
print("OK")

bind/bind.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type BindCfg struct {
2626
VM string
2727
// package prefix used when generating python import statements
2828
PkgPrefix string
29+
// rename Go exported symbols to python PEP snake_case
30+
RenameCase bool
2931
}
3032

3133
// ErrorList is a list of errors

bind/gen_func.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,12 @@ func (g *pyGen) genFuncSig(sym *symbol, fsym *Func) bool {
8383
return false
8484
}
8585

86-
gname, gdoc, err := extractPythonName(fsym.GoName(), fsym.Doc())
86+
gname := fsym.GoName()
87+
if g.cfg.RenameCase {
88+
gname = toSnakeCase(gname)
89+
}
90+
91+
gname, gdoc, err := extractPythonName(gname, fsym.Doc())
8792
if err != nil {
8893
return false
8994
}

bind/gen_struct.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,25 @@ func (g *pyGen) genStructMemberGetter(s *Struct, i int, f types.Object) {
183183
return
184184
}
185185

186+
gname := f.Name()
187+
if g.cfg.RenameCase {
188+
gname = toSnakeCase(gname)
189+
}
190+
191+
if newName, err := extractPythonNameFieldTag(gname, s.Struct().Tag(i)); err == nil {
192+
gname = newName
193+
}
194+
186195
cgoFn := fmt.Sprintf("%s_%s_Get", s.ID(), f.Name())
187196

188197
g.pywrap.Printf("@property\n")
189-
g.pywrap.Printf("def %[1]s(self):\n", f.Name())
198+
g.pywrap.Printf("def %[1]s(self):\n", gname)
190199
g.pywrap.Indent()
200+
if gdoc := g.pkg.getDoc(s.Obj().Name(), f); gdoc != "" {
201+
g.pywrap.Printf(`"""`)
202+
g.pywrap.Printf(gdoc)
203+
g.pywrap.Println(`"""`)
204+
}
191205
if ret.hasHandle() {
192206
cvnm := ret.pyPkgId(g.pkg.pkg)
193207
g.pywrap.Printf("return %s(handle=_%s.%s(self.handle))\n", cvnm, pkgname, cgoFn)
@@ -224,10 +238,19 @@ func (g *pyGen) genStructMemberSetter(s *Struct, i int, f types.Object) {
224238
return
225239
}
226240

241+
gname := f.Name()
242+
if g.cfg.RenameCase {
243+
gname = toSnakeCase(gname)
244+
}
245+
246+
if newName, err := extractPythonNameFieldTag(gname, s.Struct().Tag(i)); err == nil {
247+
gname = newName
248+
}
249+
227250
cgoFn := fmt.Sprintf("%s_%s_Set", s.ID(), f.Name())
228251

229-
g.pywrap.Printf("@%s.setter\n", f.Name())
230-
g.pywrap.Printf("def %[1]s(self, value):\n", f.Name())
252+
g.pywrap.Printf("@%s.setter\n", gname)
253+
g.pywrap.Printf("def %[1]s(self, value):\n", gname)
231254
g.pywrap.Indent()
232255
g.pywrap.Printf("if isinstance(value, go.GoClass):\n")
233256
g.pywrap.Indent()

bind/gen_varconst.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ func (g *pyGen) genVarGetter(v *Var) {
3636
gopkg := g.pkg.Name()
3737
pkgname := g.cfg.Name
3838
cgoFn := v.Name() // plain name is the getter
39+
if g.cfg.RenameCase {
40+
cgoFn = toSnakeCase(cgoFn)
41+
}
3942
qCgoFn := gopkg + "_" + cgoFn
4043
qFn := "_" + pkgname + "." + qCgoFn
4144
qVn := gopkg + "." + v.Name()
@@ -76,6 +79,9 @@ func (g *pyGen) genVarSetter(v *Var) {
7679
gopkg := g.pkg.Name()
7780
pkgname := g.cfg.Name
7881
cgoFn := fmt.Sprintf("Set_%s", v.Name())
82+
if g.cfg.RenameCase {
83+
cgoFn = toSnakeCase(cgoFn)
84+
}
7985
qCgoFn := gopkg + "_" + cgoFn
8086
qFn := "_" + pkgname + "." + qCgoFn
8187
qVn := gopkg + "." + v.Name()

bind/package.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package bind
66

77
import (
88
"fmt"
9+
"go/ast"
910
"go/doc"
1011
"go/types"
1112
"path/filepath"
@@ -109,7 +110,7 @@ func (p *Package) AddPyImport(ipath string, extra bool) {
109110
// parent is the name of the containing scope ("" for global scope)
110111
func (p *Package) getDoc(parent string, o types.Object) string {
111112
n := o.Name()
112-
switch o.(type) {
113+
switch tp := o.(type) {
113114
case *types.Const:
114115
for _, c := range p.doc.Consts {
115116
for _, cn := range c.Names {
@@ -120,6 +121,38 @@ func (p *Package) getDoc(parent string, o types.Object) string {
120121
}
121122

122123
case *types.Var:
124+
if tp.IsField() && parent != "" {
125+
// Find the package-scoped struct
126+
for _, typ := range p.doc.Types {
127+
_ = typ
128+
if typ.Name != parent {
129+
continue
130+
}
131+
// Name matches package-scoped struct.
132+
// Make sure it is a struct type.
133+
for _, spec := range typ.Decl.Specs {
134+
typSpec, ok := spec.(*ast.TypeSpec)
135+
if !ok {
136+
continue
137+
}
138+
structSpec, ok := typSpec.Type.(*ast.StructType)
139+
if !ok {
140+
continue
141+
}
142+
// We have the package-scoped struct matching the parent name.
143+
// Find the matching field.
144+
for _, field := range structSpec.Fields.List {
145+
for _, fieldName := range field.Names {
146+
if fieldName.Name == tp.Name() {
147+
return field.Doc.Text()
148+
}
149+
}
150+
}
151+
}
152+
153+
}
154+
}
155+
// Otherwise just check the captured vars
123156
for _, v := range p.doc.Vars {
124157
for _, vn := range v.Names {
125158
if n == vn {

bind/utils.go

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os"
1313
"os/exec"
1414
"path/filepath"
15+
"reflect"
1516
"regexp"
1617
"strconv"
1718
"strings"
@@ -197,19 +198,78 @@ func getGoVersion(version string) (int64, int64, error) {
197198
return major, minor, nil
198199
}
199200

201+
var (
202+
rxValidPythonName = regexp.MustCompile(`^[\pL_][\pL_\pN]+$`)
203+
)
204+
200205
func extractPythonName(gname, gdoc string) (string, string, error) {
201-
const PythonName = "\ngopy:name "
202-
i := strings.Index(gdoc, PythonName)
206+
const (
207+
PythonName = "gopy:name "
208+
NLPythonName = "\n" + PythonName
209+
)
210+
i := -1
211+
var tag string
212+
// Check for either a doc string that starts with our tag,
213+
// or as the first token of a newline
214+
if strings.HasPrefix(gdoc, PythonName) {
215+
i = 0
216+
tag = PythonName
217+
} else {
218+
i = strings.Index(gdoc, NLPythonName)
219+
tag = NLPythonName
220+
}
203221
if i < 0 {
204222
return gname, gdoc, nil
205223
}
206-
s := gdoc[i+len(PythonName):]
224+
s := gdoc[i+len(tag):]
207225
if end := strings.Index(s, "\n"); end > 0 {
208-
validIdPattern := regexp.MustCompile(`^[\pL_][\pL_\pN]+$`)
209-
if !validIdPattern.MatchString(s[:end]) {
226+
if !isValidPythonName(s[:end]) {
210227
return "", "", fmt.Errorf("gopy: invalid identifier: %s", s[:end])
211228
}
212229
return s[:end], gdoc[:i] + s[end:], nil
213230
}
214231
return gname, gdoc, nil
215232
}
233+
234+
// extractPythonNameFieldTag parses a struct field tag and returns
235+
// a new python name. If the tag is not defined then the original
236+
// name is returned.
237+
// If the tag name is specified but is an invalid python identifier,
238+
// then an error is returned.
239+
func extractPythonNameFieldTag(gname, tag string) (string, error) {
240+
const tagKey = "gopy"
241+
if tag == "" {
242+
return gname, nil
243+
}
244+
tagVal := reflect.StructTag(tag).Get(tagKey)
245+
if tagVal == "" {
246+
return gname, nil
247+
}
248+
if !isValidPythonName(tagVal) {
249+
return "", fmt.Errorf("gopy: invalid identifier for struct field tag: %s", tagVal)
250+
}
251+
return tagVal, nil
252+
}
253+
254+
// isValidPythonName returns true if the string is a valid
255+
// python identifier name
256+
func isValidPythonName(name string) bool {
257+
if name == "" {
258+
return false
259+
}
260+
return rxValidPythonName.MatchString(name)
261+
}
262+
263+
var (
264+
rxMatchFirstCap = regexp.MustCompile("([A-Z])([A-Z][a-z])")
265+
rxMatchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
266+
)
267+
268+
// toSnakeCase converts the provided string to snake_case.
269+
// Based on https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6
270+
func toSnakeCase(input string) string {
271+
output := rxMatchFirstCap.ReplaceAllString(input, "${1}_${2}")
272+
output = rxMatchAllCap.ReplaceAllString(output, "${1}_${2}")
273+
output = strings.ReplaceAll(output, "-", "_")
274+
return strings.ToLower(output)
275+
}

cmd_build.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ex:
4040
cmd.Flag.String("main", "", "code string to run in the go main() function in the cgo library")
4141
cmd.Flag.String("package-prefix", ".", "custom package prefix used when generating import "+
4242
"statements for generated package")
43+
cmd.Flag.Bool("rename", false, "rename Go symbols to python PEP snake_case")
4344
cmd.Flag.Bool("symbols", true, "include symbols in output")
4445
cmd.Flag.Bool("no-warn", false, "suppress warning messages, which may be expected")
4546
cmd.Flag.Bool("no-make", false, "do not generate a Makefile, e.g., when called from Makefile")
@@ -59,6 +60,7 @@ func gopyRunCmdBuild(cmdr *commander.Command, args []string) error {
5960
cfg.Main = cmdr.Flag.Lookup("main").Value.Get().(string)
6061
cfg.VM = cmdr.Flag.Lookup("vm").Value.Get().(string)
6162
cfg.PkgPrefix = cmdr.Flag.Lookup("package-prefix").Value.Get().(string)
63+
cfg.RenameCase = cmdr.Flag.Lookup("rename").Value.Get().(bool)
6264
cfg.Symbols = cmdr.Flag.Lookup("symbols").Value.Get().(bool)
6365
cfg.NoWarn = cmdr.Flag.Lookup("no-warn").Value.Get().(bool)
6466
cfg.NoMake = cmdr.Flag.Lookup("no-make").Value.Get().(bool)

cmd_exe.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ ex:
4747
"-- defaults to GoPyMainRun() but typically should be overriden")
4848
cmd.Flag.String("package-prefix", ".", "custom package prefix used when generating import "+
4949
"statements for generated package")
50+
cmd.Flag.Bool("rename", false, "rename Go symbols to python PEP snake_case")
5051
cmd.Flag.Bool("symbols", true, "include symbols in output")
5152
cmd.Flag.String("exclude", "", "comma-separated list of package names to exclude")
5253
cmd.Flag.String("user", "", "username on https://www.pypa.io/en/latest/ for package name suffix")
@@ -74,6 +75,7 @@ func gopyRunCmdExe(cmdr *commander.Command, args []string) error {
7475
cfg.Main = cmdr.Flag.Lookup("main").Value.Get().(string)
7576
cfg.VM = cmdr.Flag.Lookup("vm").Value.Get().(string)
7677
cfg.PkgPrefix = cmdr.Flag.Lookup("package-prefix").Value.Get().(string)
78+
cfg.RenameCase = cmdr.Flag.Lookup("rename").Value.Get().(bool)
7779
cfg.Symbols = cmdr.Flag.Lookup("symbols").Value.Get().(bool)
7880
cfg.NoWarn = cmdr.Flag.Lookup("no-warn").Value.Get().(bool)
7981
cfg.NoMake = cmdr.Flag.Lookup("no-make").Value.Get().(bool)

0 commit comments

Comments
 (0)