Skip to content

Instantly share code, notes, and snippets.

@nickmbailey
Created June 13, 2016 22:19
Show Gist options
  • Save nickmbailey/3f11300e944e17d92edd6d42da785ab3 to your computer and use it in GitHub Desktop.
Save nickmbailey/3f11300e944e17d92edd6d42da785ab3 to your computer and use it in GitHub Desktop.
(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