Skip to content

Support Julia options #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 108 additions & 22 deletions julia/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

from ctypes import c_void_p as void_p
from ctypes import c_char_p as char_p
from ctypes import py_object
from ctypes import py_object, c_int, c_char_p, POINTER, pointer

try:
from shutil import which
Expand All @@ -43,6 +43,7 @@
from types import ModuleType

from .find_libpython import find_libpython, linked_libpython, normalize_path
from .options import JuliaOptions, parse_jl_options

#-----------------------------------------------------------------------------
# Classes and funtions
Expand Down Expand Up @@ -337,7 +338,7 @@ class JuliaInfo(object):
>>> info = JuliaInfo.load(julia="PATH/TO/julia") # doctest: +SKIP
>>> info.julia
'julia'
>>> info.image_file # doctest: +SKIP
>>> info.sysimage # doctest: +SKIP
'/home/user/julia/lib/julia/sys.so'
>>> info.python # doctest: +SKIP
'/usr/bin/python3'
Expand Down Expand Up @@ -381,7 +382,7 @@ def load(cls, julia="julia", **popen_kwargs):

return cls(julia, *args)

def __init__(self, julia, bindir, libjulia_path, image_file,
def __init__(self, julia, bindir, libjulia_path, sysimage,
version_raw, version_major, version_minor, version_patch,
python=None, libpython_path=None):
self.julia = julia
Expand All @@ -393,7 +394,7 @@ def __init__(self, julia, bindir, libjulia_path, image_file,
self.libjulia_path = libjulia_path
""" Path to libjulia. """

self.image_file = image_file
self.sysimage = sysimage

version_major = int(version_major)
version_minor = int(version_minor)
Expand Down Expand Up @@ -489,6 +490,9 @@ def setup_libjulia(libjulia):
libjulia.jl_stderr_stream.restype = void_p
libjulia.jl_printf.restype = ctypes.c_int

libjulia.jl_parse_opts.argtypes = [POINTER(c_int),
POINTER(POINTER(c_char_p))]
libjulia.jl_set_ARGS.argtypes = [c_int, POINTER(c_char_p)]
libjulia.jl_atexit_hook.argtypes = [ctypes.c_int]


Expand Down Expand Up @@ -548,15 +552,34 @@ class LibJulia(BaseLibJulia):

Path to the system image can be configured before initializing Julia:

>>> api.image_file # doctest: +SKIP
>>> api.sysimage # doctest: +SKIP
'/home/user/julia/lib/julia/sys.so'
>>> api.image_file = "PATH/TO/CUSTOM/sys.so" # doctest: +SKIP
>>> api.sysimage = "PATH/TO/CUSTOM/sys.so" # doctest: +SKIP

Finally, the Julia runtime can be initialized using `LibJulia.init_julia`.
Note that only the first call to this function in the current Python
process takes effect.

>>> api.init_julia()

Any command-line options supported by Julia can be passed to
`init_julia`:

>>> api.init_julia(["--compiled-modules=no", "--optimize=3"])

Once `init_julia` is called, any subsequent use of `Julia` API
(thus also ``from julia import <JuliaModule>`` etc.) uses this
initialized Julia runtime.

`LibJulia` can be used to access Julia's C-API:

>>> ret = api.jl_eval_string(b"Int64(1 + 2)")
>>> int(api.jl_unbox_int64(ret))
3

However, a proper use of the C-API is more involved and presumably
very challenging without C macros. See also:
https://docs.julialang.org/en/latest/manual/embedding/
"""

@classmethod
Expand All @@ -574,13 +597,13 @@ def from_juliainfo(cls, juliainfo):
return cls(
libjulia_path=juliainfo.libjulia_path,
bindir=juliainfo.bindir,
image_file=juliainfo.image_file,
sysimage=juliainfo.sysimage,
)

def __init__(self, libjulia_path, bindir, image_file):
def __init__(self, libjulia_path, bindir, sysimage):
self.libjulia_path = libjulia_path
self.bindir = bindir
self.image_file = image_file
self.sysimage = sysimage

if not os.path.exists(libjulia_path):
raise RuntimeError("Julia library (\"libjulia\") not found! {}".format(libjulia_path))
Expand All @@ -601,27 +624,77 @@ def jl_init_with_image(self):
except AttributeError:
return self.libjulia.jl_init_with_image__threading

def init_julia(self):
def init_julia(self, options=None):
"""
Initialize `libjulia`. Calling this method twice is a no-op.

It calls `jl_init_with_image` (or `jl_init_with_image__threading`)
but makes sure that it is called only once for each process.

Parameters
----------
options : sequence of `str` or `JuliaOptions`
This is passed as command line options to the Julia runtime.

.. warning::

Any invalid command line option terminates the entire
Python process.
"""
if get_libjulia():
return

jl_init_path = self.bindir
image_file = self.image_file
if hasattr(options, "as_args"): # JuliaOptions
options = options.as_args()
if options:
# Let's materialize it here in case it's an iterator.
options = list(options)
# Record `options`. It's not used anywhere at the moment but
# may be useful for debugging.
self.options = options

if options:
ns = parse_jl_options(options)
if ns.home:
self.bindir = ns.home
if ns.sysimage:
self.sysimage = ns.sysimage

logger.debug("calling jl_init_with_image(%s, %s)", jl_init_path, image_file)
self.jl_init_with_image(jl_init_path.encode("utf-8"), image_file.encode("utf-8"))
jl_init_path = self.bindir
sysimage = self.sysimage

if options:
assert not isinstance(options, str)
# It seems that `argv_list[0]` is ignored and
# `sys.executable` is used anyway:
argv_list = [sys.executable]
argv_list.extend(options)
if sys.version_info[0] >= 3:
argv_list = [s.encode('utf-8') for s in argv_list]

argc = c_int(len(argv_list))
argv = POINTER(char_p)((char_p * len(argv_list))(*argv_list))

logger.debug("argv_list = %r", argv_list)
logger.debug("argc = %r", argc)
self.libjulia.jl_parse_opts(pointer(argc), pointer(argv))
logger.debug("jl_parse_opts called")
logger.debug("argc = %r", argc)
for i in range(argc.value):
logger.debug("argv[%d] = %r", i, argv[i])

logger.debug("calling jl_init_with_image(%s, %s)", jl_init_path, sysimage)
self.jl_init_with_image(jl_init_path.encode("utf-8"), sysimage.encode("utf-8"))
logger.debug("seems to work...")

set_libjulia(self)

self.libjulia.jl_exception_clear()

if options:
# This doesn't seem to be working.
self.libjulia.jl_set_ARGS(argc, argv)


class InProcessLibJulia(BaseLibJulia):

Expand Down Expand Up @@ -724,9 +797,12 @@ class Julia(object):
"""

def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
jl_runtime_path=None, debug=False):
jl_runtime_path=None, debug=False, **julia_options):
"""Create a Python object that represents a live Julia interpreter.

Note: Use `LibJulia` to fully control the initialization of
the Julia runtime.

Parameters
==========

Expand All @@ -735,6 +811,12 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
being called from inside an already running Julia, the flag should
be passed as False so the interpreter isn't re-initialized.

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.

runtime : str (optional)
Custom Julia binary, e.g. "/usr/local/bin/julia" or "julia-1.0.0".

Expand All @@ -745,10 +827,10 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
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.
Other keyword arguments (e.g., `compiled_modules=False`) are treated
as command line options. Only a subset of command line options is
supported. See `julia.core.JuliaOptions.show_supported()` for the
list of supported options.
"""

if debug:
Expand Down Expand Up @@ -786,7 +868,11 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
if jl_init_path:
self.api.bindir = jl_init_path

use_separate_cache = not jlinfo.is_compatible_python()
options = JuliaOptions(**julia_options)

use_separate_cache = not (
options.compiled_modules == "no" or jlinfo.is_compatible_python()
)
logger.debug("use_separate_cache = %s", use_separate_cache)
if use_separate_cache:
PYCALL_JULIA_HOME = os.path.join(
Expand All @@ -795,7 +881,7 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
os.environ["JULIA_BINDIR"] = PYCALL_JULIA_HOME
self.api.bindir = PYCALL_JULIA_HOME

self.api.init_julia()
self.api.init_julia(options)

if use_separate_cache:
if jlinfo.version_info < (0, 6):
Expand All @@ -806,7 +892,7 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
# Intercept precompilation
os.environ["PYCALL_PYTHON_EXE"] = sys.executable
os.environ["PYCALL_JULIA_HOME"] = PYCALL_JULIA_HOME
os.environ["PYJULIA_IMAGE_FILE"] = jlinfo.image_file
os.environ["PYJULIA_IMAGE_FILE"] = jlinfo.sysimage
os.environ["PYCALL_LIBJULIA_PATH"] = os.path.dirname(jlinfo.libjulia_path)
# Add a private cache directory. PyCall needs a different
# configuration and so do any packages that depend on it.
Expand Down
Loading