Skip to content

Commit cbce0d0

Browse files
author
Christopher Doris
committed
Merge branch 'main' into cxx-compat
2 parents 9c509c6 + 58e1498 commit cbce0d0

File tree

9 files changed

+182
-10
lines changed

9 files changed

+182
-10
lines changed

docs/src/releasenotes.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Release Notes
22

3+
## 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+
312
## 0.9.4 (2022-07-26)
413
* Bug fixes.
514

pysrc/juliacall/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 70 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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.4",
6+
"version": "=0.9.4",
77
"path": "../..",
88
"dev": true
99
}

src/compat/gui.jl

Lines changed: 33 additions & 0 deletions
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/extras.jl

Lines changed: 10 additions & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 21 additions & 8 deletions
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)