Skip to content

Commit 7db63ec

Browse files
committed
Allow kw argument instance functions. 2.000 Release
1 parent c4e9f7b commit 7db63ec

File tree

7 files changed

+193
-75
lines changed

7 files changed

+193
-75
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
# Time for a ChangeLog!
2+
## 2.000
3+
* You can now create instance functions that accept both positional and keyword arguments -
4+
libpython-clj2.python/make-kw-instance-fn. See test at
5+
test/libpython-clj2.python.classes-test.
6+
* latest dtype-next - see documentation for tech.v3.datatype/set-value!.
7+
* Fix for using ffi on arm64 platforms.
8+
29

310
## 2.00-beta-22
411
* Fix to PR-166 - strings in docs weren't properly escaped.
@@ -17,7 +24,7 @@
1724
* Additional fix for 162 to allow booleans to be considered primitives and be output inline.
1825
* Experimental fix for [issue 164](https://github.com/clj-python/libpython-clj/issues/164) - Unsigned int64 fails with
1926
`OverflowError: int too big to convert`.
20-
27+
2128
## 2.00-beta-17
2229
* Almost fix fo [issue 163](https://github.com/clj-python/libpython-clj/issues/163) - codegen fails with JVM primitives.
2330

deps.edn

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{:paths ["src"]
22
:deps {org.clojure/clojure {:mvn/version "1.10.2" :scope "provided"}
3-
cnuernber/dtype-next {:mvn/version "8.022"}
3+
cnuernber/dtype-next {:mvn/version "8.024"}
44
net.java.dev.jna/jna {:mvn/version "5.7.0"}
55
org.clojure/data.json {:mvn/version "1.0.0"}}
66

@@ -46,7 +46,7 @@
4646
:exec-fn hf.depstar/jar
4747
:exec-args {:group-id "clj-python"
4848
:artifact-id "libpython-clj"
49-
:version "2.00-beta-24-SNAPSHOT"
49+
:version "2.000"
5050
:sync-pom true
5151
:jar "target/libpython-clj.jar"}}
5252
:deploy

src/libpython_clj2/python.clj

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,22 @@ Options:
384384
"Make an callable instance function - a function which will be passed the 'this'
385385
object as it's first argument. In addition, this function calls `make-callable`
386386
with a `arg-converter` defaulted to `as-jvm`. See documentation for
387-
make-callable."
387+
[[libpython-clj2.python.class/make-instance-fn."
388388
([ifn options] (py-class/make-tuple-instance-fn ifn options))
389389
([ifn] (make-instance-fn ifn nil)))
390390

391391

392+
(defn make-kw-instance-fn
393+
"Make an kw callable instance function - function by default is passed 2 arguments,
394+
the positional argument vector and a map of keyword arguments. Results are marshalled
395+
back to python using [[libpython-clj2.python.fn/bridged-fn-arg->python]] which is also
396+
used when bridging an object into python. See documentation for
397+
[[libpython-clj2.python.class/make-kw-instance-fn]]."
398+
([ifn options] (py-class/make-kw-instance-fn ifn options))
399+
([ifn] (make-kw-instance-fn ifn nil)))
400+
401+
402+
392403
(defn ^:no-doc make-tuple-instance-fn
393404
[ifn & {:as options}]
394405
(make-instance-fn ifn options))

src/libpython_clj2/python/bridge_as_jvm.clj

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@
201201
py-proto/PyCall
202202
(call [callable# arglist# kw-arg-map#]
203203
(with-gil
204-
(-> (py-fn/call-py-fn @pyobj*# arglist# kw-arg-map# fn-arg->python)
204+
(-> (py-fn/call-py-fn @pyobj*# arglist# kw-arg-map# py-fn/bridged-fn-arg->python)
205205
(py-base/as-jvm))))
206206
(marshal-return [callable# retval#]
207207
(with-gil
@@ -238,35 +238,12 @@
238238
(.write ^java.io.Writer w ^String (.toString ^Object pyobj)))
239239

240240

241-
(defn fn-arg->python
242-
"Slightly clever so we can pass ranges and such as function arguments."
243-
([item opts]
244-
(cond
245-
(instance? PBridgeToPython item)
246-
(py-proto/as-python item opts)
247-
(dt-proto/convertible-to-range? item)
248-
(py-copy/->py-range item)
249-
(dtype/reader? item)
250-
(py-proto/->python (dtype/->reader item) opts)
251-
;;There is one more case here for iterables that aren't anything else -
252-
;; - specifically for sequences.
253-
(and (instance? Iterable item)
254-
(not (instance? Map item))
255-
(not (instance? String item))
256-
(not (instance? Set item)))
257-
(py-proto/as-python item opts)
258-
:else
259-
(py-base/->python item opts)))
260-
([item]
261-
(fn-arg->python item nil)))
262-
263-
264241
(defn call-impl-fn
265242
[fn-name att-map args]
266243
(if-let [py-fn* (get att-map fn-name)]
267244
;;laziness is carefully constructed here in order to allow the arguments to
268245
;;be released within the context of the function call during fn.clj call-py-fn.
269-
(-> (py-fn/call-py-fn @py-fn* args nil fn-arg->python)
246+
(-> (py-fn/call-py-fn @py-fn* args nil py-fn/bridged-fn-arg->python)
270247
(py-base/as-jvm))
271248
(throw (UnsupportedOperationException.
272249
(format "Python object has no attribute: %s"

src/libpython_clj2/python/class.clj

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
(:import [clojure.lang IFn]))
1111

1212

13-
(defn- py-fn->instance-fn
13+
(defn py-fn->instance-fn
1414
"Given a python callable, return an instance function meant to be used
1515
in class definitions."
1616
[py-fn]
@@ -39,6 +39,36 @@
3939
(py-fn->instance-fn)))))
4040

4141

42+
(defn make-kw-instance-fn
43+
"Make an instance function - a function which will be passed the 'this' object as
44+
it's first argument. In this case the default behavior is to
45+
pass as-jvm bridged python object ptr args and kw dict args to the clojure function without
46+
marshalling. Self will be the first argument of the arg vector.
47+
48+
49+
50+
Options:
51+
52+
* `:kw-arg-converter` - passed two arguments, the positional arguments as a python ptr
53+
and the keyword arguments as a python pointer. The clj-fn is 'applied' to the result
54+
of `:arg-converter` which has the same default as [[make-tuple-fn]].
55+
56+
* `:result-converter` - defaults to the same argument conversion rules of bridged
57+
objects."
58+
([clj-fn & [{:keys [arg-converter
59+
result-converter]
60+
:or {arg-converter py-base/as-jvm}
61+
:as options}]]
62+
(let [options (assoc options :arg-converter arg-converter)
63+
result-converter (or result-converter #(py-fn/bridged-fn-arg->python % options))]
64+
(py-ffi/with-gil
65+
;;Explicity set arg-converter to override make-tuple-fn's default
66+
;;->jvm arg-converter.
67+
(-> (py-fn/make-kw-fn clj-fn (assoc options :result-converter result-converter))
68+
;;Mark this as an instance function.
69+
(py-fn->instance-fn))))))
70+
71+
4272
(defn create-class
4373
"Create a new class object. Any callable values in the cls-hashmap
4474
will be presented as instance methods.

src/libpython_clj2/python/fn.clj

Lines changed: 129 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@
99
[libpython-clj2.python.base :as py-base]
1010
[libpython-clj2.python.protocols :as py-proto]
1111
[libpython-clj2.python.gc :as pygc]
12+
[libpython-clj2.python.copy :as py-copy]
1213
[tech.v3.datatype.ffi :as dt-ffi]
1314
[tech.v3.datatype.ffi.size-t :as ffi-size-t]
1415
[tech.v3.datatype.struct :as dt-struct]
16+
[tech.v3.datatype :as dtype]
17+
[tech.v3.datatype.protocols :as dt-proto]
1518
[clojure.tools.logging :as log]
1619
[clojure.stacktrace :as st])
17-
(:import [tech.v3.datatype.ffi Pointer]))
20+
(:import [tech.v3.datatype.ffi Pointer]
21+
[java.util Map Set]
22+
[libpython_clj2.python.protocols PBridgeToPython]))
1823

1924
(set! *warn-on-reflection* true)
2025

@@ -26,7 +31,10 @@
2631
{:name :ml_doc :datatype (ffi-size-t/ptr-t-type)}]))
2732

2833

29-
(def tuple-fn-iface* (delay (dt-ffi/define-foreign-interface :pointer? [:pointer :pointer])))
34+
(def tuple-fn-iface*
35+
(delay (dt-ffi/define-foreign-interface :pointer? [:pointer :pointer])))
36+
(def kw-fn-iface*
37+
(delay (dt-ffi/define-foreign-interface :pointer? [:pointer :pointer :pointer])))
3038

3139

3240
(def ^{:tag 'long} METH_VARARGS 0x0001)
@@ -35,59 +43,136 @@
3543
(def ^{:tag 'long} METH_NOARGS 0x0004)
3644

3745

46+
(defn- internal-make-py-c-fn
47+
[ifn fn-iface raw-arg-converter meth-type
48+
{:keys [name doc result-converter]
49+
:or {name "_unamed"
50+
doc "no documentation provided"}}]
51+
(py-ffi/with-gil
52+
(let [fn-inst
53+
(dt-ffi/instantiate-foreign-interface
54+
fn-iface
55+
(fn [self tuple-args & [kw-args]]
56+
(try
57+
(let [retval (apply ifn (raw-arg-converter tuple-args kw-args))]
58+
(if result-converter
59+
(py-ffi/untracked->python retval result-converter)
60+
retval))
61+
(catch Throwable e
62+
(log/error e "Error executing clojure function.")
63+
(py-ffi/PyErr_SetString
64+
(py-ffi/py-exc-type)
65+
(format "%s:%s" e (with-out-str
66+
(st/print-stack-trace e))))))))
67+
fn-ptr (dt-ffi/foreign-interface-instance->c fn-iface fn-inst)
68+
;;no resource tracking - we leak the struct
69+
method-def (dt-struct/new-struct :pymethoddef {:resource-type nil
70+
:container-type :native-heap})
71+
name (dt-ffi/string->c name {:resource-type nil})
72+
doc (dt-ffi/string->c doc {:resource-type nil})]
73+
(.put method-def :ml_name (.address (dt-ffi/->pointer name)))
74+
(.put method-def :ml_meth (.address (dt-ffi/->pointer fn-ptr)))
75+
(.put method-def :ml_flags meth-type)
76+
(.put method-def :ml_doc (.address (dt-ffi/->pointer doc)))
77+
;;the method def cannot ever go out of scope
78+
(py-ffi/retain-forever (gensym) {:md method-def
79+
:name name
80+
:doc doc
81+
:fn-ptr fn-ptr
82+
:fn-inst fn-inst})
83+
;;no self, no module reference
84+
(-> (py-ffi/PyCFunction_NewEx method-def nil nil)
85+
(py-ffi/track-pyobject)))))
86+
87+
88+
(defn raw-tuple-arg-converter
89+
[arg-converter tuple-args kw-args]
90+
;;no kw arguments
91+
(->> (range (py-ffi/PyTuple_Size tuple-args))
92+
(mapv (fn [idx]
93+
(-> (py-ffi/PyTuple_GetItem tuple-args idx)
94+
(arg-converter))))))
95+
96+
97+
(defn bridged-fn-arg->python
98+
"Slightly clever so we can pass ranges and such as function arguments."
99+
([item opts]
100+
(cond
101+
(instance? PBridgeToPython item)
102+
(py-proto/as-python item opts)
103+
(dt-proto/convertible-to-range? item)
104+
(py-copy/->py-range item)
105+
(dtype/reader? item)
106+
(py-proto/->python (dtype/->reader item) opts)
107+
;;There is one more case here for iterables that aren't anything else -
108+
;; - specifically for sequences.
109+
(and (instance? Iterable item)
110+
(not (instance? Map item))
111+
(not (instance? String item))
112+
(not (instance? Set item)))
113+
(py-proto/as-python item opts)
114+
:else
115+
(py-base/->python item opts)))
116+
([item]
117+
(bridged-fn-arg->python item nil)))
118+
119+
120+
(defn convert-kw-args
121+
[{:keys [arg-converter] :as options} tuple-args kw-args]
122+
[(raw-tuple-arg-converter arg-converter tuple-args nil)
123+
(->> (py-proto/as-jvm kw-args options)
124+
(into {}))])
125+
126+
38127
(defn make-tuple-fn
39128
([ifn {:keys [arg-converter
40129
result-converter
41130
name doc]
42131
:or {arg-converter py-base/->jvm
43132
result-converter py-base/->python
44133
name "_unamed"
45-
doc "no documentation provided"}}]
46-
(py-ffi/with-gil
47-
(let [arg-converter (or arg-converter identity)
48-
fn-inst
49-
(dt-ffi/instantiate-foreign-interface
50-
@tuple-fn-iface*
51-
(fn [self tuple-args]
52-
(try
53-
(let [retval
54-
(apply ifn
55-
(->> (range (py-ffi/PyTuple_Size tuple-args))
56-
(map (fn [idx]
57-
(-> (py-ffi/PyTuple_GetItem tuple-args idx)
58-
(arg-converter))))))]
59-
(if result-converter
60-
(py-ffi/untracked->python retval result-converter)
61-
retval))
62-
(catch Throwable e
63-
(log/error e "Error executing clojure function.")
64-
(py-ffi/PyErr_SetString
65-
(py-ffi/py-exc-type)
66-
(format "%s:%s" e (with-out-str
67-
(st/print-stack-trace e))))))))
68-
fn-ptr (dt-ffi/foreign-interface-instance->c @tuple-fn-iface* fn-inst)
69-
;;no resource tracking - we leak the struct
70-
method-def (dt-struct/new-struct :pymethoddef {:resource-type nil
71-
:container-type :native-heap})
72-
name (dt-ffi/string->c name {:resource-type nil})
73-
doc (dt-ffi/string->c doc {:resource-type nil})]
74-
(.put method-def :ml_name (.address (dt-ffi/->pointer name)))
75-
(.put method-def :ml_meth (.address (dt-ffi/->pointer fn-ptr)))
76-
(.put method-def :ml_flags METH_VARARGS)
77-
(.put method-def :ml_doc (.address (dt-ffi/->pointer doc)))
78-
;;the method def cannot ever go out of scope
79-
(py-ffi/retain-forever (gensym) {:md method-def
80-
:name name
81-
:doc doc
82-
:fn-ptr fn-ptr
83-
:fn-inst fn-inst})
84-
;;no self, no module reference
85-
(-> (py-ffi/PyCFunction_NewEx method-def nil nil)
86-
(py-ffi/track-pyobject)))))
134+
doc "no documentation provided"}
135+
:as options}]
136+
(let [arg-converter (or arg-converter identity)
137+
;;apply defaults to options map.
138+
options (assoc options
139+
:arg-converter arg-converter
140+
:result-converter result-converter
141+
:name name
142+
:doc doc)]
143+
(internal-make-py-c-fn ifn @tuple-fn-iface*
144+
#(raw-tuple-arg-converter arg-converter %1 %2)
145+
METH_VARARGS
146+
options)))
87147
([ifn]
88148
(make-tuple-fn ifn nil)))
89149

90150

151+
(defn make-kw-fn
152+
([ifn {:keys [arg-converter
153+
result-converter
154+
name doc
155+
kw-arg-converter]
156+
:or {arg-converter py-base/->jvm
157+
result-converter py-base/->python
158+
name "_unamed"
159+
doc "no documentation provided"}
160+
:as options}]
161+
(let [arg-converter (or arg-converter :identity)
162+
options (assoc options
163+
:arg-converter arg-converter
164+
:result-converter result-converter
165+
:name name
166+
:doc doc)
167+
kw-arg-converter (or kw-arg-converter #(convert-kw-args options %1 %2))]
168+
(internal-make-py-c-fn ifn @kw-fn-iface*
169+
kw-arg-converter
170+
(bit-or METH_VARARGS METH_KEYWORDS)
171+
options)))
172+
([ifn]
173+
(make-kw-fn ifn nil)))
174+
175+
91176
(defn call-py-fn
92177
[callable arglist kw-arg-map arg-converter]
93178
(py-ffi/with-gil

test/libpython_clj2/classes_test.clj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,18 @@
3333
(pr-str {"name" (py/py.- self name)
3434
"shares" (py/py.- self shares)
3535
"price" (py/py.- self price)})))
36+
"kw_clj_fn" (py/make-kw-instance-fn
37+
(fn [pos-args kw-args]
38+
(let [self (first pos-args)
39+
price (double (py/py.- self price))]
40+
;;keywords become strings!!
41+
(apply + price (kw-args "a")
42+
(drop 1 pos-args)))))
3643
"clsattr" 55})
3744
new-instance (cls-obj "ACME" 50 90)]
3845
(is (= 4500
3946
(py/$a new-instance cost)))
4047
(is (= 55 (py/py.- new-instance clsattr)))
4148
(is (= {"name" "ACME", "shares" 50, "price" 90}
42-
(edn/read-string (.toString new-instance))))))
49+
(edn/read-string (.toString new-instance))))
50+
(is (= 116.0 (py/call-attr-kw new-instance "kw_clj_fn" [1 2 3] {:a 20})))))

0 commit comments

Comments
 (0)