Merge pull request #13 from go-enry/python-wrapper

Python: API to expose highest-level enry.GetLanguage
This commit is contained in:
Alexander 2020-04-24 20:57:37 +02:00 committed by GitHub
commit 4b468762b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 40 deletions

View File

@ -1,16 +1,15 @@
# Python bindings for enry # Python bindings for enry
Python bingings thoug cFFI (API, out-of-line) for calling enr Go functions though CGo wrapper. Python bindings through cFFI (API, out-of-line) for calling enry Go functions exposed by CGo wrapper.
## Build ## Build
``` ```
$ make static $ cd .. && make static
$ python enry_build.py $ python build_enry.py
``` ```
Will build static library for Cgo wrapper `libenry`, then generate and build `enry.c` Will build a static library for Cgo wrapper `libenry`, then generate and build `enry.c` - a CPython extension that provides actual bindings.
- a CPython extension that
## Run ## Run
@ -21,9 +20,9 @@ $ python enry.py
``` ```
## TODOs ## TODOs
- [ ] try ABI mode, to aviod dependency on C compiler on install (+perf test?) - [x] helpers for sending/receiving Go slices to C
- [ ] ready `libenry.h` and generate `ffibuilder.cdef` content - [ ] read `libenry.h` and generate `ffibuilder.cdef(...)` content
- [ ] helpers for sending/recieving Go slices to C
- [ ] cover the rest of enry API - [ ] cover the rest of enry API
- [ ] add `setup.py` - [ ] add `setup.py`
- [ ] build/release automation on CI (publish on pypi) - [ ] build/release automation on CI (publish on pypi)
- [ ] try ABI mode, to avoid dependency on C compiler on install (+perf test?)

View File

@ -3,10 +3,19 @@ ffibuilder = FFI()
# cdef() expects a single string declaring the C types, functions and # cdef() expects a single string declaring the C types, functions and
# globals needed to use the shared object. It must be in valid C syntax. # globals needed to use the shared object. It must be in valid C syntax.
# Taken from java/shared/libenry.h
ffibuilder.cdef(""" ffibuilder.cdef("""
typedef unsigned char GoUint8;
typedef long long GoInt64;
typedef GoInt64 GoInt;
typedef struct { const char *p; ptrdiff_t n; } _GoString_; typedef struct { const char *p; ptrdiff_t n; } _GoString_;
typedef _GoString_ GoString; typedef _GoString_ GoString;
typedef unsigned char GoUint8;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
extern GoString GetLanguage(GoString p0, GoSlice p1);
/* Return type for GetLanguageByExtension */ /* Return type for GetLanguageByExtension */
struct GetLanguageByExtension_return { struct GetLanguageByExtension_return {

View File

@ -4,73 +4,85 @@ Python library calling enry Go implementation trough cFFI (API, out-of-line) and
from _c_enry import ffi, lib from _c_enry import ffi, lib
## Helpers ## cgo -> ffi helpers
def py_bytes_to_go(py_bytes: bytes):
c_bytes = ffi.new("char[]", len(py_bytes))
go_slice = ffi.new("GoSlice *", [c_bytes, len(py_bytes), len(py_bytes)])
return (go_slice[0], c_bytes)
def py_str_to_go(py_str: str):
str_bytes = py_str.encode()
c_str = ffi.new("char[]", str_bytes)
go_str = ffi.new("_GoString_ *", [c_str, len(str_bytes)])
return (go_str[0], c_str)
def go_str_to_py(go_str): def go_str_to_py(go_str: str):
str_len = go_str.n str_len = go_str.n
if str_len > 0: if str_len > 0:
return ffi.unpack(go_str.p, go_str.n).decode() return ffi.unpack(go_str.p, go_str.n).decode()
return "" return ""
def go_bool_to_py(go_bool: bool):
def py_str_to_go(py_str):
str_bytes = py_str.encode()
c_str = ffi.new("char[]", str_bytes)
go_str = ffi.new("_GoString_ *", [c_str, len(str_bytes)])
return go_str[0]
def go_bool_to_py(go_bool):
return go_bool == 1 return go_bool == 1
## API ## API, TODO(bzz): add docstrings
def language(filename: str, content: bytes) -> str:
fName, c_str = py_str_to_go(filename)
fContent, c_bytes = py_bytes_to_go(content)
guess = lib.GetLanguage(fName, fContent)
lang = go_str_to_py(guess)
return lang
def language_by_extension(filename: str) -> str: def language_by_extension(filename: str) -> str:
fName = py_str_to_go(filename) fName, c_str = py_str_to_go(filename)
guess = lib.GetLanguageByExtension(fName) guess = lib.GetLanguageByExtension(fName)
lang = go_str_to_py(guess.r0) lang = go_str_to_py(guess.r0)
return lang return lang
def language_by_filename(filename: str) -> str: def language_by_filename(filename: str) -> str:
fName = py_str_to_go(filename) fName, c_str = py_str_to_go(filename)
guess = lib.GetLanguageByFilename(fName) guess = lib.GetLanguageByFilename(fName)
lang = go_str_to_py(guess.r0) lang = go_str_to_py(guess.r0)
return lang return lang
def is_vendor(filename: str) -> bool: def is_vendor(filename: str) -> bool:
fName = py_str_to_go(filename) fName, c_str = py_str_to_go(filename)
guess = lib.IsVendor(fName) guess = lib.IsVendor(fName)
return go_bool_to_py(guess) return go_bool_to_py(guess)
## Tests ## Tests
from collections import namedtuple
def main(): def main():
TestFile = namedtuple("TestFile", "name, content, lang")
files = [ files = [
"Parse.hs", "some.cpp", "and.go", "type.h", ".bashrc", ".gitignore" TestFile("Parse.hs", b"", "Haskell"), TestFile("some.cpp", b"", "C++"),
TestFile("orand.go", b"", "Go"), TestFile("type.h", b"", "C"),
TestFile(".bashrc", b"", "Shell"), TestFile(".gitignore", b"", "Ignore List")
] ]
print("strategy: extension") print("\nstrategy: extension")
for filename in files: for f in files:
lang = language_by_extension(filename) lang = language_by_extension(f.name)
print("file: {:10s} language: '{}'".format(filename, lang)) print("\tfile: {:10s} language: '{}'".format(f.name, lang))
print("\nstrategy: filename") print("\nstrategy: filename")
for filename in files: for f in files:
lang = language_by_filename(filename) lang = language_by_filename(f.name)
print("file: {:10s} language: '{}'".format(filename, lang)) print("\tfile: {:10s} language: '{}'".format(f.name, lang))
print("\ncheck: is vendor?") print("\ncheck: is vendor?")
for filename in files: for f in files:
vendor = is_vendor(filename) vendor = is_vendor(f.name)
print("file: {:10s} vendor: '{}'".format(filename, vendor)) print("\tfile: {:10s} vendor: '{}'".format(f.name, vendor))
print("\nstrategy: all")
for f in files:
lang = language(f.name, f.content)
print("\tfile: {:10s} language: '{}'".format(f.name, lang))
assert lang == f.lang, "Expected '{}' but got '{}'".format(f.lang, lang)
if __name__ == "__main__": if __name__ == "__main__":
main() main()