From 69f08db8d0f9e285eb09ba0cbbd5fa200451af57 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 20 Sep 2018 23:00:13 -0700 Subject: [PATCH 01/25] Support virtual environment --- src/pyinit.jl | 108 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 7515b12c..955185d3 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -60,6 +60,71 @@ function pyjlwrap_init() end end +######################################################################### +# Virtual environment support + +_clength(x::Cstring) = ccall(:strlen, Csize_t, (Cstring,), x) + 1 +_clength(x) = length(x) + +function __leak(::Type{T}, x) where T + n = _clength(x) + ptr = ccall(:malloc, Ptr{T}, (Csize_t,), n * sizeof(T)) + unsafe_copyto!(ptr, pointer(x), n) + return ptr +end + +""" + _leak(T::Type, x::AbstractString) :: Ptr + _leak(x::Array) :: Ptr + +Leak `x` from Julia's GCer. This is meant to be used only for +`Py_SetPythonHome` and `Py_SetProgramName` where the Python +documentation demands that the passed argument must points to "static +storage whose contents will not change for the duration of the +program's execution" (although it seems that in newer CPython versions +the contents are copied internally). +""" +_leak(x::Union{Cstring, Array}) = __leak(eltype(x), x) +_leak(T::Type, x::AbstractString) = + _leak(Base.unsafe_convert(T, Base.cconvert(T, x))) +_leak(::Type{Cwstring}, x::AbstractString) = + _leak(Base.cconvert(Cwstring, x)) + +function pythonhome_of(pyprogramname::AbstractString) + if Sys.iswindows() + script = """ + import sys + sys.stdout.write(sys.exec_prefix) + """ + # See where PYTHONHOME is mentioned in ../deps/build.jl + else + script = """ + import sys + 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 + cmd = `$pyprogramname -c $script` + + # For Windows: + env = copy(ENV) + env["PYTHONIOENCODING"] = "UTF-8" + cmd = setenv(cmd, env) + + return read(cmd, String) +end + +function find_libpython(python::AbstractString) + script = joinpath(@__DIR__, "..", "deps", "find_libpython.py") + try + return read(`$python $script`, String) + catch + return nothing + end +end + ######################################################################### function __init__() @@ -76,7 +141,48 @@ function __init__() already_inited = 0 != ccall((@pysym :Py_IsInitialized), Cint, ()) - if !already_inited + if already_inited + # Importing from PyJulia takes this path. + elseif isfile(get(ENV, "PYCALL_JL_RUNTIME_PYTHON", "")) + venv_python = ENV["PYCALL_JL_RUNTIME_PYTHON"] + + # Check libpython compatibility. + venv_libpython = find_libpython(venv_python) + if venv_libpython === nothing + error(""" + `libpython` for $venv_python cannot be found. + PyCall.jl cannot initialize Python safely. + """) + elseif venv_libpython != libpython + error(""" + Incompatible `libpython` detected. + `libpython` for $venv_python is: + $venv_libpython + `libpython` for $pyprogramname is: + $libpython + PyCall.jl only supports loading Python environment using + the same `libpython`. + """) + end + + if haskey(ENV, "PYCALL_JL_RUNTIME_PYTHONHOME") + venv_home = ENV["PYCALL_JL_RUNTIME_PYTHONHOME"] + else + venv_home = pythonhome_of(venv_python) + end + if pyversion.major < 3 + ccall((@pysym :Py_SetPythonHome), Cvoid, (Cstring,), + _leak(Cstring, venv_home)) + ccall((@pysym :Py_SetProgramName), Cvoid, (Cstring,), + _leak(Cstring, venv_python)) + else + ccall((@pysym :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},), + _leak(Cwstring, venv_home)) + ccall((@pysym :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},), + _leak(Cwstring, venv_python)) + end + ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) + else Py_SetPythonHome(libpy_handle, PYTHONHOME, wPYTHONHOME, pyversion) if !isempty(pyprogramname) if pyversion.major < 3 From 66eb0696529d512f674d888f665325cb276fd6a5 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 21 Oct 2018 09:59:59 -0700 Subject: [PATCH 02/25] Add test_venv.jl --- src/pyinit.jl | 16 +++++++++--- test/runtests.jl | 1 + test/test_venv.jl | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 test/test_venv.jl diff --git a/src/pyinit.jl b/src/pyinit.jl index 955185d3..555b771c 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -106,14 +106,22 @@ function pythonhome_of(pyprogramname::AbstractString) """ # https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME end - cmd = `$pyprogramname -c $script` + return read(python_cmd(`-c $script`), String) +end + +""" + python_cmd(args::Cmd = ``) :: Cmd + +Create an appropriate `Cmd` for running Python program with command +line arguments `args`. +""" +function python_cmd(args::Cmd = ``) + cmd = `$pyprogramname $args` # For Windows: env = copy(ENV) env["PYTHONIOENCODING"] = "UTF-8" - cmd = setenv(cmd, env) - - return read(cmd, String) + return setenv(cmd, env) end function find_libpython(python::AbstractString) diff --git a/test/runtests.jl b/test/runtests.jl index a1951214..5f207fcf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -667,3 +667,4 @@ def try_call(f): end include("test_pyfncall.jl") +include("test_venv.jl") diff --git a/test/test_venv.jl b/test/test_venv.jl new file mode 100644 index 00000000..101b3044 --- /dev/null +++ b/test/test_venv.jl @@ -0,0 +1,63 @@ +using PyCall, Compat, Compat.Test +using Compat: @info + + +@testset "fuzz PyCall._leak" begin + N = 10000 + + @testset "_leak(Cstring, ...)" begin + for i in 1:N + x = String(rand('A':'z', rand(1:1000))) + y = Base.unsafe_string(PyCall._leak(Cstring, x)) + @test x == y + end + end + + @testset "_leak(Cwstring, ...)" begin + for i in 1:N + x = String(rand('A':'z', rand(1:1000))) + a = Base.cconvert(Cwstring, x) + ptr = PyCall._leak(a) + z = unsafe_wrap(Array, ptr, size(a)) + @test z[end] == 0 + y = transcode(String, z)[1:end-1] + @test x == y + end + end +end + + +@testset "venv activation" begin + if PyCall.conda + @info "Skip venv test with conda." + elseif !success(PyCall.python_cmd(`-c "import venv"`)) + @info "Skip venv test since venv package is missing." + else + mktempdir() do path + # Create a new virtualenv + run(PyCall.python_cmd(`-m venv $path`)) + newpython = joinpath(path, "bin", "python") + if Compat.Sys.iswindows() + newpython *= ".exe" + end + @test isfile(newpython) + + # Run a fresh Julia process with new Python environment + if VERSION < v"0.7.0-" + setup_code = "" + else + setup_code = Base.load_path_setup_code() + end + code = """ + $setup_code + using PyCall + print(PyCall.pyimport("sys")[:executable]) + """ + env = copy(ENV) + env["PYCALL_JL_RUNTIME_PYTHON"] = newpython + jlcmd = setenv(`$(Base.julia_cmd()) -e $code`, env) + output = read(jlcmd, String) + @test newpython == output + end + end +end From 0f80a516ff68f08f616a40b72011edfc811529e5 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 21 Oct 2018 10:27:21 -0700 Subject: [PATCH 03/25] Add python3.4-venv in Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 85c48986..1a306e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: julia +dist: trusty # update "python3.4-venv" below when updating os: - linux - osx @@ -11,6 +12,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" From f9f53e6f6c682e04a78889a5277841018151db73 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 21 Oct 2018 11:04:03 -0700 Subject: [PATCH 04/25] Fix pythonhome_of for Julia 0.6 --- src/pyinit.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 555b771c..472d9a88 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -91,7 +91,7 @@ _leak(::Type{Cwstring}, x::AbstractString) = _leak(Base.cconvert(Cwstring, x)) function pythonhome_of(pyprogramname::AbstractString) - if Sys.iswindows() + if Compat.Sys.iswindows() script = """ import sys sys.stdout.write(sys.exec_prefix) From 9be4dd71cc4923c5d6e74158b41ba7e7f5eb5ac8 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 21 Oct 2018 11:58:59 -0700 Subject: [PATCH 05/25] Properly treat venv path in Windows tests Since this is useful outside PyCall testing, I included this functionality in python_cmd function instead of inlining it to the test. --- src/pyinit.jl | 21 ++++++++++++++++++--- test/test_venv.jl | 10 +++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 472d9a88..1f368368 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -110,13 +110,28 @@ function pythonhome_of(pyprogramname::AbstractString) end """ - python_cmd(args::Cmd = ``) :: Cmd + python_cmd(args::Cmd = ``; venv) :: Cmd Create an appropriate `Cmd` for running Python program with command line arguments `args`. + +# Keyword Arguments +- `venv::String`: The path of a virtualenv to be used instead of the + default environment with which PyCall isconfigured. """ -function python_cmd(args::Cmd = ``) - cmd = `$pyprogramname $args` +function python_cmd(args::Cmd = ``; venv::Union{Nothing, String} = nothing) + if venv == nothing + py = pyprogramname + else + # See: + # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 + if Compat.Sys.iswindows() + py = joinpath(venv, "Scripts", "python.exe") + else + py = joinpath(venv, "bin", "python") + end + end + cmd = `$py $args` # For Windows: env = copy(ENV) diff --git a/test/test_venv.jl b/test/test_venv.jl index 101b3044..a0331f71 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -36,9 +36,13 @@ end mktempdir() do path # Create a new virtualenv run(PyCall.python_cmd(`-m venv $path`)) - newpython = joinpath(path, "bin", "python") - if Compat.Sys.iswindows() - newpython *= ".exe" + newpython = PyCall.python_cmd(venv=path).exec[1] + if !isfile(newpython) + @info """ + Python executable $newpython does not exists. + This directory contains only the following files: + $(join(readdir(dirname(newpython)), '\n')) + """ end @test isfile(newpython) From 31ef039dc7316c50fe500833f192bab7764050ca Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 21 Oct 2018 12:36:49 -0700 Subject: [PATCH 06/25] Mark venv test broken in Windows --- test/test_venv.jl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index a0331f71..dd51124c 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -60,8 +60,18 @@ end env = copy(ENV) env["PYCALL_JL_RUNTIME_PYTHON"] = newpython jlcmd = setenv(`$(Base.julia_cmd()) -e $code`, env) - output = read(jlcmd, String) - @test newpython == output + if Compat.Sys.iswindows() + # Marking the test broken in Windows. It seems that + # venv copies .dll on Windows and libpython check in + # PyCall.__init__ detects that. + @test_broken begin + output = read(jlcmd, String) + newpython == output + end + else + output = read(jlcmd, String) + @test newpython == output + end end end end From 131369cc20a2a83e76adce6080af4fc146a6586d Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 22 Oct 2018 18:08:20 -0700 Subject: [PATCH 07/25] Support venv properly * `venv` support is now properly tested. This finds a bug in `pythonhome_of`: `sys.base_prefix` should be used instead of `sys.prefix` (same for `sys.base_exec_prefix`). * The path to Python executable passed to `pythonhome_of` was ignored when `python_cmd` was introduced. This is fixed now. * Use `python_cmd` in `find_libpython`. This properly sets `PYTHONIOENCODING`. --- src/pyinit.jl | 80 +++++++++++++++++++++++++++++++++++------------ test/test_venv.jl | 36 +++++++++++++++++---- 2 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 1f368368..eb6dad15 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -94,23 +94,69 @@ function pythonhome_of(pyprogramname::AbstractString) if Compat.Sys.iswindows() script = """ import sys - sys.stdout.write(sys.exec_prefix) + if hasattr(sys, "base_exec_prefix"): + sys.stdout.write(sys.base_exec_prefix) + else: + sys.stdout.write(sys.exec_prefix) """ # See where PYTHONHOME is mentioned in ../deps/build.jl else script = """ import sys - sys.stdout.write(sys.prefix) - sys.stdout.write(":") - sys.stdout.write(sys.exec_prefix) + if hasattr(sys, "base_exec_prefix"): + sys.stdout.write(sys.base_prefix) + sys.stdout.write(":") + sys.stdout.write(sys.base_exec_prefix) + 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(python_cmd(`-c $script`), String) + return read(python_cmd(`-c $script`; python = pyprogramname), 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 + +venv_python(::Nothing) = pyprogramname + +function venv_python(venv::AbstractString) + # See: + # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 + if Compat.Sys.iswindows() + return joinpath(venv, "Scripts", "python.exe") + else + return joinpath(venv, "bin", "python") + end end """ - python_cmd(args::Cmd = ``; venv) :: Cmd + python_cmd(args::Cmd = ``; venv, python) :: Cmd Create an appropriate `Cmd` for running Python program with command line arguments `args`. @@ -118,20 +164,13 @@ line arguments `args`. # Keyword Arguments - `venv::String`: The path of a virtualenv to be used instead of the default environment with which PyCall isconfigured. +- `python::String`: The path to the Python executable. `venv` is ignored + when this argument is specified. """ -function python_cmd(args::Cmd = ``; venv::Union{Nothing, String} = nothing) - if venv == nothing - py = pyprogramname - else - # See: - # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 - if Compat.Sys.iswindows() - py = joinpath(venv, "Scripts", "python.exe") - else - py = joinpath(venv, "bin", "python") - end - end - cmd = `$py $args` +function python_cmd(args::Cmd = ``; + venv::Union{Nothing, AbstractString} = nothing, + python::AbstractString = venv_python(venv)) + cmd = `$python $args` # For Windows: env = copy(ENV) @@ -141,8 +180,9 @@ end function find_libpython(python::AbstractString) script = joinpath(@__DIR__, "..", "deps", "find_libpython.py") + cmd = python_cmd(`$script`; python = python) try - return read(`$python $script`, String) + return read(cmd, String) catch return nothing end diff --git a/test/test_venv.jl b/test/test_venv.jl index dd51124c..b7151d55 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -1,5 +1,5 @@ using PyCall, Compat, Compat.Test -using Compat: @info +using Compat: @info, @warn @testset "fuzz PyCall._leak" begin @@ -28,13 +28,28 @@ end @testset "venv activation" begin + if Compat.Sys.isunix() && !startswith(PyCall.pyprogramname, "/usr/bin") + @warn """ + Note that "venv activation" test does not work when PyCall is built + with a Python executable created by `virtualenv` command. You are + using possibly non-system Python executable: + $(PyCall.pyprogramname) + + Following commands may solve the failure (if any): + julia> ENV["PYTHON"] = "/usr/bin/python3" + pkg> build PyCall + pkg> test PyCall + """ + # Let's just warn it. Not sure how to reliably detect it... + end + if PyCall.conda @info "Skip venv test with conda." elseif !success(PyCall.python_cmd(`-c "import venv"`)) @info "Skip venv test since venv package is missing." else mktempdir() do path - # Create a new virtualenv + # Create a new virtual environment run(PyCall.python_cmd(`-m venv $path`)) newpython = PyCall.python_cmd(venv=path).exec[1] if !isfile(newpython) @@ -55,22 +70,31 @@ end code = """ $setup_code using PyCall - print(PyCall.pyimport("sys")[:executable]) + println(PyCall.pyimport("sys")[:executable]) + println(PyCall.pyimport("sys")[:exec_prefix]) + println(PyCall.pyimport("pip")[:__file__]) """ + # Note that `pip` is just some arbitrary non-standard + # library. Using standard library like `os` does not work + # because those files are not created. env = copy(ENV) env["PYCALL_JL_RUNTIME_PYTHON"] = newpython - jlcmd = setenv(`$(Base.julia_cmd()) -e $code`, env) + jlcmd = setenv(`$(Base.julia_cmd()) --startup-file=no -e $code`, env) if Compat.Sys.iswindows() # Marking the test broken in Windows. It seems that # venv copies .dll on Windows and libpython check in # PyCall.__init__ detects that. @test_broken begin output = read(jlcmd, String) - newpython == output + sys_executable, exec_prefix, mod_file = split(output, "\n") + newpython == sys_executable end else output = read(jlcmd, String) - @test newpython == output + sys_executable, exec_prefix, mod_file = split(output, "\n") + @test newpython == sys_executable + @test startswith(exec_prefix, path) + @test startswith(mod_file, path) end end end From 7866c931b05864442f585f374b055a141e7b0dcd Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 22 Oct 2018 21:21:11 -0700 Subject: [PATCH 08/25] Test with virtualenv command as well --- test/test_venv.jl | 125 +++++++++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index b7151d55..98cc6002 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -27,6 +27,84 @@ using Compat: @info, @warn end +function test_venv_has_python(path) + newpython = PyCall.python_cmd(venv=path).exec[1] + if !isfile(newpython) + @info """ + Python executable $newpython does not exists. + This directory contains only the following files: + $(join(readdir(dirname(newpython)), '\n')) + """ + end + @test isfile(newpython) +end + + +function test_venv_activation(path) + newpython = PyCall.python_cmd(venv=path).exec[1] + + # Run a fresh Julia process with new Python environment + if VERSION < v"0.7.0-" + setup_code = "" + else + setup_code = Base.load_path_setup_code() + end + code = """ + $setup_code + using PyCall + println(PyCall.pyimport("sys")[:executable]) + println(PyCall.pyimport("sys")[:exec_prefix]) + println(PyCall.pyimport("pip")[:__file__]) + """ + # Note that `pip` is just some arbitrary non-standard + # library. Using standard library like `os` does not work + # because those files are not created. + env = copy(ENV) + env["PYCALL_JL_RUNTIME_PYTHON"] = newpython + jlcmd = setenv(`$(Base.julia_cmd()) --startup-file=no -e $code`, env) + if Compat.Sys.iswindows() + # Marking the test broken in Windows. It seems that + # venv copies .dll on Windows and libpython check in + # PyCall.__init__ detects that. + @test_broken begin + output = read(jlcmd, String) + sys_executable, exec_prefix, mod_file = split(output, "\n") + newpython == sys_executable + end + else + output = read(jlcmd, String) + sys_executable, exec_prefix, mod_file = split(output, "\n") + @test newpython == sys_executable + @test startswith(exec_prefix, path) + @test startswith(mod_file, path) + end +end + + +@testset "virtualenv activation" begin + if Compat.Sys.which("virtualenv") === nothing + @info "No virtualenv command. Skipping the test..." + else + mktempdir() do path + run(`virtualenv $path`) + test_venv_has_python(path) + + newpython = PyCall.python_cmd(venv=path).exec[1] + venv_libpython = PyCall.find_libpython(newpython) + if venv_libpython != PyCall.libpython + @info """ + virtualenv created an environment with incompatible libpython: + $venv_libpython + """ + return + end + + test_venv_activation(path) + end + end +end + + @testset "venv activation" begin if Compat.Sys.isunix() && !startswith(PyCall.pyprogramname, "/usr/bin") @warn """ @@ -51,51 +129,8 @@ end mktempdir() do path # Create a new virtual environment run(PyCall.python_cmd(`-m venv $path`)) - newpython = PyCall.python_cmd(venv=path).exec[1] - if !isfile(newpython) - @info """ - Python executable $newpython does not exists. - This directory contains only the following files: - $(join(readdir(dirname(newpython)), '\n')) - """ - end - @test isfile(newpython) - - # Run a fresh Julia process with new Python environment - if VERSION < v"0.7.0-" - setup_code = "" - else - setup_code = Base.load_path_setup_code() - end - code = """ - $setup_code - using PyCall - println(PyCall.pyimport("sys")[:executable]) - println(PyCall.pyimport("sys")[:exec_prefix]) - println(PyCall.pyimport("pip")[:__file__]) - """ - # Note that `pip` is just some arbitrary non-standard - # library. Using standard library like `os` does not work - # because those files are not created. - env = copy(ENV) - env["PYCALL_JL_RUNTIME_PYTHON"] = newpython - jlcmd = setenv(`$(Base.julia_cmd()) --startup-file=no -e $code`, env) - if Compat.Sys.iswindows() - # Marking the test broken in Windows. It seems that - # venv copies .dll on Windows and libpython check in - # PyCall.__init__ detects that. - @test_broken begin - output = read(jlcmd, String) - sys_executable, exec_prefix, mod_file = split(output, "\n") - newpython == sys_executable - end - else - output = read(jlcmd, String) - sys_executable, exec_prefix, mod_file = split(output, "\n") - @test newpython == sys_executable - @test startswith(exec_prefix, path) - @test startswith(mod_file, path) - end + test_venv_has_python(path) + test_venv_activation(path) end end end From d16a4d4fecd6d24bb85f039669991d75a6dfe336 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 22 Oct 2018 22:20:27 -0700 Subject: [PATCH 09/25] Specify Python version in virtualenv activation test --- test/test_venv.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index 98cc6002..f8f62a0e 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -82,11 +82,14 @@ end @testset "virtualenv activation" begin + pyname = "python$(pyversion.major).$(pyversion.minor)" if Compat.Sys.which("virtualenv") === nothing @info "No virtualenv command. Skipping the test..." + elseif Compat.Sys.which(pyname) === nothing + @info "No $pyname command. Skipping the test..." else mktempdir() do path - run(`virtualenv $path`) + run(`virtualenv --python=$pyname $path`) test_venv_has_python(path) newpython = PyCall.python_cmd(venv=path).exec[1] From 9dde9a87209cf84db642a53f131b2d106c46445b Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 22 Oct 2018 22:50:45 -0700 Subject: [PATCH 10/25] Move pythonhome_of to depsutils.jl and use it during build --- deps/build.jl | 32 +----------------- deps/depsutils.jl | 83 +++++++++++++++++++++++++++++++++++++++++++++++ src/pyinit.jl | 60 +--------------------------------- 3 files changed, 85 insertions(+), 90 deletions(-) diff --git a/deps/build.jl b/deps/build.jl index 85fed4cd..3d43c4ed 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -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')") @@ -194,15 +172,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") - Compat.Sys.iswindows() ? exec_prefix : pysys(python, "prefix") * ":" * exec_prefix + pythonhome_of(python) else ENV["PYTHONHOME"] end diff --git a/deps/depsutils.jl b/deps/depsutils.jl index acf0defd..c6371fd7 100644 --- a/deps/depsutils.jl +++ b/deps/depsutils.jl @@ -31,3 +31,86 @@ 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) + 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 Compat.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) + 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 diff --git a/src/pyinit.jl b/src/pyinit.jl index eb6dad15..890b5680 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -90,59 +90,6 @@ _leak(T::Type, x::AbstractString) = _leak(::Type{Cwstring}, x::AbstractString) = _leak(Base.cconvert(Cwstring, x)) -function pythonhome_of(pyprogramname::AbstractString) - if Compat.Sys.iswindows() - script = """ - import sys - if hasattr(sys, "base_exec_prefix"): - sys.stdout.write(sys.base_exec_prefix) - else: - sys.stdout.write(sys.exec_prefix) - """ - # See where PYTHONHOME is mentioned in ../deps/build.jl - 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) - 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(python_cmd(`-c $script`; python = pyprogramname), 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 - venv_python(::Nothing) = pyprogramname function venv_python(venv::AbstractString) @@ -170,12 +117,7 @@ line arguments `args`. function python_cmd(args::Cmd = ``; venv::Union{Nothing, AbstractString} = nothing, python::AbstractString = venv_python(venv)) - cmd = `$python $args` - - # For Windows: - env = copy(ENV) - env["PYTHONIOENCODING"] = "UTF-8" - return setenv(cmd, env) + return pythonenv(`$python $args`) end function find_libpython(python::AbstractString) From 7bb0d135b386eb56ea97cb566d237456a69652e7 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 23 Oct 2018 01:07:51 -0700 Subject: [PATCH 11/25] Support "venv activation" test inside virtualenv --- src/pyinit.jl | 6 +++--- test/test_venv.jl | 35 ++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 890b5680..6647b671 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -92,13 +92,13 @@ _leak(::Type{Cwstring}, x::AbstractString) = venv_python(::Nothing) = pyprogramname -function venv_python(venv::AbstractString) +function venv_python(venv::AbstractString, suffix::AbstractString = "") # See: # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 if Compat.Sys.iswindows() - return joinpath(venv, "Scripts", "python.exe") + return joinpath(venv, "Scripts", "python$suffix.exe") else - return joinpath(venv, "bin", "python") + return joinpath(venv, "bin", "python$suffix") end end diff --git a/test/test_venv.jl b/test/test_venv.jl index f8f62a0e..2f7f2cee 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -109,29 +109,34 @@ end @testset "venv activation" begin - if Compat.Sys.isunix() && !startswith(PyCall.pyprogramname, "/usr/bin") - @warn """ - Note that "venv activation" test does not work when PyCall is built - with a Python executable created by `virtualenv` command. You are - using possibly non-system Python executable: - $(PyCall.pyprogramname) - - Following commands may solve the failure (if any): - julia> ENV["PYTHON"] = "/usr/bin/python3" - pkg> build PyCall - pkg> test PyCall - """ - # Let's just warn it. Not sure how to reliably detect it... + # In case PyCall is built with a Python executable created by + # `virtualenv`, let's try to find the original Python executable. + # Otherwise, `venv` does not work with this Python executable: + # https://bugs.python.org/issue30811 + sys = PyCall.pyimport("sys") + if haskey(sys, :real_prefix) + # sys.real_prefix is set by virtualenv and does not exist in + # standard Python: + # https://github.com/pypa/virtualenv/blob/16.0.0/virtualenv_embedded/site.py#L554 + candidates = [ + PyCall.venv_python(sys[:real_prefix], "$(pyversion.major).$(pyversion.minor)"), + PyCall.venv_python(sys[:real_prefix], "$(pyversion.major)"), + PyCall.venv_python(sys[:real_prefix]), + PyCall.pyprogramname, # must exists + ] + python = candidates[findfirst(isfile, candidates)] + else + python = PyCall.pyprogramname end if PyCall.conda @info "Skip venv test with conda." - elseif !success(PyCall.python_cmd(`-c "import venv"`)) + elseif !success(PyCall.python_cmd(`-c "import venv"`, python=python)) @info "Skip venv test since venv package is missing." else mktempdir() do path # Create a new virtual environment - run(PyCall.python_cmd(`-m venv $path`)) + run(PyCall.python_cmd(`-m venv $path`, python=python)) test_venv_has_python(path) test_venv_activation(path) end From ef27d1dc23076464c2a54779a43a03a68221c565 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 23 Oct 2018 20:20:37 -0700 Subject: [PATCH 12/25] Show correct Python executable when import fails --- src/PyCall.jl | 8 +++++++- src/pyinit.jl | 14 +++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/PyCall.jl b/src/PyCall.jl index 8e5ccc46..725ee5bf 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -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) + ######################################################################### # Mirror of C PyObject struct (for non-debugging Python builds). @@ -461,7 +467,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. diff --git a/src/pyinit.jl b/src/pyinit.jl index 6647b671..864d7a7d 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -149,19 +149,19 @@ function __init__() if already_inited # Importing from PyJulia takes this path. elseif isfile(get(ENV, "PYCALL_JL_RUNTIME_PYTHON", "")) - venv_python = ENV["PYCALL_JL_RUNTIME_PYTHON"] + _current_python[] = ENV["PYCALL_JL_RUNTIME_PYTHON"] # Check libpython compatibility. - venv_libpython = find_libpython(venv_python) + venv_libpython = find_libpython(current_python()) if venv_libpython === nothing error(""" - `libpython` for $venv_python cannot be found. + `libpython` for $(current_python()) cannot be found. PyCall.jl cannot initialize Python safely. """) elseif venv_libpython != libpython error(""" Incompatible `libpython` detected. - `libpython` for $venv_python is: + `libpython` for $(current_python()) is: $venv_libpython `libpython` for $pyprogramname is: $libpython @@ -173,18 +173,18 @@ function __init__() if haskey(ENV, "PYCALL_JL_RUNTIME_PYTHONHOME") venv_home = ENV["PYCALL_JL_RUNTIME_PYTHONHOME"] else - venv_home = pythonhome_of(venv_python) + venv_home = pythonhome_of(current_python()) end if pyversion.major < 3 ccall((@pysym :Py_SetPythonHome), Cvoid, (Cstring,), _leak(Cstring, venv_home)) ccall((@pysym :Py_SetProgramName), Cvoid, (Cstring,), - _leak(Cstring, venv_python)) + _leak(Cstring, current_python())) else ccall((@pysym :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},), _leak(Cwstring, venv_home)) ccall((@pysym :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},), - _leak(Cwstring, venv_python)) + _leak(Cwstring, current_python())) end ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) else From 83b52161075e8b9d0083dcf299b605b73e9b2e75 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 26 Oct 2018 22:56:27 -0700 Subject: [PATCH 13/25] Use static memory location than malloc --- src/pyinit.jl | 50 ++++++++++++++++++++++------------------------- test/test_venv.jl | 25 ------------------------ 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 864d7a7d..5081a96a 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -63,32 +63,28 @@ end ######################################################################### # Virtual environment support -_clength(x::Cstring) = ccall(:strlen, Csize_t, (Cstring,), x) + 1 -_clength(x) = length(x) - -function __leak(::Type{T}, x) where T - n = _clength(x) - ptr = ccall(:malloc, Ptr{T}, (Csize_t,), n * sizeof(T)) - unsafe_copyto!(ptr, pointer(x), n) - return ptr -end +# Static buffer to make sure the string passed to libpython persists +# for the lifetime of the program, as CPython API requires: +const __venv_programname = Vector{UInt8}(undef, 1024) +const __venv_pythonhome = Vector{UInt8}(undef, 1024) """ - _leak(T::Type, x::AbstractString) :: Ptr - _leak(x::Array) :: Ptr - -Leak `x` from Julia's GCer. This is meant to be used only for -`Py_SetPythonHome` and `Py_SetProgramName` where the Python -documentation demands that the passed argument must points to "static -storage whose contents will not change for the duration of the -program's execution" (although it seems that in newer CPython versions -the contents are copied internally). + _preserveas!(dest::Vector{UInt8}, (Cstring|Cwstring), x::String) :: Ptr + +Copy `x` as `Cstring` or `Cwstring` to `dest`. """ -_leak(x::Union{Cstring, Array}) = __leak(eltype(x), x) -_leak(T::Type, x::AbstractString) = - _leak(Base.unsafe_convert(T, Base.cconvert(T, x))) -_leak(::Type{Cwstring}, x::AbstractString) = - _leak(Base.cconvert(Cwstring, x)) +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!(reinterpret(Int32, dest), s) + return pointer(dest) +end venv_python(::Nothing) = pyprogramname @@ -177,14 +173,14 @@ function __init__() end if pyversion.major < 3 ccall((@pysym :Py_SetPythonHome), Cvoid, (Cstring,), - _leak(Cstring, venv_home)) + _preserveas!(__venv_pythonhome, Cstring, venv_home)) ccall((@pysym :Py_SetProgramName), Cvoid, (Cstring,), - _leak(Cstring, current_python())) + _preserveas!(__venv_programname, Cstring, current_python())) else ccall((@pysym :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},), - _leak(Cwstring, venv_home)) + _preserveas!(__venv_pythonhome, Cwstring, venv_home)) ccall((@pysym :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},), - _leak(Cwstring, current_python())) + _preserveas!(__venv_programname, Cwstring, current_python())) end ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) else diff --git a/test/test_venv.jl b/test/test_venv.jl index 2f7f2cee..ca01a1db 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -2,31 +2,6 @@ using PyCall, Compat, Compat.Test using Compat: @info, @warn -@testset "fuzz PyCall._leak" begin - N = 10000 - - @testset "_leak(Cstring, ...)" begin - for i in 1:N - x = String(rand('A':'z', rand(1:1000))) - y = Base.unsafe_string(PyCall._leak(Cstring, x)) - @test x == y - end - end - - @testset "_leak(Cwstring, ...)" begin - for i in 1:N - x = String(rand('A':'z', rand(1:1000))) - a = Base.cconvert(Cwstring, x) - ptr = PyCall._leak(a) - z = unsafe_wrap(Array, ptr, size(a)) - @test z[end] == 0 - y = transcode(String, z)[1:end-1] - @test x == y - end - end -end - - function test_venv_has_python(path) newpython = PyCall.python_cmd(venv=path).exec[1] if !isfile(newpython) From 9fcdff707e328e0b4c8f9f9214f509e6495ed983 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 26 Oct 2018 23:13:06 -0700 Subject: [PATCH 14/25] Create virtual environment at non-ascii evil path --- test/test_venv.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index ca01a1db..65646b08 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -63,7 +63,8 @@ end elseif Compat.Sys.which(pyname) === nothing @info "No $pyname command. Skipping the test..." else - mktempdir() do path + mktempdir() do tmppath + path = joinpath(tmppath, "ϵνιℓ") run(`virtualenv --python=$pyname $path`) test_venv_has_python(path) @@ -109,7 +110,8 @@ end elseif !success(PyCall.python_cmd(`-c "import venv"`, python=python)) @info "Skip venv test since venv package is missing." else - mktempdir() do path + mktempdir() do tmppath + path = joinpath(tmppath, "ϵνιℓ") # Create a new virtual environment run(PyCall.python_cmd(`-m venv $path`, python=python)) test_venv_has_python(path) From 8fe0cec5d3849ec48ea441dbdcc705bb306cedb6 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 26 Oct 2018 23:23:11 -0700 Subject: [PATCH 15/25] Be kind to Python 2 --- test/test_venv.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index 65646b08..3c8dc886 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -64,7 +64,11 @@ end @info "No $pyname command. Skipping the test..." else mktempdir() do tmppath - path = joinpath(tmppath, "ϵνιℓ") + if PyCall.pyversion.major == 2 + path = joinpath(tmppath, "kind") + else + path = joinpath(tmppath, "ϵνιℓ") + end run(`virtualenv --python=$pyname $path`) test_venv_has_python(path) @@ -111,7 +115,11 @@ end @info "Skip venv test since venv package is missing." else mktempdir() do tmppath - path = joinpath(tmppath, "ϵνιℓ") + if PyCall.pyversion.major == 2 + path = joinpath(tmppath, "kind") + else + path = joinpath(tmppath, "ϵνιℓ") + end # Create a new virtual environment run(PyCall.python_cmd(`-m venv $path`, python=python)) test_venv_has_python(path) From 41a3107a48c564cbe2962af0be1b5ea7f23ef93a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 26 Oct 2018 23:46:39 -0700 Subject: [PATCH 16/25] Unify Py_SetPythonHome wrapper functions --- deps/build.jl | 7 ------- deps/depsutils.jl | 53 ++++++++++++++++++++++++++++++++++++++++------- src/pyinit.jl | 46 ++++------------------------------------ src/startup.jl | 2 +- 4 files changed, 50 insertions(+), 58 deletions(-) diff --git a/deps/build.jl b/deps/build.jl index 3d43c4ed..c562773f 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -92,11 +92,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. @@ -190,10 +185,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 diff --git a/deps/depsutils.jl b/deps/depsutils.jl index c6371fd7..5c88c9cf 100644 --- a/deps/depsutils.jl +++ b/deps/depsutils.jl @@ -13,22 +13,59 @@ 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`. +""" +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!(reinterpret(Int32, dest), s) + return pointer(dest) +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}, ())) diff --git a/src/pyinit.jl b/src/pyinit.jl index 5081a96a..8d5b5b11 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -63,29 +63,6 @@ end ######################################################################### # Virtual environment support -# Static buffer to make sure the string passed to libpython persists -# for the lifetime of the program, as CPython API requires: -const __venv_programname = Vector{UInt8}(undef, 1024) -const __venv_pythonhome = Vector{UInt8}(undef, 1024) - -""" - _preserveas!(dest::Vector{UInt8}, (Cstring|Cwstring), x::String) :: Ptr - -Copy `x` as `Cstring` or `Cwstring` to `dest`. -""" -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!(reinterpret(Int32, dest), s) - return pointer(dest) -end - venv_python(::Nothing) = pyprogramname function venv_python(venv::AbstractString, suffix::AbstractString = "") @@ -171,27 +148,12 @@ function __init__() else venv_home = pythonhome_of(current_python()) end - if pyversion.major < 3 - ccall((@pysym :Py_SetPythonHome), Cvoid, (Cstring,), - _preserveas!(__venv_pythonhome, Cstring, venv_home)) - ccall((@pysym :Py_SetProgramName), Cvoid, (Cstring,), - _preserveas!(__venv_programname, Cstring, current_python())) - else - ccall((@pysym :Py_SetPythonHome), Cvoid, (Ptr{Cwchar_t},), - _preserveas!(__venv_pythonhome, Cwstring, venv_home)) - ccall((@pysym :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},), - _preserveas!(__venv_programname, Cwstring, current_python())) - end + Py_SetPythonHome(libpy_handle, pyversion, venv_home) + Py_SetProgramName(libpy_handle, pyversion, current_python()) ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) else - Py_SetPythonHome(libpy_handle, PYTHONHOME, wPYTHONHOME, pyversion) - if !isempty(pyprogramname) - if pyversion.major < 3 - ccall((@pysym :Py_SetProgramName), Cvoid, (Cstring,), pyprogramname) - else - ccall((@pysym :Py_SetProgramName), Cvoid, (Ptr{Cwchar_t},), wpyprogramname) - end - end + Py_SetPythonHome(libpy_handle, pyversion, PYTHONHOME) + Py_SetProgramName(libpy_handle, pyversion, pyprogramname) ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) end diff --git a/src/startup.jl b/src/startup.jl index daf3d615..5714ee91 100644 --- a/src/startup.jl +++ b/src/startup.jl @@ -46,7 +46,7 @@ if !symbols_present # Only to be used at top-level - pointer will be invalid after reload libpy_handle = Libdl.dlopen(libpython, Libdl.RTLD_LAZY|Libdl.RTLD_DEEPBIND|Libdl.RTLD_GLOBAL) # need SetPythonHome to avoid warning, #299 - Py_SetPythonHome(libpy_handle, PYTHONHOME, wPYTHONHOME, pyversion_build) + Py_SetPythonHome(libpy_handle, pyversion_build, PYTHONHOME) else @static if Compat.Sys.iswindows() pathbuf = Vector{UInt16}(undef, 1024) From 6d47896a4735e8f39fda2621e92e7d720c6a116c Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 1 Nov 2018 23:32:41 -0700 Subject: [PATCH 17/25] reinterpret source array instead of dest --- deps/depsutils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/depsutils.jl b/deps/depsutils.jl index 5c88c9cf..1149c47d 100644 --- a/deps/depsutils.jl +++ b/deps/depsutils.jl @@ -61,7 +61,7 @@ end function _preserveas!(dest::Vector{UInt8}, ::Type{Cwstring}, x::AbstractString) s = Base.cconvert(Cwstring, x) - copyto!(reinterpret(Int32, dest), s) + copyto!(dest, reinterpret(UInt8, s)) return pointer(dest) end From 66991405e33910b67d2143dad769f84a0b76c579 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 4 Nov 2018 05:21:58 -0800 Subject: [PATCH 18/25] Improve docs and comments --- deps/depsutils.jl | 4 +++- src/pyinit.jl | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/deps/depsutils.jl b/deps/depsutils.jl index 1149c47d..b946872e 100644 --- a/deps/depsutils.jl +++ b/deps/depsutils.jl @@ -50,7 +50,9 @@ end """ _preserveas!(dest::Vector{UInt8}, (Cstring|Cwstring), x::String) :: Ptr -Copy `x` as `Cstring` or `Cwstring` to `dest`. +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)) diff --git a/src/pyinit.jl b/src/pyinit.jl index 8d5b5b11..547eaa89 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -66,13 +66,15 @@ end venv_python(::Nothing) = pyprogramname function venv_python(venv::AbstractString, suffix::AbstractString = "") - # See: - # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 + # `suffix` is used to insert version number (e.g., "3.7") in tests + # (see ../test/test_venv.jl) if Compat.Sys.iswindows() return joinpath(venv, "Scripts", "python$suffix.exe") else return joinpath(venv, "bin", "python$suffix") end + # "Scripts" is used only in Windows and "bin" elsewhere: + # https://github.com/python/cpython/blob/3.7/Lib/venv/__init__.py#L116 end """ From 5e3d621e7f407dad8a0c714114aade71645da6ce Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 4 Nov 2018 05:23:46 -0800 Subject: [PATCH 19/25] Simplify __init__() by merging venv code path --- src/pyinit.jl | 63 +++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/pyinit.jl b/src/pyinit.jl index 547eaa89..a088d594 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -121,42 +121,41 @@ function __init__() already_inited = 0 != ccall((@pysym :Py_IsInitialized), Cint, ()) - if already_inited - # Importing from PyJulia takes this path. - elseif isfile(get(ENV, "PYCALL_JL_RUNTIME_PYTHON", "")) - _current_python[] = ENV["PYCALL_JL_RUNTIME_PYTHON"] - - # Check libpython compatibility. - venv_libpython = find_libpython(current_python()) - if venv_libpython === nothing - error(""" - `libpython` for $(current_python()) cannot be found. - PyCall.jl cannot initialize Python safely. - """) - elseif venv_libpython != libpython - error(""" - Incompatible `libpython` detected. - `libpython` for $(current_python()) is: - $venv_libpython - `libpython` for $pyprogramname is: - $libpython - PyCall.jl only supports loading Python environment using - the same `libpython`. - """) - end + if !already_inited + pyhome = PYTHONHOME + + if isfile(get(ENV, "PYCALL_JL_RUNTIME_PYTHON", "")) + _current_python[] = ENV["PYCALL_JL_RUNTIME_PYTHON"] + + # Check libpython compatibility. + venv_libpython = find_libpython(current_python()) + if venv_libpython === nothing + error(""" + `libpython` for $(current_python()) cannot be found. + PyCall.jl cannot initialize Python safely. + """) + elseif venv_libpython != libpython + error(""" + Incompatible `libpython` detected. + `libpython` for $(current_python()) is: + $venv_libpython + `libpython` for $pyprogramname is: + $libpython + PyCall.jl only supports loading Python environment using + the same `libpython`. + """) + end - if haskey(ENV, "PYCALL_JL_RUNTIME_PYTHONHOME") - venv_home = ENV["PYCALL_JL_RUNTIME_PYTHONHOME"] - else - venv_home = pythonhome_of(current_python()) + if haskey(ENV, "PYCALL_JL_RUNTIME_PYTHONHOME") + pyhome = ENV["PYCALL_JL_RUNTIME_PYTHONHOME"] + else + pyhome = pythonhome_of(current_python()) + end end - Py_SetPythonHome(libpy_handle, pyversion, venv_home) + + Py_SetPythonHome(libpy_handle, pyversion, pyhome) Py_SetProgramName(libpy_handle, pyversion, current_python()) ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) - else - Py_SetPythonHome(libpy_handle, pyversion, PYTHONHOME) - Py_SetProgramName(libpy_handle, pyversion, pyprogramname) - ccall((@pysym :Py_InitializeEx), Cvoid, (Cint,), 0) end # Will get reinitialized properly on first use From c56ec242fa89e1e604701c93395df1288f15d684 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 23 Jan 2019 19:05:40 -0800 Subject: [PATCH 20/25] Stop using Compat in test_venv.jl as in 45cbceee1fe70ae611acf14755b373c5ed6e6a2d --- test/test_venv.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index 3c8dc886..9068d994 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -1,5 +1,4 @@ -using PyCall, Compat, Compat.Test -using Compat: @info, @warn +using PyCall, Test function test_venv_has_python(path) @@ -37,7 +36,7 @@ function test_venv_activation(path) env = copy(ENV) env["PYCALL_JL_RUNTIME_PYTHON"] = newpython jlcmd = setenv(`$(Base.julia_cmd()) --startup-file=no -e $code`, env) - if Compat.Sys.iswindows() + if Sys.iswindows() # Marking the test broken in Windows. It seems that # venv copies .dll on Windows and libpython check in # PyCall.__init__ detects that. @@ -58,9 +57,9 @@ end @testset "virtualenv activation" begin pyname = "python$(pyversion.major).$(pyversion.minor)" - if Compat.Sys.which("virtualenv") === nothing + if Sys.which("virtualenv") === nothing @info "No virtualenv command. Skipping the test..." - elseif Compat.Sys.which(pyname) === nothing + elseif Sys.which(pyname) === nothing @info "No $pyname command. Skipping the test..." else mktempdir() do tmppath From 99ec5427ba7ed9d445f63f170b9f44453074419a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 23 Jan 2019 19:29:53 -0800 Subject: [PATCH 21/25] Use property-based syntax in test_venv.jl --- test/test_venv.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index 9068d994..2f2dad32 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -26,9 +26,9 @@ function test_venv_activation(path) code = """ $setup_code using PyCall - println(PyCall.pyimport("sys")[:executable]) - println(PyCall.pyimport("sys")[:exec_prefix]) - println(PyCall.pyimport("pip")[:__file__]) + println(PyCall.pyimport("sys").executable) + println(PyCall.pyimport("sys").exec_prefix) + println(PyCall.pyimport("pip").__file__) """ # Note that `pip` is just some arbitrary non-standard # library. Using standard library like `os` does not work @@ -93,14 +93,14 @@ end # Otherwise, `venv` does not work with this Python executable: # https://bugs.python.org/issue30811 sys = PyCall.pyimport("sys") - if haskey(sys, :real_prefix) + if hasproperty(sys, :real_prefix) # sys.real_prefix is set by virtualenv and does not exist in # standard Python: # https://github.com/pypa/virtualenv/blob/16.0.0/virtualenv_embedded/site.py#L554 candidates = [ - PyCall.venv_python(sys[:real_prefix], "$(pyversion.major).$(pyversion.minor)"), - PyCall.venv_python(sys[:real_prefix], "$(pyversion.major)"), - PyCall.venv_python(sys[:real_prefix]), + PyCall.venv_python(sys.real_prefix, "$(pyversion.major).$(pyversion.minor)"), + PyCall.venv_python(sys.real_prefix, "$(pyversion.major)"), + PyCall.venv_python(sys.real_prefix), PyCall.pyprogramname, # must exists ] python = candidates[findfirst(isfile, candidates)] From 923feb962948df2c087b41ec6a7e7d362fbac96f Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 23 Jan 2019 20:28:21 -0800 Subject: [PATCH 22/25] Add docs on virtual environment usage --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 66c835ec..95655190 100644 --- a/README.md +++ b/README.md @@ -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 environment](#python-virtual-environment) +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) @@ -587,6 +589,43 @@ any other Anaconda-based Python distro for which the user has installation privi (Note that you cannot use `@pyimport` safely with precompilation, because that declares a global constant that internally has a pointer to the module. You can use `pywrap(pyimport(...))` in your `__init__` function to a assign a global variable using the `.` notation like `@pyimport`, however, albeit without the type stability of the global `const` as above.) +## Python virtual environment + +Python's virtual environment created by [`venv`] and [`virtualenv`] +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 environment created by `conda` is not +supported. + +To use `PyCall` with a certain virtual environment, set environment +variable `PYCALL_JL_RUNTIME_PYTHON` *before* importing `PyCall` to +path to the Python executable. 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" +``` + +This feature works by calling [`Py_SetProgramName`] with the value +specified by `PYCALL_JL_RUNTIME_PYTHON`. Similarly, the path passed +to [`Py_SetPythonHome`] can be controlled by environment variable +`PYCALL_JL_RUNTIME_PYTHONHOME`. + +[`venv`]: https://docs.python.org/3/library/venv.html +[`virtualenv`]: https://virtualenv.pypa.io/en/latest/ +[`Py_SetProgramName`]: https://docs.python.org/3/c-api/init.html#c.Py_SetProgramName +[`Py_SetPythonHome`]: https://docs.python.org/3/c-api/init.html#c.Py_SetPythonHome + ## Author This package was written by [Steven G. Johnson](http://math.mit.edu/~stevenj/). From 44beb7cf686362ec64c9bf738235cc66f067f296 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 4 Feb 2019 18:07:49 -0800 Subject: [PATCH 23/25] Remove unnecessary VERSION < v"0.7.0-" branch --- test/test_venv.jl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/test_venv.jl b/test/test_venv.jl index 2f2dad32..01255462 100644 --- a/test/test_venv.jl +++ b/test/test_venv.jl @@ -18,13 +18,8 @@ function test_venv_activation(path) newpython = PyCall.python_cmd(venv=path).exec[1] # Run a fresh Julia process with new Python environment - if VERSION < v"0.7.0-" - setup_code = "" - else - setup_code = Base.load_path_setup_code() - end code = """ - $setup_code + $(Base.load_path_setup_code()) using PyCall println(PyCall.pyimport("sys").executable) println(PyCall.pyimport("sys").exec_prefix) From 6a20a5fcbc052ff8b11c55e379fcb0efe2430846 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 6 Feb 2019 18:42:58 -0500 Subject: [PATCH 24/25] grammar fixes; rm references to internal details about Python API functions --- README.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bfc3faff..b46c0778 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ 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 environment](#python-virtual-environment) +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 @@ -545,17 +545,17 @@ 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 environment +## Python virtual environments -Python's virtual environment created by [`venv`] and [`virtualenv`] +Python virtual environments created by [`venv`] and [`virtualenv`] 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 environment created by `conda` is not +by `PyCall`*. Note that virtual environments created by `conda` are not supported. -To use `PyCall` with a certain virtual environment, set environment +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. Example: +path to the Python executable. For example: ```julia $ source PATH/TO/bin/activate # activate virtual environment in system shell @@ -571,10 +571,7 @@ julia> using PyCall julia> pyimport("sys").executable "PATH/TO/bin/python3" ``` - -This feature works by calling [`Py_SetProgramName`] with the value -specified by `PYCALL_JL_RUNTIME_PYTHON`. Similarly, the path passed -to [`Py_SetPythonHome`] can be controlled by environment variable +Similarly, the `PYTHONHOME` path can be changed by the environment variable `PYCALL_JL_RUNTIME_PYTHONHOME`. [`venv`]: https://docs.python.org/3/library/venv.html From 5d50878f07c3104f3739401ded1edfb762f02042 Mon Sep 17 00:00:00 2001 From: "Steven G. Johnson" Date: Wed, 6 Feb 2019 18:44:06 -0500 Subject: [PATCH 25/25] inline links --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index b46c0778..0092426a 100644 --- a/README.md +++ b/README.md @@ -547,7 +547,7 @@ any other Anaconda-based Python distro for which the user has installation privi ## Python virtual environments -Python virtual environments created by [`venv`] and [`virtualenv`] +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 @@ -574,11 +574,6 @@ julia> pyimport("sys").executable Similarly, the `PYTHONHOME` path can be changed by the environment variable `PYCALL_JL_RUNTIME_PYTHONHOME`. -[`venv`]: https://docs.python.org/3/library/venv.html -[`virtualenv`]: https://virtualenv.pypa.io/en/latest/ -[`Py_SetProgramName`]: https://docs.python.org/3/c-api/init.html#c.Py_SetProgramName -[`Py_SetPythonHome`]: https://docs.python.org/3/c-api/init.html#c.Py_SetPythonHome - ## Author This package was written by [Steven G. Johnson](http://math.mit.edu/~stevenj/).