async-kernel 0.20.2__tar.gz → 0.21.0__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 (117) hide show
  1. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/ci.yml +6 -6
  2. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/new_release.yml +1 -1
  3. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/publish-docs.yml +1 -1
  4. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/publish-to-pypi.yml +1 -1
  5. {async_kernel-0.20.2 → async_kernel-0.21.0}/.pre-commit-config.yaml +4 -4
  6. {async_kernel-0.20.2 → async_kernel-0.21.0}/CHANGELOG.md +36 -1
  7. {async_kernel-0.20.2 → async_kernel-0.21.0}/PKG-INFO +1 -1
  8. {async_kernel-0.20.2 → async_kernel-0.21.0}/_version.py +2 -2
  9. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/event_loop.md +2 -0
  10. {async_kernel-0.20.2 → async_kernel-0.21.0}/pyproject.toml +2 -2
  11. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/caller.py +114 -112
  12. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/command.py +3 -3
  13. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/common.py +0 -1
  14. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/debugger.py +12 -23
  15. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/event_loop/run.py +11 -7
  16. async_kernel-0.21.0/src/async_kernel/event_loop/zmq_poll.py +228 -0
  17. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/interface/base.py +45 -41
  18. async_kernel-0.21.0/src/async_kernel/interface/zmq.py +282 -0
  19. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/kernel.py +17 -81
  20. async_kernel-0.21.0/src/async_kernel/outstream.py +102 -0
  21. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/pending.py +25 -8
  22. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/shell/ipshell.py +2 -0
  23. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/typing.py +9 -7
  24. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/utils.py +2 -2
  25. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/conftest.py +29 -30
  26. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_base_interface.py +4 -2
  27. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_callable_interface.py +4 -3
  28. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_caller.py +44 -35
  29. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_command.py +19 -12
  30. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_event_loop.py +7 -0
  31. async_kernel-0.21.0/tests/test_eventloop_zmq_poll.py +137 -0
  32. async_kernel-0.20.2/tests/test_iostream.py → async_kernel-0.21.0/tests/test_outstream.py +1 -1
  33. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_zmq_interface.py +47 -43
  34. async_kernel-0.21.0/tests/test_zmq_interface_ip.py +61 -0
  35. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/utils.py +2 -2
  36. {async_kernel-0.20.2 → async_kernel-0.21.0}/uv.lock +554 -557
  37. async_kernel-0.20.2/src/async_kernel/interface/zmq.py +0 -346
  38. async_kernel-0.20.2/tests/test_zmq_interface_ip.py +0 -31
  39. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/dependabot.yaml +0 -0
  40. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/release.yml +0 -0
  41. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/enforce-label.yml +0 -0
  42. {async_kernel-0.20.2 → async_kernel-0.21.0}/.github/workflows/pre-commit.yml +0 -0
  43. {async_kernel-0.20.2 → async_kernel-0.21.0}/.gitignore +0 -0
  44. {async_kernel-0.20.2 → async_kernel-0.21.0}/.vscode/launch.json +0 -0
  45. {async_kernel-0.20.2 → async_kernel-0.21.0}/.vscode/settings.json +0 -0
  46. {async_kernel-0.20.2 → async_kernel-0.21.0}/.vscode/spellright.dict +0 -0
  47. {async_kernel-0.20.2 → async_kernel-0.21.0}/CONTRIBUTING.md +0 -0
  48. {async_kernel-0.20.2 → async_kernel-0.21.0}/IPYTHON_LICENSE +0 -0
  49. {async_kernel-0.20.2 → async_kernel-0.21.0}/LICENSE +0 -0
  50. {async_kernel-0.20.2 → async_kernel-0.21.0}/README.md +0 -0
  51. {async_kernel-0.20.2 → async_kernel-0.21.0}/cliff.toml +0 -0
  52. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/about/changelog.md +0 -0
  53. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/about/contributing.md +0 -0
  54. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/about/index.md +0 -0
  55. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/about/license.md +0 -0
  56. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/index.md +0 -0
  57. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/javascripts/extra.js +0 -0
  58. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/notebooks/caller.ipynb +0 -0
  59. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/notebooks/concurrency.ipynb +0 -0
  60. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/notebooks/custom_kernel.ipynb +0 -0
  61. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/overrides/main.html +0 -0
  62. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/caller.md +0 -0
  63. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/comm.md +0 -0
  64. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/command.md +0 -0
  65. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/common.md +0 -0
  66. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/debugger.md +0 -0
  67. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/index.md +0 -0
  68. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/interface.md +0 -0
  69. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/ipshell.md +0 -0
  70. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/kernel.md +0 -0
  71. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/kernelspec.md +0 -0
  72. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/pending.md +0 -0
  73. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/shell.md +0 -0
  74. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/typing.md +0 -0
  75. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/reference/utils.md +0 -0
  76. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/stylesheets/extra.css +0 -0
  77. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/thread_safety.md +0 -0
  78. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/usage/commands.md +0 -0
  79. {async_kernel-0.20.2 → async_kernel-0.21.0}/docs/usage/index.md +0 -0
  80. {async_kernel-0.20.2 → async_kernel-0.21.0}/hatch_build.py +0 -0
  81. {async_kernel-0.20.2 → async_kernel-0.21.0}/mkdocs.yml +0 -0
  82. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/__init__.py +0 -0
  83. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/__main__.py +0 -0
  84. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/comm.py +0 -0
  85. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/compat/attr_docs.py +0 -0
  86. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/compat/json.py +0 -0
  87. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/compiler.py +0 -0
  88. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/event_loop/__init__.py +0 -0
  89. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/event_loop/asyncio_guest.py +0 -0
  90. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/event_loop/qt_host.py +0 -0
  91. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/event_loop/tk_host.py +0 -0
  92. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/interface/__init__.py +0 -0
  93. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/interface/callable.py +0 -0
  94. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/interface/ip_app.py +0 -0
  95. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/kernelspec.py +0 -0
  96. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/py.typed +0 -0
  97. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/resources/logo-32x32.png +0 -0
  98. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/resources/logo-64x64.png +0 -0
  99. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/resources/logo-svg.svg +0 -0
  100. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/shell/__init__.py +0 -0
  101. {async_kernel-0.20.2 → async_kernel-0.21.0}/src/async_kernel/shell/base.py +0 -0
  102. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/__init__.py +0 -0
  103. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/references.py +0 -0
  104. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_comm.py +0 -0
  105. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_common.py +0 -0
  106. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_compat.py +0 -0
  107. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_debugger.py +0 -0
  108. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_debugger_static.py +0 -0
  109. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_encryption.py +0 -0
  110. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_enter_kernel.py +0 -0
  111. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_kernel_ipshell.py +0 -0
  112. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_kernelspec.py +0 -0
  113. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_message_spec.py +0 -0
  114. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_pending.py +0 -0
  115. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_subclass.py +0 -0
  116. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_typing.py +0 -0
  117. {async_kernel-0.20.2 → async_kernel-0.21.0}/tests/test_utils.py +0 -0
@@ -20,12 +20,12 @@ jobs:
20
20
  fetch-depth: 0
21
21
 
22
22
  - name: Install uv
23
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
23
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
24
24
 
25
25
  - name: Type checking with basedpyright
26
26
  run: |
27
27
  uv sync
28
- uvx basedpyright@1.39.8
28
+ uvx basedpyright@1.39.9
29
29
 
30
30
  test:
31
31
  needs: type-checking
@@ -45,7 +45,7 @@ jobs:
45
45
  uses: actions/checkout@v7
46
46
 
47
47
  - name: Install uv
48
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
48
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
49
49
  with:
50
50
  version-file: "pyproject.toml"
51
51
  python-version: ${{ matrix.python-version }}
@@ -71,7 +71,7 @@ jobs:
71
71
  if: ${{runner.debug == '1'}}
72
72
  timeout-minutes: 3
73
73
  run: |
74
- uv run pytest -vvl --override-ini=log_cli_level=DEBUG --override-ini=log_cli=true
74
+ uv run pytest -vvl --override-ini=log_cli_level=DEBUG
75
75
 
76
76
  - name: Run tests
77
77
  if: ${{runner.debug != '1'}}
@@ -87,7 +87,7 @@ jobs:
87
87
  fetch-depth: 0
88
88
 
89
89
  - name: Install uv
90
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
90
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
91
91
  with:
92
92
  version-file: "pyproject.toml"
93
93
  python-version: "3.13"
@@ -116,7 +116,7 @@ jobs:
116
116
  fetch-depth: 0
117
117
 
118
118
  - name: Install uv
119
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
119
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
120
120
  with:
121
121
  version-file: "pyproject.toml"
122
122
 
@@ -31,7 +31,7 @@ jobs:
31
31
  fetch-depth: 0
32
32
  ref: main
33
33
  - name: Install uv
34
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
34
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
35
35
  with:
36
36
  version-file: "pyproject.toml"
37
37
 
@@ -27,7 +27,7 @@ jobs:
27
27
  git config user.email 41898282+github-actions[bot]@users.noreply.github.com
28
28
 
29
29
  - name: Install uv
30
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
+ uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
31
31
  with:
32
32
  version-file: "pyproject.toml"
33
33
 
@@ -31,7 +31,7 @@ jobs:
31
31
  # Fetch full history for setuptools-scm
32
32
  fetch-depth: 0
33
33
  ref: main
34
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
34
+ - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
35
35
  with:
36
36
  version-file: "pyproject.toml"
37
37
 
@@ -28,12 +28,12 @@ repos:
28
28
  - id: check-json5
29
29
 
30
30
  - repo: https://github.com/python-jsonschema/check-jsonschema
31
- rev: 0.37.3
31
+ rev: 0.37.4
32
32
  hooks:
33
33
  - id: check-github-workflows
34
34
 
35
35
  - repo: https://github.com/rbubley/mirrors-prettier
36
- rev: v3.8.4
36
+ rev: v3.9.5
37
37
  hooks:
38
38
  - id: prettier
39
39
  types_or: [yaml, html, json]
@@ -48,7 +48,7 @@ repos:
48
48
  additional_dependencies: [black==23.7.0]
49
49
 
50
50
  - repo: https://github.com/codespell-project/codespell
51
- rev: v2.4.2
51
+ rev: v2.4.3
52
52
  hooks:
53
53
  - id: codespell
54
54
  args: ["-L", "sur,nd"]
@@ -59,7 +59,7 @@ repos:
59
59
  - id: python-use-type-annotations
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: v0.15.19
62
+ rev: v0.15.22
63
63
  hooks:
64
64
  - id: ruff-check
65
65
  types_or: [python, jupyter]
@@ -5,7 +5,39 @@ 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.20.2] - 2026-06-29
8
+ ## [0.21.0] - 2026-07-18
9
+
10
+ ### <!-- 1 --> 🚀 Features
11
+
12
+ - Add print_concat to concatenate print statements into a single string when 'file' is not specified. [#522](https://github.com/fleming79/async-kernel/pull/522)
13
+
14
+ - Added PollZMQ - a thread based zmq poller callback registry. [#515](https://github.com/fleming79/async-kernel/pull/515)
15
+
16
+ - Support loop_factory as an importable path. [#514](https://github.com/fleming79/async-kernel/pull/514)
17
+
18
+ ### <!-- 6 --> 🌀 Miscellaneous
19
+
20
+ - Poll maintenance [#529](https://github.com/fleming79/async-kernel/pull/529)
21
+
22
+ - Add threadsafe option to Poll.socket [#528](https://github.com/fleming79/async-kernel/pull/528)
23
+
24
+ - Update lock files and pre-commit and basedpyright. [#527](https://github.com/fleming79/async-kernel/pull/527)
25
+
26
+ - Kernel updates - handle external kernel interrupt. [#526](https://github.com/fleming79/async-kernel/pull/526)
27
+
28
+ - Add docstrings for Poll [#525](https://github.com/fleming79/async-kernel/pull/525)
29
+
30
+ - Bump astral-sh/setup-uv from 8.2.0 to 8.3.0 in the actions group [#518](https://github.com/fleming79/async-kernel/pull/518)
31
+
32
+ - Change PollZMQ to Poll and move to event_loop/zmq_poll.py. [#524](https://github.com/fleming79/async-kernel/pull/524)
33
+
34
+ - Add more log messages when running pytest from the command line. [#523](https://github.com/fleming79/async-kernel/pull/523)
35
+
36
+ - Move OutStream from kernel.py to outstream.py [#521](https://github.com/fleming79/async-kernel/pull/521)
37
+
38
+ - Refactor Caller. [#520](https://github.com/fleming79/async-kernel/pull/520)
39
+
40
+ ## [0.20.2] - 2026-06-30
9
41
 
10
42
  ### <!-- 5 --> 📝 Documentation
11
43
 
@@ -13,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
45
 
14
46
  ### <!-- 6 --> 🌀 Miscellaneous
15
47
 
48
+ - Prepare for release v0.20.2 [#512](https://github.com/fleming79/async-kernel/pull/512)
49
+
16
50
  - Avoid attribute error in Comm.publish_msg in case the parent is gc before the object. [#511](https://github.com/fleming79/async-kernel/pull/511)
17
51
 
18
52
  - Update uv.lock and pre-commit-config.yaml [#510](https://github.com/fleming79/async-kernel/pull/510)
@@ -1419,6 +1453,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1419
1453
 
1420
1454
  - Bump the actions group across 1 directory with 2 updates [#3](https://github.com/fleming79/async-kernel/pull/3)
1421
1455
 
1456
+ [0.21.0]: https://github.com/fleming79/async-kernel/compare/v0.20.2..v0.21.0
1422
1457
  [0.20.2]: https://github.com/fleming79/async-kernel/compare/v0.20.1..v0.20.2
1423
1458
  [0.20.1]: https://github.com/fleming79/async-kernel/compare/v0.20.0..v0.20.1
1424
1459
  [0.20.0]: https://github.com/fleming79/async-kernel/compare/v0.19.2..v0.20.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: async-kernel
3
- Version: 0.20.2
3
+ Version: 0.21.0
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.20.2'
22
- __version_tuple__ = version_tuple = (0, 20, 2)
21
+ __version__ = version = '0.21.0'
22
+ __version_tuple__ = version_tuple = (0, 21, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -7,3 +7,5 @@ show_root_heading: true
7
7
  ::: async_kernel.event_loop.qt_host.QtHost
8
8
  options:
9
9
  show_root_heading: true
10
+
11
+ ::: async_kernel.event_loop.zmq_poll
@@ -85,7 +85,7 @@ test = [
85
85
  "trio>=0.31.0",
86
86
  ]
87
87
  uvloop = ["uvloop; python_version <= '3.13' and sys_platform != 'win32' and implementation_name == 'cpython'",
88
- "winloop; sys_platform == 'win32' and implementation_name == 'cpython'"
88
+ "winloop; sys_platform == 'win32' and implementation_name == 'cpython' and python_version <= '3.13'"
89
89
  ]
90
90
  dev = [
91
91
  "matplotlib>=3.10.9; python_version < '3.15'",
@@ -248,7 +248,7 @@ reportUnusedParameter = false
248
248
  reportUnknownLambdaType = false
249
249
 
250
250
  [tool.uv]
251
- required-version = "==0.11.16"
251
+ required-version = "==0.11.29"
252
252
 
253
253
  [tool.uv.sources]
254
254
  jupyter-client = { git = "https://github.com/jupyter/jupyter_client.git" }
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import atexit
4
5
  import contextlib
5
6
  import contextvars
6
7
  import inspect
@@ -18,12 +19,13 @@ from types import CoroutineType
18
19
  from typing import TYPE_CHECKING, Any, ClassVar, Literal, Self, Unpack, final
19
20
 
20
21
  import anyio
21
- from aiologic import BinarySemaphore, Event
22
+ from aiologic import BinarySemaphore, CountdownEvent
22
23
  from aiologic.lowlevel import create_async_event, current_async_library
23
24
  from aiologic.meta import await_for, iscoroutinelike
24
25
  from typing_extensions import override
25
26
  from wrapt import lazy_import
26
27
 
28
+ import async_kernel.event_loop
27
29
  from async_kernel import utils
28
30
  from async_kernel.common import Fixed, KernelInterrupt, SingleAsyncQueue
29
31
  from async_kernel.event_loop.run import Host, get_start_guest_run
@@ -41,8 +43,6 @@ if TYPE_CHECKING:
41
43
  from types import CoroutineType
42
44
 
43
45
  import trio # noqa: TC004
44
- import zmq
45
- from aiologic.lowlevel import AsyncEvent
46
46
 
47
47
  from async_kernel.typing import P
48
48
 
@@ -195,12 +195,9 @@ class Caller(anyio.AsyncContextManagerMixin):
195
195
  CallerState.stopping: "🏁 stopping",
196
196
  CallerState.stopped: "🏁 stopped",
197
197
  }
198
- _zmq_context: zmq.Context[Any] | None = None
199
-
200
- _parent_ref: weakref.ref[Self] | None = None
201
198
 
202
199
  # Fixed
203
- _child_lock = Fixed(BinarySemaphore)
200
+ _inst_lock = Fixed(BinarySemaphore)
204
201
  _children: Fixed[Self, set[Self]] = Fixed(set)
205
202
  _tasks: Fixed[Self, set[asyncio.Task]] = Fixed(set)
206
203
  _worker_pool: Fixed[Self, deque[Self]] = Fixed(deque)
@@ -209,21 +206,18 @@ class Caller(anyio.AsyncContextManagerMixin):
209
206
  lambda c: SingleAsyncQueue(reject=c["owner"]._reject)
210
207
  )
211
208
  _guest_queues: Fixed[Self, dict[Backend, SingleAsyncQueue[Pending | tuple[Callable, tuple, dict]]]] = Fixed(dict)
212
- _guest_done_events: Fixed[Any, set[AsyncEvent]] = Fixed(set)
209
+ _guest_done_event: Fixed[Any, CountdownEvent] = Fixed(CountdownEvent)
213
210
  _stopping = Fixed(Pending[None])
214
211
  "A pending that is set done the first time stop is called."
215
212
 
216
- stopped = Fixed(Event)
217
- "An event that is set when the caller has stopped."
213
+ stopped = Fixed(Pending)
214
+ "A pending that is done when the caller is stopped."
218
215
 
216
+ _children_countdown: CountdownEvent | None = None
219
217
  _pending_var: contextvars.ContextVar[Pending | None] = contextvars.ContextVar("_pending_var", default=None)
220
218
 
221
219
  log: logging.Logger | logging.LoggerAdapter
222
220
  ""
223
- iopub_sockets: ClassVar[dict[int, zmq.Socket]] = {}
224
- ""
225
- iopub_url: ClassVar = "inproc://iopub"
226
- ""
227
221
 
228
222
  @property
229
223
  def name(self) -> str:
@@ -260,11 +254,6 @@ class Caller(anyio.AsyncContextManagerMixin):
260
254
  "Returns `True` if the caller is protected from stopping."
261
255
  return self._protected
262
256
 
263
- @property
264
- def zmq_context(self) -> zmq.Context | None:
265
- "A zmq socket, which if present indicates that an iopub socket is loaded."
266
- return self._zmq_context
267
-
268
257
  @property
269
258
  def running(self) -> bool:
270
259
  "Returns `True` when the caller is available to run requests."
@@ -288,8 +277,9 @@ class Caller(anyio.AsyncContextManagerMixin):
288
277
  @property
289
278
  def parent(self) -> Self | None:
290
279
  "The parent caller if it exists."
291
- if (ref := self._parent_ref) and (inst := ref()) and not inst.stopped:
292
- return inst
280
+ return self._parent_ref()
281
+
282
+ def _parent_ref(self) -> Self | None:
293
283
  return None
294
284
 
295
285
  def _get_info(self) -> dict[str, Any]:
@@ -334,7 +324,6 @@ class Caller(anyio.AsyncContextManagerMixin):
334
324
  - backend: The async backend to use.
335
325
  - backend_options: Options for the backend.
336
326
  - protected: Whether the Caller is protected.
337
- - zmq_context: ZeroMQ context.
338
327
  - log: Logger instance.
339
328
 
340
329
  Returns:
@@ -377,7 +366,6 @@ class Caller(anyio.AsyncContextManagerMixin):
377
366
  inst._backend_options = kwargs.get("backend_options")
378
367
  inst._host_options = kwargs.get("host_options")
379
368
  inst._protected = kwargs.get("protected", False)
380
- inst._zmq_context = kwargs.get("zmq_context")
381
369
  inst.log = kwargs.get("log") or logging.LoggerAdapter(logging.getLogger())
382
370
  if (sys.platform == "emscripten") and (caller_id is None):
383
371
  caller_id = id(inst)
@@ -390,6 +378,8 @@ class Caller(anyio.AsyncContextManagerMixin):
390
378
  inst.start_sync(no_debug=kwargs.get("no_debug", False))
391
379
  assert inst._caller_id
392
380
  assert inst._caller_id not in cls._instances
381
+ atexit.register(inst.stop, force=True)
382
+ inst._stopping.add_done_callback(lambda _: atexit.unregister(inst.stop))
393
383
  cls._instances[inst._caller_id] = inst
394
384
  return inst
395
385
 
@@ -405,6 +395,10 @@ class Caller(anyio.AsyncContextManagerMixin):
405
395
  self._state = CallerState.start_sync
406
396
 
407
397
  async def run_caller_in_context() -> None:
398
+ if self._state in [CallerState.stopping, CallerState.stopped]:
399
+ return
400
+ if not self._name:
401
+ self._name = self._thread.name
408
402
  if self._state is CallerState.start_sync:
409
403
  self._state = CallerState.initial
410
404
  if no_debug:
@@ -432,69 +426,74 @@ class Caller(anyio.AsyncContextManagerMixin):
432
426
  if not "shutdown" not in str(e):
433
427
  raise
434
428
 
435
- threading.Thread(target=to_thread, daemon=False).start()
429
+ threading.Thread(target=to_thread).start()
436
430
  else:
437
431
 
438
432
  def async_kernel_caller() -> None:
439
433
  self._thread, self._caller_id = threading.current_thread(), threading.get_ident()
434
+ settings = RunSettings(
435
+ backend=self.backend,
436
+ host=self.host,
437
+ backend_options=self.backend_options,
438
+ host_options=self.host_options,
439
+ )
440
440
  try:
441
- if not self.host:
442
- # No gui (default)
443
- anyio.run(run_caller_in_context, backend=self.backend, backend_options=self.backend_options)
444
- else:
445
- # A gui with the backend running as a guest.
446
- settings = RunSettings(
447
- backend=self.backend,
448
- host=self.host,
449
- backend_options=self.backend_options,
450
- host_options=self.host_options,
451
- )
452
- Host.run(run_caller_in_context, (), settings)
441
+ async_kernel.event_loop.run(run_caller_in_context, (), settings)
453
442
  except Exception as e:
454
443
  if not self._stopping.done():
455
- self.stop()
444
+ self.stop(force=True)
456
445
  self.log.exception("%s exited early", self, exc_info=e)
457
446
  raise
458
447
 
459
- thread = threading.Thread(target=async_kernel_caller, name=self.name or None)
448
+ thread = threading.Thread(target=async_kernel_caller, name=self._name or None)
460
449
  thread.start()
461
450
  assert thread.ident
462
451
  self._thread, self._caller_id = thread, thread.ident
463
452
 
464
- def stop(self, *, force: bool = False) -> CallerState:
453
+ def stop(self, *, force: bool = False) -> None:
465
454
  """
466
455
  Stop the caller cancelling all incomplete tasks.
467
456
 
468
457
  Args:
469
458
  force: If the caller is protected the call is a no-op unless force=True.
459
+ Returns:
460
+ Event: If stopping is initiated or complete.
461
+ None: If stop is ignored when the caller is protected.
470
462
  """
471
- if (self._protected and not force) or self._state is CallerState.stopped:
472
- return self._state
473
- state = self._state
474
- self._state = CallerState.stopping
475
- # Invoke stop callbacks
476
- self._stopping.set_result(None)
477
- if parent := self.parent:
478
- try:
479
- parent._worker_pool.remove(self)
480
- except ValueError:
481
- pass
482
- for func in self._queue_map.copy():
483
- self.queue_close(func)
484
- if state is CallerState.initial and not self._children:
485
- self._stop_finalize()
486
- else:
487
- for child in self.children:
463
+ if self._protected and not force:
464
+ self.log.warning("Non-force stop ignored for %s", self)
465
+ return
466
+ with self._inst_lock:
467
+ if self._stopping.done():
468
+ return
469
+ state = self._state
470
+ self._state = CallerState.stopping
471
+ self._stopping.set_result(None)
472
+ self.log.debug("Stopping %s", self)
473
+ # Remove from worker pool
474
+ if (parent := self.parent) and (workers := parent._worker_pool):
475
+ with contextlib.suppress(ValueError):
476
+ workers.remove(self)
477
+ # Invoke stop callbacks
478
+ for child in self._children.copy():
488
479
  child.stop(force=True)
489
- return self._state
480
+ # Shutdown queue_call
481
+ for func in self._queue_map.copy():
482
+ self.queue_close(func)
483
+ # Stop the children
484
+ if state in [CallerState.initial, CallerState.start_sync]:
485
+ self._stop_finalize()
490
486
 
491
487
  def _stop_finalize(self) -> None:
492
- self._queue.stop()
493
- self._state = CallerState.stopped
494
- self._instances.pop(self._caller_id, None)
495
- self.stopped.set()
496
- if parent := self.parent:
497
- parent._children.discard(self)
488
+ with self._inst_lock:
489
+ if not self.stopped.done():
490
+ if (countdown := self._children_countdown) is not None:
491
+ countdown.wait()
492
+ self._instances.pop(self._caller_id)
493
+ self._queue.stop()
494
+ self._state = CallerState.stopped
495
+ self.stopped.set_result(None)
496
+ self.log.debug("Stopped %s", self)
498
497
 
499
498
  @asynccontextmanager
500
499
  async def __asynccontextmanager__(self) -> AsyncGenerator[Self]:
@@ -505,35 +504,25 @@ class Caller(anyio.AsyncContextManagerMixin):
505
504
  if self._state is CallerState.stopped:
506
505
  msg = f"Stopped: {self}"
507
506
  raise RuntimeError(msg)
508
- socket = None
509
507
  try:
510
- if self._zmq_context:
511
- socket = self._zmq_context.socket(1) # zmq.SocketType.PUB
512
- socket.connect(self.iopub_url)
513
- self.iopub_sockets[self._caller_id] = socket
514
508
  async with task_factory() as create_task:
509
+ create_task(contextvars.Context(), self._scheduler, self._queue)
515
510
  try:
516
- create_task(contextvars.Context(), self._scheduler, self._queue)
517
511
  with anyio.CancelScope() as scope:
518
512
  self._stopping.add_done_callback(lambda _: self.call_direct(scope.cancel, "Stopping"))
519
513
  self._state = CallerState.running
514
+ self.log.debug("Started %s", self)
520
515
  yield self
521
516
  finally:
522
517
  self.stop(force=True)
523
- for event in self._guest_done_events:
524
- await event # Internally shielded from cancellation
525
- self._queue.stop()
526
- finally:
527
- while self._children:
528
- try:
529
- if not (caller := self._children.pop()).stopped:
518
+ if self._guest_done_event.value > 0:
519
+ # Warning: `queue` must not be stopped until guests have been stopped.
520
+ with anyio.CancelScope(shield=True):
521
+ await self._guest_done_event
522
+ if (countdown := self._children_countdown) is not None:
530
523
  with anyio.CancelScope(shield=True):
531
- await caller.stopped
532
- except KeyError:
533
- pass
534
- if socket:
535
- self.iopub_sockets.pop(self._caller_id, None)
536
- socket.close(linger=0)
524
+ await countdown
525
+ finally:
537
526
  self._stop_finalize()
538
527
 
539
528
  async def _scheduler(self, queue: SingleAsyncQueue) -> None:
@@ -630,7 +619,7 @@ class Caller(anyio.AsyncContextManagerMixin):
630
619
  if worker._idle_time < cutoff:
631
620
  with contextlib.suppress(IndexError):
632
621
  caller._worker_pool.remove(worker)
633
- worker.stop()
622
+ worker.stop(force=True)
634
623
  if n:
635
624
  _cleanup_workers()
636
625
  else:
@@ -689,29 +678,38 @@ class Caller(anyio.AsyncContextManagerMixin):
689
678
  - The returned caller is added to `children` and stopped with the caller.
690
679
  - If 'backend' or 'zmq_context' are not specified they are copied from the caller.
691
680
  """
692
- if self._state in [CallerState.stopping, CallerState.stopped]:
693
- msg = f"Caller is stopping or stopped {self}"
694
- raise RuntimeError(msg)
695
- with self._child_lock:
681
+
682
+ with self._inst_lock:
683
+ if self._stopping.done():
684
+ msg = f"Caller is stopping or stopped {self}"
685
+ raise RuntimeError(msg)
696
686
  if name := kwargs.get("name"):
697
- for caller in self.children:
698
- if caller.name == name:
699
- if (backend := kwargs.get("backend")) and caller.backend != backend:
700
- msg = f"Backend mismatch! {backend=} {caller.backend=}"
687
+ for child in self._children:
688
+ if child.name == name:
689
+ if (backend := kwargs.get("backend")) and child.backend != backend:
690
+ msg = f"Backend mismatch! {backend=} {child.backend=}"
701
691
  raise RuntimeError(msg)
702
- if (host := kwargs.get("host")) and caller.host != host:
703
- msg = f"Host mismatch! {host=} {caller.host=}"
692
+ if (host := kwargs.get("host")) and child.host != host:
693
+ msg = f"Host mismatch! {host=} {child.host=}"
704
694
  raise RuntimeError(msg)
705
- return caller
695
+ return child
696
+
706
697
  if "backend" not in kwargs:
707
698
  kwargs["backend"] = self._backend
708
699
  kwargs["backend_options"] = self.backend_options
709
- if "zmq_context" not in kwargs and self._zmq_context:
710
- kwargs["zmq_context"] = self._zmq_context
711
- caller = self.__class__("NewThread", **kwargs)
712
- self._children.add(caller)
713
- caller._parent_ref = weakref.ref(self)
714
- return caller
700
+ child = self.__class__("NewThread", **kwargs)
701
+ child._parent_ref = weakref.ref(self)
702
+ if self._children_countdown is None:
703
+ self._children_countdown = CountdownEvent()
704
+ self._children.add(child)
705
+ self._children_countdown.up()
706
+
707
+ def on_child_stopped(_, children=self._children, done=self._children_countdown.down) -> None:
708
+ children.discard(child)
709
+ done()
710
+
711
+ child.stopped.add_done_callback(on_child_stopped)
712
+ return child
715
713
 
716
714
  def schedule_call(
717
715
  self,
@@ -745,7 +743,7 @@ class Caller(anyio.AsyncContextManagerMixin):
745
743
  if backend is NoValue or (backend := Backend(backend)) is self.backend:
746
744
  queue = self._queue
747
745
  elif not (queue := self._guest_queues.get(backend)):
748
- with self._child_lock:
746
+ with self._inst_lock:
749
747
  if backend is Backend.trio:
750
748
  trio.sleep # noqa: B018 # Check trio is available.
751
749
  if not (queue := self._guest_queues.get(backend)):
@@ -753,20 +751,23 @@ class Caller(anyio.AsyncContextManagerMixin):
753
751
  self._guest_queues[backend] = queue
754
752
  self._stopping.add_done_callback(lambda _: queue.stop())
755
753
 
754
+ def guest_done_callback(value: Any):
755
+ self._guest_done_event.down()
756
+ self._guest_queues.pop(backend)
757
+
756
758
  def _start_guest() -> None:
757
759
  "Start a guest event loop"
758
760
  if self._state is CallerState.running:
759
- host = Host.current(self.thread)
760
- done = create_async_event(shield=True)
761
+ host: Host[Any] | None = Host.current(self.thread)
761
762
  get_start_guest_run(backend)(
762
763
  self._scheduler,
763
764
  queue,
764
- done_callback=lambda _: done.set(),
765
+ done_callback=guest_done_callback,
765
766
  run_sync_soon_threadsafe=host.run_sync_soon_threadsafe if host else self.call_direct,
766
767
  run_sync_soon_not_threadsafe=host.run_sync_soon_not_threadsafe if host else None,
767
768
  host_uses_signal_set_wakeup_fd=host.host_uses_signal_set_wakeup_fd if host else True,
768
769
  )
769
- self._guest_done_events.add(done)
770
+ self._guest_done_event.up()
770
771
 
771
772
  self.call_direct(_start_guest)
772
773
  queue.append(pen)
@@ -896,14 +897,14 @@ class Caller(anyio.AsyncContextManagerMixin):
896
897
  """
897
898
 
898
899
  def _to_thread_on_done(_) -> None:
899
- if not caller.stopped and self.running:
900
- with self._child_lock:
900
+ if not caller.stopped.done() and self.running:
901
+ with self._inst_lock:
901
902
  if len(self._worker_pool) < self.MAX_IDLE_POOL_INSTANCES:
902
903
  caller._idle_time = time.monotonic()
903
904
  self._worker_pool.append(caller)
904
905
  self._start_idle_worker_cleanup_thead()
905
906
  else:
906
- caller.stop()
907
+ caller.stop(force=True)
907
908
 
908
909
  try:
909
910
  caller = self._worker_pool.popleft()
@@ -950,7 +951,7 @@ class Caller(anyio.AsyncContextManagerMixin):
950
951
  trackers and **not** PendingGroup.
951
952
  """
952
953
  if not (pen_ := self._queue_map.get(key := hash(func))):
953
- with self._child_lock:
954
+ with self._inst_lock:
954
955
  if not (pen_ := self._queue_map.get(key)):
955
956
  queue = SingleAsyncQueue[tuple[Callable, tuple, dict]](reject=self._reject)
956
957
  with contextlib.suppress(TypeError):
@@ -962,9 +963,10 @@ class Caller(anyio.AsyncContextManagerMixin):
962
963
  try:
963
964
  async for item in queue:
964
965
  try:
965
- await item[0](*item[1], **item[2])
966
- except TypeError:
967
- continue
966
+ result = item[0](*item[1], **item[2])
967
+ if iscoroutinelike(result):
968
+ await result
969
+ del result
968
970
  except (anyio.get_cancelled_exc_class(), Exception) as e:
969
971
  if pen.cancelled():
970
972
  raise