Skip to content

Commit 6c5c88a

Browse files
authored
Merge pull request #1206 from JuliaLang/matplotlib
Fix opening comms from the frontend side in the PythonCall extension
2 parents 51e1938 + 534e46d commit 6c5c88a

File tree

3 files changed

+78
-17
lines changed

3 files changed

+78
-17
lines changed

docs/src/_changelog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Changelog](https://keepachangelog.com).
1111

1212
### Fixed
1313
- Fixed the display of `UnionAll` types such as `Pair.body` ([#1203]).
14+
- Fixed a bug in the PythonCall extension that would break opening comms from
15+
the frontend side ([#1206]).
1416

1517
### Changed
1618
- Replaced JSON.jl with a vendored copy of
@@ -19,7 +21,7 @@ Changelog](https://keepachangelog.com).
1921
by JSON.jl. Load time is also slightly improved, from ~0.08s to ~0.05s on
2022
Julia 1.12.
2123
- Switched the default matplotlib backend for [`IJulia.init_matplotlib()`](@ref)
22-
to `widget`, which should be more backwards compatible ([#1205]).
24+
to the ipympl default, which should be more backwards compatible ([#1206]).
2325
- IJulia now checks if juliaup is used during the build step when installing the
2426
default kernel, and if it is used then it will set the kernel command to the
2527
equivalent of `julia +major.minor` ([#1201]). This has the advantage of not

ext/IJuliaPythonCallExt.jl

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import PrecompileTools: @compile_workload
1515
# the MIME's an object supports at once.
1616
function IJulia.display_dict(x::Py)
1717
if hasproperty(x, :_repr_mimebundle_) && !pyis(x._repr_mimebundle_, pybuiltins.None)
18-
pyconvert(Dict, x._repr_mimebundle_())
18+
recursive_pyconvert(x._repr_mimebundle_())
1919
else
2020
IJulia._display_dict(x)
2121
end
@@ -40,9 +40,20 @@ function recursive_pyconvert(x)
4040
return x
4141
end
4242

43+
function recursive_pydict(x)
44+
dict_py = pydict(x)
45+
for (key, value) in x
46+
if value isa Dict
47+
dict_py[key] = recursive_pydict(value)
48+
end
49+
end
50+
51+
dict_py
52+
end
53+
4354
function convert_buffers(buffers)
4455
if !(buffers isa Py)
45-
x
56+
buffers
4657
elseif pyis(buffers, pybuiltins.None)
4758
Vector{UInt8}[]
4859
else
@@ -65,14 +76,28 @@ function arrays_to_pylist!(dict::Dict)
6576
end
6677
end
6778

68-
function pycomm_init(self; target_name="comm", data=nothing, metadata=nothing, buffers=nothing, comm_id=IJulia.uuid4())
79+
function pycomm_init(self; target_name="comm", data=nothing, metadata=nothing, buffers=nothing,
80+
comm_id=IJulia.uuid4(), comm=nothing)
6981
try
70-
target_name = pyconvert(String, target_name)
71-
data = recursive_pyconvert(data)
72-
metadata = recursive_pyconvert(metadata)
82+
if target_name isa Py
83+
target_name = pyconvert(String, target_name)
84+
end
85+
if data isa Py
86+
data = recursive_pyconvert(data)
87+
end
88+
if metadata isa Py
89+
metadata = recursive_pyconvert(metadata)
90+
end
91+
if comm_id isa Py
92+
comm_id = pyconvert(String, comm_id)
93+
end
7394
buffers = convert_buffers(buffers)
7495

75-
self._comm = IJulia.Comm(target_name, comm_id, true; data, metadata, buffers)
96+
if isnothing(comm)
97+
comm = IJulia.Comm(target_name, comm_id, true; data, metadata, buffers)
98+
end
99+
self._comm = comm
100+
IJuliaPythonCallExt.pycomm_registry[comm_id] = self
76101
catch e
77102
@error "pycomm_init() failed" exception=(e, catch_backtrace())
78103
end
@@ -91,11 +116,11 @@ function pycomm_on_msg(self, callback)
91116
arrays_to_pylist!(msg.content)
92117

93118
msg_dict = Dict("idents" => msg.idents,
94-
"header" => msg.header,
95-
"content" => msg.content,
96-
"parent_header" => msg.parent_header,
97-
"metadata" => msg.metadata,
98-
"buffers" => msg.buffers
119+
"header" => msg.header,
120+
"content" => msg.content,
121+
"parent_header" => msg.parent_header,
122+
"metadata" => msg.metadata,
123+
"buffers" => msg.buffers
99124
)
100125
callback(msg_dict)
101126
catch e
@@ -122,11 +147,17 @@ function pycomm_send(self; data=Dict(), metadata=Dict(), buffers=nothing)
122147
end
123148
end
124149

150+
# This method is needed by ipywidgets. Unlike Julia, Python allows mixing
151+
# keyword and positional arguments so we need to have overloads for all the
152+
# calls with different numbers of positional arguments.
153+
pycomm_send(self, data; buffers=nothing) = pycomm_send(self; data, buffers)
154+
125155
function pycomm_close(self)
126156
try
127157
if !isnothing(IJulia._default_kernel)
128158
comm = IJulia._default_kernel.comms[pyconvert(String, self.comm_id)]
129159
IJulia.CommManager.close_comm(comm)
160+
delete!(IJuliaPythonCallExt.pycomm_registry, comm.id)
130161
end
131162
catch e
132163
@error "pycomm_close() failed" exception=(e, catch_backtrace())
@@ -143,6 +174,8 @@ pycommmanager_notimplemented(func_name::String) = pyfunc(Base.Fix1(py_notimpleme
143174
PyComm::Union{Py, Nothing} = nothing
144175
PyCommManager::Union{Py, Nothing} = nothing
145176

177+
const pycomm_registry = Dict{String, Py}()
178+
146179
function manager_register_target(self, target_name, callback)
147180
try
148181
if callback isa String
@@ -152,11 +185,33 @@ function manager_register_target(self, target_name, callback)
152185

153186
target_name = pyconvert(String, target_name)
154187
comm_sym = Symbol(target_name)
188+
self.on_open_callbacks[comm_sym] = callback
155189

156190
if @ccall(jl_generating_output()::Cint) == 0
157191
# Only create the method if we aren't precompiling
158-
@eval function IJulia.CommManager.register_comm(comm::IJulia.CommManager.Comm{$(QuoteNode(comm_sym))}, msg)
159-
comm.on_msg = (msg) -> callback(comm, msg)
192+
@eval function IJulia.CommManager.register_comm(comm::IJulia.CommManager.Comm{$(QuoteNode(comm_sym))}, msg::IJulia.Msg)
193+
msg_dict = Dict("idents" => msg.idents,
194+
"header" => msg.header,
195+
"content" => msg.content,
196+
"parent_header" => msg.parent_header,
197+
"metadata" => msg.metadata,
198+
"buffers" => msg.buffers)
199+
# We need to convert the Msg to a Python dict because that's
200+
# what the callbacks expect, and they may call `.get()` etc on
201+
# the dict.
202+
msg_dict_py = recursive_pydict(msg_dict)
203+
callback = IJuliaPythonCallExt.PyCommManager.on_open_callbacks[$(QuoteNode(comm_sym))]
204+
205+
# Register a PyComm corresponding to the new `comm`
206+
if !haskey(IJuliaPythonCallExt.pycomm_registry, comm.id)
207+
PyComm(; target_name=$(target_name), comm_id=comm.id, comm)
208+
end
209+
210+
try
211+
callback(IJuliaPythonCallExt.pycomm_registry[comm.id], msg_dict_py)
212+
catch ex
213+
@error "PyCommManager.register_target() callback failed" exception=(ex, catch_backtrace())
214+
end
160215
end
161216
end
162217
catch e
@@ -191,6 +246,7 @@ end
191246

192247
function create_pycommmanager()
193248
pytype("PyCommManager", (), [
249+
"on_open_callbacks" => Dict{Symbol, Py}(),
194250
pyfunc(manager_register_target; name="register_target"),
195251
pycommmanager_notimplemented("unregister_target"),
196252
pycommmanager_notimplemented("register_comm"),
@@ -225,7 +281,7 @@ function IJulia.init_ipywidgets()
225281
nothing
226282
end
227283

228-
function IJulia.init_matplotlib(backend::String="widget")
284+
function IJulia.init_matplotlib(backend::String="module://ipympl.backend_nbagg")
229285
IJulia.init_ipywidgets()
230286

231287
# Make sure it's in interactive mode and it's using the backend
@@ -256,6 +312,8 @@ precompile(pycomm_close, (Py,))
256312
create_pycomm()
257313
create_pycommmanager()
258314

315+
recursive_pydict(Dict{String, Any}("foo" => 2, "bar" => Dict("baz" => "quux")))
316+
259317
# If ipywidgets is installed in the environment try to precompile its
260318
# initializer. This is useful because the `ipywigets.register_comm_target()`
261319
# line is pretty heavy.
@@ -269,6 +327,7 @@ precompile(pycomm_close, (Py,))
269327
finally
270328
global PyComm = nothing
271329
global PyCommManager = nothing
330+
empty!(pycomm_registry)
272331
end
273332
end
274333

src/IJulia.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ Julia's `display()` instead.
551551
function init_ipython end
552552

553553
"""
554-
init_matplotlib(backend="widget")
554+
init_matplotlib(backend="module://ipympl.backend_nbagg")
555555
556556
Initialize the integration with matplotlib.
557557
"""

0 commit comments

Comments
 (0)