-
Notifications
You must be signed in to change notification settings - Fork 101
/
core.py
535 lines (432 loc) · 19.1 KB
/
core.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
"""
Bridge Python and Julia by initializing the Julia interpreter inside Python.
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2013 The IPython and Julia Development Teams.
#
# Distributed under the terms of the BSD License. The full license is in
# the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------
#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------
# Stdlib
from __future__ import print_function, absolute_import
import ctypes
import ctypes.util
import os
import sys
import keyword
import subprocess
import time
import warnings
from ctypes import c_void_p as void_p
from ctypes import c_char_p as char_p
from ctypes import py_object
# this is python 3.3 specific
from types import ModuleType, FunctionType
#-----------------------------------------------------------------------------
# Classes and funtions
#-----------------------------------------------------------------------------
python_version = sys.version_info
if python_version.major == 3:
def iteritems(d): return iter(d.items())
else:
iteritems = dict.iteritems
class JuliaError(Exception):
pass
def remove_prefix(string, prefix):
return string[len(prefix):] if string.startswith(prefix) else string
def jl_name(name):
if name.endswith('_b'):
return name[:-2] + '!'
return name
def py_name(name):
if name.endswith('!'):
return name[:-1] + '_b'
return name
class JuliaModule(ModuleType):
def __init__(self, loader, *args, **kwargs):
super(JuliaModule, self).__init__(*args, **kwargs)
self._julia = loader.julia
self.__loader__ = loader
@property
def __all__(self):
juliapath = remove_prefix(self.__name__, "julia.")
names = set(self._julia.eval("names({})".format(juliapath)))
names.discard(juliapath.rsplit('.', 1)[-1])
return [py_name(n) for n in names if is_accessible_name(n)]
def __dir__(self):
if python_version.major == 2:
names = set()
else:
names = set(super(JuliaModule, self).__dir__())
names.update(self.__all__)
return list(names)
# Override __dir__ method so that completing member names work
# well in Python REPLs like IPython.
def __getattr__(self, name):
try:
return self.__try_getattr(name)
except AttributeError:
if name.endswith("_b"):
try:
return self.__try_getattr(jl_name(name))
except AttributeError:
pass
raise
def __try_getattr(self, name):
jl_module = remove_prefix(self.__name__, "julia.")
jl_fullname = ".".join((jl_module, name))
# If `name` is a top-level module, don't import it as a
# submodule. Note that it handles the case that `name` is
# `Base` and `Core`.
is_toplevel = isdefined(self._julia, 'Main', name)
if not is_toplevel and isamodule(self._julia, jl_fullname):
# FIXME: submodules from other modules still hit this code
# path and they are imported as submodules.
return self.__loader__.load_module(".".join((self.__name__, name)))
if isdefined(self._julia, jl_module, name):
return self._julia.eval(jl_fullname)
raise AttributeError(name)
class JuliaMainModule(JuliaModule):
def __setattr__(self, name, value):
if name.startswith('_'):
super(JuliaMainModule, self).__setattr__(name, value)
else:
juliapath = remove_prefix(self.__name__, "julia.")
setter = '''
Main.PyCall.pyfunctionret(
(x) -> eval({}, :({} = $x)),
Any,
PyCall.PyAny)
'''.format(juliapath, jl_name(name))
self._julia.eval(setter)(value)
help = property(lambda self: self._julia.help)
eval = property(lambda self: self._julia.eval)
using = property(lambda self: self._julia.using)
# add custom import behavior for the julia "module"
class JuliaImporter(object):
# find_module was deprecated in v3.4
def find_module(self, fullname, path=None):
if fullname.startswith("julia."):
return JuliaModuleLoader()
class JuliaModuleLoader(object):
@property
def julia(self):
self.__class__.julia = julia = Julia()
return julia
# load module was deprecated in v3.4
def load_module(self, fullname):
juliapath = remove_prefix(fullname, "julia.")
if juliapath == 'Main':
return sys.modules.setdefault(fullname,
JuliaMainModule(self, fullname))
elif isafunction(self.julia, juliapath):
return self.julia.eval(juliapath)
try:
self.julia.eval("import {}".format(juliapath))
except JuliaError:
pass
else:
if isamodule(self.julia, juliapath):
return sys.modules.setdefault(fullname,
JuliaModule(self, fullname))
raise ImportError("{} not found".format(juliapath))
def ismacro(name):
""" Is the name a macro?
>>> ismacro('@time')
True
>>> ismacro('sum')
False
"""
return name.startswith("@")
def isoperator(name):
return not name[0].isalpha()
def isprotected(name):
return name.startswith("_")
def notascii(name):
try:
name.encode("ascii")
return False
except:
return True
def is_accessible_name(name):
"""
Check if a Julia variable `name` is (easily) accessible from Python.
Return `True` if `name` can be safely converted to a Python
identifier using `py_name` function. For example,
>>> is_accessible_name('A_mul_B!')
True
Since it can be accessed as `A_mul_B_b` in Python.
"""
return not (ismacro(name) or
isoperator(name) or
isprotected(name) or
notascii(name))
def isdefined(julia, parent, member):
return julia.eval("isdefined({}, :({}))".format(parent, member))
def isamodule(julia, julia_name):
try:
return julia.eval("isa({}, Module)".format(julia_name))
except JuliaError:
return False # assuming this is an `UndefVarError`
def isafunction(julia, julia_name, mod_name=""):
code = "isa({}, Function)".format(julia_name)
if mod_name:
code = "isa({}.{}, Function)".format(mod_name, julia_name)
try:
return julia.eval(code)
except:
return False
def determine_if_statically_linked():
"""Determines if this python executable is statically linked"""
# Windows and OS X are generally always dynamically linked
if not sys.platform.startswith('linux'):
return False
lddoutput = subprocess.check_output(["ldd",sys.executable])
return not (b"libpython" in lddoutput)
_julia_runtime = [False]
class Julia(object):
"""
Implements a bridge to the Julia interpreter or library.
This uses the Julia PyCall module to perform type conversions and allow
full access to the entire Julia interpreter.
"""
def __init__(self, init_julia=True, jl_runtime_path=None, jl_init_path=None,
debug=False):
"""Create a Python object that represents a live Julia interpreter.
Parameters
==========
init_julia : bool
If True, try to initialize the Julia interpreter. If this code is
being called from inside an already running Julia, the flag should
be passed as False so the interpreter isn't re-initialized.
jl_runtime_path : str (optional)
Path to your Julia binary, e.g. "/usr/local/bin/julia"
jl_init_path : str (optional)
Path to give to jl_init relative to which we find sys.so,
(defaults to jl_runtime_path or NULL)
debug : bool
If True, print some debugging information to STDERR
Note that it is safe to call this class constructor twice in the same
process with `init_julia` set to True, as a global reference is kept
to avoid re-initializing it. The purpose of the flag is only to manage
situations when Julia was initialized from outside this code.
"""
self.is_debugging = debug
# Ugly hack to register the julia interpreter globally so we can reload
# this extension without trying to re-open the shared lib, which kills
# the python interpreter. Nasty but useful while debugging
if _julia_runtime[0]:
self.api = _julia_runtime[0]
return
if init_julia:
if jl_runtime_path:
runtime = jl_runtime_path
else:
runtime = 'julia'
juliainfo = subprocess.check_output(
[runtime, "-e",
"""
println(VERSION < v"0.7.0-DEV.3073" ? JULIA_HOME : Base.Sys.BINDIR)
println(Libdl.dlpath(string("lib", splitext(Base.julia_exename())[1])))
println(unsafe_string(Base.JLOptions().image_file))
PyCall_depsfile = Pkg.dir("PyCall","deps","deps.jl")
if isfile(PyCall_depsfile)
eval(Module(:__anon__),
Expr(:toplevel,
:(Main.Base.include($PyCall_depsfile)),
:(println(pyprogramname))))
else
println("nowhere")
end
"""])
JULIA_HOME, libjulia_path, image_file, depsjlexe = juliainfo.decode("utf-8").rstrip().split("\n")
exe_differs = not depsjlexe == sys.executable
self._debug("JULIA_HOME = %s, libjulia_path = %s" % (JULIA_HOME, libjulia_path))
if not os.path.exists(libjulia_path):
raise JuliaError("Julia library (\"libjulia\") not found! {}".format(libjulia_path))
# fixes a specific issue with python 2.7.13
# ctypes.windll.LoadLibrary refuses unicode argument
# http://bugs.python.org/issue29294
if sys.version_info >= (2,7,13) and sys.version_info < (2,7,14):
libjulia_path = libjulia_path.encode("ascii")
self.api = ctypes.PyDLL(libjulia_path, ctypes.RTLD_GLOBAL)
if not jl_init_path:
if jl_runtime_path:
jl_init_path = os.path.dirname(os.path.realpath(jl_runtime_path)).encode("utf-8")
else:
jl_init_path = JULIA_HOME.encode("utf-8") # initialize with JULIA_HOME
use_separate_cache = exe_differs or determine_if_statically_linked()
if use_separate_cache:
PYCALL_JULIA_HOME = os.path.join(
os.path.dirname(os.path.realpath(__file__)),"fake-julia").replace("\\","\\\\")
os.environ["JULIA_HOME"] = PYCALL_JULIA_HOME # TODO: this line can be removed when dropping Julia v0.6
os.environ["JULIA_BINDIR"] = PYCALL_JULIA_HOME
jl_init_path = PYCALL_JULIA_HOME.encode("utf-8")
if not hasattr(self.api, "jl_init_with_image"):
if hasattr(self.api, "jl_init_with_image__threading"):
self.api.jl_init_with_image = self.api.jl_init_with_image__threading
else:
raise ImportError("No libjulia entrypoint found! (tried jl_init_with_image and jl_init_with_image__threading)")
self.api.jl_init_with_image.argtypes = [char_p, char_p]
self._debug("calling jl_init_with_image(%s, %s)" % (jl_init_path, image_file))
self.api.jl_init_with_image(jl_init_path, image_file.encode("utf-8"))
self._debug("seems to work...")
else:
# we're assuming here we're fully inside a running Julia process,
# so we're fishing for symbols in our own process table
self.api = ctypes.PyDLL('')
# Store the running interpreter reference so we can start using it via self.call
self.api.jl_.argtypes = [void_p]
self.api.jl_.restype = None
# Set the return types of some of the bridge functions in ctypes terminology
self.api.jl_eval_string.argtypes = [char_p]
self.api.jl_eval_string.restype = void_p
self.api.jl_exception_occurred.restype = void_p
self.api.jl_typeof_str.argtypes = [void_p]
self.api.jl_typeof_str.restype = char_p
self.api.jl_call2.argtypes = [void_p, void_p, void_p]
self.api.jl_call2.restype = void_p
self.api.jl_get_field.restype = void_p
self.api.jl_typename_str.restype = char_p
self.api.jl_typeof_str.restype = char_p
self.api.jl_unbox_voidpointer.restype = py_object
self.api.jl_exception_clear.restype = None
self.api.jl_stderr_obj.argtypes = []
self.api.jl_stderr_obj.restype = void_p
self.api.jl_stderr_stream.argtypes = []
self.api.jl_stderr_stream.restype = void_p
self.api.jl_printf.restype = ctypes.c_int
self.api.jl_exception_clear()
# We use show() for displaying uncaught exceptions.
self.api.show = self._call("Base.show")
if init_julia:
if use_separate_cache:
# First check that this is supported
self._call("""
if VERSION < v"0.5-"
error(\"""Using pyjulia with a statically-compiled version
of python or with a version of python that
differs from that used by PyCall.jl is not
supported on julia 0.4""\")
end
""")
# Intercept precompilation
os.environ["PYCALL_PYTHON_EXE"] = sys.executable
os.environ["PYCALL_JULIA_HOME"] = PYCALL_JULIA_HOME
os.environ["PYJULIA_IMAGE_FILE"] = image_file
os.environ["PYCALL_LIBJULIA_PATH"] = os.path.dirname(libjulia_path)
# Add a private cache directory. PyCall needs a different
# configuration and so do any packages that depend on it.
self._call(u"unshift!(Base.LOAD_CACHE_PATH, abspath(Pkg.Dir._pkgroot()," +
"\"lib\", \"pyjulia%s-v$(VERSION.major).$(VERSION.minor)\"))" % sys.version_info[0])
# If PyCall.ji does not exist, create an empty file to force
# recompilation
self._call(u"""
isdir(Base.LOAD_CACHE_PATH[1]) ||
mkpath(Base.LOAD_CACHE_PATH[1])
depsfile = joinpath(Base.LOAD_CACHE_PATH[1],"PyCall.ji")
isfile(depsfile) || touch(depsfile)
""")
self._call(u"using PyCall")
# Whether we initialized Julia or not, we MUST create at least one
# instance of PyObject and the convert function. Since these will be
# needed on every call, we hold them in the Julia object itself so
# they can survive across reinitializations.
self.api.PyObject = self._call("PyCall.PyObject")
self.api.convert = self._call("convert")
# We use show() for displaying uncaught exceptions.
self.api.show = self._call("Base.show")
# Flag process-wide that Julia is initialized and store the actual
# runtime interpreter, so we can reuse it across calls and module
# reloads.
_julia_runtime[0] = self.api
self.sprint = self.eval('sprint')
self.showerror = self.eval('showerror')
def _debug(self, msg):
"""
Print some debugging stuff, if enabled
"""
if self.is_debugging:
print(msg, file=sys.stderr)
def _call(self, src):
"""
Low-level call to execute a snippet of Julia source.
This only raises an exception if Julia itself throws an error, but it
does NO type conversion into usable Python objects nor any memory
management. It should never be used for returning the result of Julia
expressions, only to execute statements.
"""
# self._debug("_call(%s)" % src)
ans = self.api.jl_eval_string(src.encode('utf-8'))
self.check_exception(src)
return ans
def check_exception(self, src=None):
exoc = self.api.jl_exception_occurred()
self._debug("exception occured? " + str(exoc))
if not exoc:
# self._debug("No Exception")
self.api.jl_exception_clear()
return
# If, theoretically, an exception happens in early stage of
# self.__init__, showerror and sprint as below does not work.
# Let's use jl_typeof_str in such case.
try:
sprint = self.sprint
showerror = self.showerror
except AttributeError:
res = None
else:
res = self.api.jl_call2(void_p(self.api.convert),
void_p(self.api.PyObject),
void_p(exoc))
if res is None:
exception = self.api.jl_typeof_str(exoc).decode('utf-8')
else:
exception = sprint(showerror, self._as_pyobj(res))
raise JuliaError(u'Exception \'{}\' occurred while calling julia code:\n{}'
.format(exception, src))
def _typeof_julia_exception_in_transit(self):
exception = void_p.in_dll(self.api, 'jl_exception_in_transit')
msg = self.api.jl_typeof_str(exception)
return char_p(msg).value
def help(self, name):
""" Return help string for function by name. """
if name is None:
return None
return self.eval('Markdown.plain(@doc("{}"))'.format(name))
def eval(self, src):
""" Execute code in Julia, then pull some results back to Python. """
if src is None:
return None
ans = self._call(src)
if not ans:
return None
res = self.api.jl_call2(void_p(self.api.convert), void_p(self.api.PyObject), void_p(ans))
if res is None:
self.check_exception(src)
return self._as_pyobj(res, "convert(PyCall.PyObject, {})".format(src))
def _as_pyobj(self, res, src=None):
if res == 0:
return None
boxed_obj = self.api.jl_get_field(void_p(res), b'o')
pyobj = self.api.jl_unbox_voidpointer(void_p(boxed_obj))
# make sure we incref it before returning it,
# as this is a borrowed reference
ctypes.pythonapi.Py_IncRef(ctypes.py_object(pyobj))
return pyobj
def using(self, module):
"""Load module in Julia by calling the `using module` command"""
self.eval("using %s" % module)
class LegacyJulia(Julia):
def __getattr__(self, name):
from julia import Main
warnings.warn(
"Accessing `Julia().<name>` to obtain Julia objects is"
" deprecated. Use `from julia import Main; Main.<name>` or"
" `jl = Julia(); jl.eval('<name>')`.",
DeprecationWarning)
return getattr(Main, name)
sys.meta_path.append(JuliaImporter())