diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/.travis.yml b/.travis.yml index 678d6f3..a13c9f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.9.x - - 1.10.x + - 1.14.x + - 1.13.x before_install: - sudo apt-get update -qq diff --git a/README.md b/README.md index 11e7ac3..2e82335 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ go-python ========= -[![Build Status](https://drone.io/github.com/sbinet/go-python/status.png)](https://drone.io/github.com/sbinet/go-python/latest) +**`sbinet/go-python` only supports CPython2. CPython2 isn't supported anymore by [python.org](https://python.org). Thus, `sbinet/go-python` is now archived.** +**A possible alternative may be to use and contribute to [go-python/cpy3](https://github.com/go-python/cpy3) instead.** + +[![Build Status](https://travis-ci.org/sbinet/go-python.svg?branch=master)](https://travis-ci.org/sbinet/go-python) +[![Build status](https://ci.appveyor.com/api/projects/status/n0ujg8no487a89vo/branch/master?svg=true)](https://ci.appveyor.com/project/sbinet/go-python/branch/master) +[![GoDocs](https://godocs.io/github.com/sbinet/go-python?status.svg)](https://godocs.io/github.com/sbinet/go-python) Naive `go` bindings towards the C-API of CPython-2. @@ -47,9 +52,9 @@ If ``go get`` + ``pkg-config`` failed: Documentation ------------- -Is available on ``godoc``: +Is available on ``godocs``: - http://godoc.org/github.com/sbinet/go-python + https://godocs.io/github.com/sbinet/go-python Example: diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..ad332c1 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +build: off + +clone_folder: c:\gopath\src\github.com\sbinet\go-python + +cache: + - '%LocalAppData%\\go-build' + - '%LocalAppData%\\pip' + +branches: + only: + - master + +environment: + GOPATH: c:\gopath + GOPY_APPVEYOR_CI: '1' + GODEBUG: 'cgocheck=0' + GOTRACEBACK: 'crash' + PYTHONUNBUFFERED: "1" + CPYTHON2DIR: "C:\\Python27-x64" + PATH: '%GOPATH%\bin;%CPYTHON2DIR%;%CPYTHON2DIR%\\Scripts;C:\msys64\mingw64\bin;C:\msys64\usr\bin\;%PATH%' + +stack: go 1.13 + +build_script: + - "%CPYTHON2DIR%\\python --version" + - go get -v -t ./... + +test_script: + - go test ./... diff --git a/capi.go b/capi.go index 2d8d777..deec35f 100644 --- a/capi.go +++ b/capi.go @@ -16,11 +16,13 @@ func Py_BuildValue(format string, args ...interface{}) *PyObject { // ml_doc char * points to the contents of the docstring type PyMethodDef struct { Name string // name of the method - Meth func(self, args *PyObject) *PyObject + Meth PyCFunction Flags MethodDefFlags Doc string } +type PyCFunction C.PyCFunction + type MethodDefFlags int const ( diff --git a/cgoflags.go b/cgoflags_unix.go similarity index 81% rename from cgoflags.go rename to cgoflags_unix.go index 5f1f0fa..4a38570 100644 --- a/cgoflags.go +++ b/cgoflags_unix.go @@ -1,7 +1,7 @@ +// +build !windows + package python // #cgo pkg-config: python-2.7 // #include "go-python.h" import "C" - -// EOF diff --git a/cgoflags_windows.go b/cgoflags_windows.go new file mode 100644 index 0000000..cb02cf2 --- /dev/null +++ b/cgoflags_windows.go @@ -0,0 +1,12 @@ +// +build windows + +package python + +// #cgo 386 CFLAGS: -I C:/Python27/include +// #cgo amd64 CFLAGS: -I C:/Python27-x64/include +// #cgo amd64 CFLAGS: -D MS_WIN64 +// #cgo 386 LDFLAGS: -L C:/Python27/libs -lpython27 +// #cgo amd64 LDFLAGS: -L C:/Python27-x64/libs -lpython27 +// +// #include "go-python.h" +import "C" diff --git a/file.go b/file.go index 983ebe8..2d2c5b4 100644 --- a/file.go +++ b/file.go @@ -1,8 +1,8 @@ package python /* -#include #include "go-python.h" +#include PyObject* _gopy_PyFile_FromFile(int fd, char *name, char *mode) { diff --git a/gen-cgoflags.go b/gen-cgoflags.go new file mode 100644 index 0000000..ce86c5c --- /dev/null +++ b/gen-cgoflags.go @@ -0,0 +1,141 @@ +// +build ignore + +// simple command to generate CGOFLAGS for a given python VM +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/pkg/errors" +) + +func main() { + vm := flag.String("vm", "python", "path to a python VM") + flag.Parse() + + if *vm == "" { + log.Fatalf("need a python VM") + } + + cfg, err := getPythonConfig(*vm) + if err != nil { + log.Fatalf("could not infer python configuration: %v", err) + } + + oname := "cgoflags_unix.go" + switch runtime.GOOS { + case "windows": + oname = "cgoflags_windows.go" + } + + err = ioutil.WriteFile(oname, []byte(fmt.Sprintf(tmpl, + cfg.cflags, + cfg.ldflags, + runtime.GOOS, + )), 0644) + if err != nil { + log.Fatalf("could not write %q: %v", oname, err) + } +} + +// getPythonConfig returns the needed python configuration for the given +// python VM (python, python2, python3, pypy, etc...) +func getPythonConfig(vm string) (pyconfig, error) { + code := `import sys +import distutils.sysconfig as ds +import json +print(ds.get_config_vars()) +print(ds.get_python_lib()) +print(json.dumps({ + "version": sys.version_info.major, + "prefix": ds.get_config_var("prefix"), + "incdir": ds.get_python_inc(), + "libdir": ds.get_config_var("LIBDIR"), + "libdest": ds.get_config_var("LIBDEST"), + "libpy": ds.get_config_var("LIBRARY"), + "shlibs": ds.get_config_var("SHLIBS"), + "syslibs": ds.get_config_var("SYSLIBS"), + "shlinks": ds.get_config_var("LINKFORSHARED"), + "DLLs": ds.get_config_var("DLLLIBRARY"), +})) +` + + var cfg pyconfig + bin, err := exec.LookPath(vm) + if err != nil { + return cfg, errors.Wrapf(err, "could not locate python vm %q", vm) + } + + buf := new(bytes.Buffer) + cmd := exec.Command(bin, "-c", code) + cmd.Stdin = os.Stdin + cmd.Stdout = buf + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return cfg, errors.Wrap(err, "could not run python-config script") + } + + log.Printf("distutils:\n%s", buf.String()) + + var raw struct { + Version int `json:"version"` + IncDir string `json:"incdir"` + LibDir string `json:"libdir"` + LibPy string `json:"libpy"` + ShLibs string `json:"shlibs"` + SysLibs string `json:"syslibs"` + } + err = json.NewDecoder(buf).Decode(&raw) + if err != nil { + return cfg, errors.Wrapf(err, "could not decode JSON script output") + } + + if strings.HasSuffix(raw.LibPy, ".a") { + raw.LibPy = raw.LibPy[:len(raw.LibPy)-len(".a")] + } + if strings.HasPrefix(raw.LibPy, "lib") { + raw.LibPy = raw.LibPy[len("lib"):] + } + + cfg.version = raw.Version + cfg.cflags = strings.Join([]string{ + "-I " + raw.IncDir, + }, " ") + cfg.ldflags = strings.Join([]string{ + "-L " + raw.LibDir, + "-l " + raw.LibPy, + raw.ShLibs, + raw.SysLibs, + }, " ") + + return cfg, nil +} + +type pyconfig struct { + version int + cflags string + ldflags string +} + +const tmpl = `// Automatically generated. Do not edit. + +// +build %[3]s + +package python + +// #cgo %[3]s CFLAGS: %[1]s +// #cgo %[3]s LDFLAGS: %[2]s +// +// #include "go-python.h" +import "C" +` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8dc3bb5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sbinet/go-python + +go 1.13 diff --git a/heap.go b/heap.go index d3a00a1..d5f0c27 100644 --- a/heap.go +++ b/heap.go @@ -24,7 +24,7 @@ func cpyMethodDefs(name string, methods []PyMethodDef) *C.PyMethodDef { for i, meth := range methods { cmeth := C.PyMethodDef{ ml_name: C.CString(meth.Name), - ml_meth: (C.PyCFunction)(unsafe.Pointer(&meth.Meth)), + ml_meth: C.PyCFunction(meth.Meth), ml_flags: C.int(meth.Flags), ml_doc: C.CString(meth.Doc), } @@ -43,6 +43,7 @@ func Py_InitModule(name string, methods []PyMethodDef) (*PyObject, error) { obj := togo(C._gopy_InitModule(c_mname, cmeths)) if obj == nil { + PyErr_Print() return nil, errors.New("python: internal error; module creation failed.") } return obj, nil @@ -59,6 +60,7 @@ func Py_InitModule3(name string, methods []PyMethodDef, doc string) (*PyObject, obj := togo(C._gopy_InitModule3(cname, cmeths, cdoc)) if obj == nil { + PyErr_Print() return nil, errors.New("python: internal error; module creation failed.") } return obj, nil diff --git a/none.go b/none.go index 914899e..427c4cf 100644 --- a/none.go +++ b/none.go @@ -6,6 +6,6 @@ import "C" // The Python None object, denoting lack of value. This object has no methods. // It needs to be treated just like any other object with respect to reference // counts. -var Py_None = togo(C._gopy_pynone()) +var Py_None = &PyObject{ptr: C._gopy_pynone()} // EOF diff --git a/numeric.go b/numeric.go index 31a2c2a..97125ad 100644 --- a/numeric.go +++ b/numeric.go @@ -332,13 +332,13 @@ func PyBool_Check(self *PyObject) bool { // The Python False object. This object has no methods. // It needs to be treated just like any other object with respect to // reference counts. -var Py_False = togo(C._gopy_pyfalse()) +var Py_False = &PyObject{ptr: C._gopy_pyfalse()} // PyObject* Py_True // The Python True object. This object has no methods. // It needs to be treated just like any other object with respect to // reference counts. -var Py_True = togo(C._gopy_pytrue()) +var Py_True = &PyObject{ptr: C._gopy_pytrue()} /* Py_RETURN_FALSE diff --git a/object.go b/object.go index 274fcfd..2e78c2d 100644 --- a/object.go +++ b/object.go @@ -15,6 +15,13 @@ type PyObject struct { ptr *C.PyObject } +// String returns a string representation of the PyObject +func (self *PyObject) String() string { + o := self.Str() + defer o.DecRef() + return PyString_AsString(o) +} + func (self *PyObject) topy() *C.PyObject { return self.ptr } @@ -27,10 +34,18 @@ func topy(self *PyObject) *C.PyObject { } func togo(obj *C.PyObject) *PyObject { - if obj == nil { + switch obj { + case nil: return nil + case Py_None.ptr: + return Py_None + case Py_True.ptr: + return Py_True + case Py_False.ptr: + return Py_False + default: + return &PyObject{ptr: obj} } - return &PyObject{ptr: obj} } // PyObject_FromVoidPtr converts a PyObject from an unsafe.Pointer diff --git a/python_test.go b/python_test.go index 0adeb4e..564b64e 100644 --- a/python_test.go +++ b/python_test.go @@ -112,7 +112,7 @@ func TestErrFetch(t *testing.T) { t.Parallel() testPkg(t, pkg{ path: "tests/errfetch", - want: []byte("exc=&{}\nval=&{}\ntb=&{}\n"), + want: []byte("exc=\nval=\ntb=\n"), }) } @@ -140,3 +140,12 @@ func TestIssue61(t *testing.T) { `), }) } + +func TestCheckNone(t *testing.T) { + t.Parallel() + testPkg(t, pkg{ + path: "tests/none-check", + want: []byte(`type=, str=None, eq_none=true +`), + }) +} diff --git a/tests/none-check/get_none.py b/tests/none-check/get_none.py new file mode 100644 index 0000000..7d27c14 --- /dev/null +++ b/tests/none-check/get_none.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python2 + +def get_none(): + return None + diff --git a/tests/none-check/main.go b/tests/none-check/main.go new file mode 100644 index 0000000..990fec4 --- /dev/null +++ b/tests/none-check/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "log" + + "github.com/sbinet/go-python" +) + +func init() { + err := python.Initialize() + if err != nil { + log.Panic(err) + } +} + +func main() { + module := python.PyImport_ImportModule("get_none") + if module == nil { + log.Fatal("could not import 'get_none' module") + } + get_none := module.GetAttrString("get_none") + if get_none == nil { + log.Fatal("could not import 'get_none' function") + } + none := get_none.CallFunction() + fmt.Printf("type=%s, str=%s, eq_none=%t\n", + python.PyString_AsString(none.Type().Str()), + python.PyString_AsString(none.Str()), + none == python.Py_None, + ) +}