Skip to content

Commit 6ea28a7

Browse files
author
Christopher Doris
committed
Merge branch 'main' into gc
2 parents 627bba8 + 3ca4cd2 commit 6ea28a7

24 files changed

+529
-41
lines changed

Project.toml

+2-2
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.3"
4+
version = "0.9.4"
55

66
[deps]
77
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
@@ -16,7 +16,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
1616
UnsafePointers = "e17b2a0c-0bdf-430a-bd0c-3a23cae4ff39"
1717

1818
[compat]
19-
CondaPkg = "0.2.11"
19+
CondaPkg = "0.2.12"
2020
MacroTools = "0.5"
2121
Requires = "1"
2222
Tables = "1"

docs/src/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ Bringing [**Python®**](https://www.python.org/) and [**Julia**](https://juliala
55
- Simple syntax, so the Python code looks like Python and the Julia code looks like Julia.
66
- Intuitive and flexible conversions between Julia and Python: anything can be converted, you are in control.
77
- Fast non-copying conversion of numeric arrays in either direction: modify Python arrays (e.g. `bytes`, `array.array`, `numpy.ndarray`) from Julia or Julia arrays from Python.
8-
- Helpful wrappers: interpret Python sequences, dictionaries, arrays, dataframes and IO streams as their Julia couterparts, and vice versa.
8+
- Helpful wrappers: interpret Python sequences, dictionaries, arrays, dataframes and IO streams as their Julia counterparts, and vice versa.
99
- Beautiful stack-traces.
1010
- Works anywhere: tested on Windows, MacOS and Linux, 32- and 64-bit, Julia Julia 1.6.1 upwards and Python 3.7 upwards.

docs/src/releasenotes.md

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Release Notes
22

33
## Unreleased
4+
* Experimental new function `juliacall.interactive()` allows the Julia async event loop to
5+
run in the background of the Python REPL.
6+
* Experimental new IPython extension `juliacall.ipython` providing the `%jl` and `%%jl`
7+
magics for executing Julia code.
8+
* Experimental new module `juliacall.importer` allowing you to write Python modules in
9+
Julia.
10+
* Bug fixes.
11+
12+
## 0.9.4 (2022-07-26)
413
* Bug fixes.
514

615
## 0.9.3 (2022-07-02)

pysrc/juliacall/__init__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This module gets modified by PythonCall when it is loaded, e.g. to include Core, Base
22
# and Main modules.
33

4-
__version__ = '0.9.3'
4+
__version__ = '0.9.4'
55

66
_newmodule = None
77

@@ -21,6 +21,13 @@ def convert(T, x):
2121
_convert = PythonCall.seval("pyjlcallback((T,x)->pyjl(pyconvert(pyjlvalue(T)::Type,x)))")
2222
return _convert(T, x)
2323

24+
def interactive(enable=True):
25+
"Allow the Julia event loop to run in the background of the Python REPL."
26+
if enable:
27+
PythonCall._set_python_input_hook()
28+
else:
29+
PythonCall._unset_python_input_hook()
30+
2431
class JuliaError(Exception):
2532
"An error arising in Julia code."
2633
def __init__(self, exception, backtrace=None):
@@ -122,6 +129,7 @@ def args_from_config():
122129
CONFIG['opt_sysimage'] = sysimg = path_option('sysimage', check_exists=True)[0]
123130
CONFIG['opt_threads'] = int_option('threads', accept_auto=True)[0]
124131
CONFIG['opt_warn_overwrite'] = choice('warn_overwrite', ['yes', 'no'])[0]
132+
CONFIG['opt_handle_signals'] = 'no'
125133

126134
# Stop if we already initialised
127135
if CONFIG['inited']:
@@ -178,6 +186,7 @@ def jlstr(x):
178186
return 'raw"' + x.replace('"', '\\"').replace('\\', '\\\\') + '"'
179187
script = '''
180188
try
189+
Base.require(Main, :CompilerSupportLibraries_jll)
181190
import Pkg
182191
ENV["JULIA_PYTHONCALL_LIBPTR"] = {}
183192
ENV["JULIA_PYTHONCALL_EXE"] = {}

pysrc/juliacall/importer.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import base64
2+
import io
3+
import os
4+
import sys
5+
6+
from . import newmodule, Base
7+
from importlib.machinery import ModuleSpec, SourceFileLoader
8+
9+
class Finder:
10+
def __init__(self, jlext='.jl', pyext='.py'):
11+
self.jlext = jlext
12+
self.pyext = pyext
13+
14+
def find_spec(self, fullname, path, target=None):
15+
if path is None:
16+
path = sys.path
17+
if '.' in fullname:
18+
return
19+
name = fullname
20+
else:
21+
name = fullname.split('.')[-1]
22+
for root in path:
23+
jlfile = os.path.join(root, name + self.jlext)
24+
if os.path.isfile(jlfile):
25+
jlfile = os.path.realpath(jlfile)
26+
pyfile = os.path.join(root, name + self.pyext)
27+
gen_file(jlfile, pyfile)
28+
return ModuleSpec(fullname, SourceFileLoader(fullname, pyfile), origin=jlfile)
29+
30+
def install(**kw):
31+
finder = Finder(**kw)
32+
sys.meta_path.insert(0, finder)
33+
return finder
34+
35+
def uninstall(finder):
36+
sys.meta_path.remove(finder)
37+
38+
def gen_code(jl):
39+
buf = io.StringIO()
40+
pr = lambda x: print(x, file=buf)
41+
jl2 = jl.replace('\\', '\\\\').replace("'", "\\'")
42+
pr('# This file was automatically generated by juliacall.importer')
43+
pr('import juliacall.importer')
44+
pr('juliacall.importer.exec_module(__name__,')
45+
pr("'''"+jl2+"''')")
46+
return buf.getvalue()
47+
48+
def gen_file(jl, py):
49+
with open(jl, encoding='utf-8') as fp:
50+
jlcode = fp.read()
51+
pycode = gen_code(jlcode)
52+
with open(py, 'w', encoding='utf-8') as fp:
53+
fp.write(pycode)
54+
55+
def exec_module(name, code):
56+
pymod = sys.modules[name]
57+
jlmod = newmodule(name)
58+
jlmod.seval('begin\n' + code + '\nend')
59+
delattr(pymod, 'juliacall')
60+
setattr(pymod, '__jl_code__', code)
61+
setattr(pymod, '__jl_module__', jlmod)
62+
ks = [str(k) for k in Base.names(jlmod)]
63+
ks = [k for k in ks if k != name]
64+
if not ks:
65+
ks = [str(k) for k in Base.names(jlmod, all=True)]
66+
ks = [k for k in ks if not (k == name or k == 'include' or k == 'eval' or k.startswith('_') or '#' in k)]
67+
setattr(pymod, '__all__', ks)
68+
setattr(pymod, '__doc__', str(Base.Docs.doc(jlmod)))
69+
for k in ks:
70+
setattr(pymod, k, getattr(jlmod, k))

pysrc/juliacall/ipython.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from IPython.core.magic import Magics, magics_class, line_cell_magic
2+
from . import Main, Base, PythonCall
3+
4+
@magics_class
5+
class JuliaMagics(Magics):
6+
7+
@line_cell_magic
8+
def julia(self, line, cell=None):
9+
code = line if cell is None else cell
10+
ans = Main.seval('begin\n' + code + '\nend')
11+
Base.flush(Base.stdout)
12+
Base.flush(Base.stderr)
13+
if not code.strip().endswith(';'):
14+
return ans
15+
16+
def load_ipython_extension(ip):
17+
# register magics
18+
ip.register_magics(JuliaMagics(ip))
19+
# redirect stdout/stderr
20+
PythonCall.seval("""begin
21+
const _redirected_stdout = redirect_stdout()
22+
const _redirected_stderr = redirect_stderr()
23+
const _py_stdout = PyIO(pyimport("sys" => "stdout"), buflen=1)
24+
const _py_stderr = PyIO(pyimport("sys" => "stderr"), buflen=1)
25+
const _redirect_stdout_task = @async write($_py_stdout, $_redirected_stdout)
26+
const _redirect_stderr_task = @async write($_py_stderr, $_redirected_stderr)
27+
end""")

pysrc/juliacall/juliapkg-dev.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"packages": {
44
"PythonCall": {
55
"uuid": "6099a3de-0909-46bc-b1f4-468b9a2dfc0d",
6-
"version": "0.9.3",
6+
"version": "=0.9.4",
77
"path": "../..",
88
"dev": true
99
}

pysrc/juliacall/juliapkg.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"packages": {
44
"PythonCall": {
55
"uuid": "6099a3de-0909-46bc-b1f4-468b9a2dfc0d",
6-
"version": "0.9.3"
6+
"version": "=0.9.4"
77
}
88
}
99
}

setup.cfg

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = juliacall
3-
version = 0.9.3
3+
version = 0.9.4
44
description = Julia and Python in seamless harmony
55
long_description = file: README.md
66
long_description_content_type = text/markdown
@@ -15,9 +15,9 @@ zip_safe = False
1515
package_dir =
1616
=pysrc
1717
packages = juliacall
18-
python_requires = ~=3.5
18+
python_requires = ~=3.7
1919
install_requires =
20-
juliapkg ~=0.1.0
20+
juliapkg ~=0.1.8
2121

2222
[options.package_data]
2323
juliacall =

src/Py.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,8 @@ Base.powermod(x::Number, y::Py, z::Number) = pypow(x, y, z)
445445
Base.powermod(x::Py, y::Number, z::Number) = pypow(x, y, z)
446446

447447
# documentation
448-
function Base.Docs.getdoc(x::Py, @nospecialize(sig))
448+
function Base.Docs.getdoc(x::Py, @nospecialize(sig)=Union{})
449+
pyisnull(x) && return nothing
449450
parts = []
450451
inspect = pyimport("inspect")
451452
# head line

src/PythonCall.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module PythonCall
22

3-
const VERSION = v"0.9.3"
3+
const VERSION = v"0.9.4"
44
const ROOT_DIR = dirname(@__DIR__)
55

66
using Base: @propagate_inbounds

src/compat/gui.jl

+33
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,36 @@ function event_loop_on(g::Symbol; interval::Real = 0.04, fix::Bool = false)
165165
callback = new_event_loop_callback(string(g), Float64(interval))
166166
EVENT_LOOPS[g] = Timer(t -> callback(), 0; interval = interval)
167167
end
168+
169+
function _python_input_hook()
170+
try
171+
@static if Sys.iswindows()
172+
# on windows, we can call yield in a loop because _kbhit() lets us know
173+
# when to stop
174+
while true
175+
yield()
176+
if ccall(:_kbhit, Cint, ()) != 0
177+
break
178+
end
179+
sleep(0.01)
180+
end
181+
else
182+
# on other platforms, if readline is enabled, the input hook is called
183+
# repeatedly so the loop is not required
184+
yield()
185+
end
186+
catch
187+
return Cint(1)
188+
end
189+
return Cint(0)
190+
end
191+
192+
function _set_python_input_hook()
193+
C.PyOS_SetInputHook(@cfunction(_python_input_hook, Cint, ()))
194+
return
195+
end
196+
197+
function _unset_python_input_hook()
198+
C.PyOS_SetInputHook(C_NULL)
199+
return
200+
end

src/cpython/context.jl

+20
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,26 @@ function init_context()
5252
# Find Python executable
5353
exe_path = get(ENV, "JULIA_PYTHONCALL_EXE", "")
5454
if exe_path == "" || exe_path == "@CondaPkg"
55+
if Sys.islinux()
56+
# Ensure libstdc++ in the Conda environment is compatible with the one
57+
# linked in Julia. This is platform/version dependent, so needs to occur at
58+
# runtime.
59+
#
60+
# To figure out cxx_version for a given Julia version, run
61+
# strings /path/to/julia/lib/julia/libstdc++.so.6 | grep GLIBCXX
62+
# then look at
63+
# https://gcc.gnu.org/onlinedocs/gcc-12.1.0/libstdc++/manual/manual/abi.html
64+
# for the highest GCC version compatible with the highest GLIBCXX version.
65+
if Base.VERSION <= v"1.6.2"
66+
# GLIBCXX_3.4.26
67+
cxx_version = ">=3.4,<9.2"
68+
else
69+
# GLIBCXX_3.4.29
70+
# checked up to v1.8.0
71+
cxx_version = ">=3.4,<11.4"
72+
end
73+
CondaPkg.add("libstdcxx-ng", version=cxx_version, channel="conda-forge", temp=true, file=joinpath(@__DIR__, "..", "..", "CondaPkg.toml"), resolve=false)
74+
end
5575
# By default, we use Python installed by CondaPkg.
5676
exe_path = Sys.iswindows() ? joinpath(CondaPkg.envdir(), "python.exe") : joinpath(CondaPkg.envdir(), "bin", "python")
5777
# It's not sufficient to only activate the env while Python is initialising,

src/cpython/extras.jl

+10-1
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@ PyBuffer_Release(_b) = begin
4141
return
4242
end
4343

44+
function PyOS_SetInputHook(hook::Ptr{Cvoid})
45+
Base.unsafe_store!(POINTERS.PyOS_InputHookPtr, hook)
46+
return
47+
end
48+
49+
function PyOS_GetInputHook()
50+
return Base.unsafe_load(POINTERS.PyOS_InputHookPtr)
51+
end
52+
4453
function PyOS_RunInputHook()
45-
hook = Base.unsafe_load(Ptr{Ptr{Cvoid}}(dlsym(CTX.lib_ptr, :PyOS_InputHook)))
54+
hook = PyOS_GetInputHook()
4655
if hook == C_NULL
4756
return false
4857
else

src/cpython/pointers.jl

+2
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ const CAPI_OBJECTS = Set([
273273
$([:($name :: Ptr{Cvoid} = C_NULL) for name in CAPI_FUNCS]...)
274274
$([:($name :: PyPtr = C_NULL) for name in CAPI_EXCEPTIONS]...)
275275
$([:($name :: PyPtr = C_NULL) for name in CAPI_OBJECTS]...)
276+
PyOS_InputHookPtr :: Ptr{Ptr{Cvoid}} = C_NULL
276277
PyJuliaBase_Type :: PyPtr = C_NULL
277278
PyExc_JuliaError :: PyPtr = C_NULL
278279
end
@@ -290,6 +291,7 @@ const POINTERS = CAPIPointers()
290291
]...)
291292
$([:(p.$name = Base.unsafe_load(Ptr{PyPtr}(dlsym(lib, $(QuoteNode(name)))))) for name in CAPI_EXCEPTIONS]...)
292293
$([:(p.$name = dlsym(lib, $(QuoteNode(name)))) for name in CAPI_OBJECTS]...)
294+
p.PyOS_InputHookPtr = dlsym(CTX.lib_ptr, :PyOS_InputHook)
293295
end
294296

295297
for (name, (argtypes, rettype)) in CAPI_FUNC_SIGS

src/jlwrap/callback.jl

+21-8
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@ function pyjlcallback_call(self, args_::Py, kwargs_::Py)
1010
args = pyconvert(Vector{Py}, args_)
1111
kwargs = pyconvert(Dict{Symbol,Py}, kwargs_)
1212
Py(self(args...; kwargs...))
13-
elseif pylen(args_) > 0
13+
elseif (nargs = pylen(args_)) > 0
1414
args = pyconvert(Vector{Py}, args_)
15-
if length(args) == 1
15+
@assert length(args) == nargs
16+
if nargs == 1
1617
Py(self(args[1]))
17-
elseif length(args) == 2
18+
elseif nargs == 2
1819
Py(self(args[1], args[2]))
19-
elseif length(args) == 3
20+
elseif nargs == 3
2021
Py(self(args[1], args[2], args[3]))
22+
elseif nargs == 4
23+
Py(self(args[1], args[2], args[3], args[4]))
24+
elseif nargs == 5
25+
Py(self(args[1], args[2], args[3], args[4], args[5]))
2126
else
2227
Py(self(args...))
2328
end
@@ -53,7 +58,7 @@ end
5358
pyjlcallback(f) = pyjl(pyjlcallbacktype, f)
5459

5560
"""
56-
pyfunc(f; name=nothing, qualname=name, doc=nothing, signature=nothing)
61+
pyfunc(f; [name], [qualname], [doc], [signature])
5762
5863
Wrap the callable `f` as an ordinary Python function.
5964
@@ -63,10 +68,18 @@ The name, qualname, docstring or signature can optionally be set with `name`, `q
6368
Unlike `Py(f)` (or `pyjl(f)`), the arguments passed to `f` are always of type `Py`, i.e.
6469
they are never converted.
6570
"""
66-
function pyfunc(f; name=nothing, qualname=name, doc=nothing, signature=nothing)
71+
function pyfunc(f; name=nothing, qualname=name, doc=nothing, signature=nothing, wrap=pywrapcallback)
6772
f2 = ispy(f) ? f : pyjlcallback(f)
68-
f3 = pywrapcallback(f2)
69-
pydel!(f2)
73+
if wrap isa Pair
74+
wrapargs, wrapfunc = wrap
75+
else
76+
wrapargs, wrapfunc = (), wrap
77+
end
78+
if wrapfunc isa AbstractString
79+
f3 = pybuiltins.eval(wrapfunc, pydict())(f2, wrapargs...)
80+
else
81+
f3 = wrapfunc(f2, wrapargs...)
82+
end
7083
f3.__name__ = name === nothing ? "<lambda>" : name
7184
f3.__qualname__ = name === nothing ? "<lambda>" : qualname
7285
if doc !== nothing

0 commit comments

Comments
 (0)