async-kernel 0.22.0__tar.gz → 0.22.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {async_kernel-0.22.0 → async_kernel-0.22.2}/CHANGELOG.md +34 -0
  2. {async_kernel-0.22.0 → async_kernel-0.22.2}/PKG-INFO +1 -1
  3. {async_kernel-0.22.0 → async_kernel-0.22.2}/_version.py +2 -2
  4. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/caller.py +5 -4
  5. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/zmq_poll.py +49 -31
  6. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/interface/base.py +12 -18
  7. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/interface/callable.py +11 -10
  8. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/messaging/base.py +7 -3
  9. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/messaging/zmq.py +17 -12
  10. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_callable_interface.py +1 -3
  11. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_eventloop_zmq_poll.py +5 -5
  12. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/utils.py +8 -2
  13. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/dependabot.yaml +0 -0
  14. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/release.yml +0 -0
  15. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/ci.yml +0 -0
  16. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/enforce-label.yml +0 -0
  17. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/new_release.yml +0 -0
  18. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/pre-commit.yml +0 -0
  19. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/publish-docs.yml +0 -0
  20. {async_kernel-0.22.0 → async_kernel-0.22.2}/.github/workflows/publish-to-pypi.yml +0 -0
  21. {async_kernel-0.22.0 → async_kernel-0.22.2}/.gitignore +0 -0
  22. {async_kernel-0.22.0 → async_kernel-0.22.2}/.pre-commit-config.yaml +0 -0
  23. {async_kernel-0.22.0 → async_kernel-0.22.2}/.vscode/launch.json +0 -0
  24. {async_kernel-0.22.0 → async_kernel-0.22.2}/.vscode/settings.json +0 -0
  25. {async_kernel-0.22.0 → async_kernel-0.22.2}/.vscode/spellright.dict +0 -0
  26. {async_kernel-0.22.0 → async_kernel-0.22.2}/CONTRIBUTING.md +0 -0
  27. {async_kernel-0.22.0 → async_kernel-0.22.2}/IPYTHON_LICENSE +0 -0
  28. {async_kernel-0.22.0 → async_kernel-0.22.2}/LICENSE +0 -0
  29. {async_kernel-0.22.0 → async_kernel-0.22.2}/README.md +0 -0
  30. {async_kernel-0.22.0 → async_kernel-0.22.2}/cliff.toml +0 -0
  31. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/about/changelog.md +0 -0
  32. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/about/contributing.md +0 -0
  33. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/about/index.md +0 -0
  34. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/about/license.md +0 -0
  35. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/index.md +0 -0
  36. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/javascripts/extra.js +0 -0
  37. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/notebooks/caller.ipynb +0 -0
  38. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/notebooks/concurrency.ipynb +0 -0
  39. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/notebooks/custom_kernel.ipynb +0 -0
  40. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/overrides/main.html +0 -0
  41. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/caller.md +0 -0
  42. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/comm.md +0 -0
  43. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/command.md +0 -0
  44. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/common.md +0 -0
  45. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/debugger.md +0 -0
  46. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/event_loop.md +0 -0
  47. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/index.md +0 -0
  48. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/interface.md +0 -0
  49. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/ipshell.md +0 -0
  50. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/kernel.md +0 -0
  51. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/kernelspec.md +0 -0
  52. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/messaging.md +0 -0
  53. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/pending.md +0 -0
  54. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/shell.md +0 -0
  55. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/typing.md +0 -0
  56. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/reference/utils.md +0 -0
  57. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/stylesheets/extra.css +0 -0
  58. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/thread_safety.md +0 -0
  59. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/usage/commands.md +0 -0
  60. {async_kernel-0.22.0 → async_kernel-0.22.2}/docs/usage/index.md +0 -0
  61. {async_kernel-0.22.0 → async_kernel-0.22.2}/hatch_build.py +0 -0
  62. {async_kernel-0.22.0 → async_kernel-0.22.2}/mkdocs.yml +0 -0
  63. {async_kernel-0.22.0 → async_kernel-0.22.2}/pyproject.toml +0 -0
  64. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/__init__.py +0 -0
  65. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/__main__.py +0 -0
  66. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/comm.py +0 -0
  67. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/command.py +0 -0
  68. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/common.py +0 -0
  69. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/compat/attr_docs.py +0 -0
  70. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/compat/json.py +0 -0
  71. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/compiler.py +0 -0
  72. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/debugger.py +0 -0
  73. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/__init__.py +0 -0
  74. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/asyncio_guest.py +0 -0
  75. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/qt_host.py +0 -0
  76. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/run.py +0 -0
  77. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/event_loop/tk_host.py +0 -0
  78. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/interface/__init__.py +0 -0
  79. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/interface/ip_app.py +0 -0
  80. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/kernel.py +0 -0
  81. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/kernelspec.py +0 -0
  82. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/messaging/__init__.py +0 -0
  83. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/outstream.py +0 -0
  84. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/pending.py +0 -0
  85. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/py.typed +0 -0
  86. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/resources/logo-32x32.png +0 -0
  87. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/resources/logo-64x64.png +0 -0
  88. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/resources/logo-svg.svg +0 -0
  89. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/shell/__init__.py +0 -0
  90. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/shell/base.py +0 -0
  91. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/shell/ipshell.py +0 -0
  92. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/typing.py +0 -0
  93. {async_kernel-0.22.0 → async_kernel-0.22.2}/src/async_kernel/utils.py +0 -0
  94. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/__init__.py +0 -0
  95. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/conftest.py +0 -0
  96. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/references.py +0 -0
  97. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_caller.py +0 -0
  98. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_comm.py +0 -0
  99. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_command.py +0 -0
  100. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_common.py +0 -0
  101. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_compat.py +0 -0
  102. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_connection.py +0 -0
  103. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_debugger.py +0 -0
  104. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_debugger_static.py +0 -0
  105. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_enter_kernel.py +0 -0
  106. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_event_loop.py +0 -0
  107. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_interface.py +0 -0
  108. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_ipapp.py +0 -0
  109. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_kernel_ipshell.py +0 -0
  110. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_kernelspec.py +0 -0
  111. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_message_spec.py +0 -0
  112. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_outstream.py +0 -0
  113. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_pending.py +0 -0
  114. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_subclass.py +0 -0
  115. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_typing.py +0 -0
  116. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_utils.py +0 -0
  117. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_zmq_connection.py +0 -0
  118. {async_kernel-0.22.0 → async_kernel-0.22.2}/tests/test_zmq_subprocess.py +0 -0
  119. {async_kernel-0.22.0 → async_kernel-0.22.2}/uv.lock +0 -0
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.22.2] - 2026-08-21
9
+
10
+ ### <!-- 1 --> 🚀 Features
11
+
12
+ - ZMQPoll._wake minor optimization. [#574](https://github.com/fleming79/async-kernel/pull/574)
13
+
14
+ - Replace BinarySemaphore with threading.Lock for faster performance in heavily used places. [#571](https://github.com/fleming79/async-kernel/pull/571)
15
+
16
+ ### <!-- 6 --> 🌀 Miscellaneous
17
+
18
+ - Tweak Interface startup sequence. [#573](https://github.com/fleming79/async-kernel/pull/573)
19
+
20
+ - Alan Fleming Refactor ZMQConnection to send the welcome message directly. [#572](https://github.com/fleming79/async-kernel/pull/572)
21
+
22
+ ## [0.22.1] - 2026-08-19
23
+
24
+ ### <!-- 0 --> 🏗️ Breaking changes
25
+
26
+ - Update zmq_poll docstrings. [#567](https://github.com/fleming79/async-kernel/pull/567)
27
+
28
+ ### <!-- 5 --> 📝 Documentation
29
+
30
+ - ZMQPoll typing and doc fixes. [#568](https://github.com/fleming79/async-kernel/pull/568)
31
+
32
+ ### <!-- 6 --> 🌀 Miscellaneous
33
+
34
+ - Prepare for release v0.22.1 [#570](https://github.com/fleming79/async-kernel/pull/570)
35
+
36
+ - Refactor create_interface_messge_callback_handler to only use one connection [#569](https://github.com/fleming79/async-kernel/pull/569)
37
+
8
38
  ## [0.22.0] - 2026-08-17
9
39
 
10
40
  ### <!-- 0 --> 🏗️ Breaking changes
@@ -37,6 +67,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
67
 
38
68
  ### <!-- 6 --> 🌀 Miscellaneous
39
69
 
70
+ - Prepare for release v0.22.0 [#566](https://github.com/fleming79/async-kernel/pull/566)
71
+
40
72
  - Bump uv from 0.11.32 to 0.12.4 [#562](https://github.com/fleming79/async-kernel/pull/562)
41
73
 
42
74
  - Bump astral-sh/setup-uv from 9.0.0 to 10.0.1 in the actions group [#559](https://github.com/fleming79/async-kernel/pull/559)
@@ -1529,6 +1561,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1529
1561
 
1530
1562
  - Bump the actions group across 1 directory with 2 updates [#3](https://github.com/fleming79/async-kernel/pull/3)
1531
1563
 
1564
+ [0.22.2]: https://github.com/fleming79/async-kernel/compare/v0.22.1..v0.22.2
1565
+ [0.22.1]: https://github.com/fleming79/async-kernel/compare/v0.22.0..v0.22.1
1532
1566
  [0.22.0]: https://github.com/fleming79/async-kernel/compare/v0.21.0..v0.22.0
1533
1567
  [0.21.0]: https://github.com/fleming79/async-kernel/compare/v0.20.2..v0.21.0
1534
1568
  [0.20.2]: https://github.com/fleming79/async-kernel/compare/v0.20.1..v0.20.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: async-kernel
3
- Version: 0.22.0
3
+ Version: 0.22.2
4
4
  Summary: A concurrent python kernel for Jupyter supporting AnyIO, AsyncIO and Trio.
5
5
  Project-URL: Homepage, https://fleming79.github.io/async-kernel
6
6
  Project-URL: Documentation, https://fleming79.github.io/async-kernel
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.22.0'
22
- __version_tuple__ = version_tuple = (0, 22, 0)
21
+ __version__ = version = '0.22.2'
22
+ __version_tuple__ = version_tuple = (0, 22, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Concatenate, Generic, Literal,
19
19
  from weakref import ReferenceType
20
20
 
21
21
  import anyio
22
- from aiologic import BinarySemaphore, CountdownEvent
22
+ from aiologic import CountdownEvent
23
23
  from aiologic.lowlevel import async_checkpoint, create_async_event, current_async_library
24
24
  from aiologic.meta import await_for, iscoroutinelike
25
25
  from typing_extensions import override
@@ -191,7 +191,7 @@ class Caller:
191
191
 
192
192
  _caller_token = contextvars.ContextVar("caller_tokens", default=CALLER_MAIN_THREAD_ID)
193
193
  _instances: ClassVar[weakref.WeakValueDictionary[int, Self]] = weakref.WeakValueDictionary()
194
- _lock: ClassVar = BinarySemaphore()
194
+ _lock: ClassVar = threading.Lock()
195
195
 
196
196
  _thread: threading.Thread
197
197
  _caller_id: int
@@ -219,7 +219,7 @@ class Caller:
219
219
  _worker_pool: Fixed[Self, deque[Self]] = Fixed(deque)
220
220
 
221
221
  # Private
222
- _inst_lock: BinarySemaphore
222
+ _inst_lock: threading.Lock
223
223
  _children: set[Self]
224
224
  _tasks: set[asyncio.Task]
225
225
  _queue_map: dict[int, Pending]
@@ -394,7 +394,7 @@ class Caller:
394
394
  thread = threading.current_thread()
395
395
 
396
396
  # Add private objects
397
- inst._inst_lock = BinarySemaphore()
397
+ inst._inst_lock = threading.Lock()
398
398
  inst._children = set()
399
399
  inst._tasks = set()
400
400
  inst._queue_map = {}
@@ -949,6 +949,7 @@ class Caller:
949
949
  pass
950
950
  except IndexError:
951
951
  caller = self.get()
952
+ caller._name = f"async-kernel worker of {self.id}"
952
953
  pen = caller.call_soon(func, *args, **kwargs)
953
954
  pen.add_done_callback(_to_thread_on_done)
954
955
  return pen
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Literal, Self
14
14
  import zmq
15
15
  from aiologic import BinarySemaphore, BusyResourceError
16
16
  from typing_extensions import override
17
- from zmq.backend import zmq_poll
17
+ from zmq.backend import zmq_poll as _zmq_poll
18
18
 
19
19
  from async_kernel import Caller, utils
20
20
  from async_kernel.common import Fixed
@@ -41,7 +41,7 @@ class ZMQPollSocket(zmq.sugar.Socket[bytes]):
41
41
  """
42
42
 
43
43
  _zmq_poll_ref: weakref.ref[ZMQPoll]
44
- lock: BinarySemaphore
44
+ lock: threading.Lock
45
45
 
46
46
  if TYPE_CHECKING:
47
47
  # magic attributes cannot be be stored in `__annotations__`.
@@ -68,7 +68,7 @@ class ZMQPollSocket(zmq.sugar.Socket[bytes]):
68
68
  zmq_poll: ZMQPoll,
69
69
  copy_threshold: int | None = None,
70
70
  ) -> None:
71
- self.lock = BinarySemaphore()
71
+ self.lock = threading.Lock()
72
72
  self._zmq_poll_ref = weakref.ref(zmq_poll)
73
73
 
74
74
  if zmq_poll.stopped.done():
@@ -99,10 +99,13 @@ class ZMQPollSocket(zmq.sugar.Socket[bytes]):
99
99
  track: bool = False,
100
100
  **kwargs,
101
101
  ) -> MessageTracker | None:
102
- with self.lock:
102
+ self.lock.acquire()
103
+ try:
103
104
  if self.closed:
104
105
  return None
105
106
  return super().send_multipart(msg_parts, flags, copy, track)
107
+ finally:
108
+ self.lock.release()
106
109
 
107
110
  @override
108
111
  def close(self, linger=None) -> None:
@@ -144,19 +147,30 @@ class ZMQPollSocket(zmq.sugar.Socket[bytes]):
144
147
  self.zmq_poll.execute(super().unsubscribe, topic)
145
148
 
146
149
 
147
- T_key = tuple[Any | ZMQPollSocket, int]
150
+ T_key = tuple[ZMQPollSocket, int]
148
151
 
149
152
 
150
153
  class ZMQPoll:
151
154
  """A [zmq_poll](https://libzmq.readthedocs.io/en/latest/zmq_poll.html) based event loop.
152
155
 
153
- This event loop is synchronous and is intended for quick message dispatch to separate threads
154
- for handling. It provides thread-safe execution and manages its own custom sockets which use
155
- the same executor for setting attributes and calling methods.
156
+ When a `(socket, flags) -> handler` mapping context manager is used [ZMQPoll.event_handler][]
157
+ `handler` is called with the socket and event when the event occurs on the socket. The
158
+ callback occurs in the zmq poll thread. [async_kernel.caller.Caller][] is recommended to
159
+ to scheduling code execution in different threads.
160
+
161
+ Only [ZMQPollSocket][] sockets created using the [ZMQPoll.socket][] factory function are
162
+ allowed.
163
+
164
+ The methods [ZMQPoll.execute][] and the async version [ZMQPoll.aexecute][] are provided
165
+ to executed code in the zmq_poll thread, which is useful when creating and configuring
166
+ sockets to reduce thread switching.
156
167
  """
157
168
 
158
169
  stopped: Fixed[Self, ProtectedPending] = Fixed(ProtectedPending)
170
+ """Set when the poll thread event has stopped."""
171
+
159
172
  sockets: Fixed[Self, set[ZMQPollSocket]] = Fixed(set)
173
+ """The sockets currently registered with this instance."""
160
174
 
161
175
  def __init__(self) -> None:
162
176
 
@@ -178,7 +192,6 @@ class ZMQPoll:
178
192
  self._handlers: dict[T_key, Callable[[ZMQPollSocket, int], Any]] = {}
179
193
  self._count: dict[T_key, tuple[int, Callable[[], Any]] | None] = {}
180
194
  self._execute: deque[Pending] = deque[Pending[Any]]()
181
- self._not_started = False
182
195
  self.log = logging.LoggerAdapter(logging.getLogger())
183
196
  self._cancellers = deque()
184
197
  self._ctx_count = 0
@@ -235,7 +248,7 @@ class ZMQPoll:
235
248
  sock.recv()
236
249
 
237
250
  def do_execute() -> None:
238
- """Execute pending items added by the `execute` and `execute_async` methods."""
251
+ """Execute pending items added by the `execute` and `aexecute` methods."""
239
252
  while execute:
240
253
  md = (pen := execute.popleft()).metadata
241
254
  try:
@@ -244,15 +257,16 @@ class ZMQPoll:
244
257
  pen.set_exception(e)
245
258
  del pen
246
259
 
247
- send = context.socket(zmq.SocketType.PAIR)
248
- wake = context.socket(zmq.SocketType.PAIR)
260
+ send: ZMQPollSocket = context.socket(zmq.SocketType.PAIR) # pyright: ignore[reportAssignmentType]
261
+ wake: ZMQPollSocket = context.socket(zmq.SocketType.PAIR) # pyright: ignore[reportAssignmentType]
249
262
  addr = "inproc://async_kernel_zmq_poller_wake"
250
263
  sockets = None
251
264
  handlers[(wake, zmq.POLLIN)] = on_wake
252
265
 
253
266
  with context, wake, send, wake.bind(addr), send.connect(addr):
267
+ k: T_key
254
268
  c: tuple[int, Callable] | None
255
- started.set_result(send) # pyright: ignore[reportArgumentType]
269
+ started.set_result(send)
256
270
  # The main loop polls the handler keys for events in a loop.
257
271
  # It will block until an event occurs.
258
272
  try:
@@ -263,9 +277,9 @@ class ZMQPoll:
263
277
  do_execute()
264
278
  continue
265
279
  try:
266
- for k in zmq_poll(sockets, timeout=-1):
280
+ for k in _zmq_poll(sockets, timeout=-1): # pyright: ignore[reportAssignmentType]
267
281
  try:
268
- handlers[k](*k) # pyright: ignore[reportArgumentType]
282
+ handlers[k](*k)
269
283
  except KeyError:
270
284
  sockets = None
271
285
  except SystemExit:
@@ -309,14 +323,16 @@ class ZMQPoll:
309
323
  send = started.wait_sync()
310
324
 
311
325
  def _wake(sock=send, lock=send.lock) -> None:
312
- with lock:
313
- sock.send(b"")
326
+ lock.acquire()
327
+ sock.send(b"")
328
+ lock.release()
314
329
 
315
330
  self._wake = _wake
316
331
  self.stopped.add_done_callback(lambda _: (self := ref()) and self._on_stopped())
317
332
  self.log.debug("ZMQPoll event loop started")
318
333
 
319
334
  def validate_socket(self, sock: ZMQPollSocket | Any) -> ZMQPollSocket:
335
+ """Check `sock` is correctly registered."""
320
336
  if sock not in self.sockets:
321
337
  msg = f"Invalid socket detected! {sock=}"
322
338
  raise ValueError(msg)
@@ -334,7 +350,7 @@ class ZMQPoll:
334
350
  return self.validate_socket(self.execute(self._zmq_context.socket, socket_type))
335
351
 
336
352
  def execute(self, func: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
337
- """Execute `func` in the thread waiting for the result synchronously."""
353
+ """Execute `func` in the 'zmq_poll' thread waiting for the result synchronously."""
338
354
  if hasattr(self, "thread"):
339
355
  if threading.current_thread() is self.thread:
340
356
  return func(*args, **kwargs)
@@ -346,17 +362,17 @@ class ZMQPoll:
346
362
  finally:
347
363
  pen.metadata.clear()
348
364
  del pen
349
- msg = f"Unable to execute {func=} in {self}. Execution is only support while in context."
365
+ msg = f"Unable to execute {func=} in {self}. Execution is only supported while in context."
350
366
  raise RuntimeError(msg)
351
367
 
352
- async def execute_async(self, func: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
353
- """Execute `func` in the thread waiting for the result asynchronously."""
368
+ async def aexecute(self, func: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs) -> T:
369
+ """Execute `func` in the 'zmq_poll' thread waiting for the result asynchronously."""
354
370
  if hasattr(self, "thread"):
355
371
  self._execute.append(pen := Pending[T](func=func, args=args, kwargs=kwargs))
356
372
  if not self.stopped.done():
357
373
  self._wake()
358
374
  return await pen
359
- msg = f"Unable to execute {func=} in {self}. Execution is only support while in context."
375
+ msg = f"Unable to execute {func=} in {self}. Execution is only supported while in context."
360
376
  raise RuntimeError(msg)
361
377
 
362
378
  @contextmanager
@@ -370,14 +386,12 @@ class ZMQPoll:
370
386
  count: tuple[int, Callable[[], Any]] | None = None,
371
387
  canceller: Callable[[], Any] | NoValue | None = NoValue,
372
388
  ) -> Generator[None, Any, None]:
373
- """A context manager where `handler` is called with the event number when it occurs for `sock`.
389
+ """A context manager where `handler` is called in the 'zmq_poll' thread with the event number when it occurs for `sock`.
374
390
 
375
- Only one `handler` is allowed per `(socket, flags)` combination. If this context manager is entered
376
- inside a caller managed task (call_soon, etc) and the ZMQPoll is stopped, the associated pending
377
- will be cancelled.
391
+ Only one `handler` is allowed per `(socket, flags)` combination.
378
392
 
379
393
  Args:
380
- sock: A zmq socket or a IO style object with a `fileno`.
394
+ sock: A registered [ZMQPollSocket][].
381
395
  handler: A handler to handle the event. The handler is called inside the
382
396
  zmq_poll thread. Thread-safe primitives must be used by the handler such
383
397
  as [async_kernel.caller.Caller.call_soon][],[async_kernel.caller.Caller.queue_call][], etc.
@@ -388,12 +402,16 @@ class ZMQPoll:
388
402
  The callback could be an `event.set` to release the context.
389
403
  canceller: A callback to use on the event the poll is stopped. The default cancellation
390
404
  behavior is to cancel the pending returned by [async_kernel.caller.Caller.current_pending][].
391
- Set to None to disable cancellation support. This is fine when this context manager is inside
392
- the context of this object.
405
+ Set to `None` to disable cancellation support. This is safe when this context manager is
406
+ inside the context of this instance.
407
+
408
+ Raises:
409
+ BusyResourceError: If the `(sock, flags)` combination is already in use.
410
+ RuntimeError: If the default canceller can not be created.
393
411
 
394
412
  Tip:
395
- The handler is called inside a dedicated thread which may have been marked using
396
- [async_kernel.utils.mark_thread_pydev_do_not_trace][] which disables debug breakpoints.
413
+ The zmq_poll thread normally disables debugging in the zmq_poll thread so inserting breakpoints
414
+ in the event handler may interfere with debugging.
397
415
  """
398
416
  assert not self.stopped.done()
399
417
  if canceller is NoValue:
@@ -12,7 +12,7 @@ import weakref
12
12
  from typing import TYPE_CHECKING, Any, Generic, Literal, Self, final
13
13
 
14
14
  from aiologic import BinarySemaphore
15
- from aiologic.lowlevel import AsyncLibraryNotFoundError, current_async_library
15
+ from aiologic.lowlevel import AsyncLibraryNotFoundError, async_checkpoint, current_async_library
16
16
  from traitlets import import_item, traitlets
17
17
  from traitlets.config import Config, Configurable
18
18
  from traitlets.config.application import Application, ClassesType
@@ -184,9 +184,6 @@ class Interface(StartStopTask, Application, Generic[T_shell_co]):
184
184
  _connections: tuple[Connection[Self], ...] = ()
185
185
  """The connections to the interface for messaging."""
186
186
 
187
- iopub_send_first_connection_only = traitlets.Bool(False).tag(config=True)
188
- """Send iopub messages on the first connection only."""
189
-
190
187
  _connections_lock = Fixed(BinarySemaphore)
191
188
 
192
189
  shell: Fixed[Self, T_shell_co] = Fixed(lambda c: c["owner"].kernel.main_shell)
@@ -226,10 +223,6 @@ class Interface(StartStopTask, Application, Generic[T_shell_co]):
226
223
 
227
224
  return IPShell
228
225
 
229
- @traitlets.default("iopub_send_first_connection_only")
230
- def _default_iopub_send_first_connection_only(self) -> bool:
231
- return sys.platform == "emscripten"
232
-
233
226
  @classmethod
234
227
  @override
235
228
  def initialized(cls) -> bool:
@@ -402,19 +395,22 @@ class Interface(StartStopTask, Application, Generic[T_shell_co]):
402
395
  await self._pre_start()
403
396
  self.log.info("Interface started: %s", self.summary)
404
397
  started()
405
- del started
398
+ # Allow connections to react to being started.
399
+ await async_checkpoint(force=True)
400
+ # Send iopub messages captured during startup.
401
+ del started, self.iopub_send
402
+ while self._iopub_cache:
403
+ self._iopub_cache.reverse()
404
+ args, kwargs = self._iopub_cache.pop()
405
+ self.iopub_send(*args, **kwargs)
406
406
  await stop
407
407
 
408
408
  async def _pre_start(self) -> None:
409
- """Opens the kernel, starts autostart connections and releases iopub messages."""
409
+ """Perform tasks just prior to setting as started."""
410
+ # Autostart connections waiting until they have started.
410
411
  if pending := [import_item(pth)().start().started for pth in self.autostart_connections]:
411
412
  self.log.info("Waiting for connections to establish %d", len(pending))
412
- await self.caller.wait(pending)
413
- del self.iopub_send
414
- while self._iopub_cache:
415
- self._iopub_cache.reverse()
416
- args, kwargs = self._iopub_cache.pop()
417
- self.iopub_send(*args, **kwargs)
413
+ await self.caller.wait(pending)
418
414
 
419
415
  def update_connections(self, *new: Connection[Self]) -> None:
420
416
  """Update the list of connections.
@@ -490,8 +486,6 @@ class Interface(StartStopTask, Application, Generic[T_shell_co]):
490
486
  for c in self._connections:
491
487
  try:
492
488
  c.iopub_send(msg_type, content, metadata=metadata, parent=parent, ident=ident, buffers=buffers)
493
- if self.iopub_send_first_connection_only:
494
- break
495
489
  except Exception as e:
496
490
  self.log.exception("iopub_send failed for connection %r", c, exc_info=e)
497
491
 
@@ -79,38 +79,39 @@ def create_interface_messge_callback_handler(
79
79
  pack_unpack: tuple[Callable[[Message], T], Callable[[T], Message]] = (pack_json_str, unpack_json),
80
80
  ) -> Callable[[T, BuffersType], None]:
81
81
  ""
82
- cache: dict[str, Connection] = {}
82
+ conn: Connection | None = None
83
83
  lock = BinarySemaphore()
84
84
  session_calls = set()
85
85
  pack, unpack = pack_unpack
86
86
 
87
87
  def handle_msg(packed_msg: T, buffers: BuffersType | None = None) -> None:
88
88
  """Handle a packed message."""
89
+ nonlocal conn
89
90
  msg: Message = unpack(packed_msg)
90
91
  msg["buffers"] = [] if buffers is None else buffers
91
92
  session: str = msg["header"]["session"]
92
93
  session_calls.add(session)
93
- conn: Connection | None
94
94
 
95
- if (conn := cache.get(session)) is None or conn.stopping.done():
95
+ if conn is None:
96
96
  with lock:
97
- if (conn := cache.get(session)) is None or conn.stopping.done():
97
+ if conn is None:
98
98
  conn = Connection(interface.caller, session_id=session)
99
99
 
100
- def transmit_msg(msg: Message, ident: list[bytes]) -> None:
100
+ def transmit_msg(msg: Message, ident: list[bytes], conn=conn) -> None:
101
101
  """Pack and send a message."""
102
- # `ident` is not sent.
102
+ # Jupyterlite
103
+ if parent := msg["parent_header"]:
104
+ msg["header"]["session"] = parent["session"]
105
+
103
106
  buffers: BuffersType = msg.pop("buffers") # pyright: ignore[reportAssignmentType]
104
107
  reply = send(pack(msg), buffers, blocking_reply := msg["channel"] == Channel.stdin)
105
108
  if blocking_reply:
106
- conn.handle_incoming_msg(unpack(reply), [conn.bsession])
109
+ conn.handle_incoming_msg(unpack(reply), [])
107
110
 
108
111
  conn.transmit_msg = transmit_msg
109
112
  conn.stopped.add_done_callback(lambda _: delattr(conn, "transmit_msg"))
110
113
  conn.start()
111
- conn.stopping.add_done_callback(lambda _: cache.pop(session))
112
114
 
113
- cache[session] = conn
114
- conn.handle_incoming_msg(msg, [conn.bsession])
115
+ conn.handle_incoming_msg(msg, [])
115
116
 
116
117
  return handle_msg
@@ -74,9 +74,6 @@ class BaseMessage(StartStopTask, LoggingConfigurable, MessageProtocol):
74
74
  session_id: Fixed[Self, str] = Fixed(lambda c: c["owner"]._session_id)
75
75
  """Used to identify this object as the `session` in a message header."""
76
76
 
77
- bsession: Fixed[Self, bytes] = Fixed[Self, bytes](lambda c: c["owner"].session_id.encode())
78
- """Used to identfiy this object as the origin of a message."""
79
-
80
77
  _pending_messages: Fixed[Self, dict[str, PendingMessage[Any]]] = Fixed(dict)
81
78
  """A mapping of the `msg_id` of message requests to the pending that is resolved with a reply."""
82
79
 
@@ -201,6 +198,13 @@ class Connection(HasInterface[T_interface_co], BaseMessage, Generic[T_interface_
201
198
 
202
199
  @override
203
200
  def handle_incoming_msg(self, msg: Message, ident: list[bytes]) -> None:
201
+ """The handler for messages received on this connection.
202
+
203
+ Args:
204
+ msg: A new message.
205
+ ident: A list of bytes to route a reply message back to the origin. This can
206
+ be an empty list when there is only one connection, such as [LocalClient][].
207
+ """
204
208
  if msg["header"]["msg_type"].endswith("_reply"):
205
209
  self.handle_reply(msg)
206
210
  else:
@@ -158,8 +158,8 @@ class ZMQConnection(ZMQMessage, Connection[T_interface_co], Generic[T_interface_
158
158
  msg = socket.recv()
159
159
  if msg[0] == 1:
160
160
  ident = msg[1:]
161
- # Note: The welcome message is cached until parent._started is called.
162
- self.parent.iopub_send(MsgType.iopub_welcome, content={"subscription": ident.decode()}, ident=ident)
161
+ msg = self.msg(MsgType.iopub_welcome, {"subscription": ident.decode()}, Channel.iopub)
162
+ self.session.send(socket, msg, ident=[ident]) # pyright: ignore[reportArgumentType]
163
163
 
164
164
  def handler(sock, event, channel: Channel, recv=self.session.recv, handle_msg=self.handle_incoming_msg) -> None:
165
165
  # Thread: zmq_poll_thread
@@ -169,6 +169,9 @@ class ZMQConnection(ZMQMessage, Connection[T_interface_co], Generic[T_interface_
169
169
 
170
170
  with self.zmq_poll as zpoll:
171
171
  await self._bind_sockets()
172
+ self.parent.update_connections(self)
173
+ started()
174
+ await self.parent.started
172
175
  with (
173
176
  zpoll.event_handler(self._sockets[Channel.control], partial(handler, channel=Channel.control)),
174
177
  zpoll.event_handler(self._sockets[Channel.shell], functools.partial(handler, channel=Channel.shell)),
@@ -176,7 +179,8 @@ class ZMQConnection(ZMQMessage, Connection[T_interface_co], Generic[T_interface_
176
179
  zpoll.event_handler(self._sockets[Channel.heartbeat], heartbeat_handler),
177
180
  zpoll.event_handler(self._sockets[Channel.iopub], iopub_reg_handler),
178
181
  ):
179
- await super().connection_task(started, stop)
182
+ await stop
183
+ self.parent.update_connections()
180
184
 
181
185
  async def _bind_sockets(self):
182
186
  """Create, configure and bind all sockets."""
@@ -211,7 +215,7 @@ class ZMQConnection(ZMQMessage, Connection[T_interface_co], Generic[T_interface_
211
215
  self._sockets[channel] = socket
212
216
  socket.bind(addr)
213
217
 
214
- await self.zmq_poll.execute_async(bind_sockets)
218
+ await self.zmq_poll.aexecute(bind_sockets)
215
219
 
216
220
  @override
217
221
  def connection_info(self) -> str:
@@ -277,7 +281,7 @@ class ZMQClient(BaseClient[T_interface_co], ZMQMessage, Generic[T_interface_co])
277
281
  self._sockets[channel] = socket
278
282
  return socket
279
283
 
280
- return await self.zmq_poll.execute_async(open_socket)
284
+ return await self.zmq_poll.aexecute(open_socket)
281
285
 
282
286
  async def _establish_connection(self, timeout: float | None) -> None:
283
287
  # Wait for welcome
@@ -395,7 +399,7 @@ class ZMQClient(BaseClient[T_interface_co], ZMQMessage, Generic[T_interface_co])
395
399
  @asynccontextmanager
396
400
  @override
397
401
  async def iopub_subscribe(
398
- self, topic=b"", *, timeout: float | None = 1.0
402
+ self, topic=b"", *, timeout: float | None = 10.0
399
403
  ) -> AsyncGenerator[SingleAsyncQueue[Message]]:
400
404
 
401
405
  def forward_messages(sock: ZMQPollSocket, event: int) -> None:
@@ -414,10 +418,11 @@ class ZMQClient(BaseClient[T_interface_co], ZMQMessage, Generic[T_interface_co])
414
418
  iopub = await self._connect_socket(Channel.iopub)
415
419
  with iopub, self.zmq_poll.event_handler(iopub, forward_messages, canceller=canceller), scope:
416
420
  iopub.subscribe(topic)
417
- self.log.debug("Waiting for welcome message.")
418
- if await ready.with_(timeout=timeout):
419
- self.log.debug("Welcome message received.")
420
- else:
421
- msg = f"Welcome message not received after {timeout:0.1f}s!"
422
- raise TimeoutError(msg)
421
+ if timeout is not None:
422
+ self.log.debug("Waiting for welcome message.")
423
+ if await ready.with_(timeout=timeout):
424
+ self.log.debug("Welcome message received.")
425
+ else:
426
+ msg = f"Welcome message not received after {timeout:0.1f}s!"
427
+ raise TimeoutError(msg)
423
428
  yield queue
@@ -35,9 +35,7 @@ async def test_start_kernel_callable_interface(anyio_backend: Backend):
35
35
  messages.append(msg)
36
36
  return None
37
37
 
38
- callable_interface = await start_kernel_callable_interface(
39
- send=from_interface, stopped=stopped.set, settings={"Interface.iopub_send_first_connection_only": True}
40
- )
38
+ callable_interface = await start_kernel_callable_interface(send=from_interface, stopped=stopped.set)
41
39
  interface = Interface.instance()
42
40
  async with LocalClient().start() as client:
43
41
  await client.kernel_info()
@@ -158,15 +158,15 @@ class Test_zmq_Poll:
158
158
 
159
159
  async def test_poll_execute_states(self, caller: Caller):
160
160
  zmq_poll = ZMQPoll()
161
- match = "Execution is only support while in context"
161
+ match = "Unable to execute"
162
162
  # Pre-running
163
163
  with pytest.raises(RuntimeError, match=match):
164
164
  zmq_poll.execute(lambda: 1 + 1)
165
165
  with pytest.raises(RuntimeError, match=match):
166
- await zmq_poll.execute_async(lambda: 1 + 1)
166
+ await zmq_poll.aexecute(lambda: 1 + 1)
167
167
  # Running
168
168
  with zmq_poll:
169
- assert (await zmq_poll.execute_async(lambda: 1 + 1)) == 2
169
+ assert (await zmq_poll.aexecute(lambda: 1 + 1)) == 2
170
170
  assert (zmq_poll.execute(lambda: 1 + 1)) == 2
171
171
  with pytest.raises(RuntimeError, match="stopped"), zmq_poll:
172
172
  None # noqa: B018 # pyright: ignore[reportUnusedExpression]
@@ -174,11 +174,11 @@ class Test_zmq_Poll:
174
174
  with pytest.raises(RuntimeError, match=match):
175
175
  assert zmq_poll.execute(threading.current_thread) is zmq_poll.thread
176
176
  with pytest.raises(RuntimeError, match=match):
177
- assert await zmq_poll.execute_async(threading.current_thread) is zmq_poll.thread
177
+ assert await zmq_poll.aexecute(threading.current_thread) is zmq_poll.thread
178
178
 
179
179
  async def test_poll_socket_states(self, caller: Caller):
180
180
  zmq_poll = ZMQPoll()
181
- match = "Execution is only support while in context"
181
+ match = "Unable to execute"
182
182
  # Pre-running
183
183
  with pytest.raises(RuntimeError, match=match):
184
184
  zmq_poll.socket(zmq.SocketType.DEALER)
@@ -4,6 +4,7 @@ import importlib.util
4
4
  import os
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ import anyio
7
8
  import pytest
8
9
 
9
10
  import async_kernel.utils
@@ -66,8 +67,13 @@ async def read_until_msg_type(reader: AsyncGenerator, msg_type: IOPubMsgTypeAlia
66
67
  Args:
67
68
  reader: An async iterator for a queue returned from iopub subscribe.
68
69
  msg_type: The type of iopub message to wait for."""
69
- while (msg := await anext(reader))["header"]["msg_type"] != msg_type:
70
- continue
70
+ try:
71
+ with anyio.fail_after(TIMEOUT):
72
+ while (msg := await anext(reader))["header"]["msg_type"] != msg_type:
73
+ continue
74
+ except TimeoutError as e:
75
+ msg_ = f"Timeout waiting for {msg_type=} {content_checks=}"
76
+ raise TimeoutError(msg_) from e
71
77
  check_pub_message(msg, msg_type=msg_type, **content_checks)
72
78
  return msg
73
79
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes