Skip to content

Commit c0750ab

Browse files
committed
Force propertynames(::Py) to occur on the main thread
1 parent 71666ca commit c0750ab

File tree

3 files changed

+75
-16
lines changed

3 files changed

+75
-16
lines changed

src/C/context.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ function _atpyexit()
2929
return
3030
end
3131

32+
33+
const MAIN_THREAD_TASK_LOCK = ReentrantLock()
34+
const MAIN_THREAD_CHANNEL_INPUT = Channel(1)
35+
const MAIN_THREAD_CHANNEL_OUTPUT = Channel(1)
36+
37+
# Execute f() on the main thread.
38+
function on_main_thread(f)
39+
@lock MAIN_THREAD_TASK_LOCK begin
40+
put!(MAIN_THREAD_CHANNEL_INPUT, f)
41+
take!(MAIN_THREAD_CHANNEL_OUTPUT)
42+
end
43+
end
44+
45+
3246
function init_context()
3347

3448
CTX.is_embedded = hasproperty(Base.Main, :__PythonCall_libptr)
@@ -240,6 +254,15 @@ function init_context()
240254
"Only Python 3.9+ is supported, this is Python $(CTX.version) at $(CTX.exe_path===missing ? "unknown location" : CTX.exe_path).",
241255
)
242256

257+
main_thread_task = Task() do
258+
while true
259+
f = take!(MAIN_THREAD_CHANNEL_INPUT)
260+
put!(MAIN_THREAD_CHANNEL_OUTPUT, f())
261+
end
262+
end
263+
set_task_tid!(main_thread_task, Threads.threadid())
264+
schedule(main_thread_task)
265+
243266
@debug "Initialized PythonCall.jl" CTX.is_embedded CTX.is_initialized CTX.exe_path CTX.lib_path CTX.lib_ptr CTX.pyprogname CTX.pyhome CTX.version
244267

245268
return
@@ -260,3 +283,26 @@ const PYTHONCALL_PKGID = Base.PkgId(PYTHONCALL_UUID, "PythonCall")
260283

261284
const PYCALL_UUID = Base.UUID("438e738f-606a-5dbb-bf0a-cddfbfd45ab0")
262285
const PYCALL_PKGID = Base.PkgId(PYCALL_UUID, "PyCall")
286+
287+
288+
# taken from StableTasks.jl, itself taken from Dagger.jl
289+
function set_task_tid!(task::Task, tid::Integer)
290+
task.sticky = true
291+
ctr = 0
292+
while true
293+
ret = ccall(:jl_set_task_tid, Cint, (Any, Cint), task, tid-1)
294+
if ret == 1
295+
break
296+
elseif ret == 0
297+
yield()
298+
else
299+
error("Unexpected retcode from jl_set_task_tid: $ret")
300+
end
301+
ctr += 1
302+
if ctr > 10
303+
@warn "Setting task TID to $tid failed, giving up!"
304+
return
305+
end
306+
end
307+
@assert Threads.threadid(task) == tid "jl_set_task_tid failed!"
308+
end

src/Core/Py.jl

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -257,25 +257,29 @@ Base.setproperty!(x::Py, k::Symbol, v) = pysetattr(x, string(k), v)
257257
Base.setproperty!(x::Py, k::String, v) = pysetattr(x, k, v)
258258

259259
function Base.propertynames(x::Py, private::Bool = false)
260-
# this follows the logic of rlcompleter.py
261-
function classmembers(c)
262-
r = pydir(c)
263-
if pyhasattr(c, "__bases__")
264-
for b in c.__bases__
265-
r = pyiadd(r, classmembers(b))
260+
properties = C.on_main_thread() do
261+
# this follows the logic of rlcompleter.py
262+
function classmembers(c)
263+
r = pydir(c)
264+
if pyhasattr(c, "__bases__")
265+
for b in c.__bases__
266+
r = pyiadd(r, classmembers(b))
267+
end
266268
end
269+
return r
267270
end
268-
return r
269-
end
270-
words = pyset(pydir(x))
271-
words.discard("__builtins__")
272-
if pyhasattr(x, "__class__")
273-
words.add("__class__")
274-
words.update(classmembers(x.__class__))
275-
end
276-
words = map(pystr_asstring, words)
271+
272+
words = pyset(pydir(x::Py))
273+
words.discard("__builtins__")
274+
if pyhasattr(x, "__class__")
275+
words.add("__class__")
276+
words.update(classmembers(x.__class__))
277+
end
278+
map(pystr_asstring, words)
279+
end::Vector{String} # explicit type since on_main_thread() is type-unstable
280+
277281
# private || filter!(w->!startswith(w, "_"), words)
278-
map(Symbol, words)
282+
map(Symbol, properties)
279283
end
280284

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

test/Core.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,3 +827,12 @@ end
827827
@test !isdir(tname)
828828
end
829829
end
830+
831+
@testitem "propertynames" begin
832+
x = pyint(7)
833+
task = Threads.@spawn propertynames(x)
834+
properties = propertynames(x)
835+
@test :__init__ in properties
836+
prop_task = fetch(task)
837+
@test properties == prop_task
838+
end

0 commit comments

Comments
 (0)