Skip to content

Commit 815c0de

Browse files
author
Christopher Doris
committed
2 parents 6ea6c8e + c821297 commit 815c0de

File tree

9 files changed

+144
-18
lines changed

9 files changed

+144
-18
lines changed

.github/workflows/benchmark_pr.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Benchmark PR
2+
on:
3+
pull_request:
4+
5+
jobs:
6+
bench:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: MilesCranmer/AirspeedVelocity.jl@action-v1
10+
with:
11+
julia-version: "1"
12+
tune: "true"
13+
# Post to "summary" tab of workflow run:
14+
job-summary: "true"
15+
# Run benchmark using PR's version of the script:
16+
bench-on: ${{ github.event.pull_request.head.sha }}

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ jobs:
6767
python-version: ${{ matrix.pyversion }}
6868

6969
- name: Set up uv
70-
uses: astral-sh/setup-uv@v5
70+
uses: astral-sh/setup-uv@v6
7171
with:
7272
python-version: ${{ matrix.pyversion }}
7373

benchmark/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[deps]
2+
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"

benchmark/benchmarks.jl

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using BenchmarkTools
2+
using PythonCall
3+
using PythonCall: pydel!, pyimport, pydict, pystr, pyrange
4+
5+
const SUITE = BenchmarkGroup()
6+
7+
function test_pydict_init()
8+
random = pyimport("random").random
9+
x = pydict()
10+
for i in pyrange(1000)
11+
x[pystr(i)] = i + random()
12+
end
13+
return x
14+
end
15+
16+
SUITE["basic"]["julia"]["pydict"]["init"] = @benchmarkable test_pydict_init()
17+
18+
function test_pydict_pydel()
19+
random = pyimport("random").random
20+
x = pydict()
21+
for i in pyrange(1000)
22+
k = pystr(i)
23+
r = random()
24+
v = i + r
25+
x[k] = v
26+
pydel!(k)
27+
pydel!(r)
28+
pydel!(v)
29+
pydel!(i)
30+
end
31+
return x
32+
end
33+
34+
SUITE["basic"]["julia"]["pydict"]["pydel"] = @benchmarkable test_pydict_pydel()
35+
36+
@generated function test_atpy(::Val{use_pydel}) where {use_pydel}
37+
quote
38+
@py begin
39+
import random: random
40+
x = {}
41+
for i in range(1000)
42+
x[str(i)] = i + random()
43+
$(use_pydel ? :(@jl PythonCall.pydel!(i)) : :(nothing))
44+
end
45+
x
46+
end
47+
end
48+
end
49+
50+
SUITE["basic"]["@py"]["pydict"]["init"] = @benchmarkable test_atpy(Val(false))
51+
SUITE["basic"]["@py"]["pydict"]["pydel"] = @benchmarkable test_atpy(Val(true))
52+
53+
54+
include("gcbench.jl")
55+
using .GCBench: append_lots
56+
57+
SUITE["gc"]["full"] = @benchmarkable(
58+
GC.gc(true),
59+
setup=(GC.gc(true); append_lots(size=159)),
60+
seconds=30,
61+
evals=1,
62+
)

benchmark/gcbench.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module GCBench
2+
3+
using PythonCall
4+
5+
function append_lots(; iters=100 * 1024, size=1596)
6+
v = pylist()
7+
for i = 1:iters
8+
v.append(pylist(rand(size)))
9+
end
10+
return v
11+
end
12+
13+
end

docs/src/faq.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ To force PythonCall to use the same Python interpreter as PyCall, set the enviro
1010

1111
Alternatively, to force PyCall to use the same interpreter as PythonCall, set the environment variable `PYTHON` to [`PythonCall.python_executable_path()`](@ref) and then `Pkg.build("PyCall")`. You will need to do this each time you change project, because PythonCall by default uses a different Python for each project.
1212

13-
## Is PythonCall/JuliaCall thread safe?
13+
## [Is PythonCall/JuliaCall thread safe?](@id faq-multi-threading)
1414

1515
Yes, as of v0.9.22, provided you handle the GIL correctly. See the guides for
1616
[PythonCall](@ref jl-multi-threading) and [JuliaCall](@ref py-multi-threading).
1717

1818
Before, tricks such as disabling the garbage collector were required. See the
1919
[old docs](https://juliapy.github.io/PythonCall.jl/v0.9.21/faq/#Is-PythonCall/JuliaCall-thread-safe?).
2020

21+
When starting a Julia REPL with multiple threads, there must be exactly one interactive thread,
22+
to avoid triggering a segmentation fault on tab completion (issue [#586](https://github.com/JuliaPy/PythonCall.jl/issues/586)).
23+
Check this with `Threads.nthreads(:interactive)` or `versioninfo()`, set it with `JULIA_NUM_THREADS=X,1`,
24+
where `X` is the number of default threads, or use the Julia `--threads` CLI flag, see `julia --help`.
25+
2126
Related issues:
2227
[#201](https://github.com/JuliaPy/PythonCall.jl/issues/201),
2328
[#202](https://github.com/JuliaPy/PythonCall.jl/issues/202),

docs/src/pythoncall.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ Both `@unlock` and `@lock` are important. If the GIL were not unlocked, then a d
392392
would occur when attempting to lock the already-locked GIL from the threads. If the GIL
393393
were not re-locked, then Python would crash when interacting with it.
394394

395+
With multiple Julia threads you need exactly one interactive thread, see the [FAQ](@ref faq-multi-threading).
396+
395397
You can also use [multi-threading from Python](@ref py-multi-threading).
396398

397399
### Caveat: Garbage collection

src/Compat/gui.jl

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,49 @@ This fixes the problem that Qt does not know where to find its `qt.conf` file, b
77
always looks relative to `sys.executable`, which can be the Julia executable not the Python
88
one when using this package.
99
10-
If `CONFIG.auto_fix_qt_plugin_path` is true, then this is run automatically before `PyQt4`, `PyQt5`, `PySide` or `PySide2` are imported.
10+
If `CONFIG.auto_fix_qt_plugin_path` is true, then this is run automatically before `PyQt4`, `PyQt5`, `PySide`, `PySide2` or `PySide6` are imported.
1111
"""
1212
function fix_qt_plugin_path()
1313
C.CTX.exe_path === nothing && return false
1414
e = pyosmodule.environ
1515
"QT_PLUGIN_PATH" in e && return false
16-
qtconf = joinpath(dirname(C.CTX.exe_path::AbstractString), "qt.conf")
17-
isfile(qtconf) || return false
18-
for line in eachline(qtconf)
19-
m = match(r"^\s*prefix\s*=(.*)$"i, line)
20-
if m !== nothing
21-
path = strip(m.captures[1]::AbstractString)
22-
path[1] == path[end] == '"' && (path = path[2:end-1])
23-
path = joinpath(path, "plugins")
24-
if isdir(path)
25-
e["QT_PLUGIN_PATH"] = realpath(path)
26-
return true
16+
17+
exe_dir = dirname(C.CTX.exe_path::AbstractString)
18+
19+
# Check for Qt6 configuration first (PySide6)
20+
qt6conf = joinpath(exe_dir, "qt6.conf")
21+
if isfile(qt6conf)
22+
for line in eachline(qt6conf)
23+
m = match(r"^\s*Libraries\s*=(.*)$"i, line)
24+
if m !== nothing
25+
path = strip(m.captures[1]::AbstractString)
26+
path[1] == path[end] == '"' && (path = path[2:end-1])
27+
path = joinpath(path, "qt6", "plugins")
28+
if isdir(path)
29+
e["QT_PLUGIN_PATH"] = realpath(path)
30+
return true
31+
end
32+
end
33+
end
34+
end
35+
36+
# Check for Qt5 configuration (PyQt4, PyQt5, PySide, PySide2)
37+
qtconf = joinpath(exe_dir, "qt.conf")
38+
if isfile(qtconf)
39+
for line in eachline(qtconf)
40+
m = match(r"^\s*prefix\s*=(.*)$"i, line)
41+
if m !== nothing
42+
path = strip(m.captures[1]::AbstractString)
43+
path[1] == path[end] == '"' && (path = path[2:end-1])
44+
path = joinpath(path, "plugins")
45+
if isdir(path)
46+
e["QT_PLUGIN_PATH"] = realpath(path)
47+
return true
48+
end
2749
end
2850
end
2951
end
52+
3053
return false
3154
end
3255

@@ -65,7 +88,7 @@ function init_gui()
6588
pyexec(
6689
"""
6790
def new_event_loop_callback(g, interval=0.04):
68-
if g in ("pyqt4","pyqt5","pyside","pyside2"):
91+
if g in ("pyqt4","pyqt5","pyside","pyside2","pyside6"):
6992
if g == "pyqt4":
7093
import PyQt4.QtCore as QtCore
7194
elif g == "pyqt5":
@@ -74,6 +97,8 @@ function init_gui()
7497
import PySide.QtCore as QtCore
7598
elif g == "pyside2":
7699
import PySide2.QtCore as QtCore
100+
elif g == "pyside6":
101+
import PySide6.QtCore as QtCore
77102
instance = QtCore.QCoreApplication.instance
78103
AllEvents = QtCore.QEventLoop.AllEvents
79104
processEvents = QtCore.QCoreApplication.processEvents
@@ -139,6 +164,7 @@ function init_gui()
139164
pymodulehooks.add_hook("PyQt5", fixqthook)
140165
pymodulehooks.add_hook("PySide", fixqthook)
141166
pymodulehooks.add_hook("PySide2", fixqthook)
167+
pymodulehooks.add_hook("PySide6", fixqthook)
142168
end
143169
end
144170

@@ -161,11 +187,11 @@ Activate an event loop for the GUI framework `g`, so that the framework can run
161187
162188
The event loop runs every `interval` seconds. If `fix` is true and `g` is a Qt framework, then [`fix_qt_plugin_path`](@ref PythonCall.fix_qt_plugin_path) is called.
163189
164-
Supported values of `g` (and the Python module they relate to) are: `:pyqt4` (PyQt4), `:pyqt5` (PyQt5), `:pyside` (PySide), `:pyside2` (PySide2), `:gtk` (gtk), `:gtk3` (gi), `:wx` (wx), `:tkinter` (tkinter).
190+
Supported values of `g` (and the Python module they relate to) are: `:pyqt4` (PyQt4), `:pyqt5` (PyQt5), `:pyside` (PySide), `:pyside2` (PySide2), `:pyside6` (PySide6), `:gtk` (gtk), `:gtk3` (gi), `:wx` (wx), `:tkinter` (tkinter).
165191
"""
166192
function event_loop_on(g::Symbol; interval::Real = 0.04, fix::Bool = false)
167193
haskey(EVENT_LOOPS, g) && return EVENT_LOOPS[g]
168-
fix && g in (:pyqt4, :pyqt5, :pyside, :pyside2) && fix_qt_plugin_path()
194+
fix && g in (:pyqt4, :pyqt5, :pyside, :pyside2, :pyside6) && fix_qt_plugin_path()
169195
callback = new_event_loop_callback(string(g), Float64(interval))
170196
EVENT_LOOPS[g] = Timer(t -> callback(), 0; interval = interval)
171197
end

test/Compat.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@test PythonCall.fix_qt_plugin_path() === false
66
end
77
@testset "event_loop_on/off" begin
8-
for g in [:pyqt4, :pyqt5, :pyside, :pyside2, :gtk, :gtk3, :wx]
8+
for g in [:pyqt4, :pyqt5, :pyside, :pyside2, :pyside6, :gtk, :gtk3, :wx]
99
# TODO: actually test the various GUIs somehow?
1010
@show g
1111
@test_throws PyException PythonCall.event_loop_on(g)

0 commit comments

Comments
 (0)