Created
June 13, 2016 22:19
-
-
Save nickmbailey/3f11300e944e17d92edd6d42da785ab3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns lcm.utils.jython | |
"This NS has functionality for interop between jython and clojure" | |
(:require [clojure.tools.logging :as log] | |
[clojure.pprint :refer [pprint]] | |
[slingshot.slingshot :refer [throw+ try+]]) | |
(:import [org.python.core | |
Py | |
JyAttribute | |
PyString | |
PyInteger | |
PyObject | |
PySystemState])) | |
;; The instancing of PySystemState __must__be done. | |
;; I believe it has to do with maintaining a strong reference | |
;; to an instance of PySystemState because Jython only holds a | |
;; weak reference to it somehow. | |
(defonce sys-module (PySystemState.)) | |
;; The fn call wrapper is an odd circular dependency. It uses the | |
;; cast functions to convert args and return values automatically but | |
;; because one of those might be a function, the cast rules need to use | |
;; it. So it is forward declared here. | |
(declare wrap-jython-fn) | |
(declare cast:py->clj) | |
(declare pylist-to-vector) | |
(declare pydictionary-to-map) | |
(declare call-method) | |
;; | |
;; Utility | |
;; | |
(defn- invoked-with-keywords? | |
"Checks to see if the args used to invoke a clojure function look like the | |
arguments follow python's keyword argument style. This function does not handle | |
mixed argument passing, which should evaluate to false." | |
[args] | |
(and (even? (count args)) | |
(keyword? (first args)))) | |
(defn call-with-dict | |
"Helper function for calling wrapped python functions. | |
This function takes a dictionary and calls the python function | |
with the :keyword / value pairs inside of the dictionary. | |
Example: | |
(call-with-dict wrapped-python-function | |
{:name 'John' :age 45})" | |
[wrapped-function params] | |
(apply wrapped-function | |
(flatten (into [] params)))) | |
(defn getattr | |
"Prettify calling __getattr__" | |
[obj name] | |
(.__getattr__ obj name)) | |
(defn- dir | |
"Prettify calling __dir__" | |
[obj] | |
(.__dir__ obj)) | |
(defn pytuple->clojure-vector | |
[pytuple] | |
(let [pylist (.getArray pytuple) | |
;; Note that vec will alias the elements, not copy them | |
vector-of-pyobjects (vec pylist) | |
vector-of-cljobjects (into [] (map cast:py->clj vector-of-pyobjects))] | |
vector-of-cljobjects)) | |
(defn pyinstance->clojure-map | |
"Convert a PyInstance to a clojure map of the instance attributes." | |
[pyinstance] | |
(let [pyinstance-dict (getattr pyinstance "__dict__")] | |
(pydictionary-to-map pyinstance-dict))) | |
(defn cast-pytype | |
"PyType should be handled specially, because it is kind of a | |
container for other types. However, we do not need it yet" | |
[obj] | |
;; TODO FIXME in the future | |
nil) | |
;; | |
;; Casting | |
;; | |
;; Define rules for casing from clojure objects to their jython equivalent. This | |
;; list is not an exhaustive since it doesn't take clojure maps or sequences | |
;; into account. Adding casting for collections is a bit error-prone and finicky, | |
;; and we will implement new conversions as needed. | |
(defmulti cast:clj->py type) | |
(defmethod cast:clj->py java.lang.Integer [obj] (Py/newInteger obj)) | |
(defmethod cast:clj->py java.lang.Long [obj] (Py/newLong obj)) | |
(defmethod cast:clj->py java.lang.Float [obj] (Py/newFloat obj)) | |
(defmethod cast:clj->py java.lang.Double [obj] (Py/newFloat obj)) | |
(defmethod cast:clj->py java.lang.String [obj] (Py/newString obj)) | |
(defmethod cast:clj->py java.util.Date [obj] (Py/newDate obj)) | |
(defmethod cast:clj->py java.sql.Timestamp [obj] (Py/newDatetime obj)) | |
(defmethod cast:clj->py java.sql.Time [obj] (Py/newTime obj)) | |
(defmethod cast:clj->py :default [obj] obj) | |
;; Define rules for casting from jython objects to their clojure equivalent. This | |
;; list it not exhaustive it doesn't account for python dictionaries or tuple types. | |
;; | |
;; WARNING: DO NOT dispatch on the (type obj). | |
;; Dispatching on (type obj) causes us to have to embed the jython class | |
;; in the defmethod. This can cause us to hit http://bugs.jython.org/issue2492 | |
;; That issue can be incredibly difficult to diagnose. | |
;; Instead, dispatch on (str (type obj)), this avoids the jython bug. | |
;; However, this does mean that you must include "class " as a prefix | |
;; in front of class names. | |
(defmulti cast:py->clj | |
(fn [obj] | |
(str (type obj)))) | |
(defmethod cast:py->clj "class org.python.core.PyInteger" [obj] (.asInt obj)) | |
(defmethod cast:py->clj "class org.python.core.PyLong" [obj] (.asLong obj)) | |
(defmethod cast:py->clj "class org.python.core.PyFloat" [obj] (.asDouble obj)) | |
(defmethod cast:py->clj "class org.python.core.PyString" [obj] (.asString obj)) | |
(defmethod cast:py->clj "class org.python.core.PyUnicode" [obj] (.asString obj)) | |
(defmethod cast:py->clj "class org.python.core.PyList" [obj] (pylist-to-vector obj true)) | |
(defmethod cast:py->clj "class org.python.core.PyDictionary" [obj] (pydictionary-to-map obj)) | |
(defmethod cast:py->clj "class org.python.core.PyFunction" [obj] (wrap-jython-fn obj)) | |
(defmethod cast:py->clj "class org.python.core.PyInstance" [obj] (pyinstance->clojure-map obj)) | |
(defmethod cast:py->clj "class org.python.core.PyTuple" [obj] (pytuple->clojure-vector obj)) | |
(defmethod cast:py->clj "class org.python.core.PyType" [obj] (cast-pytype obj)) | |
;; For now, we are converting PyDictionaryDerived using | |
;; the normal dictionary conversion. A common use case for | |
;; this type is converting an ordered dictionary. If we want | |
;; to specifically handle those and preserve order, we can look | |
;; into a clojure data structure such as array-map, which | |
;; has some properties of preserving keys insertion order. | |
;; However, simple conversion to clojure maps is good enough | |
;; for our current needs. | |
(defmethod cast:py->clj "class org.python.core.PyDictionaryDerived" | |
[obj] | |
(pydictionary-to-map obj)) | |
;; For more information on PyObjectDerived, take a look at | |
;; src/org/python/core/PyJavaType.java in the jython source, | |
;; specifically at the wrapJavaObject method. | |
(defmethod cast:py->clj "class org.python.core.PyObjectDerived" | |
[obj] | |
(JyAttribute/getAttr obj JyAttribute/JAVA_PROXY_ATTR)) | |
(defmethod cast:py->clj :default | |
[obj] | |
obj) | |
(defn len | |
"Calls the jython len method and converts the result to a | |
clojure int." | |
[lenable-pyobject] | |
(cast:py->clj (call-method lenable-pyobject "__len__"))) | |
(defn repr | |
"Calls __repr__() on the supplied object" | |
[instance] | |
(cast:py->clj | |
(call-method instance "__repr__"))) | |
(defn call-method | |
"Call a method on a jython object" | |
([instance method-name] | |
(let [jymethod (getattr instance method-name) | |
result (.__call__ jymethod)] | |
result)) | |
([instance method-name first-arg] | |
(let [jymethod (getattr instance method-name) | |
result (.__call__ jymethod first-arg)] | |
result))) | |
;; | |
;; Working with functions | |
;; | |
(defn convert-args | |
"Helper function to convert a sequence of clojure arguments into a PyObject | |
Java array with each argument converted to the corresponding python type." | |
[clj-args] | |
(into-array PyObject (map cast:clj->py clj-args))) | |
(defn wrap-jython-fn | |
"Wraps up a PyFunction object type so that it can called from Clojure like a native | |
function. Input arguments can be provided in a purely ordinal fashion or they can be | |
keyword specified. Return values are also converted." | |
[alien-fn] | |
(fn [& args] | |
"TODO: Is it possible to inject the __doc__ string from alien-fn here?" | |
(let [result (if (invoked-with-keywords? args) | |
(.__call__ alien-fn (convert-args (take-nth 2 (rest args))) | |
(into-array java.lang.String (map name (take-nth 2 args)))) | |
(.__call__ alien-fn (convert-args args)))] | |
(cast:py->clj result)))) | |
;; | |
;; Collection casting | |
;; | |
(defn pylist-to-vector | |
"Convert a python list to a clojure vector. | |
If a boolean true is passed as the second argument, | |
then the elements of the list will be converted to clojure equivalents." | |
([pylist] | |
(pylist-to-vector pylist false)) | |
([pylist convert-elements] | |
(let [iter (call-method pylist "__iter__") | |
iter-len (len pylist)] | |
(loop [results [] | |
iteration 0 | |
current-iter iter] | |
(if (< iteration iter-len) | |
(recur (conj results | |
;; Warning: this does not handle recursive data structures | |
(if convert-elements | |
(cast:py->clj (call-method current-iter "next")) | |
(call-method current-iter "next"))) | |
(+ 1 iteration) | |
current-iter) | |
results))))) | |
(defn pydictionary-to-map | |
"Convert a PyDictionary to a clojure map" | |
[pydictionary] | |
(let [dest {} | |
pykeys (call-method pydictionary "keys") | |
iter-len (len pykeys) | |
cljkeys (pylist-to-vector pykeys)] | |
(loop [results {} | |
iteration 0 | |
current-keys cljkeys] | |
(let [current-key (first current-keys)] | |
(if (< iteration iter-len) | |
(recur (assoc results | |
(cast:py->clj current-key) | |
(cast:py->clj | |
(call-method pydictionary | |
"get" | |
current-key))) | |
(+ 1 iteration) | |
(rest current-keys)) | |
results))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment