Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Release Please

on:
push:
branches:
- master
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
release-please:
runs-on: ubuntu-latest
outputs:
releases_created: ${{ steps.release.outputs.releases_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: actions/checkout@v4

- uses: googleapis/release-please-action@v5
id: release
with:
config-file: .release-please-config.json
manifest-file: .release-please-manifest.json
target-branch: master

# Build & publish binaries only when release-please actually cut a release
# (i.e. a release-please PR was just merged to master).
publish:
needs: release-please
if: ${{ needs.release-please.outputs.releases_created == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ needs.release-please.outputs.tag_name }}
fetch-depth: 0

- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
# release-please already created the GitHub Release (notes, tag);
# GoReleaser just builds & uploads the binary archives to it.
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# IDE files
.idea/
*.iml

# goreleaser build output
/dist/
51 changes: 51 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
version: 2
project_name: gopy

before:
hooks:
- go mod tidy

builds:
- id: gopy
main: .
binary: gopy
env:
- CGO_ENABLED=0
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
ldflags:
- -s -w
- -X main.GitCommit={{ .ShortCommit }}
- -X 'main.VersionDate={{ .CommitDate }} UTC'

archives:
- id: gopy
formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
name_template: >-
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}

checksum:
name_template: "checksums.txt"

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
- "^Merge pull request"

release:
github:
owner: go-python
name: gopy
# release-please already created the GitHub Release for this tag (with its
# own changelog-derived notes); keep those notes as-is and just attach
# the built archives/checksums to it.
mode: keep-existing
prerelease: auto
name_template: "{{ .Tag }}"
31 changes: 31 additions & 0 deletions .release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"release-type": "go",
"include-v-in-tag": true,
"bootstrap-sha": "d09ff0f8254c8bd4a131157bf276c83330930cb5",
"packages": {
".": {
"release-type": "go",
"package-name": "gopy",
"changelog-path": "CHANGELOG.md",
"extra-files": [
{
"path": "version.go",
"type": "generic"
}
]
}
},
"changelog-sections": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": true },
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": true },
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
{ "type": "test", "section": "Tests", "hidden": true },
{ "type": "build", "section": "Build System", "hidden": false },
{ "type": "ci", "section": "Continuous Integration", "hidden": true }
]
}
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.4.10"
}
91 changes: 91 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`gopy` is a Go program (module `github.com/go-python/gopy`) that generates and compiles CPython extension
modules from Go packages. It parses a target Go package with `go/packages` + `go/ast`, and emits both a cgo
shim (Go) and a `.py` wrapper module (using pybindgen under the hood), then optionally compiles them into an
importable Python module.

## Build / test / lint commands

Requires Go (module-based build, see `go.mod` for the minimum toolchain version) plus a Python 3 interpreter,
`pybindgen`, and `goimports` on `PATH`:

```sh
python3 -m pip install pybindgen
go install golang.org/x/tools/cmd/goimports@latest
```

- Build the `gopy` binary: `make` (or `go build -v ./...`)
- Run the full test suite: `make test` (or `go test -v ./...`)
- Run a single test: `go test -v -run=TestHi` (test names correspond 1:1 to `_examples/*` fixtures, e.g.
`TestHi` -> `_examples/hi`, `TestBindStructs` -> `_examples/structs`)
- `go vet`: `make vet`, also runs as `TestGovet` inside the suite
- Format: `make fmts` (gofmt -s -w); import-formatting is also checked by `TestGofmt` (uses `goimports` if
present, falls back to `gofmt`)
- `go.mod`/dependency housekeeping: `make tidy`, `make mod-update`

On Windows, tests that check for memory leaks require `psutil` (`python -m pip install psutil`) since the
`resource` module is Unix-only.

### Enforced minimum Python version

There is no hard-coded minimum in the Go code itself — `getPythonVersion` (python.go) only checks that
`sys.version_info.major == 3` (Python 2 is explicitly rejected, no minor-version floor is asserted).
In practice, treat **Python 3.11** as the baseline: it's the only interpreter version CI installs
(`actions/setup-python` pinned to `'3.11'` in `.github/workflows/ci.yml`) and exercises across all three
platforms (Linux/macOS/Windows) and the whole Go version matrix. Nothing in the generated-code templates
(`bind/gen_*.go`, `pkgsetup.go`) or the example fixtures (`_examples/*/test.py`) uses syntax newer than
basic Python 3 (no f-strings, walrus operator, structural pattern matching, or type-hint syntax appear in
emitted code), so there's no code-level reason to require anything past 3.11 — that pin is just what's
actually validated.

## Architecture

**Entry point & commands** (`main.go`, `cmd_*.go`): `main.go` wires up four `commander` subcommands, each
thin wrappers around `genPkg` (gen.go):
- `gopy gen` (cmd_gen.go) — emit bindings only, no compile
- `gopy build` (cmd_build.go) — emit + compile a dynamic module for local testing (this is what the test
suite in main_test.go uses for every `_examples/*` fixture)
- `gopy pkg` (cmd_pkg.go) — like build, plus generates a full installable Python package (`setup.py`,
`MANIFEST.in`, `LICENSE`, `Makefile` — see pkgsetup.go)
- `gopy exe` (cmd_exe.go) — like pkg, but produces a standalone executable with Go's `main()` retained
(for cases like GUI event loops that need the real OS main thread)

These four map to `bind.BuildMode` (`ModeGen`/`ModeBuild`/`ModePkg`/`ModeExe`, defined in bind/gen.go), which
is threaded through the generator to decide what gets emitted.

**Package loading** (gen.go): `loadPackage` uses `golang.org/x/tools/go/packages` to resolve and type-check
the target Go package (optionally `go build`-ing it first), then `parsePackage` re-parses it with `go/ast` +
`go/doc` to recover doc comments, producing a `bind.Package` (bind/package.go) that the generator walks.

**Binding generation** (`bind/` package): this is the core of the tool.
- `gen.go` / `printer.go` — top-level generator (`Generator`) orchestrating output: a cgo `.go`/`.h`/`.c`
file and a Python wrapper `.py` file, tuned per `BuildMode`.
- `gen_func.go`, `gen_struct.go`, `gen_type.go`, `gen_slice.go`, `gen_map.go`, `gen_varconst.go` — one file
per Go language construct being bound (functions/methods, structs -> Python classes, named types, slices,
maps, vars/consts). Struct embedding is mapped to Python class inheritance (first embedded field only).
- `stdtypes.go`, `types.go`, `symbols.go` — Go<->Python type mapping tables, symbol table / name-collision
and Python-keyword handling (structs and generated symbols get an int64 opaque "handle" instead of a raw
pointer, so nothing unsafe crosses the C/Python GC boundary).
- `utils.go` — shared codegen helpers.

**Testing model**: there's no separate unit-test layer for the generator — correctness is validated
end-to-end. Each subdirectory of `_examples/` is a small Go package plus a hand-written `test.py`.
main_test.go's `testPkg` helper runs `gopy build` on the example package, then executes its `test.py`
against the compiled module and diffs stdout against an expected `want` byte string embedded in the test
function. `TestCheckSupportMatrix` regenerates SUPPORT_MATRIX.md from the `features` map at the top of
main_test.go — update that map (not SUPPORT_MATRIX.md directly) when adding/removing an example. Platform
differences live in main_unix_test.go / main_windows_test.go (`//go:build` tags), e.g. picking which Python
VM name to probe for and whether memory-leak checks run.

**Versioning**: releases are managed by release-please (`.github/workflows/release-please.yml`,
`.release-please-config.json`, `.release-please-manifest.json`). Merging its release PR to `master` bumps
the `Version` var in version.go (via the `x-release-please-version` marker — don't hand-edit that line),
tags the commit, and publishes the GitHub Release; a follow-on job then runs GoReleaser
(`.goreleaser.yaml`) to build and attach binary archives to that release. `GitCommit` and `VersionDate` are
no longer stored in version.go — they're stamped at build time via `-ldflags` (see the `build` target in
Makefile and the `builds.ldflags` entry in .goreleaser.yaml).
37 changes: 9 additions & 28 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ DIRS=`go list ./...`
PYTHON=python3
PIP=$(PYTHON) -m pip

GIT_COMMIT=`git rev-parse --short HEAD`
VERS_DATE=`date -u +%Y-%m-%d\ %H:%M`
LDFLAGS=-X 'main.GitCommit=$(GIT_COMMIT)' -X 'main.VersionDate=$(VERS_DATE) UTC'

all: build

build:
build:
@echo "GO111MODULE = $(value GO111MODULE)"
$(GOBUILD) -v $(DIRS)
$(GOBUILD) -v -ldflags "$(LDFLAGS)" $(DIRS)

test:
@echo "GO111MODULE = $(value GO111MODULE)"
Expand Down Expand Up @@ -52,30 +56,7 @@ prereq:
@echo " _PyInit__gi, referenced from:..."
@echo


# NOTE: MUST update version number here prior to running 'make release' and edit this file!
VERS=v0.4.10
PACKAGE=main
GIT_COMMIT=`git rev-parse --short HEAD`
VERS_DATE=`date -u +%Y-%m-%d\ %H:%M`
VERS_FILE=version.go

release:
/bin/rm -f $(VERS_FILE)
@echo "// WARNING: auto-generated by Makefile release target -- run 'make release' to update" > $(VERS_FILE)
@echo "" >> $(VERS_FILE)
@echo "package $(PACKAGE)" >> $(VERS_FILE)
@echo "" >> $(VERS_FILE)
@echo "const (" >> $(VERS_FILE)
@echo " Version = \"$(VERS)\"" >> $(VERS_FILE)
@echo " GitCommit = \"$(GIT_COMMIT)\" // the commit JUST BEFORE the release" >> $(VERS_FILE)
@echo " VersionDate = \"$(VERS_DATE)\" // UTC" >> $(VERS_FILE)
@echo ")" >> $(VERS_FILE)
@echo "" >> $(VERS_FILE)
goimports -w $(VERS_FILE)
/bin/cat $(VERS_FILE)
git commit -am "$(VERS) release"
git tag -a $(VERS) -m "$(VERS) release"
git push
git push origin --tags
# Releases are managed by release-please (.github/workflows/release-please.yml):
# merging its release PR to master bumps version.go, tags the commit, and
# publishes the GitHub Release. GoReleaser then builds & uploads binaries to it.

3 changes: 3 additions & 0 deletions bind/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type BindCfg struct {
PkgPrefix string
// rename Go exported symbols to python PEP snake_case
RenameCase bool
// gopy version string embedded in this binary, stamped into generated
// file headers so output can be traced back to the release that produced it
Version string
}

// ErrorList is a list of errors
Expand Down
Loading
Loading