Skip to content

Commit be253f5

Browse files
authored
fix bug when calling propertynames on a different thread with gil handled properly (#770)
the fix is to do the propertynames logic directly on the current thread when it already holds the gil, rather than using on_main_thread Co-authored-by: Christopher Rowley <github.com/cjdoris>
1 parent 26b5482 commit be253f5

3 files changed

Lines changed: 43 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
## Unreleased
44
* Add configuration via Preferences in addition to environment variables (e.g. `exe`
55
rather than `JULIA_PYTHONCALL_EXE`.)
6-
* Internals: removed the cache of unused Python objects. This makes `pydel!` faster and
7-
removes a race condition in free-threaded python.
6+
* Bug fixes.
87

98
## 0.9.32 (2026-05-14)
109
* Added `juliacall.TypeValue.__numpy_dtype__` attribute to allow converting Julia types

src/Core/Py.jl

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -270,30 +270,35 @@ Base.hasproperty(x::Py, k::String) = pyhasattr(x, k)
270270
Base.setproperty!(x::Py, k::Symbol, v) = pysetattr(x, string(k), v)
271271
Base.setproperty!(x::Py, k::String, v) = pysetattr(x, k, v)
272272

273-
function Base.propertynames(x::Py, private::Bool = false)
274-
properties = C.on_main_thread() do
275-
# this follows the logic of rlcompleter.py
276-
function classmembers(c)
277-
r = pydir(c)
278-
if pyhasattr(c, "__bases__")
279-
for b in c.__bases__
280-
r = pyiadd(r, classmembers(b))
281-
end
273+
function _propertynames(x::Py, private::Bool)
274+
# this follows the logic of rlcompleter.py
275+
function classmembers(c)
276+
r = pydir(c)
277+
if pyhasattr(c, "__bases__")
278+
for b in c.__bases__
279+
r = pyiadd(r, classmembers(b))
282280
end
283-
return r
284281
end
282+
return r
283+
end
285284

286-
words = pyset(pydir(x::Py))
287-
words.discard("__builtins__")
288-
if pyhasattr(x, "__class__")
289-
words.add("__class__")
290-
words.update(classmembers(x.__class__))
291-
end
292-
map(pystr_asstring, words)
293-
end::Vector{String} # explicit type since on_main_thread() is type-unstable
285+
words = pyset(pydir(x::Py))
286+
words.discard("__builtins__")
287+
if pyhasattr(x, "__class__")
288+
words.add("__class__")
289+
words.update(classmembers(x.__class__))
290+
end
291+
return Symbol[Symbol(pystr_asstring(word)) for word in words]
292+
end
294293

295-
# private || filter!(w->!startswith(w, "_"), words)
296-
map(Symbol, properties)
294+
function Base.propertynames(x::Py, private::Bool = false)
295+
if C.PyGILState_Check() == 1
296+
_propertynames(x, private)
297+
else
298+
C.on_main_thread() do
299+
_propertynames(x, private)
300+
end::Vector{Symbol}
301+
end
297302
end
298303

299304
Base.Bool(x::Py) = pytruth(x)

test/Core.jl

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -832,12 +832,23 @@ end
832832
end
833833

834834
@testitem "propertynames" begin
835-
x = pyint(7)
836-
task = Threads.@spawn propertynames(x)
837-
properties = propertynames(x)
838-
@test :__init__ in properties
839-
prop_task = fetch(task)
840-
@test properties == prop_task
835+
@testset "basic" begin
836+
x = pyint(7)
837+
task = Threads.@spawn propertynames(x)
838+
properties = propertynames(x)
839+
@test :__init__ in properties
840+
prop_task = fetch(task)
841+
@test properties == prop_task
842+
end
843+
@testset "gil (#751)" begin
844+
o = pyint(751)
845+
t = Base.Threads.@spawn begin
846+
PythonCall.GIL.@lock begin
847+
propertynames(o)
848+
end
849+
end
850+
PythonCall.GIL.@unlock wait(t)
851+
end
841852
end
842853

843854
@testitem "on_main_thread" begin

0 commit comments

Comments
 (0)