Skip to content

Commit db86116

Browse files
authored
Merge branch 'main' into libptr-by-env
2 parents 573d7b9 + 34d3f55 commit db86116

6 files changed

Lines changed: 84 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44
* Add option `lib` to JuliaCall. Setting this will skip the discovery subprocess.
55
* Add support for using a system image in `juliacall` that has `PythonCall` baked in.
6+
* Add method `pynext(x, d)` to return a default value `d` if there are no more elements.
67
* Bug fixes.
78

89
## 0.9.34 (2026-05-18)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ classifiers = [
1313
"Operating System :: OS Independent",
1414
]
1515
requires-python = ">=3.10, <4"
16-
dependencies = ["juliapkg >=0.1.21, <0.2"]
16+
dependencies = ["juliapkg >=0.1.24, <0.2"]
1717

1818
[dependency-groups]
1919
dev = [

pysrc/juliacall/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ def args_from_config(config):
201201
# Find the Julia executable and project
202202
CONFIG['exepath'] = exepath = juliapkg.executable()
203203
CONFIG['project'] = project = juliapkg.project()
204+
if libpath is None:
205+
CONFIG['libpath'] = libpath = juliapkg.libjulia()
204206
else:
205207
raise Exception("Both PYTHON_JULIACALL_PROJECT and PYTHON_JULIACALL_EXE must be set together, not only one of them.")
206208
if (libpath is not None) and (exepath is None):

src/Core/builtins.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,11 +507,27 @@ Equivalent to `iter(x)` in Python.
507507
pyiter(x) = pynew(errcheck(@autopy x C.PyObject_GetIter(x_)))
508508

509509
"""
510-
pynext(x)
510+
pynext(x, [d])
511511
512-
Equivalent to `next(x)` in Python.
512+
Equivalent to `next(x, d)` in Python.
513+
514+
Returns the next item from the iterator `x`. If there are no more items, returns `d` if
515+
given, else raises `StopIteration`.
513516
"""
514-
pynext(x) = pybuiltins.next(x)
517+
function pynext(x)
518+
ptr = errcheck_ambig(C.PyIter_Next(x))
519+
if ptr == C.PyNULL
520+
errset(pybuiltins.StopIteration)
521+
pythrow()
522+
else
523+
pynew(ptr)
524+
end
525+
end
526+
527+
function pynext(x, d)
528+
ptr = errcheck_ambig(C.PyIter_Next(x))
529+
ptr == C.PyNULL ? d : pynew(ptr)
530+
end
515531

516532
"""
517533
unsafe_pynext(x)

src/JlWrap/C.jl

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,14 @@ const PyJuliaBase_Type = Ref(C.PyNULL)
2323
# we store the actual julia values here
2424
# the `value` field of `PyJuliaValueObject` indexes into here
2525
const PYJLVALUES = []
26-
# unused indices in PYJLVALUES
26+
# unused indices in PYJLVALUES; kept the same length as PYJLVALUES (extra slots
27+
# are pushed alongside PYJLVALUES so the dealloc/finalizer path never has to
28+
# resize this vector). Only the first PYJLFREEVALUECOUNT[] entries are valid
29+
# free indices; the rest are placeholders.
2730
const PYJLFREEVALUES = Int[]
31+
const PYJLFREEVALUECOUNT = Ref(0)
32+
# lock protecting PYJLVALUES, PYJLFREEVALUES and PYJLFREEVALUECOUNT
33+
const PYJL_LOCK = Threads.SpinLock()
2834

2935
function _pyjl_new(t::C.PyPtr, ::C.PyPtr, ::C.PyPtr)
3036
alloc = C.PyType_GetSlot(t, C.Py_tp_alloc)
@@ -39,8 +45,14 @@ end
3945
function _pyjl_dealloc(o::C.PyPtr)
4046
idx = C.@ft UnsafePtr{PyJuliaValueObject}(o).value[]
4147
if idx != 0
48+
# No allocation in this critical section: PYJLFREEVALUES has been pre-sized
49+
# to match PYJLVALUES by PyJuliaValue_SetValue, so recording a free index is
50+
# just a write to an existing slot plus a counter bump. This avoids the GC-triggered
51+
lock(PYJL_LOCK)
4252
PYJLVALUES[idx] = nothing
43-
push!(PYJLFREEVALUES, idx)
53+
PYJLFREEVALUECOUNT[] += 1
54+
PYJLFREEVALUES[PYJLFREEVALUECOUNT[]] = idx
55+
unlock(PYJL_LOCK)
4456
end
4557
(C.@ft UnsafePtr{PyJuliaValueObject}(o).weaklist[!]) == C.PyNULL || C.PyObject_ClearWeakRefs(o)
4658
freeptr = C.PyType_GetSlot(C.Py_Type(o), C.Py_tp_free)
@@ -375,13 +387,19 @@ PyJuliaValue_SetValue(_o, @nospecialize(v)) = Base.GC.@preserve _o begin
375387
o = C.asptr(_o)
376388
idx = C.@ft UnsafePtr{PyJuliaValueObject}(o).value[]
377389
if idx == 0
378-
if isempty(PYJLFREEVALUES)
390+
lock(PYJL_LOCK)
391+
if PYJLFREEVALUECOUNT[] == 0
392+
# Grow both vectors together so length(PYJLFREEVALUES) == length(PYJLVALUES)
393+
# is preserved. The 0 in PYJLFREEVALUES is just a placeholder
379394
push!(PYJLVALUES, v)
395+
push!(PYJLFREEVALUES, 0)
380396
idx = length(PYJLVALUES)
381397
else
382-
idx = pop!(PYJLFREEVALUES)
398+
idx = PYJLFREEVALUES[PYJLFREEVALUECOUNT[]]
399+
PYJLFREEVALUECOUNT[] -= 1
383400
PYJLVALUES[idx] = v
384401
end
402+
unlock(PYJL_LOCK)
385403
C.@ft UnsafePtr{PyJuliaValueObject}(o).value[] = idx
386404
else
387405
PYJLVALUES[idx] = v

test/Core.jl

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -223,25 +223,45 @@
223223
end
224224

225225
@testitem "iter" begin
226-
@test_throws PyException pyiter(pybuiltins.None)
227-
@test_throws PyException pyiter(pybuiltins.True)
228-
# unsafe_pynext
229-
it = pyiter(pyrange(2))
230-
x = PythonCall.unsafe_pynext(it)
231-
@test !PythonCall.pyisnull(x)
232-
@test pyeq(Bool, x, 0)
233-
x = PythonCall.unsafe_pynext(it)
234-
@test !PythonCall.pyisnull(x)
235-
@test pyeq(Bool, x, 1)
236-
x = PythonCall.unsafe_pynext(it)
237-
@test PythonCall.pyisnull(x)
238-
# pynext
239-
it = pyiter(pyrange(2))
240-
x = pynext(it)
241-
@test pyeq(Bool, x, 0)
242-
x = pynext(it)
243-
@test pyeq(Bool, x, 1)
244-
@test_throws PyException pynext(it)
226+
@testset "non-iterables" begin
227+
@test_throws PyException pyiter(pybuiltins.None)
228+
@test_throws PyException pyiter(pybuiltins.True)
229+
end
230+
@testset "unsafe_pynext" begin
231+
it = pyiter(pyrange(2))
232+
x = PythonCall.unsafe_pynext(it)
233+
@test x isa Py
234+
@test !PythonCall.pyisnull(x)
235+
@test pyeq(Bool, x, 0)
236+
x = PythonCall.unsafe_pynext(it)
237+
@test x isa Py
238+
@test !PythonCall.pyisnull(x)
239+
@test pyeq(Bool, x, 1)
240+
x = PythonCall.unsafe_pynext(it)
241+
@test x isa Py
242+
@test PythonCall.pyisnull(x)
243+
end
244+
@testset "pynext" begin
245+
it = pyiter(pyrange(2))
246+
x = pynext(it)
247+
@test x isa Py
248+
@test pyeq(Bool, x, 0)
249+
x = pynext(it)
250+
@test x isa Py
251+
@test pyeq(Bool, x, 1)
252+
@test_throws PyException pynext(it)
253+
end
254+
@testset "pynext with default" begin
255+
it = pyiter(pyrange(2))
256+
x = pynext(it, nothing)
257+
@test x isa Py
258+
@test pyeq(Bool, x, 0)
259+
x = pynext(it, nothing)
260+
@test x isa Py
261+
@test pyeq(Bool, x, 1)
262+
x = pynext(it, nothing)
263+
@test x === nothing
264+
end
245265
end
246266

247267
@testitem "number" begin

0 commit comments

Comments
 (0)