Skip to content
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

Support virtualenv/venv without separate cache #190

Merged
merged 19 commits into from
Sep 5, 2018
Merged

Conversation

tkf
Copy link
Member

@tkf tkf commented Aug 22, 2018

(To be rebased after #188)

With this patch, we should be able to have multiple Python virtual environments without duplicating Julia's precompilation cache, provided that those Python executables are linked against same libpython.

Since PyCall.jl only cares about the identity of libpython when it is already initialized #182 (comment), i.e., it does not call Py_SetPythonHome etc., I think we only need to compare the path to libpython.

@stevengj Is it correct? It seems that it works in my laptop.

@tkf tkf force-pushed the venv branch 2 times, most recently from 8a459a9 to 6bd383f Compare August 22, 2018 22:34
cache = joinpath(path, "PyCall.ji")
backup = joinpath(path, "PyCall.ji.backup")
if isfile(cache)
mv(cache, backup; remove_destination=true)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit orthogonal to this PR but I figured fixing #175 is required for passing the test. Merging this fix should also resolve #109 and #169.

In .travis.yml we have:

pyjulia/.travis.yml

Lines 62 to 64 in 138d47d

- if [ "$CROSS_VERSION" = "1" ]; then
$PYTHON -m tox -e py,py27 -- -s;
fi

Previously, use_separate_cache was True for both py and py27 virtual environments. Thus, no global cache ~/.julia/lib/v0.6/PyCall.ji existed while running the above test. However, with this patch, use_separate_cache is False in py because the code I added sees that PYTHON configured for PyCall uses the same libpython used by py virtual environment prepared by tox. So, after running tox -e py, there is a file ~/.julia/lib/v0.6/PyCall.ji. Now then running tox -e py27 hits the bug #175 because julia 0.6 does not recompile PyCall when it finds ~/.julia/lib/v0.6/PyCall.ji in Base.LOAD_CACHE_PATH[2].

julia/core.py Outdated
return libpython_from_ldd_output(subprocess.check_output(
["ldd", path],
universal_newlines=True))
# TODO: somebody has to write it for in Windows and macOS:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it only works in Linux at the moment

julia/core.py Outdated
["ldd", path],
universal_newlines=True))
return None
# TODO: somebody has to write it for Windows and macOS:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: it only works in Linux at the moment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this at all? Oh, I see, to compare the path of libpython.

@tkf
Copy link
Member Author

tkf commented Aug 28, 2018

I merged #188 and rebased this PR onto master. There are test failures in macOS. But they are likely to be due to a bug in the latest pytest pytest-dev/pytest#3888 (Those failures are coming from doctests. Functions tested there are pure Python "pure" functions. So it doesn't make sense to have different results in different OSes.)

@stevengj Can you have a look at it? Also please let me know if the idea of checking only the libpython path (my reasoning is in the issue description #190 (comment)) is the correct direction.

@stevengj
Copy link
Member

It's annoying that finding libpython is so hard. See here for how PyCall does it.

The analogue of ldd on MacOS is otool -L, but that's only installed if the user installs the optional developer tools.

So, I think that we should fall back to searching the distutils.sysconfig.get_config_var('...') paths as PyCall does.

@coveralls
Copy link

Pull Request Test Coverage Report for Build 448

  • 20 of 26 (76.92%) changed or added relevant lines in 1 file are covered.
  • 6 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+3.7%) to 80.585%

Changes Missing Coverage Covered Lines Changed/Added Lines %
julia/core.py 20 26 76.92%
Files with Coverage Reduction New Missed Lines %
julia/core.py 6 87.46%
Totals Coverage Status
Change from base Build 447: 3.7%
Covered Lines: 303
Relevant Lines: 376

💛 - Coveralls

@coveralls
Copy link

coveralls commented Aug 29, 2018

Pull Request Test Coverage Report for Build 467

  • 106 of 144 (73.61%) changed or added relevant lines in 2 files are covered.
  • 4 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+1.6%) to 78.427%

Changes Missing Coverage Covered Lines Changed/Added Lines %
julia/core.py 15 25 60.0%
julia/find_libpython.py 91 119 76.47%
Files with Coverage Reduction New Missed Lines %
julia/core.py 4 84.87%
Totals Coverage Status
Change from base Build 447: 1.6%
Covered Lines: 389
Relevant Lines: 496

💛 - Coveralls

@ihnorton
Copy link
Member

The analogue of ldd on MacOS is otool -L, but that's only installed if the user installs the optional developer tools.

FWIW, there is a pure-Julia library which can do this: https://github.com/staticfloat/ObjectFile.jl (see find_libraries).

@tkf
Copy link
Member Author

tkf commented Aug 29, 2018

@ihnorton Thanks. It looks like a good solution. Maybe PyCall.jl can use it in the build script and also expose it as a script tool. Then PyJulia can just call it.

But I've already ported find_libpython to a Python function :) I was actually about to suggest to use the script find_libpython.py I added from PyCall.jl as well since there is no point in repeating this and doing from Python is slightly easier. But if there is an out-of-the-box solution in Julia why don't we use it.

@tkf
Copy link
Member Author

tkf commented Aug 29, 2018

Actually, maybe we should just fetch dli_fbase as done in PyCall/src/startup.jl?
https://github.com/JuliaPy/PyCall.jl/blob/9224c606545d7659ab89d8389ac9f44b1f45d1f9/src/startup.jl#L64-L77
(Edit: implemented in 435ccf4)

# 32 julia latest Python-35
- JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe"
# 32 julia-1.0 Python-35
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/1.0/julia-1.0-latest-win32.exe"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing with Julia nightly yields some bizarre failures (see below). The same code works with Julia 1.0. I suggest to test with stable release.

From Python 3.5:

ErrorException("ccall: could not find function PyString_AsStringAndSize in library C:\projects\pyjulia\.tox\py\Scripts\python35")
--- https://ci.appveyor.com/project/Keno/pyjulia/build/1.0.175/job/wwr318to7jr1b5h6#L433

From Python 2.7:

    def test_call_julia_function_with_python_args(self):
        self.assertEqual(['A', 'B', 'C'],
                         list(julia.map(julia.uppercase,
>                                       array.array('u', [u'a', u'b', u'c']))))
E       RuntimeError: Julia exception: MethodError(uppercase, (PyObject u'a',), 0x000061ca)

--- https://ci.appveyor.com/project/Keno/pyjulia/build/1.0.175/job/wwr318to7jr1b5h6#L547

if is_same_path(jlinfo.pyprogramname, sys.executable):
# In macOS and Windows, find_libpython does not work as good
# as in Linux. We add this shortcut so that PyJulia can work
# in those environments.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed this shortcut to make it work since find_libpython.py and PyCall/deps/build.jl sometimes disagree:

For example, in macOS, PyCall/deps/build.jl finds:

L957 jlinfo.libpython = /usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/libpython3.7m

but find_libpython.py finds:

L958 py_libpython = /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Python

Another example in Windows: PyCall/deps/build.jl cannot find a fullpath:

L517 jlinfo.libpython = python27

but find_libpython.py "finds" it:

L518 py_libpython = C:\Windows\system32\python27.dl

(But it looks like the same path was found in win32 and win64. Ultimately, we need someone to write a function that uses dladdr equivalent in Windows. It should be possible by just extending: https://stackoverflow.com/a/16659821)

I think it's better to use the same script to discover libpython in PyCall.jl and PyJulia. We can then avoid a hack like this. Since find_libpython.py can call libdl.dladdr (though it works only if libpython is dynamically linked), I think it's better approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the cases where they disagree, which is correct? Should deps/build.jl be fixed?

Copy link
Member Author

@tkf tkf Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have access to macOS so I'm not sure. But PyJulia printed

jlinfo.libpython = /usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/libpython3.7m
py_libpython = /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Python
jl_libpython = None

where jlinfo.libpython is libpython saved in deps/deps.jl and jl_libpython is the path I tried to resolved by

if not os.path.isabs(path):
return None
if os.path.exists(path):
return os.path.realpath(path)
if os.path.exists(path + suffix):
return os.path.realpath(path + suffix)
return None

So jl_libpython = None means that libpython3.7m.dylib did not exist (I set suffix = ".dylib" in macOS). Maybe we should try libpython3.7m.so in macOS too?

In the case of Windows, deps/build.jl just gave up (jlinfo.libpython = python27) whereas ctypes.util.find_library finds C:\Windows\system32\python27.dl.

Copy link
Member Author

@tkf tkf Aug 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying it: 1d599be

os.name and sys.platform are more well-defined/documented and we don't
need more detail than them.
@tkf
Copy link
Member Author

tkf commented Aug 29, 2018

Re: #190 (comment)

So with some more manual handling in macOS cdbefb1 now normalize_path can normalize what deps/build.jl finds to what find_libpython.py finds.

jlinfo.libpython = /usr/local/opt/python/Frameworks/Python.framework/Versions/3.7/lib/libpython3.7m
py_libpython = /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Python
jl_libpython = /usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/Python
use_separate_cache = False

--- https://travis-ci.org/JuliaPy/pyjulia/jobs/422307471#L954

if is_apple:
# sysconfig.get_config_var("SHLIB_SUFFIX") can be ".so" in macOS.
# Let's not use the value from sysconfig.
SHLIB_SUFFIX = ".dylib"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the output of python -m sysconfig from Python 3.7 in macOS https://travis-ci.org/JuliaPy/pyjulia/jobs/421924267#L1439

@tkf
Copy link
Member Author

tkf commented Aug 29, 2018

So to directly answer #190 (comment) deps/build.jl and find_libpython.py agree in all cases where deps/build.jl finds the path.

That is to say, they both agree in Linux and macOS. In Windows and Python 2.7, deps/build.jl does not find libpython but find_libpython.py finds it via ctypes.util.find_library. Presumably, this is equivalent to what dlopen does. Python document says:

The purpose of the find_library() function is to locate a library in a way similar to what the compiler or runtime loader does (on platforms with several versions of a shared library the most recent should be loaded), while the ctypes library loaders act like when a program is run, and call the runtime loader directly.

--- https://docs.python.org/3/library/ctypes.html#finding-shared-libraries

@stevengj
Copy link
Member

stevengj commented Sep 3, 2018

Sounds like deps/build.jl should be updated, then.

@tkf
Copy link
Member Author

tkf commented Sep 3, 2018

Why don't we use find_libpython.py in PyCall.jl as well? Since we can use dladdr/GetModuleFileName inside a Python process (if libpython is dynamically linked), I think this is much more robust solution.

@tkf tkf mentioned this pull request Sep 5, 2018
@stevengj stevengj merged commit 9d1833f into JuliaPy:master Sep 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants