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 virtual environment #578

Merged
merged 29 commits into from
Feb 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
69f08db
Support virtual environment
tkf Sep 21, 2018
66eb069
Add test_venv.jl
tkf Oct 21, 2018
0f80a51
Add python3.4-venv in Travis
tkf Oct 21, 2018
f9f53e6
Fix pythonhome_of for Julia 0.6
tkf Oct 21, 2018
9be4dd7
Properly treat venv path in Windows tests
tkf Oct 21, 2018
31ef039
Mark venv test broken in Windows
tkf Oct 21, 2018
131369c
Support venv properly
tkf Oct 23, 2018
7866c93
Test with virtualenv command as well
tkf Oct 23, 2018
d16a4d4
Specify Python version in virtualenv activation test
tkf Oct 23, 2018
9dde9a8
Move pythonhome_of to depsutils.jl and use it during build
tkf Oct 23, 2018
7bb0d13
Support "venv activation" test inside virtualenv
tkf Oct 23, 2018
ef27d1d
Show correct Python executable when import fails
tkf Oct 24, 2018
83b5216
Use static memory location than malloc
tkf Oct 27, 2018
9fcdff7
Create virtual environment at non-ascii evil path
tkf Oct 27, 2018
8fe0cec
Be kind to Python 2
tkf Oct 27, 2018
41a3107
Unify Py_SetPythonHome wrapper functions
tkf Oct 27, 2018
6d47896
reinterpret source array instead of dest
tkf Nov 2, 2018
6699140
Improve docs and comments
tkf Nov 4, 2018
5e3d621
Simplify __init__() by merging venv code path
tkf Nov 4, 2018
b517aa3
Merge remote-tracking branch 'origin/master' into venv
tkf Nov 13, 2018
cc96a4b
Merge remote-tracking branch 'origin/master' into venv
tkf Nov 15, 2018
e9a6c52
Merge remote-tracking branch 'origin/master' into venv
tkf Jan 24, 2019
c56ec24
Stop using Compat in test_venv.jl
tkf Jan 24, 2019
99ec542
Use property-based syntax in test_venv.jl
tkf Jan 24, 2019
923feb9
Add docs on virtual environment usage
tkf Jan 24, 2019
21f08b3
Merge remote-tracking branch 'origin/master' into venv
tkf Jan 24, 2019
44beb7c
Remove unnecessary VERSION < v"0.7.0-" branch
tkf Feb 5, 2019
6a20a5f
grammar fixes; rm references to internal details about Python API fun…
stevengj Feb 6, 2019
5d50878
inline links
stevengj Feb 6, 2019
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
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: julia
dist: trusty # update "python3.4-venv" below when updating
os:
- linux
julia:
Expand All @@ -10,6 +11,7 @@ addons:
packages:
- python-numpy
- python3-numpy
- python3.4-venv # Ubuntu Trusty 14.04 does not have python3-venv
env:
global:
- PYCALL_DEBUG_BUILD="yes"
Expand Down
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ unless you set the `PYTHON` environment variable or delete the file

**Note:** If you use Python
[virtualenvs](http://docs.python-guide.org/en/latest/dev/virtualenvs/),
then be aware that PyCall *uses the virtualenv it was built with*, even
if you switch virtualenvs. If you want to switch PyCall to use a
different virtualenv, then you should switch virtualenvs and run
`rm(Pkg.dir("PyCall","deps","PYTHON")); Pkg.build("PyCall")`.
then be aware that PyCall *uses the virtualenv it was built with* by
default, even if you switch virtualenvs. If you want to switch PyCall
to use a different virtualenv, then you should switch virtualenvs and
run `rm(Pkg.dir("PyCall","deps","PYTHON")); Pkg.build("PyCall")`.
Alternatively, see [Python virtual environments](#python-virtual-environments)
section below for switching virtual environment at run-time.

**Note:** Usually, the necessary libraries are installed along with
Python, but [pyenv on MacOS](https://github.com/JuliaPy/PyCall.jl/issues/122)
Expand Down Expand Up @@ -543,6 +545,35 @@ and so on.
Here, instead of `pyimport`, we have used the function `pyimport_conda`. The second argument is the name of the [Anaconda package](https://docs.continuum.io/anaconda/pkg-docs) that provides this module. This way, if importing `scipy.optimize` fails because the user hasn't installed `scipy`, it will either (a) automatically install `scipy` and retry the `pyimport` if PyCall is configured to use the [Conda](https://github.com/Luthaf/Conda.jl) Python install (or
any other Anaconda-based Python distro for which the user has installation privileges), or (b) throw an error explaining that `scipy` needs to be installed, and explain how to configure PyCall to use Conda so that it can be installed automatically. More generally, you can call `pyimport(module, package, channel)` to specify an optional Anaconda "channel" for installing non-standard Anaconda packages.

## Python virtual environments

Python virtual environments created by [`venv`](https://docs.python.org/3/library/venv.html) and [`virtualenv`](https://virtualenv.pypa.io/en/latest/)
can be used from `PyCall`, *provided that the Python executable used
in the virtual environment is linked against the same `libpython` used
by `PyCall`*. Note that virtual environments created by `conda` are not
supported.

To use `PyCall` with a certain virtual environment, set the environment
variable `PYCALL_JL_RUNTIME_PYTHON` *before* importing `PyCall` to
path to the Python executable. For example:

```julia
$ source PATH/TO/bin/activate # activate virtual environment in system shell

$ julia # start Julia
...

julia> ENV["PYCALL_JL_RUNTIME_PYTHON"] = Sys.which("python")
"PATH/TO/bin/python3"

julia> using PyCall

julia> pyimport("sys").executable
"PATH/TO/bin/python3"
```
Similarly, the `PYTHONHOME` path can be changed by the environment variable
`PYCALL_JL_RUNTIME_PYTHONHOME`.

## Author

This package was written by [Steven G. Johnson](http://math.mit.edu/~stevenj/).
39 changes: 1 addition & 38 deletions deps/build.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,6 @@ struct UseCondaPython <: Exception end

#########################################################################

# Fix the environment for running `python`, and setts IO encoding to UTF-8.
# If cmd is the Conda python, then additionally removes all PYTHON* and
# CONDA* environment variables.
function pythonenv(cmd::Cmd)
env = copy(ENV)
if dirname(cmd.exec[1]) == abspath(Conda.PYTHONDIR)
pythonvars = String[]
for var in keys(env)
if startswith(var, "CONDA") || startswith(var, "PYTHON")
push!(pythonvars, var)
end
end
for var in pythonvars
pop!(env, var)
end
end
# set PYTHONIOENCODING when running python executable, so that
# we get UTF-8 encoded text as output (this is not the default on Windows).
env["PYTHONIOENCODING"] = "UTF-8"
setenv(cmd, env)
end

pyvar(python::AbstractString, mod::AbstractString, var::AbstractString) = chomp(read(pythonenv(`$python -c "import $mod; print($mod.$var)"`), String))

pyconfigvar(python::AbstractString, var::AbstractString) = pyvar(python, "distutils.sysconfig", "get_config_var('$var')")
Expand Down Expand Up @@ -117,11 +95,6 @@ include("depsutils.jl")

#########################################################################

# A couple of key strings need to be stored as constants so that
# they persist throughout the life of the program. In Python 3,
# they need to be wchar_t* data.
wstringconst(s) = string("Base.cconvert(Cwstring, \"", escape_string(s), "\")")

# we write configuration files only if they change, both
# to prevent unnecessary recompilation and to minimize
# problems in the unlikely event of read-only directories.
Expand Down Expand Up @@ -197,15 +170,7 @@ try # make sure deps.jl file is removed on error
# Get PYTHONHOME, either from the environment or from Python
# itself (if it is not in the environment or if we are using Conda)
PYTHONHOME = if !haskey(ENV, "PYTHONHOME") || use_conda
# PYTHONHOME tells python where to look for both pure python
# and binary modules. When it is set, it replaces both
# `prefix` and `exec_prefix` and we thus need to set it to
# both in case they differ. This is also what the
# documentation recommends. However, they are documented
# to always be the same on Windows, where it causes
# problems if we try to include both.
exec_prefix = pysys(python, "exec_prefix")
Sys.iswindows() ? exec_prefix : pysys(python, "prefix") * ":" * exec_prefix
pythonhome_of(python)
else
ENV["PYTHONHOME"]
end
Expand All @@ -223,10 +188,8 @@ try # make sure deps.jl file is removed on error
const python = "$(escape_string(python))"
const libpython = "$(escape_string(libpy_name))"
const pyprogramname = "$(escape_string(programname))"
const wpyprogramname = $(wstringconst(programname))
const pyversion_build = $(repr(pyversion))
const PYTHONHOME = "$(escape_string(PYTHONHOME))"
const wPYTHONHOME = $(wstringconst(PYTHONHOME))

"True if we are using the Python distribution in the Conda package."
const conda = $use_conda
Expand Down
138 changes: 130 additions & 8 deletions deps/depsutils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,143 @@ function findsym(lib, syms...)
error("no symbol found from: ", syms)
end

# Static buffer to make sure the string passed to libpython persists
# for the lifetime of the program, as CPython API requires:
const __buf_programname = Vector{UInt8}(undef, 1024)
const __buf_pythonhome = Vector{UInt8}(undef, 1024)

# Need to set PythonHome before calling GetVersion to avoid warning (#299).
# Unfortunately, this poses something of a chicken-and-egg problem because
# we need to know the Python version to set PythonHome via the API. Note
# that the string (or array) passed to Py_SetPythonHome needs to be a
# constant that lasts for the lifetime of the program, which is why we
# can't use Cwstring here (since that creates a temporary copy).
function Py_SetPythonHome(libpy, PYTHONHOME, wPYTHONHOME, pyversion)
if !isempty(PYTHONHOME)
if pyversion.major < 3
ccall(Libdl.dlsym(libpy, :Py_SetPythonHome), Cvoid, (Cstring,), PYTHONHOME)
else
ccall(Libdl.dlsym(libpy, :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},), wPYTHONHOME)
end
# prepare static buffer __buf_pythonhome, copy the string to it, and then
# pass the pointer to the buffer to the CPython API.
function Py_SetPythonHome(libpy, pyversion, PYTHONHOME::AbstractString)
isempty(PYTHONHOME) && return
if pyversion.major < 3
ccall(Libdl.dlsym(libpy, :Py_SetPythonHome), Cvoid, (Cstring,),
_preserveas!(__buf_pythonhome, Cstring, PYTHONHOME))
else
ccall(Libdl.dlsym(libpy, :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},),
_preserveas!(__buf_pythonhome, Cwstring, PYTHONHOME))
end
end

function Py_SetProgramName(libpy, pyversion, programname::AbstractString)
isempty(programname) && return
if pyversion.major < 3
ccall(Libdl.dlsym(libpy, :Py_SetProgramName), Cvoid, (Cstring,),
_preserveas!(__buf_programname, Cstring, programname))
else
ccall(Libdl.dlsym(libpy, :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},),
_preserveas!(__buf_programname, Cwstring, programname))
end
end

"""
_preserveas!(dest::Vector{UInt8}, (Cstring|Cwstring), x::String) :: Ptr

Copy `x` as `Cstring` or `Cwstring` to `dest` and return a pointer to
`dest`. Thus, this pointer is safe to use as long as `dest` is
protected from GC.
"""
function _preserveas!(dest::Vector{UInt8}, ::Type{Cstring}, x::AbstractString)
s = transcode(UInt8, String(x))
copyto!(dest, s)
dest[length(s) + 1] = 0
return pointer(dest)
end

function _preserveas!(dest::Vector{UInt8}, ::Type{Cwstring}, x::AbstractString)
s = Base.cconvert(Cwstring, x)
copyto!(dest, reinterpret(UInt8, s))
return pointer(dest)
stevengj marked this conversation as resolved.
Show resolved Hide resolved
end


# need to be able to get the version before Python is initialized
Py_GetVersion(libpy) = unsafe_string(ccall(Libdl.dlsym(libpy, :Py_GetVersion), Ptr{UInt8}, ()))

# Fix the environment for running `python`, and setts IO encoding to UTF-8.
# If cmd is the Conda python, then additionally removes all PYTHON* and
# CONDA* environment variables.
function pythonenv(cmd::Cmd)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it sufficient to just run python -E to ignore PYTHON*? Why do we need to remove CONDA* since we are not running conda?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I guess we don't want to ignore these variables for non-Conda Python.

Copy link
Member Author

Choose a reason for hiding this comment

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

pythonenv is just moved from build.jl to depsutils.jl to make it usable at runtime.

env = copy(ENV)
if dirname(cmd.exec[1]) == abspath(Conda.PYTHONDIR)
pythonvars = String[]
for var in keys(env)
if startswith(var, "CONDA") || startswith(var, "PYTHON")
push!(pythonvars, var)
end
end
for var in pythonvars
pop!(env, var)
end
end
# set PYTHONIOENCODING when running python executable, so that
# we get UTF-8 encoded text as output (this is not the default on Windows).
env["PYTHONIOENCODING"] = "UTF-8"
setenv(cmd, env)
end


function pythonhome_of(pyprogramname::AbstractString)
if Sys.iswindows()
# PYTHONHOME tells python where to look for both pure python
# and binary modules. When it is set, it replaces both
# `prefix` and `exec_prefix` and we thus need to set it to
# both in case they differ. This is also what the
# documentation recommends. However, they are documented
# to always be the same on Windows, where it causes
# problems if we try to include both.
script = """
import sys
if hasattr(sys, "base_exec_prefix"):
sys.stdout.write(sys.base_exec_prefix)
else:
sys.stdout.write(sys.exec_prefix)
"""
else
script = """
import sys
if hasattr(sys, "base_exec_prefix"):
sys.stdout.write(sys.base_prefix)
sys.stdout.write(":")
sys.stdout.write(sys.base_exec_prefix)
Copy link
Member Author

@tkf tkf Oct 23, 2018

Choose a reason for hiding this comment

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

It looks like we should be using sys.base_prefix and sys.base_exec_prefix instead of sys.prefix and sys.exec_prefix to support venv. With this change, this PR (accidentally) solves #410 as well. The reason is commented just below: 9dde9a8#diff-d16c3d4423fa5349bf01729c8beea4cdR90

else:
sys.stdout.write(sys.prefix)
sys.stdout.write(":")
sys.stdout.write(sys.exec_prefix)
"""
# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
end
return read(pythonenv(`$pyprogramname -c $script`), String)
end
# To support `venv` standard library (as well as `virtualenv`), we
# need to use `sys.base_prefix` and `sys.base_exec_prefix` here.
# Otherwise, initializing Python in `__init__` below fails with
# unrecoverable error:
#
# Fatal Python error: initfsencoding: unable to load the file system codec
# ModuleNotFoundError: No module named 'encodings'
#
# This is because `venv` does not symlink standard libraries like
# `virtualenv`. For example, `lib/python3.X/encodings` does not
# exist. Rather, `venv` relies on the behavior of Python runtime:
#
# If a file named "pyvenv.cfg" exists one directory above
# sys.executable, sys.prefix and sys.exec_prefix are set to that
# directory and it is also checked for site-packages
# --- https://docs.python.org/3/library/venv.html
#
# Thus, we need point `PYTHONHOME` to `sys.base_prefix` and
# `sys.base_exec_prefix`. If the virtual environment is created by
# `virtualenv`, those `sys.base_*` paths point to the virtual
# environment. Thus, above code supports both use cases.
#
# See also:
# * https://docs.python.org/3/library/venv.html
# * https://docs.python.org/3/library/site.html
# * https://docs.python.org/3/library/sys.html#sys.base_exec_prefix
# * https://github.com/JuliaPy/PyCall.jl/issues/410
8 changes: 7 additions & 1 deletion src/PyCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import Base.Iterators: filter
include(joinpath(dirname(@__FILE__), "..", "deps","depsutils.jl"))
include("startup.jl")

"""
Python executable used by PyCall in the current process.
"""
current_python() = _current_python[]
const _current_python = Ref(pyprogramname)
tkf marked this conversation as resolved.
Show resolved Hide resolved

#########################################################################

# Mirror of C PyObject struct (for non-debugging Python builds).
Expand Down Expand Up @@ -506,7 +512,7 @@ you want to use, run Pkg.build("PyCall"), and re-launch Julia.
msg = msg * """
PyCall is currently configured to use the Python version at:

$python
$(current_python())

and you should use whatever mechanism you usually use (apt-get, pip, conda,
etcetera) to install the Python package containing the $name module.
Expand Down
Loading