Skip to content

Commit bd7e81a

Browse files
author
Christopher Doris
committed
Merge branch 'main' into v1
2 parents d0cc3f7 + c2af996 commit bd7e81a

File tree

18 files changed

+119
-40
lines changed

18 files changed

+119
-40
lines changed

.github/workflows/docscleanup.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@ jobs:
1515

1616
- name: Delete preview and history
1717
run: |
18-
git config user.name "Documenter.jl"
19-
git config user.email "[email protected]"
20-
git rm -rf "previews/PR$PRNUM"
21-
git commit -m "delete preview"
22-
git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree})
18+
if [ -d "$DIR" ]; then
19+
git config user.name "Documenter.jl"
20+
git config user.email "[email protected]"
21+
git rm -rf "$DIR"
22+
git commit -m "delete preview"
23+
git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree})
24+
git push --force origin gh-pages-new:gh-pages
25+
fi
2326
env:
24-
PRNUM: ${{ github.event.number }}
27+
DIR: "previews/PR${{ github.event.number }}"
2528

26-
- name: Push changes
27-
run: |
28-
git push --force origin gh-pages-new:gh-pages
29-
3029
# Workflow copied from https://github.com/CliMA/TimeMachine.jl

Project.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,19 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1717
UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39"
1818

1919
[compat]
20+
Aqua = "0 - 999"
2021
CondaPkg = "0.2.21"
22+
Dates = "1"
23+
Libdl = "1"
2124
MacroTools = "0.5"
25+
Markdown = "1"
26+
Pkg = "1"
27+
REPL = "1"
2228
Requires = "1"
29+
Serialization = "1"
2330
Tables = "1"
31+
Test = "1"
32+
TestItemRunner = "0 - 999"
2433
UnsafePointers = "1"
2534
julia = "1.6.1"
2635

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1><img src="https://raw.githubusercontent.com/JuliaPy/PythonCall.jl/main/docs/src/assets/logo.png" alt="PythonCall.jl logo" style="width: 100px;"><br>PythonCall &amp;&nbsp;JuliaCall</h1>
1+
<h1><img src="docs/src/assets/logo.png" alt="PythonCall.jl logo" style="width: 100px;"><br>PythonCall &amp;&nbsp;JuliaCall</h1>
22

33
[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
44
[![Stable Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliapy.github.io/PythonCall.jl/stable)

docs/make.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ include("customdocs.jl")
55
makedocs(
66
sitename = "PythonCall & JuliaCall",
77
modules = [PythonCall],
8+
format = Documenter.HTML(
9+
assets = ["assets/favicon.ico"],
10+
),
811
warnonly = [:missing_docs], # avoid raising error when docs are missing
912
pages = [
1013
"Home" => "index.md",

docs/src/assets/favicon.ico

49.4 KB
Binary file not shown.

docs/src/assets/logo.png

31.8 KB
Loading

docs/src/faq.md

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,6 @@ Some rules if you are writing multithreaded code:
1818

1919
Related issues: [#201](https://github.com/JuliaPy/PythonCall.jl/issues/201), [#202](https://github.com/JuliaPy/PythonCall.jl/issues/202)
2020

21-
## Does it work on Apple silicon (ARM, M1, M2, ...)?
22-
23-
Maybe. Your mileage may vary.
24-
25-
In general, PythonCall and JuliaCall are only supported on platforms with
26-
[Tier 1](https://julialang.org/downloads/#supported_platforms) level of support by Julia.
27-
Currently, Apple silicon is Tier 2, so is not supported.
28-
29-
Due to time constraints, issues affecting only unsupported platforms will not be
30-
investigated. It is much more likely to be an issue with Julia itself than PythonCall.
31-
3221
## Issues when Numpy arrays are expected
3322

3423
When a Julia array is passed to Python, it is wrapped as a [`ArrayValue`](#juliacall.ArrayValue).
@@ -92,4 +81,6 @@ Related issues: [#255](https://github.com/JuliaPy/PythonCall.jl/issues/255)
9281

9382
## Can I use JuliaCall to run Julia inside applications with embedded Python?
9483

95-
Yes, it may be possible. See an example of how to have Julia running inside the Python that is running inside Blender here https://discourse.julialang.org/t/running-julia-inside-blender-through-vscode-using-pythoncall-juliacall/96838.
84+
Yes, it may be possible. A good example of that is having Julia running inside the Python that is running inside Blender, as presented in [this Discourse post](https://discourse.julialang.org/t/running-julia-inside-blender-through-vscode-using-pythoncall-juliacall/96838/6).
85+
From the point that one has JuliaCall running inside Python, if it has access to the terminal, one can even launch a Julia REPL there, and if needed connect with VSCode Julia extension to it.
86+
The full Python script to install, launch JuliaCall, and launch a Julia REPL in Blender is [here](https://gist.github.com/cdsousa/d820d27174238c0d48e5252355584172).

docs/src/juliacall.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pip install juliacall
99

1010
Developers may wish to clone the repo (https://github.com/JuliaPy/PythonCall.jl) directly
1111
and pip install the module in editable mode. You should add `"dev":true, "path":"../.."` to
12-
`python/juliacall/juliapkg.json` to ensure you use the development version of PythonCall
12+
`pysrc/juliacall/juliapkg.json` to ensure you use the development version of PythonCall
1313
in conjunction with JuliaCall.
1414

1515
## Getting started
@@ -119,7 +119,7 @@ be configured in two ways:
119119
| `-X juliacall-min-optlevel=<0\|1\|2\|3>` | `PYTHON_JULIACALL_MIN_OPTLEVEL=<0\|1\|2\|3>` | Optimization level. |
120120
| `-X juliacall-optimize=<0\|1\|2\|3>` | `PYTHON_JULIACALL_OPTIMIZE=<0\|1\|2\|3>` | Minimum optimization level. |
121121
| `-X juliacall-procs=<N\|auto>` | `PYTHON_JULIACALL_PROCS=<N\|auto>` | Launch N local worker process. |
122-
| `-X juliacall-startup-file=<yes|no>` | `PYTHON_JULIACALL_STARTUP_FILE=<yes|no>` | Enable or disable your startup.jl file. |
122+
| `-X juliacall-startup-file=<yes\|no>` | `PYTHON_JULIACALL_STARTUP_FILE=<yes\|no>` | Enable or disable your startup.jl file. |
123123
| `-X juliacall-sysimage=<file>` | `PYTHON_JULIACALL_SYSIMAGE=<file>` | Use the given system image. |
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. |

docs/src/releasenotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
## Unreleased
77
* New unexported functions: `python_executable_path`, `python_library_path`, `python_library_handle` and `python_version`.
88
* `Py` is now treated as a scalar when broadcasting.
9+
* `PyArray` is now serializable.
910
* Bug fixes.
1011

1112
## 0.9.15 (2023-10-25)

pysrc/juliacall/__init__.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,16 @@ def int_option(name, *, accept_auto=False, **kw):
109109
raise ValueError(f'{s}: expecting an int'+(' or auto' if accept_auto else ""))
110110

111111
def args_from_config():
112-
argv = ["--"+opt[4:].replace("_", "-")+"="+val for opt, val in CONFIG.items()
113-
if val is not None and opt.startswith("opt_")]
114-
argv = [CONFIG['exepath']]+argv
115-
if sys.version_info[0] >= 3:
116-
argv = [s.encode("utf-8") for s in argv]
112+
argv = [CONFIG['exepath']]
113+
for opt, val in CONFIG.items():
114+
if opt.startswith('opt_'):
115+
if val is None:
116+
if opt == 'opt_handle_signals':
117+
val = 'no'
118+
else:
119+
continue
120+
argv.append('--' + opt[4:].replace('_', '-') + '=' + val)
121+
argv = [s.encode("utf-8") for s in argv]
117122

118123
argc = len(argv)
119124
argc = c.c_int(argc)
@@ -138,7 +143,7 @@ def args_from_config():
138143
CONFIG['opt_sysimage'] = sysimg = path_option('sysimage', check_exists=True)[0]
139144
CONFIG['opt_threads'] = int_option('threads', accept_auto=True)[0]
140145
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no'])[0]
141-
CONFIG['opt_handle_signals'] = choice('handle_signals', ['yes', 'no'], default='no')[0]
146+
CONFIG['opt_handle_signals'] = choice('handle_signals', ['yes', 'no'])[0]
142147
CONFIG['opt_startup_file'] = choice('startup_file', ['yes', 'no'])[0]
143148

144149
# Stop if we already initialised
@@ -225,6 +230,24 @@ def jlstr(x):
225230

226231
CONFIG['inited'] = True
227232

233+
if CONFIG['opt_handle_signals'] is None:
234+
if Base.Threads.nthreads() > 1:
235+
# a warning to help multithreaded users
236+
# TODO: should we set PYTHON_JULIACALL_HANDLE_SIGNALS=yes whenever PYTHON_JULIACALL_THREADS != 1?
237+
warnings.warn(
238+
"Julia was started with multiple threads "
239+
"but multithreading support is experimental in JuliaCall. "
240+
"It is recommended to restart Python with the environment variable "
241+
"PYTHON_JULIACALL_HANDLE_SIGNALS=yes "
242+
"set, otherwise you may experience segfaults or other crashes. "
243+
"Note however that this interferes with Python's own signal handling, "
244+
"so for example Ctrl-C will not raise KeyboardInterrupt. "
245+
"See https://juliapy.github.io/PythonCall.jl/stable/faq/#Is-PythonCall/JuliaCall-thread-safe? "
246+
"for further information. "
247+
"You can suppress this warning by setting "
248+
"PYTHON_JULIACALL_HANDLE_SIGNALS=no."
249+
)
250+
228251
init()
229252

230253
def load_ipython_extension(ip):

pysrc/juliacall/ipython.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def julia(self, line, cell=None):
5151
# run the code
5252
ans = Main.seval('begin\n' + code + '\nend')
5353
# flush stderr/stdout
54-
PythonCall._flush_stdio()
54+
PythonCall._ipython._flush_stdio()
5555
# copy variables back to Python
5656
# only copy those which are new or have changed value
5757
for k in outvars + syncvars:
@@ -74,18 +74,22 @@ def load_ipython_extension(ip):
7474
end
7575
end""")
7676
else:
77+
# In Julia 1.7+ redirect_stdout() returns a Pipe object. Earlier versions of Julia
78+
# just return a tuple of the two pipe ends. This is why we have [1] and [2] below.
79+
# They can be dropped on earlier versions.
7780
PythonCall.seval("""module _ipython
81+
using ..PythonCall
7882
const _redirected_stdout = redirect_stdout()
7983
const _redirected_stderr = redirect_stderr()
8084
const _py_stdout = PyIO(pyimport("sys" => "stdout"); line_buffering=true)
8185
const _py_stderr = PyIO(pyimport("sys" => "stderr"); line_buffering=true)
82-
const _redirect_stdout_task = @async write($_py_stdout, $_redirected_stdout)
83-
const _redirect_stderr_task = @async write($_py_stderr, $_redirected_stderr)
86+
const _redirect_stdout_task = @async write($_py_stdout, $_redirected_stdout[1])
87+
const _redirect_stderr_task = @async write($_py_stderr, $_redirected_stderr[1])
8488
function _flush_stdio()
8589
flush(stderr)
8690
flush(stdout)
87-
flush(_redirected_stderr)
88-
flush(_redirected_stdout)
91+
flush(_redirected_stderr[2])
92+
flush(_redirected_stdout[2])
8993
flush(_py_stderr)
9094
flush(_py_stdout)
9195
nothing

pytest/test_all.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,22 @@ def test_issue_394():
1717
assert jl.f is f
1818
assert jl.y is y
1919
assert jl.seval("f(x)") == 4
20+
21+
def test_issue_433():
22+
"https://github.com/JuliaPy/PythonCall.jl/issues/433"
23+
from juliacall import Main as jl
24+
25+
# Smoke test
26+
jl.seval("x=1\nx=1")
27+
assert jl.x == 1
28+
29+
# Do multiple things
30+
out = jl.seval(
31+
"""
32+
function _issue_433_g(x)
33+
return x^2
34+
end
35+
_issue_433_g(5)
36+
"""
37+
)
38+
assert out == 25

src/Compat/Compat.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Compat
88
using ..Core
99
using ..Core: C, Utils, pynew, incref, getptr, pycopy!, pymodulehooks, pyisnull, pybytes_asvector, pysysmodule, pyosmodule, pystr_fromUTF8
1010
using ..Convert: pyconvert, @pyconvert
11-
using ..Wrap: PyPandasDataFrame
11+
using ..Wrap: PyArray, PyPandasDataFrame
1212
using Serialization: Serialization, AbstractSerializer, serialize, deserialize
1313
using Tables: Tables
1414
using Requires: @require

src/Compat/serialization.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,19 @@ function Serialization.serialize(s::AbstractSerializer, x::PyException)
4242
end
4343

4444
Serialization.deserialize(s::AbstractSerializer, ::Type{PyException}) = PyException(deserialize_py(s))
45+
46+
### PyArray
47+
#
48+
# This type holds a pointer and a handle (usually a python memoryview or capsule) which are
49+
# not serializable by default, and even if they were would not be consistent after
50+
# serializing each field independently. So we just serialize the wrapped Python object.
51+
52+
function Serialization.serialize(s::AbstractSerializer, x::PyArray)
53+
Serialization.serialize_type(s, typeof(x), false)
54+
serialize_py(s, x.py)
55+
end
56+
57+
function Serialization.deserialize(s::AbstractSerializer, ::Type{T}) where {T<:PyArray}
58+
# TODO: set buffer and array args too?
59+
T(deserialize_py(s); copy=false)
60+
end

src/Convert/pyconvert.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Add a new conversion rule for `pyconvert`.
2323
### Arguments
2424
2525
- `tname` is a string of the form `"__module__:__qualname__"` identifying a Python type `t`,
26-
such as `"builtins:dict"`. This rule only applies to Python objects of this type.
26+
such as `"builtins:dict"` or `"sympy.core.symbol:Symbol"`. This rule only applies to
27+
Python objects of this type.
2728
- `T` is a Julia type, such that this rule only applies when the target type intersects
2829
with `T`.
2930
- `func` is the function implementing the rule.

src/Core/stdlib.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function init_stdlib()
1111
# This uses some internals, but Base._start() gets the state more like Julia
1212
# is if you call the executable directly, in particular it creates workers when
1313
# the --procs argument is given.
14-
push!(Core.ARGS, joinpath(ROOT_DIR, "pysrc", "juliacall", "init.jl"))
14+
push!(Base.Core.ARGS, joinpath(ROOT_DIR, "pysrc", "juliacall", "init.jl"))
1515
Base._start()
1616
Base.eval(:(PROGRAM_FILE = ""))
1717

src/JlWrap/module.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function pyjlmodule_dir(self::Module)
1010
end
1111

1212
function pyjlmodule_seval(self::Module, expr::Py)
13-
Py(Base.eval(self, Meta.parse(strip(pyconvert(String, expr)))))
13+
Py(Base.eval(self, Meta.parseall(strip(pyconvert(String, expr)))))
1414
end
1515

1616
function init_module()

test/Wrap.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,19 @@
6969
@test y == [1, 2, 3]
7070
@test z == [1, 2, 3]
7171
end
72+
@testset "serialize" begin
73+
using Serialization: serialize, deserialize
74+
io = IOBuffer()
75+
serialize(io, y)
76+
seekstart(io)
77+
y2 = deserialize(io)
78+
@test typeof(y) == typeof(y2)
79+
@test eltype(y) == eltype(y2)
80+
@test ndims(y) == ndims(y2)
81+
@test size(y) == size(y2)
82+
@test strides(y) == strides(y2)
83+
@test y == y2
84+
end
7285
end
7386

7487
@testitem "PyDict" begin

0 commit comments

Comments
 (0)