├── comm ├── py.typed ├── __init__.py └── base_comm.py ├── .gitignore ├── RELEASE.md ├── SECURITY.md ├── .github ├── dependabot.yml └── workflows │ ├── publish-changelog.yml │ ├── downstream.yml │ ├── prep-release.yml │ ├── publish-release.yml │ └── python-tests.yml ├── tests └── test_comm.py ├── README.md ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml └── CHANGELOG.md /comm/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | comm.egg-info/ 2 | **/__pycache__/ 3 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a Release 2 | 3 | The recommended way to make a release is to use [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html). 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | All IPython and Jupyter related security are handled via security@ipython.org. 6 | You can find more information on the Jupyter website. https://jupyter.org/security 7 | 8 | ## Tidelift 9 | 10 | You can also report security concerns for the `comm` package via the [Tidelift platform](https://tidelift.com/security). 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | actions: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | groups: 16 | actions: 17 | patterns: 18 | - "*" 19 | -------------------------------------------------------------------------------- /tests/test_comm.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from comm.base_comm import BaseComm, CommManager 4 | 5 | 6 | class MyComm(BaseComm): 7 | def publish_msg( 8 | self, 9 | msg_type: str, 10 | data: Any = None, 11 | metadata: Any = None, 12 | buffers: Any = None, 13 | **keys: Any, 14 | ) -> None: 15 | pass 16 | 17 | 18 | def test_comm_manager() -> None: 19 | test = CommManager() 20 | assert test.targets == {} 21 | 22 | 23 | def test_base_comm() -> None: 24 | test = MyComm() 25 | assert test.target_name == "comm" 26 | -------------------------------------------------------------------------------- /.github/workflows/publish-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Changelog" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: "The branch to target" 10 | required: false 11 | 12 | jobs: 13 | publish_changelog: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | steps: 17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - uses: actions/create-github-app-token@v2 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.APP_ID }} 23 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 24 | 25 | - name: Publish changelog 26 | id: publish-changelog 27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2 28 | with: 29 | token: ${{ steps.app-token.outputs.token }} 30 | branch: ${{ github.event.inputs.branch }} 31 | 32 | - name: "** Next Step **" 33 | run: | 34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comm 2 | 3 | It provides a way to register a Kernel Comm implementation, as per the Jupyter kernel protocol. 4 | It also provides a base Comm implementation and a default CommManager that can be used. 5 | 6 | ## Register a comm implementation in the kernel: 7 | 8 | ### Case 1: Using the default CommManager and the BaseComm implementations 9 | 10 | We provide default implementations for usage in IPython: 11 | 12 | ```python 13 | import comm 14 | 15 | 16 | class MyCustomComm(comm.base_comm.BaseComm): 17 | def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys): 18 | # TODO implement the logic for sending comm messages through the iopub channel 19 | pass 20 | 21 | 22 | comm.create_comm = MyCustomComm 23 | ``` 24 | 25 | This is typically what ipykernel and JupyterLite's pyolite kernel will do. 26 | 27 | ### Case 2: Providing your own comm manager creation implementation 28 | 29 | ```python 30 | import comm 31 | 32 | comm.create_comm = custom_create_comm 33 | comm.get_comm_manager = custom_comm_manager_getter 34 | ``` 35 | 36 | This is typically what xeus-python does (it has its own manager implementation using xeus's C++ messaging logic). 37 | 38 | ## Comm users 39 | 40 | Libraries like ipywidgets can then use the comms implementation that has been registered by the kernel: 41 | 42 | ```python 43 | from comm import create_comm, get_comm_manager 44 | 45 | # Create a comm 46 | comm_manager = get_comm_manager() 47 | comm = create_comm() 48 | 49 | comm_manager.register_comm(comm) 50 | ``` 51 | -------------------------------------------------------------------------------- /.github/workflows/downstream.yml: -------------------------------------------------------------------------------- 1 | name: Test downstream projects 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | concurrency: 9 | group: downstream-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | ipykernel: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 15 16 | steps: 17 | - uses: actions/checkout@v5 18 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 20 | with: 21 | package_name: ipykernel 22 | 23 | ipywidgets: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v5 28 | 29 | - name: Base Setup 30 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 31 | 32 | - name: Run Test 33 | uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 34 | with: 35 | package_name: ipywidgets 36 | test_command: pytest -vv -raXxs -k \"not deprecation_fa_icons and not tooltip_deprecation and not on_submit_deprecation\" -W default --durations 10 --color=yes 37 | 38 | downstream_check: # This job does nothing and is only used for the branch protection 39 | if: always() 40 | needs: 41 | - ipykernel 42 | - ipywidgets 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Decide whether the needed jobs succeeded or failed 46 | uses: re-actors/alls-green@release/v1 47 | with: 48 | jobs: ${{ toJSON(needs) }} 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Jupyter 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /comm/__init__.py: -------------------------------------------------------------------------------- 1 | """Comm package. 2 | 3 | Copyright (c) IPython Development Team. 4 | Distributed under the terms of the Modified BSD License. 5 | 6 | This package provides a way to register a Kernel Comm implementation, as per 7 | the Jupyter kernel protocol. 8 | It also provides a base Comm implementation and a default CommManager for the IPython case. 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | from typing import Any 14 | 15 | from .base_comm import BaseComm, BuffersType, CommManager, MaybeDict 16 | 17 | __version__ = "0.2.3" 18 | __all__ = [ 19 | "__version__", 20 | "create_comm", 21 | "get_comm_manager", 22 | ] 23 | 24 | _comm_manager = None 25 | 26 | 27 | class DummyComm(BaseComm): 28 | def publish_msg( 29 | self, 30 | msg_type: str, 31 | data: MaybeDict = None, 32 | metadata: MaybeDict = None, 33 | buffers: BuffersType = None, 34 | **keys: Any, 35 | ) -> None: 36 | pass 37 | 38 | 39 | def _create_comm(*args: Any, **kwargs: Any) -> BaseComm: 40 | """Create a Comm. 41 | 42 | This method is intended to be replaced, so that it returns your Comm instance. 43 | """ 44 | return DummyComm(*args, **kwargs) 45 | 46 | 47 | def _get_comm_manager() -> CommManager: 48 | """Get the current Comm manager, creates one if there is none. 49 | 50 | This method is intended to be replaced if needed (if you want to manage multiple CommManagers). 51 | """ 52 | global _comm_manager # noqa: PLW0603 53 | 54 | if _comm_manager is None: 55 | _comm_manager = CommManager() 56 | 57 | return _comm_manager 58 | 59 | 60 | create_comm = _create_comm 61 | get_comm_manager = _get_comm_manager 62 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | silent: 16 | description: "Set a placeholder in the changelog and don't publish the release." 17 | required: false 18 | type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: "** Next Step **" 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v2 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 42 | with: 43 | token: ${{ steps.app-token.outputs.token }} 44 | release_url: ${{ steps.populate-release.outputs.release_url }} 45 | 46 | - name: "** Next Step **" 47 | if: ${{ success() }} 48 | run: | 49 | echo "Verify the final release" 50 | echo ${{ steps.finalize-release.outputs.release_url }} 51 | 52 | - name: "** Failure Message **" 53 | if: ${{ failure() }} 54 | run: | 55 | echo "Failed to Publish the Draft Release Url:" 56 | echo ${{ steps.populate-release.outputs.release_url }} 57 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: monthly 3 | autoupdate_commit_msg: "chore: update pre-commit hooks" 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v5.0.0 8 | hooks: 9 | - id: check-case-conflict 10 | - id: check-ast 11 | - id: check-docstring-first 12 | - id: check-executables-have-shebangs 13 | - id: check-added-large-files 14 | - id: check-case-conflict 15 | - id: check-merge-conflict 16 | - id: check-json 17 | - id: check-toml 18 | - id: check-yaml 19 | - id: debug-statements 20 | - id: end-of-file-fixer 21 | - id: trailing-whitespace 22 | 23 | - repo: https://github.com/python-jsonschema/check-jsonschema 24 | rev: 0.31.3 25 | hooks: 26 | - id: check-github-workflows 27 | 28 | - repo: https://github.com/executablebooks/mdformat 29 | rev: 0.7.22 30 | hooks: 31 | - id: mdformat 32 | additional_dependencies: 33 | [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] 34 | 35 | - repo: https://github.com/pre-commit/mirrors-prettier 36 | rev: "v4.0.0-alpha.8" 37 | hooks: 38 | - id: prettier 39 | types_or: [yaml, html, json] 40 | 41 | - repo: https://github.com/adamchainz/blacken-docs 42 | rev: "1.19.1" 43 | hooks: 44 | - id: blacken-docs 45 | additional_dependencies: [black==23.7.0] 46 | 47 | - repo: https://github.com/codespell-project/codespell 48 | rev: "v2.4.1" 49 | hooks: 50 | - id: codespell 51 | args: ["-L", "sur,nd"] 52 | 53 | - repo: https://github.com/pre-commit/pygrep-hooks 54 | rev: "v1.10.0" 55 | hooks: 56 | - id: rst-backticks 57 | - id: rst-directive-colons 58 | - id: rst-inline-touching-normal 59 | 60 | - repo: https://github.com/pre-commit/mirrors-mypy 61 | rev: "v1.15.0" 62 | hooks: 63 | - id: mypy 64 | files: comm 65 | stages: [manual] 66 | additional_dependencies: ["traitlets>=5.13"] 67 | 68 | - repo: https://github.com/astral-sh/ruff-pre-commit 69 | rev: v0.11.0 70 | hooks: 71 | - id: ruff 72 | types_or: [python, jupyter] 73 | args: ["--fix", "--show-fixes"] 74 | - id: ruff-format 75 | types_or: [python, jupyter] 76 | 77 | - repo: https://github.com/scientific-python/cookie 78 | rev: "2025.01.22" 79 | hooks: 80 | - id: sp-repo-review 81 | additional_dependencies: ["repo-review[cli]"] 82 | -------------------------------------------------------------------------------- /.github/workflows/python-tests.yml: -------------------------------------------------------------------------------- 1 | name: Comm Tests 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * *" 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 20 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | python-version: ["3.8", "3.12", "3.13"] 19 | include: 20 | - os: windows-latest 21 | python-version: "3.9" 22 | - os: ubuntu-latest 23 | python-version: "pypy-3.9" 24 | - os: ubuntu-latest 25 | python-version: "3.10" 26 | - os: macos-latest 27 | python-version: "3.11" 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v5 31 | - name: Base Setup 32 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | - name: Run test 34 | run: hatch run test:test 35 | 36 | test_lint: 37 | name: Test Lint 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v5 41 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 42 | - name: Run Linters 43 | run: | 44 | hatch run typing:test 45 | hatch run lint:build 46 | pipx run 'validate-pyproject[all]' pyproject.toml 47 | 48 | check_release: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v5 53 | - name: Base Setup 54 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 55 | - name: Install Dependencies 56 | run: | 57 | pip install -e . 58 | - name: Check Release 59 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 60 | with: 61 | version_spec: 100.100.100 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | check_links: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v5 68 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 69 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 70 | 71 | check: # This job does nothing and is only used for the branch protection 72 | if: always() 73 | needs: 74 | - check_links 75 | - check_release 76 | - test_lint 77 | - build 78 | runs-on: ubuntu-latest 79 | steps: 80 | - name: Decide whether the needed jobs succeeded or failed 81 | uses: re-actors/alls-green@release/v1 82 | with: 83 | jobs: ${{ toJSON(needs) }} 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.10"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "comm" 7 | dynamic = ["version"] 8 | description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." 9 | readme = "README.md" 10 | license = { file="LICENSE" } 11 | requires-python = ">=3.8" 12 | authors = [ 13 | { name = "Jupyter contributors" }, 14 | ] 15 | keywords = [ 16 | "ipykernel", 17 | "jupyter", 18 | "xeus-python", 19 | ] 20 | classifiers = [ 21 | "Framework :: Jupyter", 22 | "License :: OSI Approved :: BSD License", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3", 25 | ] 26 | 27 | [project.optional-dependencies] 28 | test = [ 29 | "pytest", 30 | ] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/ipython/comm" 34 | 35 | [tool.hatch.version] 36 | path = "comm/__init__.py" 37 | 38 | [tool.hatch.build.targets.sdist] 39 | include = [ 40 | "/comm", 41 | ] 42 | 43 | [tool.hatch.envs.test] 44 | features = ["test"] 45 | [tool.hatch.envs.test.scripts] 46 | test = "python -m pytest -vv {args}" 47 | nowarn = "test -W default {args}" 48 | 49 | [tool.hatch.envs.typing] 50 | dependencies = ["pre-commit"] 51 | detached = true 52 | [tool.hatch.envs.typing.scripts] 53 | test = "pre-commit run --all-files --hook-stage manual mypy" 54 | 55 | [tool.hatch.envs.lint] 56 | dependencies = ["pre-commit"] 57 | detached = true 58 | [tool.hatch.envs.lint.scripts] 59 | build = [ 60 | "pre-commit run --all-files ruff", 61 | "pre-commit run --all-files ruff-format" 62 | ] 63 | 64 | [tool.pytest.ini_options] 65 | minversion = "6.0" 66 | testpaths = ["tests"] 67 | xfail_strict = true 68 | log_cli_level = "info" 69 | addopts = [ 70 | "-raXs", "--durations=10", "--color=yes", "--doctest-modules", 71 | "--showlocals", "--strict-markers", "--strict-config" 72 | ] 73 | filterwarnings = ["error"] 74 | 75 | [tool.mypy] 76 | files = "comm" 77 | python_version = "3.8" 78 | strict = true 79 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] 80 | warn_unreachable = true 81 | 82 | [tool.ruff] 83 | line-length = 100 84 | 85 | [tool.ruff.lint] 86 | extend-select = [ 87 | "B", # flake8-bugbear 88 | "I", # isort 89 | "ARG", # flake8-unused-arguments 90 | "C4", # flake8-comprehensions 91 | "EM", # flake8-errmsg 92 | "ICN", # flake8-import-conventions 93 | "G", # flake8-logging-format 94 | "PGH", # pygrep-hooks 95 | "PIE", # flake8-pie 96 | "PL", # pylint 97 | "PTH", # flake8-use-pathlib 98 | "PT", # flake8-pytest-style 99 | "RET", # flake8-return 100 | "RUF", # Ruff-specific 101 | "SIM", # flake8-simplify 102 | "T20", # flake8-print 103 | "UP", # pyupgrade 104 | "YTT", # flake8-2020 105 | "EXE", # flake8-executable 106 | "PYI", # flake8-pyi 107 | "S", # flake8-bandit 108 | ] 109 | ignore = [ 110 | "PLR", # Design related pylint codes 111 | "S101", # Use of `assert` detected 112 | "G201" # Logging `.exception(...)` should be used instead 113 | ] 114 | unfixable = [ 115 | # Don't touch print statements 116 | "T201", 117 | # Don't touch noqa lines 118 | "RUF100", 119 | ] 120 | 121 | [tool.ruff.lint.per-file-ignores] 122 | "tests/*" = [] 123 | 124 | [tool.ruff.lint.flake8-pytest-style] 125 | fixture-parentheses = false 126 | mark-parentheses = false 127 | parametrize-names-type = "csv" 128 | 129 | [tool.repo-review] 130 | ignore = ["PP308", "PY004", "GH102", "MY101", "RTD100"] 131 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | 7 | ## 0.2.3 8 | 9 | ([Full Changelog](https://github.com/ipython/comm/compare/v0.2.2...ec3eb438c07314b47c057c1cc3ce5fe43c294e90)) 10 | 11 | ### Merged PRs 12 | 13 | - Remove `traitlets` dependency [#32](https://github.com/ipython/comm/pull/32) ([@davidbrochart](https://github.com/davidbrochart)) 14 | - Add Security.md file. [#29](https://github.com/ipython/comm/pull/29) ([@Carreau](https://github.com/Carreau)) 15 | - pre-commit updates and fixes [#28](https://github.com/ipython/comm/pull/28) ([@Carreau](https://github.com/Carreau)) 16 | 17 | ### Contributors to this release 18 | 19 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2024-03-12&to=2025-07-25&type=c)) 20 | 21 | [@Carreau](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3ACarreau+updated%3A2024-03-12..2025-07-25&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Adavidbrochart+updated%3A2024-03-12..2025-07-25&type=Issues) 22 | 23 | 24 | 25 | ## 0.2.2 26 | 27 | ([Full Changelog](https://github.com/ipython/comm/compare/v0.2.1...76149e7ee0f331772c964ae86cdb8bafebe6dfa2)) 28 | 29 | ### Maintenance and upkeep improvements 30 | 31 | - Update Release Scripts [#27](https://github.com/ipython/comm/pull/27) ([@blink1073](https://github.com/blink1073)) 32 | 33 | ### Other merged PRs 34 | 35 | - chore: update pre-commit hooks [#26](https://github.com/ipython/comm/pull/26) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 36 | 37 | ### Contributors to this release 38 | 39 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2024-01-02&to=2024-03-12&type=c)) 40 | 41 | [@blink1073](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Ablink1073+updated%3A2024-01-02..2024-03-12&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Apre-commit-ci+updated%3A2024-01-02..2024-03-12&type=Issues) 42 | 43 | ## 0.2.1 44 | 45 | ([Full Changelog](https://github.com/ipython/comm/compare/v0.2.0...46e07dc298d19c1b7ade765d0a435f794e69a020)) 46 | 47 | ### Maintenance and upkeep improvements 48 | 49 | - Update ruff config [#23](https://github.com/ipython/comm/pull/23) ([@blink1073](https://github.com/blink1073)) 50 | 51 | ### Other merged PRs 52 | 53 | - chore: update pre-commit hooks [#25](https://github.com/ipython/comm/pull/25) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 54 | - Prevent exception spam during interpreter teardown [#24](https://github.com/ipython/comm/pull/24) ([@apmorton](https://github.com/apmorton)) 55 | - chore: update pre-commit hooks [#22](https://github.com/ipython/comm/pull/22) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 56 | - chore: update pre-commit hooks [#21](https://github.com/ipython/comm/pull/21) ([@pre-commit-ci](https://github.com/pre-commit-ci)) 57 | 58 | ### Contributors to this release 59 | 60 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2023-11-06&to=2024-01-02&type=c)) 61 | 62 | [@apmorton](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Aapmorton+updated%3A2023-11-06..2024-01-02&type=Issues) | [@blink1073](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Ablink1073+updated%3A2023-11-06..2024-01-02&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Apre-commit-ci+updated%3A2023-11-06..2024-01-02&type=Issues) 63 | 64 | ## 0.2.0 65 | 66 | ([Full Changelog](https://github.com/ipython/comm/compare/v0.1.4...5e4ad3166b80feba3f74ad074b7b5f98d7a99439)) 67 | 68 | ### Maintenance and upkeep improvements 69 | 70 | - Add downstream checks [#20](https://github.com/ipython/comm/pull/20) ([@blink1073](https://github.com/blink1073)) 71 | - Adopt sp-repo-review [#19](https://github.com/ipython/comm/pull/19) ([@blink1073](https://github.com/blink1073)) 72 | 73 | ### Other merged PRs 74 | 75 | - Bump actions/checkout from 3 to 4 [#17](https://github.com/ipython/comm/pull/17) ([@dependabot](https://github.com/dependabot)) 76 | 77 | ### Contributors to this release 78 | 79 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2023-08-02&to=2023-11-06&type=c)) 80 | 81 | [@blink1073](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Ablink1073+updated%3A2023-08-02..2023-11-06&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Adependabot+updated%3A2023-08-02..2023-11-06&type=Issues) 82 | 83 | ## 0.1.4 84 | 85 | ([Full Changelog](https://github.com/ipython/comm/compare/v0.1.3...136c099e4fb1cc83040661796ad7ea349af04be8)) 86 | 87 | ### Improvements 88 | 89 | - Loosen dependency on traitlet so it is available in python 3.6 [#16](https://github.com/ipython/comm/pull/16) ([@vincent-grosbois](https://github.com/vincent-grosbois)) 90 | 91 | ### Contributors to this release 92 | 93 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2023-03-22&to=2023-08-02&type=c)) 94 | 95 | [@martinRenou](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3AmartinRenou+updated%3A2023-03-22..2023-08-02&type=Issues) | [@vincent-grosbois](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Avincent-grosbois+updated%3A2023-03-22..2023-08-02&type=Issues) 96 | 97 | ## 0.1.3 98 | 99 | ([Full Changelog](https://github.com/ipython/comm/compare/0.1.2...309b8295ca950a9ca9bdc0daa796215d72a7cb09)) 100 | 101 | ### Maintenance and upkeep improvements 102 | 103 | - Adopt linters and releaser [#12](https://github.com/ipython/comm/pull/12) ([@blink1073](https://github.com/blink1073)) 104 | 105 | ### Other merged PRs 106 | 107 | - feat: provide a default implementation [#13](https://github.com/ipython/comm/pull/13) ([@maartenbreddels](https://github.com/maartenbreddels)) 108 | 109 | ### Contributors to this release 110 | 111 | ([GitHub contributors page for this release](https://github.com/ipython/comm/graphs/contributors?from=2022-12-08&to=2023-03-22&type=c)) 112 | 113 | [@blink1073](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Ablink1073+updated%3A2022-12-08..2023-03-22&type=Issues) | [@maartenbreddels](https://github.com/search?q=repo%3Aipython%2Fcomm+involves%3Amaartenbreddels+updated%3A2022-12-08..2023-03-22&type=Issues) 114 | 115 | ## 0.1.2 116 | 117 | Initial release 118 | -------------------------------------------------------------------------------- /comm/base_comm.py: -------------------------------------------------------------------------------- 1 | """Default classes for Comm and CommManager, for usage in IPython.""" 2 | 3 | # Copyright (c) IPython Development Team. 4 | # Distributed under the terms of the Modified BSD License. 5 | from __future__ import annotations 6 | 7 | import contextlib 8 | import logging 9 | import typing as t 10 | import uuid 11 | 12 | import comm 13 | 14 | if t.TYPE_CHECKING: 15 | from zmq.eventloop.zmqstream import ZMQStream 16 | 17 | logger = logging.getLogger("Comm") 18 | 19 | MessageType = t.Dict[str, t.Any] 20 | MaybeDict = t.Optional[t.Dict[str, t.Any]] 21 | BuffersType = t.Optional[t.List[bytes]] 22 | CommCallback = t.Callable[[MessageType], None] 23 | CommTargetCallback = t.Callable[["BaseComm", MessageType], None] 24 | 25 | 26 | class BaseComm: 27 | """Class for communicating between a Frontend and a Kernel 28 | 29 | Must be subclassed with a publish_msg method implementation which 30 | sends comm messages through the iopub channel. 31 | """ 32 | 33 | def __init__( 34 | self, 35 | target_name: str = "comm", 36 | data: MaybeDict = None, 37 | metadata: MaybeDict = None, 38 | buffers: BuffersType = None, 39 | comm_id: str | None = None, 40 | primary: bool = True, 41 | target_module: str | None = None, 42 | topic: bytes | None = None, 43 | _open_data: MaybeDict = None, 44 | _close_data: MaybeDict = None, 45 | **kwargs: t.Any, 46 | ) -> None: 47 | super().__init__(**kwargs) 48 | 49 | self.comm_id = comm_id if comm_id else uuid.uuid4().hex 50 | self.primary = primary 51 | self.target_name = target_name 52 | self.target_module = target_module 53 | self.topic = topic if topic else (f"comm-{self.comm_id}").encode("ascii") 54 | 55 | self._open_data = _open_data if _open_data else {} 56 | self._close_data = _close_data if _close_data else {} 57 | 58 | self._msg_callback: CommCallback | None = None 59 | self._close_callback: CommCallback | None = None 60 | 61 | self._closed = True 62 | 63 | if self.primary: 64 | # I am primary, open my peer. 65 | self.open(data=data, metadata=metadata, buffers=buffers) 66 | else: 67 | self._closed = False 68 | 69 | def publish_msg( 70 | self, 71 | msg_type: str, 72 | data: MaybeDict = None, 73 | metadata: MaybeDict = None, 74 | buffers: BuffersType = None, 75 | **keys: t.Any, 76 | ) -> None: 77 | msg = "publish_msg Comm method is not implemented" 78 | raise NotImplementedError(msg) 79 | 80 | def __del__(self) -> None: 81 | """trigger close on gc""" 82 | with contextlib.suppress(Exception): 83 | # any number of things can have gone horribly wrong 84 | # when called during interpreter teardown 85 | self.close(deleting=True) 86 | 87 | # publishing messages 88 | 89 | def open( 90 | self, data: MaybeDict = None, metadata: MaybeDict = None, buffers: BuffersType = None 91 | ) -> None: 92 | """Open the frontend-side version of this comm""" 93 | 94 | if data is None: 95 | data = self._open_data 96 | comm_manager = comm.get_comm_manager() 97 | if comm_manager is None: 98 | msg = "Comms cannot be opened without a comm_manager." # type:ignore[unreachable] 99 | raise RuntimeError(msg) 100 | 101 | comm_manager.register_comm(self) 102 | try: 103 | self.publish_msg( 104 | "comm_open", 105 | data=data, 106 | metadata=metadata, 107 | buffers=buffers, 108 | target_name=self.target_name, 109 | target_module=self.target_module, 110 | ) 111 | self._closed = False 112 | except Exception: 113 | comm_manager.unregister_comm(self) 114 | raise 115 | 116 | def close( 117 | self, 118 | data: MaybeDict = None, 119 | metadata: MaybeDict = None, 120 | buffers: BuffersType = None, 121 | deleting: bool = False, 122 | ) -> None: 123 | """Close the frontend-side version of this comm""" 124 | if self._closed: 125 | # only close once 126 | return 127 | self._closed = True 128 | if data is None: 129 | data = self._close_data 130 | self.publish_msg( 131 | "comm_close", 132 | data=data, 133 | metadata=metadata, 134 | buffers=buffers, 135 | ) 136 | if not deleting: 137 | # If deleting, the comm can't be registered 138 | comm.get_comm_manager().unregister_comm(self) 139 | 140 | def send( 141 | self, data: MaybeDict = None, metadata: MaybeDict = None, buffers: BuffersType = None 142 | ) -> None: 143 | """Send a message to the frontend-side version of this comm""" 144 | self.publish_msg( 145 | "comm_msg", 146 | data=data, 147 | metadata=metadata, 148 | buffers=buffers, 149 | ) 150 | 151 | # registering callbacks 152 | 153 | def on_close(self, callback: CommCallback | None) -> None: 154 | """Register a callback for comm_close 155 | 156 | Will be called with the `data` of the close message. 157 | 158 | Call `on_close(None)` to disable an existing callback. 159 | """ 160 | self._close_callback = callback 161 | 162 | def on_msg(self, callback: CommCallback | None) -> None: 163 | """Register a callback for comm_msg 164 | 165 | Will be called with the `data` of any comm_msg messages. 166 | 167 | Call `on_msg(None)` to disable an existing callback. 168 | """ 169 | self._msg_callback = callback 170 | 171 | # handling of incoming messages 172 | 173 | def handle_close(self, msg: MessageType) -> None: 174 | """Handle a comm_close message""" 175 | logger.debug("handle_close[%s](%s)", self.comm_id, msg) 176 | if self._close_callback: 177 | self._close_callback(msg) 178 | 179 | def handle_msg(self, msg: MessageType) -> None: 180 | """Handle a comm_msg message""" 181 | logger.debug("handle_msg[%s](%s)", self.comm_id, msg) 182 | if self._msg_callback: 183 | from IPython import get_ipython 184 | 185 | shell = get_ipython() 186 | if shell: 187 | shell.events.trigger("pre_execute") 188 | self._msg_callback(msg) 189 | if shell: 190 | shell.events.trigger("post_execute") 191 | 192 | 193 | class CommManager: 194 | """Default CommManager singleton implementation for Comms in the Kernel""" 195 | 196 | # Public APIs 197 | 198 | def __init__(self) -> None: 199 | self.comms: dict[str, BaseComm] = {} 200 | self.targets: dict[str, CommTargetCallback] = {} 201 | 202 | def register_target(self, target_name: str, f: CommTargetCallback | str) -> None: 203 | """Register a callable f for a given target name 204 | 205 | f will be called with two arguments when a comm_open message is received with `target`: 206 | 207 | - the Comm instance 208 | - the `comm_open` message itself. 209 | 210 | f can be a Python callable or an import string for one. 211 | """ 212 | if isinstance(f, str): 213 | parts = f.rsplit(".", 1) 214 | if len(parts) == 2: 215 | # called with 'foo.bar....' 216 | package, obj = parts 217 | module = __import__(package, fromlist=[obj]) 218 | try: 219 | f = getattr(module, obj) 220 | except AttributeError as e: 221 | error_msg = f"No module named {obj}" 222 | raise ImportError(error_msg) from e 223 | else: 224 | # called with un-dotted string 225 | f = __import__(parts[0]) 226 | 227 | self.targets[target_name] = t.cast(CommTargetCallback, f) 228 | 229 | def unregister_target(self, target_name: str, f: CommTargetCallback) -> CommTargetCallback: # noqa: ARG002 230 | """Unregister a callable registered with register_target""" 231 | return self.targets.pop(target_name) 232 | 233 | def register_comm(self, comm: BaseComm) -> str: 234 | """Register a new comm""" 235 | comm_id = comm.comm_id 236 | self.comms[comm_id] = comm 237 | return comm_id 238 | 239 | def unregister_comm(self, comm: BaseComm) -> None: 240 | """Unregister a comm, and close its counterpart""" 241 | # unlike get_comm, this should raise a KeyError 242 | comm = self.comms.pop(comm.comm_id) 243 | 244 | def get_comm(self, comm_id: str) -> BaseComm | None: 245 | """Get a comm with a particular id 246 | 247 | Returns the comm if found, otherwise None. 248 | 249 | This will not raise an error, 250 | it will log messages if the comm cannot be found. 251 | """ 252 | try: 253 | return self.comms[comm_id] 254 | except KeyError: 255 | logger.warning("No such comm: %s", comm_id) 256 | if logger.isEnabledFor(logging.DEBUG): 257 | # don't create the list of keys if debug messages aren't enabled 258 | logger.debug("Current comms: %s", list(self.comms.keys())) 259 | return None 260 | 261 | # Message handlers 262 | 263 | def comm_open(self, stream: ZMQStream, ident: str, msg: MessageType) -> None: # noqa: ARG002 264 | """Handler for comm_open messages""" 265 | from comm import create_comm 266 | 267 | content = msg["content"] 268 | comm_id = content["comm_id"] 269 | target_name = content["target_name"] 270 | f = self.targets.get(target_name, None) 271 | comm = create_comm( 272 | comm_id=comm_id, 273 | primary=False, 274 | target_name=target_name, 275 | ) 276 | self.register_comm(comm) 277 | if f is None: 278 | logger.error("No such comm target registered: %s", target_name) 279 | else: 280 | try: 281 | f(comm, msg) 282 | return 283 | except Exception: 284 | logger.error("Exception opening comm with target: %s", target_name, exc_info=True) 285 | 286 | # Failure. 287 | try: 288 | comm.close() 289 | except Exception: 290 | logger.error( 291 | """Could not close comm during `comm_open` failure 292 | clean-up. The comm may not have been opened yet.""", 293 | exc_info=True, 294 | ) 295 | 296 | def comm_msg(self, stream: ZMQStream, ident: str, msg: MessageType) -> None: # noqa: ARG002 297 | """Handler for comm_msg messages""" 298 | content = msg["content"] 299 | comm_id = content["comm_id"] 300 | comm = self.get_comm(comm_id) 301 | if comm is None: 302 | return 303 | 304 | try: 305 | comm.handle_msg(msg) 306 | except Exception: 307 | logger.error("Exception in comm_msg for %s", comm_id, exc_info=True) 308 | 309 | def comm_close(self, stream: ZMQStream, ident: str, msg: MessageType) -> None: # noqa: ARG002 310 | """Handler for comm_close messages""" 311 | content = msg["content"] 312 | comm_id = content["comm_id"] 313 | comm = self.get_comm(comm_id) 314 | if comm is None: 315 | return 316 | 317 | self.comms[comm_id]._closed = True 318 | del self.comms[comm_id] 319 | 320 | try: 321 | comm.handle_close(msg) 322 | except Exception: 323 | logger.error("Exception in comm_close for %s", comm_id, exc_info=True) 324 | 325 | 326 | __all__ = ["BaseComm", "CommManager"] 327 | --------------------------------------------------------------------------------