Skip to content

Commit

Permalink
custom which to work in julia 1.6
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilipVinc committed Jan 18, 2022
1 parent 1b30c6f commit 6a92054
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 3 deletions.
2 changes: 1 addition & 1 deletion PyPreferences.jl/src/PyPreferences.jl
Expand Up @@ -20,7 +20,7 @@ module Implementations
module PythonUtils
include("python_utils.jl")
end

include("which.jl")
include("core.jl")
include("api.jl")
end
Expand Down
4 changes: 2 additions & 2 deletions PyPreferences.jl/src/core.jl
Expand Up @@ -100,7 +100,7 @@ get_default_python() = get(ENV,"PYTHON", "python3")
function get_python_fullpath(python)
python_fullpath = nothing
if python !== nothing
python_fullpath = Sys.which(python)
python_fullpath = _which(python)
if python_fullpath === nothing
@error "Failed to find a binary named `$(python)` in PATH."
else
Expand Down Expand Up @@ -136,7 +136,7 @@ function setup_non_failing()

try
if python !== nothing
python_fullpath = Sys.which(python)
python_fullpath = _which(python)
if python_fullpath === nothing
@error "Failed to find a binary named `$(python)` in PATH."
else
Expand Down
61 changes: 61 additions & 0 deletions PyPreferences.jl/src/which.jl
@@ -0,0 +1,61 @@
@static if VERSION >= v"1.7.0"
const _which = Sys.which
else
function _which(program_name::String)
if isempty(program_name)
return nothing
end
# Build a list of program names that we're going to try
program_names = String[]
base_pname = basename(program_name)
if Sys.iswindows()
# If the file already has an extension, try that name first
if !isempty(splitext(base_pname)[2])
push!(program_names, base_pname)
end

# But also try appending .exe and .com`
for pe in (".exe", ".com")
push!(program_names, string(base_pname, pe))
end
else
# On non-windows, we just always search for what we've been given
push!(program_names, base_pname)
end

path_dirs = String[]
program_dirname = dirname(program_name)
# If we've been given a path that has a directory name in it, then we
# check to see if that path exists. Otherwise, we search the PATH.
if isempty(program_dirname)
# If we have been given just a program name (not a relative or absolute
# path) then we should search `PATH` for it here:
pathsep = Sys.iswindows() ? ';' : ':'
path_dirs = abspath.(split(get(ENV, "PATH", ""), pathsep))

# On windows we always check the current directory as well
if Sys.iswindows()
pushfirst!(path_dirs, pwd())
end
else
push!(path_dirs, abspath(program_dirname))
end

# Here we combine our directories with our program names, searching for the
# first match among all combinations.
for path_dir in path_dirs
for pname in program_names
program_path = joinpath(path_dir, pname)
# If we find something that matches our name and we can execute
if isfile(program_path) && Sys.isexecutable(program_path)
return program_path
end
end
end

# If we couldn't find anything, don't return anything
nothing
end

_which(program_name::AbstractString) = _which(String(program_name))
end
123 changes: 123 additions & 0 deletions PyPreferences.jl/test/test_venv.jl
@@ -0,0 +1,123 @@
using PyCall, Test


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
code = """
$(Base.load_path_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 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
pyname = "python$(pyversion.major).$(pyversion.minor)"
if Sys.which("virtualenv") === nothing
@info "No virtualenv command. Skipping the test..."
elseif Sys.which(pyname) === nothing
@info "No $pyname command. Skipping the test..."
else
mktempdir() do 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)

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
# 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 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.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"`, python=python))
@info "Skip venv test since venv package is missing."
else
mktempdir() do 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)
test_venv_activation(path)
end
end
end

0 comments on commit 6a92054

Please sign in to comment.