forked from clj-python/libpython-clj
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cljbridge.py
186 lines (143 loc) · 7.83 KB
/
cljbridge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""Python bindings to start a Clojure repl from a python process. Relies on
libpython-clj being in a deps.edn pathway as clojure is called from the command
line to build the classpath. Expects javabridge to be installed and functional.
Javabridge will dynamically find the java library that corresponds with calling 'java'
from the command line and load it. We then initialize Clojure and provide pathways
to require namespaces, find symbols, and call functions.
There are two import initialization methods - init_clojure and init_clojure_repl - these
take care of starting up everything in the correct order.
"""
import subprocess
import javabridge
try:
from collections.abc import Callable # noqa
except ImportError:
from collections import Callable
def init_clojure_runtime():
"""Initialize the clojure runtime. This needs to happen at least once before
attempting to require a namespace or lookup a clojure var."""
javabridge.static_call("clojure/lang/RT", "init", "()V")
def find_clj_var(fn_ns, fn_name):
"""Use the clojure runtime to find a var. Clojure vars are placeholders in
namespaces that forward their operations to the data they point to. This allows
someone to hold a var and but recompile a namespace to get different behavior. They
implement both `clojure.lang.IFn` and `clojure.lang.IDeref` so they can act like a
function and you can dereference them to get to their original value."""
return javabridge.static_call("clojure/lang/RT",
"var",
"(Ljava/lang/String;Ljava/lang/String;)Lclojure/lang/Var;",
fn_ns,
fn_name)
class CLJFn(Callable):
"""Construct a python callable from a clojure object. This callable will forward
function calls to it's Clojure object expecting a clojure.lang.IFn interface."""
applyTo = javabridge.make_method("applyTo", "(clojure/lang/ISeq;)Ljava/lang/Object;")
def __init__(self, ifn_obj):
self.o = ifn_obj
def __call__(self, *args, **kw_args):
if not kw_args:
invoker = getattr(self, "invoke"+str(len(args)))
return invoker(*args)
else:
raise Exception("Unable to handle kw_args for now")
print(len(args), len(kw_args))
for i in range(20):
opargs = ""
for j in range(i):
opargs += "Ljava/lang/Object;"
setattr(CLJFn, "invoke" + str(i),
javabridge.make_method("invoke", "(" + opargs + ")Ljava/lang/Object;" ))
def resolve_fn(namespaced_name):
"""Resolve a clojure var given a fully qualified namespace name. The return value
is callable. Note that the namespace itself needs to be required first."""
ns_name, sym_name = namespaced_name.split("/")
return CLJFn(find_clj_var(ns_name, sym_name))
def resolve_call_fn(namespaced_fn_name, *args):
"""Resolve a function given a fully qualified namespace name and call it."""
return resolve_fn(namespaced_fn_name)(*args)
def symbol(sym_name):
"""Create a clojure symbol from a string"""
return javabridge.static_call("clojure/lang/Symbol", "intern",
"(Ljava/lang/String;)Lclojure/lang/Symbol;", sym_name)
__REQUIRE_FN = None
def require(ns_name):
"""Require a clojure namespace. This needs to happen before you find symbols
in that namespace else you will be uninitialized var errors."""
if not __REQUIRE_FN:
_REQUIRE_FN = resolve_fn("clojure.core/require")
return _REQUIRE_FN(symbol(ns_name))
def init_libpy_embedded():
"""Initialize libpy on a mode where it looks for symbols in the local process and
it itself doesn't attempt to run the python intialization procedures but expects
the python system to be previously initialized."""
require("libpython-clj2.embedded")
return resolve_call_fn("libpython-clj2.embedded/initialize!")
def classpath(classpath_args=[]):
"""Call clojure at the command line and return the classpath in as a list of
strings. Clojure will pick up a local deps.edn or deps can be specified inline."""
return subprocess.check_output(['clojure'] + list(classpath_args) + ['-Spath']).decode("utf-8").strip().split(':')
DEFAULT_NREPL_VERSION = "0.8.3"
DEFAULT_CIDER_NREPL_VERSION = "0.25.5"
def repl_classpath(nrepl_version=DEFAULT_NREPL_VERSION,
cider_nrepl_version=DEFAULT_CIDER_NREPL_VERSION,
classpath_args=[], **kw_args):
"""Return the classpath with the correct deps to run nrepl and cider.
Positional arguments are added after the -Sdeps argument to start the
nrepl server."""
return classpath(classpath_args=["-Sdeps", '{:deps {nrepl/nrepl {:mvn/version "%s"} cider/cider-nrepl {:mvn/version "%s"}}}' % (nrepl_version, cider_nrepl_version)]
+ list(classpath_args))
def init_clojure(classpath_args=[]):
"""Initialize a vanilla clojure process using the clojure command line to output
the classpath to use for the java vm. At the return of this function clojure is
initialized and libpython-clj2.python's public functions will work.
* classpath_args - List of arguments that will be passed to the clojure command
line process when building the classpath. """
javabridge.start_vm(run_headless=True,
class_path=classpath(classpath_args=classpath_args))
init_clojure_runtime()
init_libpy_embedded()
return True
def py_dict_to_keyword_map(py_dict):
hash_map = None
keyword = resolve_fn("clojure.core/keyword")
assoc = resolve_fn("clojure.core/assoc")
for k in py_dict:
hash_map = assoc(hash_map, keyword(k), py_dict[k])
return hash_map
def init_clojure_repl(**kw_args):
"""Initialize clojure with extra arguments specifically for embedding a cider-nrepl
server. Then start an nrepl server. The port will both be printed to stdout and
output to a .nrepl_server file. This function does not return as it leaves the GIL
released so that repl threads have access to Python. libpython-clj2.python is
initialized 'require-python' pathways should work.
Keyword arguments in python are mapped to a hash-map of keyword options and
passed directly to clojure so any nrepl are valid arguments to this function.
* `classpath_args` - List of additional arguments that be passed to the clojure
process when building the classpath.
* `port` - Integer port to open up repl. A random port will be found if not
provided.
* `bind` - Bind address. If you are having connectivity issues try
bind=\"0.0.0.0\""""
javabridge.start_vm(run_headless=True, class_path=repl_classpath(**kw_args))
init_clojure_runtime()
init_libpy_embedded()
resolve_call_fn("libpython-clj2.embedded/start-repl!",
py_dict_to_keyword_map(kw_args))
class GenericJavaObj:
__str__ = javabridge.make_method("toString", "()Ljava/lang/String;")
get_class = javabridge.make_method("getClass", "()Ljava/lang/Class;")
__repl__ = javabridge.make_method("toString", "()Ljava/lang/String;")
def __init__(self, jobj):
self.o = jobj
def longCast(jobj):
"Cast a java object to a primitive long value."
return javabridge.static_call("clojure/lang/RT", "longCast",
"(Ljava/lang/Object;)J", jobj)
def to_ptr(pyobj):
"""Create a tech.v3.datatype.ffi.Pointer java object from a python object. This
allows you to pass python objects directly into libpython-clj2.python-derived
pathways (such as ->jvm). If java is going to hold onto the python data for
a long time and it will fall out of Python scope object should be
'incref-tracked' - 'libpython-clj2.python.ffi/incref-track-pyobject'."""
return javabridge.static_call("tech/v3/datatype/ffi/Pointer", "constructNonZero",
"(J)Ltech/v3/datatype/ffi/Pointer;", id(pyobj))