Skip to content

Commit d399371

Browse files
committed
Test callbacks & makeShellArg, update README.md
1 parent bfecfd7 commit d399371

9 files changed

Lines changed: 136 additions & 15 deletions

File tree

‎README.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ New features:
1919

2020
Gopy now assumes that you are working with modules-based builds, and requires a valid `go.mod` file, and works only with Go versions 1.15 and above.
2121

22-
Currently using [pybindgen](https://pybindgen.readthedocs.io/en/latest/tutorial/) to generate the low-level c-to-python bindings, but support for [cffi](https://cffi.readthedocs.io/en/latest/) should be relatively straightforward for those using PyPy instead of CPython (pybindgen should be significantly faster for CPython apparently). You also need `goimports` to ensure the correct imports are included.
22+
By default, gopy uses [pybindgen](https://pybindgen.readthedocs.io/en/latest/tutorial/) to generate the low-level c-to-python bindings. You also need `goimports` to ensure the correct imports are included.
2323

2424
```sh
2525
$ python3 -m pip install pybindgen
@@ -29,6 +29,22 @@ $ go install github.com/go-python/gopy@latest
2929

3030
(This all assumes you have already installed [Go itself](https://golang.org/doc/install), and added `~/go/bin` to your `PATH`).
3131

32+
### Choosing a backend (experimental)
33+
34+
The `GOPY_BACKEND` environment variable picks a different tool for those bindings, for `gopy gen`, `gopy build` and `gopy pkg`:
35+
36+
| `GOPY_BACKEND` | Needs | |
37+
|---|---|---|
38+
| `pybindgen` (default) | `pip install pybindgen` | |
39+
| `capi` | nothing beyond python itself | writes the C-API bindings directly, without pybindgen |
40+
| `cffi` | `pip install cffi` | |
41+
| `pybind11` | `pip install pybind11`, a C++ compiler | |
42+
| `nanobind` | `pip install nanobind`, a C++ compiler | |
43+
44+
```sh
45+
$ GOPY_BACKEND=capi gopy build github.com/go-python/gopy/_examples/hi
46+
```
47+
3248
To [install python modules](https://packaging.python.org/tutorials/packaging-projects/), you will need the python install packages:
3349

3450
```sh

‎_examples/callbacks/callbacks.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,17 @@ func InGoroutine(fun func(i int)) {
102102
}()
103103
wg.Wait()
104104
}
105+
106+
var kept func(i int)
107+
108+
// Keep stores fun, for CallKept to call after Keep has returned.
109+
func Keep(fun func(i int)) {
110+
kept = fun
111+
}
112+
113+
// CallKept calls the func Keep stored, outside the call it was passed to.
114+
func CallKept(i int) {
115+
if kept != nil {
116+
kept(i)
117+
}
118+
}

‎_examples/callbacks/test.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from __future__ import print_function
66

77
import io
8+
import os
9+
import subprocess
810
import sys
911

1012
import callbacks
@@ -80,4 +82,19 @@ def boom(i, label):
8082
sys.stderr = stderr
8183
print("calls:", len(seen), "reported:", reported.count("ValueError: boom"))
8284

85+
print("--- a callback Go keeps and calls after the call it was passed to returned")
86+
# Run in a child process: a callback that outlives its call must not crash it
87+
# (cffi, pybind11 and nanobind refuse the call, cffi with a warning on stderr,
88+
# which the output compared here mustn't depend on).
89+
here = os.path.dirname(os.path.abspath(__file__))
90+
child = subprocess.run(
91+
[sys.executable, "-c", "import callbacks\ndef kept(i): pass\ncallbacks.Keep(kept)\ncallbacks.CallKept(1)\n"],
92+
cwd=here,
93+
stdout=subprocess.PIPE,
94+
stderr=subprocess.PIPE,
95+
)
96+
print("kept: exit code", child.returncode)
97+
if child.returncode != 0:
98+
print(child.stderr.decode(errors="replace"))
99+
83100
print("OK")

‎bind/cxxbuild_test.go‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2026 The go-python Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package bind
6+
7+
import (
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"testing"
12+
)
13+
14+
var makeShellArgTests = []struct {
15+
arg, want string
16+
}{
17+
{"", "''"},
18+
{"-O2", "-O2"},
19+
{"-I/usr/include/python3.12", "-I/usr/include/python3.12"},
20+
{"$(CXXLIBFLAGS)", "$(CXXLIBFLAGS)"}, // a make variable, left for make
21+
{"-Wl,-rpath,$ORIGIN", "'-Wl,-rpath,$$ORIGIN'"},
22+
{"a$b", "'a$$b'"},
23+
{"-I/opt/My Python/include", "'-I/opt/My Python/include'"},
24+
{`C:\hostedtoolcache\Python\include`, `'C:\hostedtoolcache\Python\include'`},
25+
{"it's", `'it'\''s'`},
26+
{`say "hi"`, `'say "hi"'`},
27+
{"*.cpp", "'*.cpp'"},
28+
{"a;b", "'a;b'"},
29+
{"#x", "'#x'"},
30+
}
31+
32+
func TestMakeShellArg(t *testing.T) {
33+
for _, tt := range makeShellArgTests {
34+
if got := makeShellArg(tt.arg); got != tt.want {
35+
t.Errorf("makeShellArg(%q) = %q, want %q", tt.arg, got, tt.want)
36+
}
37+
}
38+
}
39+
40+
// TestMakeShellArgRoundTrip checks makeShellArg's actual contract: written
41+
// into a recipe line, the shell that make runs it with receives arg itself.
42+
func TestMakeShellArgRoundTrip(t *testing.T) {
43+
if _, err := exec.LookPath("make"); err != nil {
44+
t.Skip("make not found")
45+
}
46+
for _, tt := range makeShellArgTests {
47+
if tt.arg == "$(CXXLIBFLAGS)" {
48+
continue // expanded by make, by design
49+
}
50+
dir := t.TempDir()
51+
mk := "all:\n\t@printf '%s' " + makeShellArg(tt.arg) + "\n"
52+
if err := os.WriteFile(filepath.Join(dir, "Makefile"), []byte(mk), 0644); err != nil {
53+
t.Fatal(err)
54+
}
55+
cmd := exec.Command("make", "-s")
56+
cmd.Dir = dir
57+
out, err := cmd.CombinedOutput()
58+
if err != nil {
59+
t.Errorf("make for %q: %v\n%s", tt.arg, err, out)
60+
continue
61+
}
62+
if string(out) != tt.arg {
63+
t.Errorf("make passed %q to the shell as %q", tt.arg, out)
64+
}
65+
}
66+
}

‎bind/gen.go‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,10 @@ except ImportError:
335335
cwd = os.getcwd()
336336
currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
337337
os.chdir(currentdir)
338-
# Windows only searches a dependent DLL's own directory for its further
339-
# dependencies (e.g. the pybind11 backend's _%[1]s.pyd needs %[1]s_go.pyd)
340-
# if that directory was added explicitly; harmless, and a no-op elsewhere.
338+
# Windows only searches a DLL's own directory for its further dependencies if
339+
# that directory was added explicitly. No backend's module currently depends
340+
# on another DLL next to it (the pybind11 backend's once needed %[1]s_go.pyd,
341+
# before it linked into a single module); harmless, and a no-op elsewhere.
341342
if hasattr(os, 'add_dll_directory'):
342343
os.add_dll_directory(currentdir)
343344
# When multiple gopy extensions coexist in one Python process each carries its own
@@ -640,12 +641,12 @@ func (g *pyGen) genPre() {
640641
g.gofile = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
641642
g.leakfile = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
642643
g.pybuild = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
643-
if g.wantMakefile() {
644+
if !NoMake {
644645
g.makefile = &printer{buf: new(bytes.Buffer), indentEach: []byte("\t")}
645646
}
646647
g.genGoPreamble()
647648
g.genPyBuildPreamble()
648-
if g.wantMakefile() {
649+
if !NoMake {
649650
g.genMakefile()
650651
}
651652
oinit, err := os.Create(filepath.Join(g.cfg.OutputDir, "__init__.py"))
@@ -675,7 +676,7 @@ func (g *pyGen) genOut() {
675676
g.splicePyBind11Trampolines()
676677
g.genPrintOut(g.cfg.Name+".go", g.gofile)
677678
g.genPrintOut("build.py", g.pybuild)
678-
if g.wantMakefile() {
679+
if !NoMake {
679680
g.makefile.Printf("\n\n")
680681
g.genPrintOut("Makefile", g.makefile)
681682
}
@@ -717,11 +718,6 @@ func (g *pyGen) genPkg(p *Package) {
717718
g.pkg = nil
718719
}
719720

720-
// wantMakefile reports whether to write a Makefile.
721-
func (g *pyGen) wantMakefile() bool {
722-
return !NoMake
723-
}
724-
725721
func (g *pyGen) genGoPreamble() {
726722
pkgimport := ""
727723
for pp, pnm := range current.imports {

‎cmd_build.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ build generates and compiles (C)Python language bindings for Go package(s).
3131
ex:
3232
$ gopy build [options] <go-package-name> [other-go-package...]
3333
$ gopy build github.com/go-python/gopy/_examples/hi
34-
`,
34+
` + backendHelp,
3535
Flag: *flag.NewFlagSet("gopy-build", flag.ExitOnError),
3636
}
3737

‎cmd_gen.go‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import (
1313
"github.com/gonuts/flag"
1414
)
1515

16+
// backendHelp ends the help text of the commands that GOPY_BACKEND applies to.
17+
const backendHelp = `
18+
backends:
19+
GOPY_BACKEND chooses the tool that binds Go to python: pybindgen (the
20+
default), capi, cffi, pybind11 or nanobind. capi needs nothing beyond python
21+
itself; the others need their own python package (pip install <name>), and
22+
pybind11 and nanobind also a C++ compiler.
23+
$ GOPY_BACKEND=capi gopy build github.com/go-python/gopy/_examples/hi
24+
`
25+
1626
func gopyMakeCmdGen() *commander.Command {
1727
cmd := &commander.Command{
1828
Run: gopyRunCmdGen,
@@ -24,7 +34,7 @@ gen generates (C)Python language bindings for Go package(s).
2434
ex:
2535
$ gopy gen [options] <go-package-name> [other-go-package...]
2636
$ gopy gen github.com/go-python/gopy/_examples/hi
27-
`,
37+
` + backendHelp,
2838
Flag: *flag.NewFlagSet("gopy-gen", flag.ExitOnError),
2939
}
3040

‎cmd_pkg.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ When including multiple packages, list in order of increasing dependency, and us
3434
ex:
3535
$ gopy pkg [options] <go-package-name> [other-go-package...]
3636
$ gopy pkg github.com/go-python/gopy/_examples/hi
37-
`,
37+
` + backendHelp,
3838
Flag: *flag.NewFlagSet("gopy-pkg", flag.ExitOnError),
3939
}
4040

‎main_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ box: [(0, 'item-0'), (1, 'item-1')]
433433
goroutine: 7
434434
--- an exception in a callback is reported, and Go carries on
435435
calls: 3 reported: 3
436+
--- a callback Go keeps and calls after the call it was passed to returned
437+
kept: exit code 0
436438
OK
437439
`),
438440
})

0 commit comments

Comments
 (0)