Skip to content

Commit 6a92054

Browse files
committed
custom which to work in julia 1.6
1 parent 1b30c6f commit 6a92054

File tree

4 files changed

+187
-3
lines changed

4 files changed

+187
-3
lines changed

PyPreferences.jl/src/PyPreferences.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module Implementations
2020
module PythonUtils
2121
include("python_utils.jl")
2222
end
23-
23+
include("which.jl")
2424
include("core.jl")
2525
include("api.jl")
2626
end

PyPreferences.jl/src/core.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ get_default_python() = get(ENV,"PYTHON", "python3")
100100
function get_python_fullpath(python)
101101
python_fullpath = nothing
102102
if python !== nothing
103-
python_fullpath = Sys.which(python)
103+
python_fullpath = _which(python)
104104
if python_fullpath === nothing
105105
@error "Failed to find a binary named `$(python)` in PATH."
106106
else
@@ -136,7 +136,7 @@ function setup_non_failing()
136136

137137
try
138138
if python !== nothing
139-
python_fullpath = Sys.which(python)
139+
python_fullpath = _which(python)
140140
if python_fullpath === nothing
141141
@error "Failed to find a binary named `$(python)` in PATH."
142142
else

PyPreferences.jl/src/which.jl

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@static if VERSION >= v"1.7.0"
2+
const _which = Sys.which
3+
else
4+
function _which(program_name::String)
5+
if isempty(program_name)
6+
return nothing
7+
end
8+
# Build a list of program names that we're going to try
9+
program_names = String[]
10+
base_pname = basename(program_name)
11+
if Sys.iswindows()
12+
# If the file already has an extension, try that name first
13+
if !isempty(splitext(base_pname)[2])
14+
push!(program_names, base_pname)
15+
end
16+
17+
# But also try appending .exe and .com`
18+
for pe in (".exe", ".com")
19+
push!(program_names, string(base_pname, pe))
20+
end
21+
else
22+
# On non-windows, we just always search for what we've been given
23+
push!(program_names, base_pname)
24+
end
25+
26+
path_dirs = String[]
27+
program_dirname = dirname(program_name)
28+
# If we've been given a path that has a directory name in it, then we
29+
# check to see if that path exists. Otherwise, we search the PATH.
30+
if isempty(program_dirname)
31+
# If we have been given just a program name (not a relative or absolute
32+
# path) then we should search `PATH` for it here:
33+
pathsep = Sys.iswindows() ? ';' : ':'
34+
path_dirs = abspath.(split(get(ENV, "PATH", ""), pathsep))
35+
36+
# On windows we always check the current directory as well
37+
if Sys.iswindows()
38+
pushfirst!(path_dirs, pwd())
39+
end
40+
else
41+
push!(path_dirs, abspath(program_dirname))
42+
end
43+
44+
# Here we combine our directories with our program names, searching for the
45+
# first match among all combinations.
46+
for path_dir in path_dirs
47+
for pname in program_names
48+
program_path = joinpath(path_dir, pname)
49+
# If we find something that matches our name and we can execute
50+
if isfile(program_path) && Sys.isexecutable(program_path)
51+
return program_path
52+
end
53+
end
54+
end
55+
56+
# If we couldn't find anything, don't return anything
57+
nothing
58+
end
59+
60+
_which(program_name::AbstractString) = _which(String(program_name))
61+
end

PyPreferences.jl/test/test_venv.jl

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using PyCall, Test
2+
3+
4+
function test_venv_has_python(path)
5+
newpython = PyCall.python_cmd(venv=path).exec[1]
6+
if !isfile(newpython)
7+
@info """
8+
Python executable $newpython does not exists.
9+
This directory contains only the following files:
10+
$(join(readdir(dirname(newpython)), '\n'))
11+
"""
12+
end
13+
@test isfile(newpython)
14+
end
15+
16+
17+
function test_venv_activation(path)
18+
newpython = PyCall.python_cmd(venv=path).exec[1]
19+
20+
# Run a fresh Julia process with new Python environment
21+
code = """
22+
$(Base.load_path_setup_code())
23+
using PyCall
24+
println(PyCall.pyimport("sys").executable)
25+
println(PyCall.pyimport("sys").exec_prefix)
26+
println(PyCall.pyimport("pip").__file__)
27+
"""
28+
# Note that `pip` is just some arbitrary non-standard
29+
# library. Using standard library like `os` does not work
30+
# because those files are not created.
31+
env = copy(ENV)
32+
env["PYCALL_JL_RUNTIME_PYTHON"] = newpython
33+
jlcmd = setenv(`$(Base.julia_cmd()) --startup-file=no -e $code`, env)
34+
if Sys.iswindows()
35+
# Marking the test broken in Windows. It seems that
36+
# venv copies .dll on Windows and libpython check in
37+
# PyCall.__init__ detects that.
38+
@test_broken begin
39+
output = read(jlcmd, String)
40+
sys_executable, exec_prefix, mod_file = split(output, "\n")
41+
newpython == sys_executable
42+
end
43+
else
44+
output = read(jlcmd, String)
45+
sys_executable, exec_prefix, mod_file = split(output, "\n")
46+
@test newpython == sys_executable
47+
@test startswith(exec_prefix, path)
48+
@test startswith(mod_file, path)
49+
end
50+
end
51+
52+
53+
@testset "virtualenv activation" begin
54+
pyname = "python$(pyversion.major).$(pyversion.minor)"
55+
if Sys.which("virtualenv") === nothing
56+
@info "No virtualenv command. Skipping the test..."
57+
elseif Sys.which(pyname) === nothing
58+
@info "No $pyname command. Skipping the test..."
59+
else
60+
mktempdir() do tmppath
61+
if PyCall.pyversion.major == 2
62+
path = joinpath(tmppath, "kind")
63+
else
64+
path = joinpath(tmppath, "ϵνιℓ")
65+
end
66+
run(`virtualenv --python=$pyname $path`)
67+
test_venv_has_python(path)
68+
69+
newpython = PyCall.python_cmd(venv=path).exec[1]
70+
venv_libpython = PyCall.find_libpython(newpython)
71+
if venv_libpython != PyCall.libpython
72+
@info """
73+
virtualenv created an environment with incompatible libpython:
74+
$venv_libpython
75+
"""
76+
return
77+
end
78+
79+
test_venv_activation(path)
80+
end
81+
end
82+
end
83+
84+
85+
@testset "venv activation" begin
86+
# In case PyCall is built with a Python executable created by
87+
# `virtualenv`, let's try to find the original Python executable.
88+
# Otherwise, `venv` does not work with this Python executable:
89+
# https://bugs.python.org/issue30811
90+
sys = PyCall.pyimport("sys")
91+
if hasproperty(sys, :real_prefix)
92+
# sys.real_prefix is set by virtualenv and does not exist in
93+
# standard Python:
94+
# https://github.com/pypa/virtualenv/blob/16.0.0/virtualenv_embedded/site.py#L554
95+
candidates = [
96+
PyCall.venv_python(sys.real_prefix, "$(pyversion.major).$(pyversion.minor)"),
97+
PyCall.venv_python(sys.real_prefix, "$(pyversion.major)"),
98+
PyCall.venv_python(sys.real_prefix),
99+
PyCall.pyprogramname, # must exists
100+
]
101+
python = candidates[findfirst(isfile, candidates)]
102+
else
103+
python = PyCall.pyprogramname
104+
end
105+
106+
if PyCall.conda
107+
@info "Skip venv test with conda."
108+
elseif !success(PyCall.python_cmd(`-c "import venv"`, python=python))
109+
@info "Skip venv test since venv package is missing."
110+
else
111+
mktempdir() do tmppath
112+
if PyCall.pyversion.major == 2
113+
path = joinpath(tmppath, "kind")
114+
else
115+
path = joinpath(tmppath, "ϵνιℓ")
116+
end
117+
# Create a new virtual environment
118+
run(PyCall.python_cmd(`-m venv $path`, python=python))
119+
test_venv_has_python(path)
120+
test_venv_activation(path)
121+
end
122+
end
123+
end

0 commit comments

Comments
 (0)