Skip to content

Commit 60e0daa

Browse files
authored
Merge branch 'JuliaPy:main' into hh-timedelta64
2 parents daf9759 + a61c022 commit 60e0daa

30 files changed

+627
-157
lines changed

.github/workflows/tests-nightly.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
- uses: julia-actions/julia-runtest@v1
3939
env:
4040
JULIA_DEBUG: PythonCall
41+
JULIA_NUM_THREADS: '2'
4142
- uses: julia-actions/julia-processcoverage@v1
4243
- uses: codecov/codecov-action@v1
4344
with:

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
uses: julia-actions/julia-runtest@v1
4444
env:
4545
JULIA_DEBUG: PythonCall
46+
JULIA_NUM_THREADS: '2'
4647
- name: Process coverage
4748
uses: julia-actions/julia-processcoverage@v1
4849
- name: Upload coverage to Codecov
@@ -82,6 +83,8 @@ jobs:
8283
- name: Run tests
8384
run: |
8485
pytest -s --nbval --cov=pysrc ./pytest/
86+
env:
87+
PYTHON_JULIACALL_THREADS: '2'
8588
- name: Upload coverage to Codecov
8689
uses: codecov/codecov-action@v2
8790
env:

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "PythonCall"
22
uuid = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
33
authors = ["Christopher Doris <github.com/cjdoris>"]
4-
version = "0.9.21"
4+
version = "0.9.23"
55

66
[deps]
77
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ In this example we use the Python module JuliaCall from an IPython notebook to t
4040

4141
## What about PyCall?
4242

43-
The existing package [PyCall](https://github.com/JuliaPy/PyCall.jl) is another similar interface to Python. Here we note some key differences, but a more detailed comparison is in the documentation.
43+
The existing package [PyCall](https://github.com/JuliaPy/PyCall.jl) is another similar interface to Python. Here we note some key differences:.
4444
- PythonCall supports a wider range of conversions between Julia and Python, and the conversion mechanism is extensible.
4545
- PythonCall by default never copies mutable objects when converting, but instead directly wraps the mutable object. This means that modifying the converted object modifies the original, and conversion is faster.
4646
- PythonCall does not usually automatically convert results to Julia values, but leaves them as Python objects. This makes it easier to do Pythonic things with these objects (e.g. accessing methods) and is type-stable.
47-
- PythonCall installs dependencies into a separate Conda environment for each Julia project. This means each Julia project can have an isolated set of Python dependencies.
48-
- PythonCall supports Julia 1.6.1+ and Python 3.8+ whereas PyCall supports Julia 0.7+ and Python 2.7+.
47+
- PythonCall installs dependencies into a separate Conda environment for each Julia project using [CondaPkg](https://github.com/JuliaPy/CondaPkg.jl). This means each Julia project can have an isolated set of Python dependencies.

bump.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function bumpver(file, pattern, oldver, newver)
2222
end
2323

2424
bumpver("Project.toml", "version = \"{}\"\n", oldver, newver)
25-
bumpver("setup.cfg", "version = {}\n", oldver, newver)
25+
bumpver("pyproject.toml", "version = \"{}\"\n", oldver, newver)
2626
bumpver("pysrc/juliacall/__init__.py", "__version__ = '{}'\n", oldver, newver)
2727
bumpver("pysrc/juliacall/juliapkg.json", "\"version\": \"={}\"", oldver, newver)
2828
bumpver("pysrc/juliacall/juliapkg-dev.json", "\"version\": \"={}\"", oldver, newver)

docs/make.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ makedocs(
1919
],
2020
"compat.md",
2121
"faq.md",
22-
"pycall.md",
2322
"releasenotes.md",
2423
],
2524
)

docs/src/faq.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,27 @@
11
# FAQ & Troubleshooting
22

3+
## Can I use PythonCall and PyCall together?
4+
5+
Yes, you can use both PyCall and PythonCall in the same Julia session. This is platform-dependent:
6+
- On most systems the Python interpreter used by PythonCall and PyCall must be the same (see below).
7+
- On Windows it appears to be possible for PythonCall and PyCall to use different interpreters.
8+
9+
To force PythonCall to use the same Python interpreter as PyCall, set the environment variable [`JULIA_PYTHONCALL_EXE`](@ref pythoncall-config) to `"@PyCall"`. Note that this will opt out of automatic dependency management using CondaPkg.
10+
11+
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.
12+
313
## Is PythonCall/JuliaCall thread safe?
414

5-
No.
6-
7-
Some rules if you are writing multithreaded code:
8-
- Only call Python functions from the first thread.
9-
- You probably also need to call `PythonCall.GC.disable()` on the main thread before any
10-
threaded block of code. Remember to call `PythonCall.GC.enable()` again afterwards.
11-
(This is because Julia finalizers can be called from any thread.)
12-
- Julia intentionally causes segmentation faults as part of the GC safepoint mechanism.
13-
If unhandled, these segfaults will result in termination of the process. To enable signal handling,
14-
set `PYTHON_JULIACALL_HANDLE_SIGNALS=yes` before any calls to import juliacall. This is equivalent
15-
to starting julia with `julia --handle-signals=yes`, the default behavior in Julia.
16-
See discussion [here](https://github.com/JuliaPy/PythonCall.jl/issues/219#issuecomment-1605087024) for more information.
17-
- You may still encounter problems.
18-
19-
Related issues: [#201](https://github.com/JuliaPy/PythonCall.jl/issues/201), [#202](https://github.com/JuliaPy/PythonCall.jl/issues/202)
15+
Yes, as of v0.9.22, provided you handle the GIL correctly. See the guides for
16+
[PythonCall](@ref jl-multi-threading) and [JuliaCall](@ref py-multi-threading).
17+
18+
Before, tricks such as disabling the garbage collector were required. See the
19+
[old docs](https://juliapy.github.io/PythonCall.jl/v0.9.21/faq/#Is-PythonCall/JuliaCall-thread-safe?).
20+
21+
Related issues:
22+
[#201](https://github.com/JuliaPy/PythonCall.jl/issues/201),
23+
[#202](https://github.com/JuliaPy/PythonCall.jl/issues/202),
24+
[#529](https://github.com/JuliaPy/PythonCall.jl/pull/529)
2025

2126
## Issues when Numpy arrays are expected
2227

docs/src/juliacall-reference.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# JuliaCall API Reference
1+
# [JuliaCall API Reference](@id jl-reference)
22

33
## Constants
44

@@ -93,8 +93,9 @@ replaced with `!!`.
9393
9494
###### Members
9595
- `_jl_raw()`: Convert to a [`RawValue`](#juliacall.RawValue). (See also [`pyjlraw`](@ref).)
96-
- `_jl_display()`: Display the object using Julia's display mechanism.
97-
- `_jl_help()`: Display help for the object.
96+
- `_jl_display(mime=None)`: Display the object using Julia's display mechanism.
97+
- `_jl_help(mime=None)`: Display help for the object.
98+
- `_jl_call_nogil(*args, **kwargs)`: Call this with the GIL disabled.
9899
`````
99100

100101
`````@customdoc
@@ -217,4 +218,5 @@ single tuple, it will need to be wrapped in another tuple.
217218
###### Members
218219
- `_jl_any()`: Convert to a [`AnyValue`](#juliacall.AnyValue) (or subclass). (See also
219220
[`pyjl`](@ref).)
221+
- `_jl_call_nogil(*args, **kwargs)`: Call this with the GIL disabled.
220222
`````

docs/src/juliacall.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,79 @@ be configured in two ways:
124124
| `-X juliacall-threads=<N\|auto>` | `PYTHON_JULIACALL_THREADS=<N\|auto>` | Launch N threads. |
125125
| `-X juliacall-warn-overwrite=<yes\|no>` | `PYTHON_JULIACALL_WARN_OVERWRITE=<yes\|no>` | Enable or disable method overwrite warnings. |
126126
| `-X juliacall-autoload-ipython-extension=<yes\|no>` | `PYTHON_JULIACALL_AUTOLOAD_IPYTHON_EXTENSION=<yes\|no>` | Enable or disable IPython extension autoloading. |
127+
128+
## [Multi-threading](@id py-multi-threading)
129+
130+
From v0.9.22, JuliaCall supports multi-threading in Julia and/or Python, with some
131+
caveats.
132+
133+
Most importantly, you can only call Python code while Python's
134+
[Global Interpreter Lock (GIL)](https://docs.python.org/3/glossary.html#term-global-interpreter-lock)
135+
is locked by the current thread. You can use JuliaCall from any Python thread, and the GIL
136+
will be locked whenever any JuliaCall function is used. However, to leverage the benefits
137+
of multi-threading, you can unlock the GIL while executing any Julia code that does not
138+
interact with Python.
139+
140+
The simplest way to do this is using the `_jl_call_nogil` method on Julia functions to
141+
call the function with the GIL unlocked.
142+
143+
```python
144+
from concurrent.futures import ThreadPoolExecutor, wait
145+
from juliacall import Main as jl
146+
pool = ThreadPoolExecutor(4)
147+
fs = [pool.submit(jl.Libc.systemsleep._jl_call_nogil, 5) for _ in range(4)]
148+
wait(fs)
149+
```
150+
151+
In the above example, we call `Libc.systemsleep(5)` on four threads. Because we
152+
called it with `_jl_call_nogil`, the GIL was unlocked, allowing the threads to run in
153+
parallel, taking about 5 seconds in total.
154+
155+
If we did not use `_jl_call_nogil` (i.e. if we did `pool.submit(jl.Libc.systemsleep, 5)`)
156+
then the above code will take 20 seconds because the sleeps run one after another.
157+
158+
It is very important that any function called with `_jl_call_nogil` does not interact
159+
with Python at all unless it re-locks the GIL first, such as by using
160+
[PythonCall.GIL.@lock](@ref).
161+
162+
You can also use [multi-threading from Julia](@ref jl-multi-threading).
163+
164+
### Caveat: Julia's task scheduler
165+
166+
If you try the above example with a Julia function that yields to the task scheduler,
167+
such as `sleep` instead of `Libc.systemsleep`, then you will likely experience a hang.
168+
169+
In this case, you need to yield back to Julia's scheduler periodically to allow the task
170+
to continue. You can use the following pattern instead of `wait(fs)`:
171+
```python
172+
jl_yield = getattr(jl, "yield")
173+
while True:
174+
# yield to Julia's task scheduler
175+
jl_yield()
176+
# wait for up to 0.1 seconds for the threads to finish
177+
state = wait(fs, timeout=0.1)
178+
# if they finished then stop otherwise try again
179+
if not state.not_done:
180+
break
181+
```
182+
183+
Set the `timeout` parameter smaller to let Julia's scheduler cycle more frequently.
184+
185+
Future versions of JuliaCall may provide tooling to make this simpler.
186+
187+
### [Caveat: Signal handling](@id py-multi-threading-signal-handling)
188+
189+
We recommend setting [`PYTHON_JULIACALL_HANDLE_SIGNALS=yes`](@ref julia-config)
190+
before importing JuliaCall with multiple threads.
191+
192+
This is because Julia intentionally causes segmentation faults as part of the GC
193+
safepoint mechanism. If unhandled, these segfaults will result in termination of the
194+
process. See discussion
195+
[here](https://github.com/JuliaPy/PythonCall.jl/issues/219#issuecomment-1605087024)
196+
for more information.
197+
198+
Note however that this interferes with Python's own signal handling, so for example
199+
Ctrl-C will not raise `KeyboardInterrupt`.
200+
201+
Future versions of JuliaCall may make this the default behaviour when using multiple
202+
threads.

docs/src/pycall.md

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

0 commit comments

Comments
 (0)