Skip to content

Commit f7fa4c2

Browse files
authored
stop using Requires.jl (use extensions instead) (#662)
* stop using Requires.jl (use extensions instead) * update changelog * don't build PyCall in tests instead, add a case to CI where we use system python for both, ensuring we test the same case. we also add tests for the non-same case. * typo * only test PyCall when we can guarantee same interpreter * Update PyCallExt.jl --------- Co-authored-by: Christopher Doris <github.com/cjdoris>
1 parent 22cbe26 commit f7fa4c2

File tree

12 files changed

+111
-75
lines changed

12 files changed

+111
-75
lines changed

.github/workflows/tests.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ on:
1212

1313
jobs:
1414
julia:
15-
name: Test Julia (${{ matrix.jlversion }}, ${{ matrix.os }})
15+
name: Test Julia (${{ matrix.jlversion }}, ${{ matrix.os }}, ${{ matrix.pythonexe }})
1616
runs-on: ${{ matrix.os }}
1717
strategy:
1818
fail-fast: false
1919
matrix:
2020
arch: [x64] # x86 unsupported by MicroMamba
2121
os: [ubuntu-latest, windows-latest, macos-latest]
2222
jlversion: ['1','1.9']
23+
pythonexe: ['@CondaPkg']
24+
include:
25+
- arch: x64
26+
os: ubuntu-latest
27+
jlversion: '1'
28+
pythonexe: python
2329

2430
steps:
2531
- uses: actions/checkout@v5
@@ -34,12 +40,16 @@ jobs:
3440

3541
- name: Build package
3642
uses: julia-actions/julia-buildpkg@v1
43+
env:
44+
PYTHON: python
3745

3846
- name: Run tests
3947
uses: julia-actions/julia-runtest@v1
4048
env:
4149
JULIA_DEBUG: PythonCall
4250
JULIA_NUM_THREADS: '2'
51+
PYTHON: python
52+
JULIA_PYTHONCALL_EXE: ${{ matrix.pythonexe }}
4353

4454
- name: Process coverage
4555
uses: julia-actions/julia-processcoverage@v1

Project.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,33 +10,41 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1010
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1111
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1212
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
13-
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
1413
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
1514
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1615
UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39"
1716

1817
[compat]
1918
Aqua = "0 - 999"
19+
CategoricalArrays = "0.10, 1"
2020
CondaPkg = "0.2.30"
2121
Dates = "1"
2222
Libdl = "1"
2323
MacroTools = "0.5"
2424
Markdown = "1"
2525
Pkg = "1"
2626
PyCall = "1"
27-
Requires = "1"
2827
Serialization = "1"
2928
Tables = "1"
3029
Test = "1"
3130
TestItemRunner = "0 - 999"
3231
UnsafePointers = "1"
3332
julia = "1.9"
3433

34+
[extensions]
35+
PyCallExt = "PyCall"
36+
CategoricalArraysExt = "CategoricalArrays"
37+
3538
[extras]
3639
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
40+
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
3741
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
3842
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3943
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
4044

4145
[targets]
4246
test = ["Aqua", "PyCall", "Test", "TestItemRunner"]
47+
48+
[weakdeps]
49+
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
50+
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"

docs/src/releasenotes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## Unreleased
4+
* Bug fixes.
5+
* Internal: switch from Requires.jl to package extensions.
6+
37
## 0.9.27 (2025-08-19)
48
* Internal: Use heap-allocated types (PyType_FromSpec) to improve ABI compatibility.
59
* Minimum supported Python version is now 3.9.

ext/CategoricalArraysExt.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module CategoricalArraysExt
2+
3+
using PythonCall
4+
5+
using CategoricalArrays: CategoricalArrays
6+
7+
function PythonCall.Compat.aspandasvector(x::CategoricalArrays.CategoricalArray)
8+
codes = map(x -> x === missing ? -1 : Int(CategoricalArrays.levelcode(x)) - 1, x)
9+
cats = CategoricalArrays.levels(x)
10+
ordered = x.pool.ordered
11+
pyimport("pandas").Categorical.from_codes(codes, cats, ordered = ordered)
12+
end
13+
14+
end

ext/PyCallExt.jl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module PyCallExt
2+
3+
# While this extension is precompilable, it can only be used without crashing if PyCall is
4+
# using the same Python interpreter. However Julia precompiles all available extensions
5+
# eagerly, which therefore crashes if PyCall is using a different interpreter. So for now
6+
# we disable precompilation of this extension entirely.
7+
__precompile__(false)
8+
9+
using PythonCall
10+
using PythonCall.Core
11+
using PythonCall.C
12+
13+
using PyCall: PyCall
14+
15+
import PythonCall: Py
16+
17+
# true if PyCall and PythonCall are using the same interpreter
18+
const SAME = Ref{Bool}(false)
19+
20+
function __init__()
21+
# see if PyCall and PythonCall are using the same interpreter by checking if a couple of memory addresses are the same
22+
ptr1 = C.Py_GetVersion()
23+
ptr2 = ccall(PyCall.@pysym(:Py_GetVersion), Ptr{Cchar}, ())
24+
SAME[] = ptr1 == ptr2
25+
if PythonCall.C.CTX.which == :PyCall
26+
@assert SAME[]
27+
end
28+
end
29+
30+
# allow explicit conversion between PythonCall.Py and PyCall.PyObject
31+
# provided they are using the same interpretr
32+
const ERRMSG = """
33+
Conversion between `PyCall.PyObject` and `PythonCall.Py` is only possible when using the same Python interpreter.
34+
35+
There are two ways to achieve this:
36+
- Set the environment variable `JULIA_PYTHONCALL_EXE` to `"@PyCall"`. This forces PythonCall to use the same
37+
interpreter as PyCall, but PythonCall loses the ability to manage its own dependencies.
38+
- Set the environment variable `PYTHON` to `PythonCall.python_executable_path()` and rebuild PyCall. This forces
39+
PyCall to use the same interpreter as PythonCall, but needs to be repeated whenever you switch Julia environment.
40+
"""
41+
42+
function Py(x::PyCall.PyObject)
43+
SAME[] || error(ERRMSG)
44+
return pynew(C.PyPtr(PyCall.pyreturn(x)))
45+
end
46+
47+
function PyCall.PyObject(x::Py)
48+
SAME[] || error(ERRMSG)
49+
return PyCall.PyObject(PyCall.PyPtr(getptr(incref(x))))
50+
end
51+
52+
end

src/C/C.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ using Base: @kwdef
99
using UnsafePointers: UnsafePtr
1010
using CondaPkg: CondaPkg
1111
using Pkg: Pkg
12-
using Requires: @require
1312
using Libdl:
1413
dlpath, dlopen, dlopen_e, dlclose, dlsym, dlsym_e, RTLD_LAZY, RTLD_DEEPBIND, RTLD_GLOBAL
1514

16-
import ..PythonCall: python_executable_path, python_library_path, python_library_handle, python_version
15+
import ..PythonCall:
16+
python_executable_path, python_library_path, python_library_handle, python_version
1717

1818

1919
include("consts.jl")

src/C/context.jl

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ A handle to a loaded instance of libpython, its interpreter, function pointers,
1717
pyhome_w::Any = missing
1818
which::Symbol = :unknown # :CondaPkg, :PyCall, :embedded or :unknown
1919
version::Union{VersionNumber,Missing} = missing
20-
matches_pycall::Union{Bool,Missing} = missing
2120
end
2221

2322
const CTX = Context()
@@ -141,15 +140,9 @@ function init_context()
141140
# Get function pointers from the library
142141
init_pointers()
143142

144-
# Compare libpath with PyCall
145-
@require PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" init_pycall(PyCall)
146-
147143
# Initialize the interpreter
148144
CTX.is_preinitialized = Py_IsInitialized() != 0
149-
if CTX.is_preinitialized
150-
@assert CTX.which == :PyCall || CTX.matches_pycall isa Bool
151-
else
152-
@assert CTX.which != :PyCall
145+
if !CTX.is_preinitialized
153146
# Find ProgramName and PythonHome
154147
script = if Sys.iswindows()
155148
"""
@@ -243,13 +236,3 @@ const PYTHONCALL_PKGID = Base.PkgId(PYTHONCALL_UUID, "PythonCall")
243236

244237
const PYCALL_UUID = Base.UUID("438e738f-606a-5dbb-bf0a-cddfbfd45ab0")
245238
const PYCALL_PKGID = Base.PkgId(PYCALL_UUID, "PyCall")
246-
247-
function init_pycall(PyCall::Module)
248-
# see if PyCall and PythonCall are using the same interpreter by checking if a couple of memory addresses are the same
249-
ptr1 = Py_GetVersion()
250-
ptr2 = @eval PyCall ccall(@pysym(:Py_GetVersion), Ptr{Cchar}, ())
251-
CTX.matches_pycall = ptr1 == ptr2
252-
if CTX.which == :PyCall
253-
@assert CTX.matches_pycall
254-
end
255-
end

src/Compat/Compat.jl

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ using ..Wrap
1313

1414
using Serialization: Serialization, AbstractSerializer, serialize, deserialize
1515
using Tables: Tables
16-
using Requires: @require
1716

1817
import ..PythonCall: event_loop_on, event_loop_off, fix_qt_plugin_path, pytable
1918

@@ -22,13 +21,10 @@ include("ipython.jl")
2221
include("multimedia.jl")
2322
include("serialization.jl")
2423
include("tables.jl")
25-
include("pycall.jl")
2624

2725
function __init__()
2826
init_gui()
2927
init_pyshow()
30-
init_tables()
31-
@require PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" init_pycall(PyCall)
3228
end
3329

3430
end

src/Compat/pycall.jl

Lines changed: 0 additions & 21 deletions
This file was deleted.

src/Compat/tables.jl

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,3 @@ function _pytable_pandas(src, cols = Tables.columns(src); opts...)
6363
opts...,
6464
)
6565
end
66-
67-
function init_tables()
68-
@require CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" @eval begin
69-
aspandasvector(x::CategoricalArrays.CategoricalArray) = begin
70-
codes = map(x -> x === missing ? -1 : Int(CategoricalArrays.levelcode(x)) - 1, x)
71-
cats = CategoricalArrays.levels(x)
72-
ordered = x.pool.ordered
73-
pyimport("pandas").Categorical.from_codes(codes, cats, ordered = ordered)
74-
end
75-
end
76-
end

0 commit comments

Comments
 (0)