gopy/bind/utils.go

311 lines
7.7 KiB
Go

// Copyright 2015 The go-python Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package bind
import (
"bytes"
"encoding/json"
"fmt"
"go/types"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
)
func isErrorType(typ types.Type) bool {
return typ == types.Universe.Lookup("error").Type()
}
func isStringer(obj types.Object) bool {
switch obj := obj.(type) {
case *types.Func:
if obj.Name() != "String" {
return false
}
sig, ok := obj.Type().(*types.Signature)
if !ok {
return false
}
if sig.Recv() == nil {
return false
}
if sig.Params().Len() != 0 {
return false
}
res := sig.Results()
if res.Len() != 1 {
return false
}
ret := res.At(0).Type()
if ret != types.Universe.Lookup("string").Type() {
return false
}
return true
default:
return false
}
}
func hasError(sig *types.Signature) bool {
res := sig.Results()
if res == nil || res.Len() <= 0 {
return false
}
nerr := 0
for i := 0; i < res.Len(); i++ {
ret := res.At(i)
if isErrorType(ret.Type()) {
nerr++
}
}
switch {
case nerr == 0:
return false
case nerr == 1:
return true
default:
panic(fmt.Errorf(
"gopy: invalid number of comma-errors (%d)",
nerr,
))
}
}
func isConstructor(sig *types.Signature) bool {
//TODO(sbinet)
return false
}
type PyConfig struct {
Version int
CFlags string
LdFlags string
LdDynamicFlags string
ExtSuffix string
}
// AllFlags returns CFlags + " " + LdFlags
func (pc *PyConfig) AllFlags() string {
return strings.TrimSpace(pc.CFlags) + " " + strings.TrimSpace(pc.LdFlags)
}
// 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
try:
import sysconfig as ds
def _get_python_inc():
return ds.get_path('include')
except ImportError:
import distutils.sysconfig as ds
_get_python_inc = ds.get_config_var
import json
import os
version=sys.version_info.major
def clear_ld_flags(s):
if s is None:
return ''
skip_first_word = s.split(' ', 1)[1] # skip compiler name
skip_bundle = skip_first_word.replace('-bundle', '') # cgo already passes -dynamiclib
return skip_bundle
if "GOPY_INCLUDE" in os.environ and "GOPY_LIBDIR" in os.environ and "GOPY_PYLIB" in os.environ:
print(json.dumps({
"version": version,
"minor": sys.version_info.minor,
"incdir": os.environ["GOPY_INCLUDE"],
"libdir": os.environ["GOPY_LIBDIR"],
"libpy": os.environ["GOPY_PYLIB"],
"shlibs": ds.get_config_var("SHLIBS"),
"syslibs": ds.get_config_var("SYSLIBS"),
"shlinks": ds.get_config_var("LINKFORSHARED"),
"shflags": clear_ld_flags(ds.get_config_var("LDSHARED")),
"extsuffix": ds.get_config_var("EXT_SUFFIX"),
}))
else:
print(json.dumps({
"version": sys.version_info.major,
"minor": sys.version_info.minor,
"incdir": _get_python_inc(),
"libdir": ds.get_config_var("LIBDIR"),
"libpy": ds.get_config_var("LIBRARY"),
"shlibs": ds.get_config_var("SHLIBS"),
"syslibs": ds.get_config_var("SYSLIBS"),
"shlinks": ds.get_config_var("LINKFORSHARED"),
"shflags": clear_ld_flags(ds.get_config_var("LDSHARED")),
"extsuffix": ds.get_config_var("EXT_SUFFIX"),
}))
`
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")
}
var raw struct {
Version int `json:"version"`
Minor int `json:"minor"`
IncDir string `json:"incdir"`
LibDir string `json:"libdir"`
LibPy string `json:"libpy"`
ShLibs string `json:"shlibs"`
SysLibs string `json:"syslibs"`
ExtSuffix string `json:"extsuffix"`
ShFlags string `json:"shflags"`
}
err = json.NewDecoder(buf).Decode(&raw)
if err != nil {
return cfg, errors.Wrapf(err, "could not decode JSON script output")
}
raw.IncDir = filepath.ToSlash(raw.IncDir)
raw.LibDir = filepath.ToSlash(raw.LibDir)
// on windows these can be empty -- use include dir which is usu good
// replace suffix case insensitive 'include' with 'libs'
if raw.LibDir == "" && raw.IncDir != "" {
regexInc := regexp.MustCompile(`(?i)\binclude$`)
raw.LibDir = regexInc.ReplaceAllString(raw.IncDir, "libs")
fmt.Printf("no LibDir -- copy from IncDir: %s\n", raw.LibDir)
}
if raw.LibPy == "" {
raw.LibPy = fmt.Sprintf("python%d%d", raw.Version, raw.Minor)
fmt.Printf("no LibPy -- set to: %s\n", raw.LibPy)
}
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.ExtSuffix = raw.ExtSuffix
cfg.CFlags = strings.Join([]string{
`"-I` + raw.IncDir + `"`,
}, " ")
cfg.LdFlags = strings.Join([]string{
`"-L` + raw.LibDir + `"`,
`"-l` + raw.LibPy + `"`,
raw.ShLibs,
raw.SysLibs,
}, " ")
cfg.LdDynamicFlags = raw.ShFlags
return cfg, nil
}
func getGoVersion(version string) (int64, int64, error) {
version_regex := regexp.MustCompile(`^go((\d+)(\.(\d+))*)`)
match := version_regex.FindStringSubmatch(version)
if match == nil {
return -1, -1, fmt.Errorf("gopy: invalid Go version information: %q", version)
}
version_info := strings.Split(match[1], ".")
major, _ := strconv.ParseInt(version_info[0], 10, 0)
minor, _ := strconv.ParseInt(version_info[1], 10, 0)
return major, minor, nil
}
var (
rxValidPythonName = regexp.MustCompile(`^[\pL_][\pL_\pN]+$`)
)
func extractPythonName(gname, gdoc string) (string, string, error) {
const (
PythonName = "gopy:name "
NLPythonName = "\n" + PythonName
)
i := -1
var tag string
// Check for either a doc string that starts with our tag,
// or as the first token of a newline
if strings.HasPrefix(gdoc, PythonName) {
i = 0
tag = PythonName
} else {
i = strings.Index(gdoc, NLPythonName)
tag = NLPythonName
}
if i < 0 {
return gname, gdoc, nil
}
s := gdoc[i+len(tag):]
if end := strings.Index(s, "\n"); end > 0 {
if !isValidPythonName(s[:end]) {
return "", "", fmt.Errorf("gopy: invalid identifier: %s", s[:end])
}
return s[:end], gdoc[:i] + s[end:], nil
}
return gname, gdoc, nil
}
// extractPythonNameFieldTag parses a struct field tag and returns
// a new python name. If the tag is not defined then the original
// name is returned.
// If the tag name is specified but is an invalid python identifier,
// then an error is returned.
func extractPythonNameFieldTag(gname, tag string) (string, error) {
const tagKey = "gopy"
if tag == "" {
return gname, nil
}
tagVal := reflect.StructTag(tag).Get(tagKey)
if tagVal == "" {
return gname, nil
}
if !isValidPythonName(tagVal) {
return "", fmt.Errorf("gopy: invalid identifier for struct field tag: %s", tagVal)
}
return tagVal, nil
}
// isValidPythonName returns true if the string is a valid
// python identifier name
func isValidPythonName(name string) bool {
if name == "" {
return false
}
return rxValidPythonName.MatchString(name)
}
var (
rxMatchFirstCap = regexp.MustCompile("([A-Z])([A-Z][a-z])")
rxMatchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
)
// toSnakeCase converts the provided string to snake_case.
// Based on https://gist.github.com/stoewer/fbe273b711e6a06315d19552dd4d33e6
func toSnakeCase(input string) string {
output := rxMatchFirstCap.ReplaceAllString(input, "${1}_${2}")
output = rxMatchAllCap.ReplaceAllString(output, "${1}_${2}")
output = strings.ReplaceAll(output, "-", "_")
return strings.ToLower(output)
}