Skip to content

Commit 6607b9d

Browse files
Extract the Comm Python package (#973)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c5ce7bf commit 6607b9d

5 files changed

Lines changed: 31 additions & 286 deletions

File tree

ipykernel/comm/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
from .comm import * # noqa
2-
from .manager import * # noqa
1+
__all__ = ["Comm", "CommManager"]
2+
3+
from comm.base_comm import CommManager # noqa
4+
5+
from .comm import Comm # noqa

ipykernel/comm/comm.py

Lines changed: 13 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,32 @@
33
# Copyright (c) IPython Development Team.
44
# Distributed under the terms of the Modified BSD License.
55

6-
import uuid
7-
8-
from traitlets import Any, Bool, Bytes, Dict, Instance, Unicode, default
9-
from traitlets.config import LoggingConfigurable
6+
from comm.base_comm import BaseComm
107

118
from ipykernel.jsonutil import json_clean
129
from ipykernel.kernelbase import Kernel
1310

1411

15-
class Comm(LoggingConfigurable):
12+
class Comm(BaseComm):
1613
"""Class for communicating between a Frontend and a Kernel"""
1714

18-
kernel = Instance("ipykernel.kernelbase.Kernel", allow_none=True)
19-
20-
@default("kernel")
21-
def _default_kernel(self):
22-
if Kernel.initialized():
23-
return Kernel.instance()
24-
25-
comm_id = Unicode()
26-
27-
@default("comm_id")
28-
def _default_comm_id(self):
29-
return uuid.uuid4().hex
30-
31-
primary = Bool(True, help="Am I the primary or secondary Comm?")
32-
33-
target_name = Unicode("comm")
34-
target_module = Unicode(
35-
None,
36-
allow_none=True,
37-
help="""requirejs module from
38-
which to load comm target.""",
39-
)
40-
41-
topic = Bytes()
42-
43-
@default("topic")
44-
def _default_topic(self):
45-
return ("comm-%s" % self.comm_id).encode("ascii")
46-
47-
_open_data = Dict(help="data dict, if any, to be included in comm_open")
48-
_close_data = Dict(help="data dict, if any, to be included in comm_close")
49-
50-
_msg_callback = Any()
51-
_close_callback = Any()
15+
def __init__(self, *args, **kwargs):
16+
self.kernel = None
5217

53-
_closed = Bool(True)
18+
super().__init__(*args, **kwargs)
5419

55-
def __init__(self, target_name="", data=None, metadata=None, buffers=None, **kwargs):
56-
if target_name:
57-
kwargs["target_name"] = target_name
58-
super().__init__(**kwargs)
59-
if self.kernel:
60-
if self.primary:
61-
# I am primary, open my peer.
62-
self.open(data=data, metadata=metadata, buffers=buffers)
63-
else:
64-
self._closed = False
65-
66-
def _publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys):
20+
def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys):
6721
"""Helper for sending a comm message on IOPub"""
22+
if not Kernel.initialized():
23+
return
24+
6825
data = {} if data is None else data
6926
metadata = {} if metadata is None else metadata
7027
content = json_clean(dict(data=data, comm_id=self.comm_id, **keys))
28+
29+
if self.kernel is None:
30+
self.kernel = Kernel.instance()
31+
7132
self.kernel.session.send(
7233
self.kernel.iopub_socket,
7334
msg_type,
@@ -78,107 +39,5 @@ def _publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys)
7839
buffers=buffers,
7940
)
8041

81-
def __del__(self):
82-
"""trigger close on gc"""
83-
self.close(deleting=True)
84-
85-
# publishing messages
86-
87-
def open(self, data=None, metadata=None, buffers=None):
88-
"""Open the frontend-side version of this comm"""
89-
if data is None:
90-
data = self._open_data
91-
comm_manager = getattr(self.kernel, "comm_manager", None)
92-
if comm_manager is None:
93-
raise RuntimeError(
94-
"Comms cannot be opened without a kernel "
95-
"and a comm_manager attached to that kernel."
96-
)
97-
98-
comm_manager.register_comm(self)
99-
try:
100-
self._publish_msg(
101-
"comm_open",
102-
data=data,
103-
metadata=metadata,
104-
buffers=buffers,
105-
target_name=self.target_name,
106-
target_module=self.target_module,
107-
)
108-
self._closed = False
109-
except Exception:
110-
comm_manager.unregister_comm(self)
111-
raise
112-
113-
def close(self, data=None, metadata=None, buffers=None, deleting=False):
114-
"""Close the frontend-side version of this comm"""
115-
if self._closed:
116-
# only close once
117-
return
118-
self._closed = True
119-
# nothing to send if we have no kernel
120-
# can be None during interpreter cleanup
121-
if not self.kernel:
122-
return
123-
if data is None:
124-
data = self._close_data
125-
self._publish_msg(
126-
"comm_close",
127-
data=data,
128-
metadata=metadata,
129-
buffers=buffers,
130-
)
131-
if not deleting:
132-
# If deleting, the comm can't be registered
133-
self.kernel.comm_manager.unregister_comm(self)
134-
135-
def send(self, data=None, metadata=None, buffers=None):
136-
"""Send a message to the frontend-side version of this comm"""
137-
self._publish_msg(
138-
"comm_msg",
139-
data=data,
140-
metadata=metadata,
141-
buffers=buffers,
142-
)
143-
144-
# registering callbacks
145-
146-
def on_close(self, callback):
147-
"""Register a callback for comm_close
148-
149-
Will be called with the `data` of the close message.
150-
151-
Call `on_close(None)` to disable an existing callback.
152-
"""
153-
self._close_callback = callback
154-
155-
def on_msg(self, callback):
156-
"""Register a callback for comm_msg
157-
158-
Will be called with the `data` of any comm_msg messages.
159-
160-
Call `on_msg(None)` to disable an existing callback.
161-
"""
162-
self._msg_callback = callback
163-
164-
# handling of incoming messages
165-
166-
def handle_close(self, msg):
167-
"""Handle a comm_close message"""
168-
self.log.debug("handle_close[%s](%s)", self.comm_id, msg)
169-
if self._close_callback:
170-
self._close_callback(msg)
171-
172-
def handle_msg(self, msg):
173-
"""Handle a comm_msg message"""
174-
self.log.debug("handle_msg[%s](%s)", self.comm_id, msg)
175-
if self._msg_callback:
176-
shell = self.kernel.shell
177-
if shell:
178-
shell.events.trigger("pre_execute")
179-
self._msg_callback(msg)
180-
if shell:
181-
shell.events.trigger("post_execute")
182-
18342

18443
__all__ = ["Comm"]

ipykernel/comm/manager.py

Lines changed: 1 addition & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,131 +3,4 @@
33
# Copyright (c) IPython Development Team.
44
# Distributed under the terms of the Modified BSD License.
55

6-
import logging
7-
8-
from traitlets import Dict, Instance
9-
from traitlets.config import LoggingConfigurable
10-
from traitlets.utils.importstring import import_item
11-
12-
from .comm import Comm
13-
14-
15-
class CommManager(LoggingConfigurable):
16-
"""Manager for Comms in the Kernel"""
17-
18-
kernel = Instance("ipykernel.kernelbase.Kernel")
19-
comms = Dict()
20-
targets = Dict()
21-
22-
# Public APIs
23-
24-
def register_target(self, target_name, f):
25-
"""Register a callable f for a given target name
26-
27-
f will be called with two arguments when a comm_open message is received with `target`:
28-
29-
- the Comm instance
30-
- the `comm_open` message itself.
31-
32-
f can be a Python callable or an import string for one.
33-
"""
34-
if isinstance(f, str):
35-
f = import_item(f)
36-
37-
self.targets[target_name] = f
38-
39-
def unregister_target(self, target_name, f):
40-
"""Unregister a callable registered with register_target"""
41-
return self.targets.pop(target_name)
42-
43-
def register_comm(self, comm):
44-
"""Register a new comm"""
45-
comm_id = comm.comm_id
46-
comm.kernel = self.kernel
47-
self.comms[comm_id] = comm
48-
return comm_id
49-
50-
def unregister_comm(self, comm):
51-
"""Unregister a comm, and close its counterpart"""
52-
# unlike get_comm, this should raise a KeyError
53-
comm = self.comms.pop(comm.comm_id)
54-
55-
def get_comm(self, comm_id):
56-
"""Get a comm with a particular id
57-
58-
Returns the comm if found, otherwise None.
59-
60-
This will not raise an error,
61-
it will log messages if the comm cannot be found.
62-
"""
63-
try:
64-
return self.comms[comm_id]
65-
except KeyError:
66-
self.log.warning("No such comm: %s", comm_id)
67-
if self.log.isEnabledFor(logging.DEBUG):
68-
# don't create the list of keys if debug messages aren't enabled
69-
self.log.debug("Current comms: %s", list(self.comms.keys()))
70-
71-
# Message handlers
72-
def comm_open(self, stream, ident, msg):
73-
"""Handler for comm_open messages"""
74-
content = msg["content"]
75-
comm_id = content["comm_id"]
76-
target_name = content["target_name"]
77-
f = self.targets.get(target_name, None)
78-
comm = Comm(
79-
comm_id=comm_id,
80-
primary=False,
81-
target_name=target_name,
82-
)
83-
self.register_comm(comm)
84-
if f is None:
85-
self.log.error("No such comm target registered: %s", target_name)
86-
else:
87-
try:
88-
f(comm, msg)
89-
return
90-
except Exception:
91-
self.log.error("Exception opening comm with target: %s", target_name, exc_info=True)
92-
93-
# Failure.
94-
try:
95-
comm.close()
96-
except Exception:
97-
self.log.error(
98-
"""Could not close comm during `comm_open` failure
99-
clean-up. The comm may not have been opened yet.""",
100-
exc_info=True,
101-
)
102-
103-
def comm_msg(self, stream, ident, msg):
104-
"""Handler for comm_msg messages"""
105-
content = msg["content"]
106-
comm_id = content["comm_id"]
107-
comm = self.get_comm(comm_id)
108-
if comm is None:
109-
return
110-
111-
try:
112-
comm.handle_msg(msg)
113-
except Exception:
114-
self.log.error("Exception in comm_msg for %s", comm_id, exc_info=True)
115-
116-
def comm_close(self, stream, ident, msg):
117-
"""Handler for comm_close messages"""
118-
content = msg["content"]
119-
comm_id = content["comm_id"]
120-
comm = self.get_comm(comm_id)
121-
if comm is None:
122-
return
123-
124-
self.comms[comm_id]._closed = True
125-
del self.comms[comm_id]
126-
127-
try:
128-
comm.handle_close(msg)
129-
except Exception:
130-
self.log.error("Exception in comm_close for %s", comm_id, exc_info=True)
131-
132-
133-
__all__ = ["CommManager"]
6+
from comm.base_comm import CommManager # noqa

ipykernel/ipkernel.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
from contextlib import contextmanager
1010
from functools import partial
1111

12+
import comm
1213
from IPython.core import release
1314
from IPython.utils.tokenutil import line_at_cursor, token_at_cursor
1415
from traitlets import Any, Bool, Instance, List, Type, observe, observe_compat
1516
from zmq.eventloop.zmqstream import ZMQStream
1617

17-
from .comm import CommManager
18+
from .comm import Comm
1819
from .compiler import XCachingCompiler
1920
from .debugger import Debugger, _is_debugpy_available
2021
from .eventloops import _use_appnope
@@ -39,6 +40,14 @@
3940
_EXPERIMENTAL_KEY_NAME = "_jupyter_types_experimental"
4041

4142

43+
def create_comm(*args, **kwargs):
44+
"""Create a new Comm."""
45+
return Comm(*args, **kwargs)
46+
47+
48+
comm.create_comm = create_comm
49+
50+
4251
class IPythonKernel(KernelBase):
4352
shell = Instance("IPython.core.interactiveshell.InteractiveShellABC", allow_none=True)
4453
shell_class = Type(ZMQInteractiveShell)
@@ -101,7 +110,7 @@ def __init__(self, **kwargs):
101110
self.shell.display_pub.session = self.session
102111
self.shell.display_pub.pub_socket = self.iopub_socket
103112

104-
self.comm_manager = CommManager(parent=self, kernel=self)
113+
self.comm_manager = comm.get_comm_manager()
105114

106115
self.shell.configurables.append(self.comm_manager)
107116
comm_msg_types = ["comm_open", "comm_msg", "comm_close"]

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ requires-python = ">=3.8"
2727
dependencies = [
2828
"debugpy>=1.0",
2929
"ipython>=7.23.1",
30+
"comm>=0.1",
3031
"traitlets>=5.1.0",
3132
"jupyter_client>=6.1.12",
3233
"tornado>=6.1",

0 commit comments

Comments
 (0)