Skip to content

Commit f7a552d

Browse files
tkfstevengj
authored andcommitted
Fail with a helpful message if separate cache is not supported (#186)
* Add Julia._unbox_as * Fail with a helpful message if separate cache is not supported * Emit different message when statically linked * Rename: test_utils.py -> test_find_libpython.py * Test raise_separate_cache_error
1 parent 65301c8 commit f7a552d

File tree

3 files changed

+182
-28
lines changed

3 files changed

+182
-28
lines changed

julia/core.py

+124-8
Original file line numberDiff line numberDiff line change
@@ -351,8 +351,84 @@ def is_compatible_exe(jlinfo, _debug=lambda *_: None):
351351
return py_libpython == jl_libpython
352352

353353

354+
_separate_cache_error_common_header = """\
355+
It seems your Julia and PyJulia setup are not supported.
356+
357+
Julia interpreter:
358+
{runtime}
359+
Python interpreter and libpython used by PyCall.jl:
360+
{jlinfo.pyprogramname}
361+
{jl_libpython}
362+
Python interpreter used to import PyJulia and its libpython.
363+
{sys.executable}
364+
{py_libpython}
365+
"""
366+
367+
368+
_separate_cache_error_common_footer = """
369+
For more information, see:
370+
https://github.com/JuliaPy/pyjulia
371+
https://github.com/JuliaPy/PyCall.jl
372+
"""
373+
374+
375+
_separate_cache_error_statically_linked = """
376+
Your Python interpreter "{sys.executable}"
377+
is statically linked to libpython. Currently, PyJulia does not support
378+
such Python interpreter. For available workarounds, see:
379+
https://github.com/JuliaPy/pyjulia/issues/185
380+
"""
381+
382+
383+
_separate_cache_error_incompatible_libpython = """
384+
In Julia >= 0.7, above two paths to `libpython` have to match exactly
385+
in order for PyJulia to work. To configure PyCall.jl to use Python
386+
interpreter "{sys.executable}",
387+
run the following commands in the Julia interpreter:
388+
389+
ENV["PYTHON"] = "{sys.executable}"
390+
using Pkg
391+
Pkg.build("PyCall")
392+
"""
393+
394+
395+
def raise_separate_cache_error(
396+
runtime, jlinfo,
397+
# For test:
398+
_determine_if_statically_linked=determine_if_statically_linked):
399+
template = _separate_cache_error_common_header
400+
if _determine_if_statically_linked():
401+
template += _separate_cache_error_statically_linked
402+
else:
403+
template += _separate_cache_error_incompatible_libpython
404+
template += _separate_cache_error_common_footer
405+
message = template.format(
406+
runtime=runtime,
407+
jlinfo=jlinfo,
408+
py_libpython=find_libpython(),
409+
jl_libpython=normalize_path(jlinfo.libpython),
410+
sys=sys)
411+
raise RuntimeError(message)
412+
413+
354414
_julia_runtime = [False]
355415

416+
417+
UNBOXABLE_TYPES = (
418+
'bool',
419+
'int8',
420+
'uint8',
421+
'int16',
422+
'uint16',
423+
'int32',
424+
'uint32',
425+
'int64',
426+
'uint64',
427+
'float32',
428+
'float64',
429+
)
430+
431+
356432
class Julia(object):
357433
"""
358434
Implements a bridge to the Julia interpreter or library.
@@ -483,6 +559,17 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
483559
self.api.jl_unbox_voidpointer.argtypes = [void_p]
484560
self.api.jl_unbox_voidpointer.restype = py_object
485561

562+
for c_type in UNBOXABLE_TYPES:
563+
jl_unbox = getattr(self.api, "jl_unbox_{}".format(c_type))
564+
jl_unbox.argtypes = [void_p]
565+
jl_unbox.restype = getattr(ctypes, "c_{}".format({
566+
"float32": "float",
567+
"float64": "double",
568+
}.get(c_type, c_type)))
569+
570+
self.api.jl_typeof.argtypes = [void_p]
571+
self.api.jl_typeof.restype = void_p
572+
486573
self.api.jl_exception_clear.restype = None
487574
self.api.jl_stderr_obj.argtypes = []
488575
self.api.jl_stderr_obj.restype = void_p
@@ -494,14 +581,20 @@ def __init__(self, init_julia=True, jl_init_path=None, runtime=None,
494581
if init_julia:
495582
if use_separate_cache:
496583
# First check that this is supported
497-
self._call("""
498-
if VERSION < v"0.5-"
499-
error(\"""Using pyjulia with a statically-compiled version
500-
of python or with a version of python that
501-
differs from that used by PyCall.jl is not
502-
supported on julia 0.4""\")
503-
end
504-
""")
584+
version_range = self._unbox_as(self._call("""
585+
Int64(if VERSION < v"0.6-"
586+
2
587+
elseif VERSION >= v"0.7-"
588+
1
589+
else
590+
0
591+
end)
592+
"""), "int64")
593+
if version_range == 2:
594+
raise RuntimeError(
595+
"PyJulia does not support Julia < 0.6 anymore")
596+
elif version_range == 1:
597+
raise_separate_cache_error(runtime, jlinfo)
505598
# Intercept precompilation
506599
os.environ["PYCALL_PYTHON_EXE"] = sys.executable
507600
os.environ["PYCALL_JULIA_HOME"] = PYCALL_JULIA_HOME
@@ -580,6 +673,29 @@ def _call(self, src):
580673

581674
return ans
582675

676+
@staticmethod
677+
def _check_unboxable(c_type):
678+
if c_type not in UNBOXABLE_TYPES:
679+
raise ValueError("Julia value cannot be unboxed as c_type={!r}.\n"
680+
"c_type supported by PyJulia are:\n"
681+
"{}".format(c_type, "\n".join(UNBOXABLE_TYPES)))
682+
683+
def _is_unboxable_as(self, pointer, c_type):
684+
self._check_unboxable(c_type)
685+
jl_type = getattr(self.api, 'jl_{}_type'.format(c_type))
686+
desired = ctypes.cast(jl_type, ctypes.POINTER(ctypes.c_void_p))[0]
687+
actual = self.api.jl_typeof(pointer)
688+
return actual == desired
689+
690+
def _unbox_as(self, pointer, c_type):
691+
self._check_unboxable(c_type)
692+
jl_unbox = getattr(self.api, 'jl_unbox_{}'.format(c_type))
693+
if self._is_unboxable_as(pointer, c_type):
694+
return jl_unbox(pointer)
695+
else:
696+
raise TypeError("Cannot unbox pointer {} as {}"
697+
.format(pointer, c_type))
698+
583699
def check_exception(self, src="<unknown code>"):
584700
exoc = self.api.jl_exception_occurred()
585701
self._debug("exception occured? " + str(exoc))

test/test_find_libpython.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import platform
2+
3+
import pytest
4+
5+
from julia.find_libpython import finding_libpython, linked_libpython
6+
from julia.core import determine_if_statically_linked
7+
8+
try:
9+
unicode
10+
except NameError:
11+
unicode = str # for Python 3
12+
13+
14+
def test_finding_libpython_yield_type():
15+
paths = list(finding_libpython())
16+
assert set(map(type, paths)) <= {str, unicode}
17+
# In a statically linked Python executable, no paths may be found. So
18+
# let's just check returned type of finding_libpython.
19+
20+
21+
@pytest.mark.xfail(platform.system() == "Windows",
22+
reason="linked_libpython is not implemented for Windows")
23+
def test_linked_libpython():
24+
if determine_if_statically_linked():
25+
assert linked_libpython() is not None

test/test_utils.py

+33-20
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,41 @@
22
Unit tests which can be done without loading `libjulia`.
33
"""
44

5-
import platform
5+
import os
66

77
import pytest
88

9-
from julia.find_libpython import finding_libpython, linked_libpython
10-
from julia.core import determine_if_statically_linked
9+
from julia.core import raise_separate_cache_error
1110

1211
try:
13-
unicode
14-
except NameError:
15-
unicode = str # for Python 3
16-
17-
18-
def test_finding_libpython_yield_type():
19-
paths = list(finding_libpython())
20-
assert set(map(type, paths)) <= {str, unicode}
21-
# In a statically linked Python executable, no paths may be found. So
22-
# let's just check returned type of finding_libpython.
23-
24-
25-
@pytest.mark.xfail(platform.system() == "Windows",
26-
reason="linked_libpython is not implemented for Windows")
27-
def test_linked_libpython():
28-
if determine_if_statically_linked():
29-
assert linked_libpython() is not None
12+
from types import SimpleNamespace
13+
except ImportError:
14+
from argparse import Namespace as SimpleNamespace # Python 2
15+
16+
17+
def dummy_juliainfo():
18+
somepath = os.devnull # some random path
19+
return SimpleNamespace(
20+
pyprogramname=somepath,
21+
libpython=somepath,
22+
)
23+
24+
25+
def test_raise_separate_cache_error_statically_linked():
26+
runtime = "julia"
27+
jlinfo = dummy_juliainfo()
28+
with pytest.raises(RuntimeError) as excinfo:
29+
raise_separate_cache_error(
30+
runtime, jlinfo,
31+
_determine_if_statically_linked=lambda: True)
32+
assert "is statically linked" in str(excinfo.value)
33+
34+
35+
def test_raise_separate_cache_error_dynamically_linked():
36+
runtime = "julia"
37+
jlinfo = dummy_juliainfo()
38+
with pytest.raises(RuntimeError) as excinfo:
39+
raise_separate_cache_error(
40+
runtime, jlinfo,
41+
_determine_if_statically_linked=lambda: False)
42+
assert "have to match exactly" in str(excinfo.value)

0 commit comments

Comments
 (0)