From da5e91b2d6b5c8ad335b2212a7aed0d2a41be074 Mon Sep 17 00:00:00 2001 From: Christopher Brannon Date: Thu, 7 Apr 2022 18:59:18 -0700 Subject: [PATCH 1/3] Improve detection of Python, unbreak on musl. Add a shebang to build.sh. Some environments require it. E.G., systems using musl libc won't automagically exec unknown executable types with the shell. Try using pkg-config to detect Python. Also support Python 3.10. --- build.scm | 16 ++++++++++++---- build.sh | 1 + pyffi.egg | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/build.scm b/build.scm index 844a909..59e8051 100644 --- a/build.scm +++ b/build.scm @@ -1,6 +1,6 @@ (import (chicken base) (chicken format) (chicken process) - (chicken process-context) srfi-13 compile-file) + (chicken process-context) srfi-1 srfi-13 compile-file pkg-config) (define args (command-line-arguments)) @@ -19,15 +19,23 @@ (condition-case (python-try-compile flags ...) (t () #f))))) +(define (python-pkg-config-test package) + (and-let* ((cflags (pkg-config:cflags package)) + (libs (pkg-config:libs package))) + (python-test ("" cflags libs)))) + (define cpp+ld-options (let ((cflags (get-environment-variable "PYTHON_CFLAGS")) (lflags (get-environment-variable "PYTHON_LFLAGS")) (libs (get-environment-variable "PYTHON_LIBS"))) (if (and cflags lflags libs) (python-test ("" cflags (string-append lflags " " libs))) - (or (python-test ("" - "-I/System/Library/Frameworks/Python.framework/Headers" - "-framework Python")) + (or + (any python-pkg-config-test '("python3-embed" "python3" "python")) + (python-test ("" + "-I/System/Library/Frameworks/Python.framework/Headers" + "-framework Python")) + (python-test ("" "-I/usr/include/python3.10" "-lpython3.10")) (python-test ("" "-I/usr/include/python3.9m" "-lpython3.9m")) (python-test ("" "-I/usr/include/python3.9" "-lpython3.9")) (python-test ("" "-I/usr/include/python3.8m" "-lpython3.8m")) diff --git a/build.sh b/build.sh index c9c09ee..6078f83 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,4 @@ +#!/bin/sh # -*- sh -*- "$CHICKEN_CSI" -q -s build.scm "$CHICKEN_CSC $@" diff --git a/pyffi.egg b/pyffi.egg index fa38d2b..1eb6cef 100644 --- a/pyffi.egg +++ b/pyffi.egg @@ -5,7 +5,7 @@ (license "GPL-3") (category ffi) (dependencies srfi-1 srfi-69 bind utf8) - (build-dependencies compile-file srfi-13) + (build-dependencies compile-file pkg-config srfi-13) (test-dependencies test) (author "Ivan Raikov") (components (extension pyffi (custom-build "build.sh") (csc-options "-d2" "-O0"))) From 209ffc9ca709f5b704f22c78112c9af6cb7c3ef1 Mon Sep 17 00:00:00 2001 From: Christopher Brannon Date: Fri, 8 Apr 2022 09:25:54 -0700 Subject: [PATCH 2/3] define-pymethod keyword handling when length of rest list is odd. Mixing #!rest and #!key can lead to bugs when the length of the #!rest list is odd. Rework argument handling so that argument lists are parsed into a #!rest list and an alist of keyword pairs, rather than letting (lambda) directly handle the #!key arguments. --- pyffi.scm | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/pyffi.scm b/pyffi.scm index a6f765c..dcbe98f 100644 --- a/pyffi.scm +++ b/pyffi.scm @@ -25,17 +25,17 @@ *py-functions* (define-pyfun PyCallable_Check Py_DecRef) (define-pyslot PyObject_GetAttrString PyObject_SetAttrString ) - (define-pymethod PyObject_GetAttrString PyObject_CallObject PyObject_Call )) + (define-pymethod PyObject_GetAttrString PyObject_CallObject PyObject_Call parse-argument-list )) - (import scheme (chicken base) (chicken foreign) (chicken syntax) + (import scheme (chicken base) (chicken foreign) (chicken keyword) (chicken syntax) (chicken blob) (chicken locative) (chicken string) (chicken condition) (only (chicken memory) pointer?) (only (chicken port) port-name) - (only srfi-1 first second every filter take-while) + (only srfi-1 first second every break) srfi-4 srfi-69 bind utf8 utf8-lolevel utf8-srfi-13 utf8-srfi-14) - (import-for-syntax (chicken base) (chicken string) - (only srfi-1 first second every filter take-while) + (import-for-syntax (chicken base) (chicken keyword) (chicken string) + (only srfi-1 first second every break) srfi-69) (define (pyffi:error x . rest) @@ -623,6 +623,20 @@ EOF (,PyObject_SetAttrString ,obj ,(->string name) (,%car ,rest)))))))))) +(define (alistify-kwargs lst accepted-keywords) + (let loop ((lst lst) (kwargs '())) + (cond + ((null? lst) kwargs) + ((null? (cdr lst)) (pyffi:error 'alistify-kwargs "Bad keyword argument.")) + ((not (keyword? (car lst))) (pyffi:error 'alistify-kwargs "Not a keyword: " (car lst))) + ((not (member (car lst) accepted-keywords)) (pyffi:error 'alistify-kwargs "Unexpected keyword argument: " (car lst))) + (#t + (loop (cddr lst) (cons (list (keyword->string (car lst)) (cadr lst)) kwargs)))))) + +(define (parse-argument-list args accepted-keywords) + (receive (args* kwargs*) + (break keyword? args) + (values args* (alistify-kwargs kwargs* accepted-keywords)))) (define-syntax define-pymethod (er-macro-transformer @@ -635,17 +649,16 @@ EOF (%quote (r 'quote)) (%cons (r 'cons)) (%list (r 'list)) - (%identity (r 'identity)) - (%filter (r 'filter)) - (%take-while (r 'take-while)) + (%break (r 'break)) (%lambda (r 'lambda)) - (%symbol? (r 'symbol?)) + (%keyword? (r 'keyword?)) (%null? (r 'null?)) (%and (r 'and)) (%not (r 'not)) (%if (r 'if)) (%list->vector (r 'list->vector)) (%->string (r '->string)) + (parse-argument-list (r 'parse-argument-list)) (PyObject_GetAttrString (r 'PyObject_GetAttrString)) (PyObject_CallObject (r 'PyObject_CallObject)) (PyObject_Call (r 'PyObject_Call)) @@ -658,14 +671,14 @@ EOF (,PyObject_CallObject (,PyObject_GetAttrString ,obj ,(->string name) ) (,%list->vector ,rest))) - (let ((kwargs (cadr kw))) - `(,%define (,proc-name ,obj #!rest ,rest #!key ,@(map (lambda (x) (list x #f)) kwargs)) - (let ((kwargs (,%filter ,%identity - (,%list - ,@(map (lambda (k x) `(,%and ,x (,%list (,%->string (quote ,k)) ,x))) kwargs kwargs))))) + (let ((kwargs (map (compose string->keyword symbol->string) (cadr kw)))) + `(,%define (,proc-name ,obj #!rest ,rest) + (receive + (args kwargs) + (,parse-argument-list ,rest ',kwargs) (,PyObject_Call (,PyObject_GetAttrString ,obj ,(->string name) ) - (,%list->vector (,%take-while (,%lambda (x) (,%not (,%symbol? x))) ,rest)) + (list->vector args) (,%if (,%null? kwargs) #f kwargs)) )) )) From fcd9c14dc1af8cdda321ff1ab9fb633e357aa750 Mon Sep 17 00:00:00 2001 From: Christopher Brannon Date: Sat, 9 Apr 2022 16:39:05 -0700 Subject: [PATCH 3/3] Add more tests. --- pyffi.egg | 2 +- tests/run.scm | 113 ++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/pyffi.egg b/pyffi.egg index 1eb6cef..2c346e7 100644 --- a/pyffi.egg +++ b/pyffi.egg @@ -6,7 +6,7 @@ (category ffi) (dependencies srfi-1 srfi-69 bind utf8) (build-dependencies compile-file pkg-config srfi-13) - (test-dependencies test) + (test-dependencies brev-separate test) (author "Ivan Raikov") (components (extension pyffi (custom-build "build.sh") (csc-options "-d2" "-O0"))) ) diff --git a/tests/run.scm b/tests/run.scm index 77e83f5..da379a0 100644 --- a/tests/run.scm +++ b/tests/run.scm @@ -1,11 +1,110 @@ -(import pyffi) +(import (chicken condition) (chicken format) (chicken sort) brev-separate pyffi test) + +(define-ir-syntax* + ((test-signals-condition test-name kind body ...) +`(test-assert ,test-name (handle-exceptions exn ((condition-predicate ,kind) exn) (begin ,@body #f))))) (py-start) -(py-eval "print(open)") (py-import "sys") -(assert (string? (py-eval "sys.version"))) +(define-pyfun ("lambda x: (x.real, x.imag)" complex->tuple) x) +(define-pyfun ("lambda x: sorted(list(x.items()))" dict-items) x) +(define-pyfun "repr" obj) +(define-pyfun ("lambda r, obj: eval(r) == obj and type(eval(r)) == type(obj)" repr-compare) r obj) + +(test-begin "basic tests") +(test-assert "major version is either 3 or 2" (member (py-eval "sys.version_info[0]") '(3 2))) +(test-assert "sys.version is a string" (string? (py-eval "sys.version"))) (print "Python version: " (py-eval "sys.version")) -(print (py-eval "1")) -(print (py-eval "1.0")) -(print (py-eval "float('-inf')")) -(print (py-eval "complex(1.0, 2.0)")) +(test-end "basic tests") + +(test-begin "python->scheme type conversion") +(test "python integer converted to scheme" 12345 (py-eval "12345")) +;; Pick a number that has an exact binary floating point representation. +(test "python float converts to scheme" 0.25 (py-eval "0.25")) +(test "python inf converts to scheme" +inf.0 (py-eval "float('inf')")) +(test "python -inf converts to scheme" -inf.0 (py-eval "float('-inf')")) +(test-assert "python nan converts to scheme" (nan? (py-eval "float('nan')"))) +(test "python string converts to scheme" "hello world" (py-eval "'hello ' + 'world'")) +(test "python complex converts to scheme" 1.0+2.0i (py-eval "complex(1.0, 2.0)")) +(test "python tuple converts to scheme vector" #(10 20 30) (py-eval "(10, 20, 30)")) +(test "python list converts to scheme" '(10 20 30) (py-eval "[10, 20, 30]")) + +(test "python dict converts to scheme alist" +'(("a" 1) ("b" 2) ("c" 3)) +(sort (py-eval "{'a': 1, 'b': 2, 'c': 3}") (lambda (x y) (stringscheme type conversion") + +(test-begin "scheme->python type conversion") +(test-assert "scheme integer converted to python" (repr-compare "12345" 12345)) +;; Pick a number that has an exact binary floating point representation. +(test-assert "Scheme float converts to python" (repr-compare "0.25" 0.25)) +(test-assert "scheme inf converts to python" (repr-compare "float('inf')" +inf.0)) +(test-assert "scheme inf converts to python" (repr-compare "float('-inf')" -inf.0)) +(test "scheme nan converts to python" "nan" (repr +nan.0)) +(test-assert "scheme string converts to python" (repr-compare "'hello'" "hello")) +(test-assert "scheme complex converts to python" (repr-compare "2.0+4.0j" 2.0+4.0i)) +(test-assert "scheme vector converts to python tuple" (repr-compare "(10, 20, 30)" #(10 20 30))) +(test-assert "scheme list converts to python" (repr-compare "[10, 20, 30]" '(10 20 30))) +(test-assert "scheme alist converts to python dict" + (repr-compare "{'a': 1, 'b': 2, 'c': 3}" '(("a" 1) ("b" 2) ("c" 3)))) +(test-end "scheme->python type conversion") + +(py-eval #< (test-failure-count) 1) "tests" "test")) + (print "=====")) +(test-exit)