diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 8f2aa225..16d68f53 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -31,16 +31,6 @@ jobs: package_name: nbclient env_values: IPYKERNEL_CELL_NAME=\ - papermill: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 - with: - package_name: papermill - nbconvert: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f6111c60..cbb6ff6d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,9 @@ defaults: run: shell: bash -eux {0} +env: + HATCH_ENV: "cov" + jobs: check_release: runs-on: ubuntu-latest @@ -39,8 +42,9 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: - - "3.9" - - "3.13" + - "3.10" + - "3.14" + - "3.14t" include: - os: windows-latest python-version: "3.12" @@ -53,18 +57,28 @@ jobs: steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Run the tests - if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.os, 'windows') }} + + - name: disable warnings on pypy or windows + if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }} run: | - hatch run cov:test --cov-fail-under 75 || hatch run test:test --lf - - name: Run the tests on pypy + echo "PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-} -W default" >> $GITHUB_ENV + + - name: disable coverage on pypy if: ${{ startsWith(matrix.python-version, 'pypy') }} run: | - hatch run test:nowarn || hatch run test:nowarn --lf - - name: Run the tests on windows - if: ${{ startsWith(matrix.os, 'windows') }} + echo "HATCH_ENV=test" >> $GITHUB_ENV + + - name: show env + run: | + hatch -e ${HATCH_ENV} run python -VV + hatch -e ${HATCH_ENV} run pip freeze + + - name: Run the tests run: | - hatch run cov:nowarn || hatch run test:nowarn --lf + hatch run ${HATCH_ENV}:test + env: + PYTHONTRACEMALLOC: "10" + - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1 coverage: @@ -116,9 +130,16 @@ jobs: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: dependency_type: minimum + python_version: "3.10" + + - name: show env + run: | + hatch -e test run python -VV + hatch -e test run pip freeze + - name: Run the unit tests run: | - hatch -vv run test:nowarn || hatch run test:nowarn --lf + hatch -vv run test:nowarn test_prereleases: name: Test Prereleases @@ -131,7 +152,7 @@ jobs: dependency_type: pre - name: Run the tests run: | - hatch run test:nowarn || hatch run test:nowarn --lf + hatch run test:nowarn make_sdist: name: Make SDist @@ -151,7 +172,7 @@ jobs: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1 with: - test_command: pytest -vv || pytest -vv --lf + test_command: pytest -vv tests_check: # This job does nothing and is only used for the branch protection if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d670542..5c74d89a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: check-case-conflict - id: check-ast @@ -21,12 +21,12 @@ repos: - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.4 + rev: 0.34.0 hooks: - id: check-github-workflows - - repo: https://github.com/executablebooks/mdformat - rev: 0.7.17 + - repo: https://github.com/hukkin/mdformat + rev: 0.7.22 hooks: - id: mdformat @@ -37,23 +37,23 @@ repos: types_or: [yaml, html, json] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.13.0" + rev: "v1.18.2" hooks: - id: mypy files: jupyter_client stages: [manual] - args: ["--install-types", "--non-interactive", "--python-version=3.9"] + args: ["--install-types", "--non-interactive"] additional_dependencies: ["traitlets>=5.13", "ipykernel>=6.26", "jupyter_core>=5.3.2"] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.16.0" + rev: "1.20.0" hooks: - id: blacken-docs additional_dependencies: [black==23.7.0] - repo: https://github.com/codespell-project/codespell - rev: "v2.2.6" + rev: "v2.4.1" hooks: - id: codespell args: ["-L", "sur,nd"] @@ -66,16 +66,16 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.14.0 hooks: - - id: ruff + - id: ruff-check types_or: [python, jupyter] args: ["--fix", "--show-fixes"] - id: ruff-format types_or: [python, jupyter] - repo: https://github.com/scientific-python/cookie - rev: "2024.01.24" + rev: "2025.10.01" hooks: - id: sp-repo-review additional_dependencies: ["repo-review[cli]"] diff --git a/CHANGELOG.md b/CHANGELOG.md index f846d88b..6ee74702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -516,16 +516,16 @@ No merged PRs ### Maintenance and upkeep improvements -- \[pre-commit.ci\] pre-commit autoupdate [#824](https://github.com/jupyter/jupyter_client/pull/824) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#821](https://github.com/jupyter/jupyter_client/pull/821) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#820](https://github.com/jupyter/jupyter_client/pull/820) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#818](https://github.com/jupyter/jupyter_client/pull/818) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#816](https://github.com/jupyter/jupyter_client/pull/816) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#815](https://github.com/jupyter/jupyter_client/pull/815) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#812](https://github.com/jupyter/jupyter_client/pull/812) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#810](https://github.com/jupyter/jupyter_client/pull/810) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#809](https://github.com/jupyter/jupyter_client/pull/809) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#807](https://github.com/jupyter/jupyter_client/pull/807) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#824](https://github.com/jupyter/jupyter_client/pull/824) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#821](https://github.com/jupyter/jupyter_client/pull/821) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#820](https://github.com/jupyter/jupyter_client/pull/820) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#818](https://github.com/jupyter/jupyter_client/pull/818) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#816](https://github.com/jupyter/jupyter_client/pull/816) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#815](https://github.com/jupyter/jupyter_client/pull/815) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#812](https://github.com/jupyter/jupyter_client/pull/812) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#810](https://github.com/jupyter/jupyter_client/pull/810) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#809](https://github.com/jupyter/jupyter_client/pull/809) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#807](https://github.com/jupyter/jupyter_client/pull/807) ([@pre-commit-ci](https://github.com/pre-commit-ci)) ### Contributors to this release @@ -544,7 +544,7 @@ No merged PRs ### Maintenance and upkeep improvements - Fix sphinx 5.0 support [#804](https://github.com/jupyter/jupyter_client/pull/804) ([@blink1073](https://github.com/blink1073)) -- \[pre-commit.ci\] pre-commit autoupdate [#799](https://github.com/jupyter/jupyter_client/pull/799) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#799](https://github.com/jupyter/jupyter_client/pull/799) ([@pre-commit-ci](https://github.com/pre-commit-ci)) ### Contributors to this release @@ -576,9 +576,9 @@ No merged PRs ### Maintenance and upkeep improvements -- \[pre-commit.ci\] pre-commit autoupdate [#792](https://github.com/jupyter/jupyter_client/pull/792) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#792](https://github.com/jupyter/jupyter_client/pull/792) ([@pre-commit-ci](https://github.com/pre-commit-ci)) - Use hatch backend [#789](https://github.com/jupyter/jupyter_client/pull/789) ([@blink1073](https://github.com/blink1073)) -- \[pre-commit.ci\] pre-commit autoupdate [#788](https://github.com/jupyter/jupyter_client/pull/788) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#788](https://github.com/jupyter/jupyter_client/pull/788) ([@pre-commit-ci](https://github.com/pre-commit-ci)) - Use flit build backend [#781](https://github.com/jupyter/jupyter_client/pull/781) ([@blink1073](https://github.com/blink1073)) ### Contributors to this release @@ -599,7 +599,7 @@ No merged PRs ### Maintenance and upkeep improvements - Allow bot PRs to be automatically labeled [#784](https://github.com/jupyter/jupyter_client/pull/784) ([@blink1073](https://github.com/blink1073)) -- \[pre-commit.ci\] pre-commit autoupdate [#783](https://github.com/jupyter/jupyter_client/pull/783) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#783](https://github.com/jupyter/jupyter_client/pull/783) ([@pre-commit-ci](https://github.com/pre-commit-ci)) ### Contributors to this release @@ -617,8 +617,8 @@ No merged PRs ### Maintenance and upkeep improvements -- \[pre-commit.ci\] pre-commit autoupdate [#773](https://github.com/jupyter/jupyter_client/pull/773) ([@pre-commit-ci](https://github.com/pre-commit-ci)) -- \[pre-commit.ci\] pre-commit autoupdate [#770](https://github.com/jupyter/jupyter_client/pull/770) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#773](https://github.com/jupyter/jupyter_client/pull/773) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#770](https://github.com/jupyter/jupyter_client/pull/770) ([@pre-commit-ci](https://github.com/pre-commit-ci)) - Improve mypy config [#769](https://github.com/jupyter/jupyter_client/pull/769) ([@blink1073](https://github.com/blink1073)) - Clean up pre-commit [#768](https://github.com/jupyter/jupyter_client/pull/768) ([@blink1073](https://github.com/blink1073)) @@ -635,7 +635,7 @@ No merged PRs ### Maintenance and upkeep improvements - Include py.typed file [#766](https://github.com/jupyter/jupyter_client/pull/766) ([@blink1073](https://github.com/blink1073)) -- \[pre-commit.ci\] pre-commit autoupdate [#765](https://github.com/jupyter/jupyter_client/pull/765) ([@pre-commit-ci](https://github.com/pre-commit-ci)) +- [pre-commit.ci] pre-commit autoupdate [#765](https://github.com/jupyter/jupyter_client/pull/765) ([@pre-commit-ci](https://github.com/pre-commit-ci)) - More Cleanup [#764](https://github.com/jupyter/jupyter_client/pull/764) ([@blink1073](https://github.com/blink1073)) ### Contributors to this release @@ -713,7 +713,7 @@ No merged PRs ### Documentation improvements -- \[DOC\] improve kernel provisioner doc [#730](https://github.com/jupyter/jupyter_client/pull/730) ([@abzymeinsjtu](https://github.com/abzymeinsjtu)) +- [DOC] improve kernel provisioner doc [#730](https://github.com/jupyter/jupyter_client/pull/730) ([@abzymeinsjtu](https://github.com/abzymeinsjtu)) - add changelog for message spec [#525](https://github.com/jupyter/jupyter_client/pull/525) ([@minrk](https://github.com/minrk)) ### Contributors to this release diff --git a/jupyter_client/__init__.py b/jupyter_client/__init__.py index 3b30a6f4..89943796 100644 --- a/jupyter_client/__init__.py +++ b/jupyter_client/__init__.py @@ -1,4 +1,5 @@ """Client-side implementations of the Jupyter protocol""" + from ._version import __version__, protocol_version, protocol_version_info, version_info from .asynchronous import AsyncKernelClient from .blocking import BlockingKernelClient diff --git a/jupyter_client/_version.py b/jupyter_client/_version.py index 411f70c0..2e73bc2b 100644 --- a/jupyter_client/_version.py +++ b/jupyter_client/_version.py @@ -1,4 +1,5 @@ """The version information for jupyter client.""" + import re from typing import Union diff --git a/jupyter_client/adapter.py b/jupyter_client/adapter.py index 677251de..caf11917 100644 --- a/jupyter_client/adapter.py +++ b/jupyter_client/adapter.py @@ -1,4 +1,5 @@ """Adapters for Jupyter msg spec versions.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import json @@ -194,7 +195,7 @@ def object_info_request(self, msg: dict[str, Any]) -> dict[str, Any]: content = msg["content"] code = content["code"] cursor_pos = content["cursor_pos"] - line, _ = code_to_line(code, cursor_pos) + _line, _ = code_to_line(code, cursor_pos) new_content = msg["content"] = {} new_content["oname"] = extract_oname_v4(code, cursor_pos) diff --git a/jupyter_client/asynchronous/client.py b/jupyter_client/asynchronous/client.py index cde8ecaf..abbc8235 100644 --- a/jupyter_client/asynchronous/client.py +++ b/jupyter_client/asynchronous/client.py @@ -1,4 +1,5 @@ """Implements an async kernel client""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -33,7 +34,7 @@ class AsyncKernelClient(KernelClient): raising :exc:`queue.Empty` if no message arrives within ``timeout`` seconds. """ - context = Instance(zmq.asyncio.Context) # type:ignore[arg-type] + context = Instance(zmq.asyncio.Context) # type:ignore[assignment] def _context_default(self) -> zmq.asyncio.Context: self._created_context = True @@ -51,11 +52,11 @@ def _context_default(self) -> zmq.asyncio.Context: wait_for_ready = KernelClient._async_wait_for_ready # The classes to use for the various channels - shell_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[arg-type] - iopub_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[arg-type] - stdin_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[arg-type] - hb_channel_class = Type(HBChannel) # type:ignore[arg-type] - control_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[arg-type] + shell_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[assignment] + iopub_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[assignment] + stdin_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[assignment] + hb_channel_class = Type(HBChannel) # type:ignore[assignment] + control_channel_class = Type(AsyncZMQSocketChannel) # type:ignore[assignment] _recv_reply = KernelClient._async_recv_reply diff --git a/jupyter_client/blocking/client.py b/jupyter_client/blocking/client.py index 5c815eb8..c59b7cc9 100644 --- a/jupyter_client/blocking/client.py +++ b/jupyter_client/blocking/client.py @@ -2,6 +2,7 @@ Useful for test suites and blocking terminal interfaces. """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -48,11 +49,11 @@ class BlockingKernelClient(KernelClient): wait_for_ready = run_sync(KernelClient._async_wait_for_ready) # The classes to use for the various channels - shell_channel_class = Type(ZMQSocketChannel) # type:ignore[arg-type] - iopub_channel_class = Type(ZMQSocketChannel) # type:ignore[arg-type] - stdin_channel_class = Type(ZMQSocketChannel) # type:ignore[arg-type] - hb_channel_class = Type(HBChannel) # type:ignore[arg-type] - control_channel_class = Type(ZMQSocketChannel) # type:ignore[arg-type] + shell_channel_class = Type(ZMQSocketChannel) # type:ignore[assignment] + iopub_channel_class = Type(ZMQSocketChannel) # type:ignore[assignment] + stdin_channel_class = Type(ZMQSocketChannel) # type:ignore[assignment] + hb_channel_class = Type(HBChannel) # type:ignore[assignment] + control_channel_class = Type(ZMQSocketChannel) # type:ignore[assignment] _recv_reply = run_sync(KernelClient._async_recv_reply) diff --git a/jupyter_client/channels.py b/jupyter_client/channels.py index 465dccdf..39f50d01 100644 --- a/jupyter_client/channels.py +++ b/jupyter_client/channels.py @@ -1,4 +1,5 @@ """Base classes to manage a Client's interaction with a running kernel""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio @@ -51,8 +52,8 @@ class HBChannel(Thread): def __init__( self, - context: t.Optional[zmq.Context] = None, - session: t.Optional[Session] = None, + context: zmq.Context | None = None, + session: Session | None = None, address: t.Union[t.Tuple[str, int], str] = "", ) -> None: """Create the heartbeat monitor thread. @@ -146,8 +147,10 @@ def run(self) -> None: """Run the heartbeat thread.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - loop.run_until_complete(self._async_run()) - loop.close() + try: + loop.run_until_complete(self._async_run()) + finally: + loop.close() def pause(self) -> None: """Pause the heartbeat.""" @@ -211,16 +214,16 @@ def __init__(self, socket: zmq.Socket, session: Session, loop: t.Any = None) -> """ super().__init__() - self.socket: t.Optional[zmq.Socket] = socket + self.socket: zmq.Socket | None = socket self.session = session def _recv(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: assert self.socket is not None msg = self.socket.recv_multipart(**kwargs) - ident, smsg = self.session.feed_identities(msg) + _ident, smsg = self.session.feed_identities(msg) return self.session.deserialize(smsg) - def get_msg(self, timeout: t.Optional[float] = None) -> t.Dict[str, t.Any]: + def get_msg(self, timeout: float | None = None) -> t.Dict[str, t.Any]: """Gets a message if there is one that is ready.""" assert self.socket is not None timeout_ms = None if timeout is None else int(timeout * 1000) # seconds to ms @@ -300,7 +303,7 @@ async def _recv(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: # type:ignore[ove return self.session.deserialize(smsg) async def get_msg( # type:ignore[override] - self, timeout: t.Optional[float] = None + self, timeout: float | None = None ) -> t.Dict[str, t.Any]: """Gets a message if there is one that is ready.""" assert self.socket is not None diff --git a/jupyter_client/channelsabc.py b/jupyter_client/channelsabc.py index af053dfa..5ef14fcf 100644 --- a/jupyter_client/channelsabc.py +++ b/jupyter_client/channelsabc.py @@ -1,4 +1,5 @@ """Abstract base classes for kernel client channels""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import abc diff --git a/jupyter_client/client.py b/jupyter_client/client.py index 67c44600..1a0c8b01 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -1,4 +1,5 @@ """Base class to manage the interaction with a running kernel""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio @@ -126,7 +127,7 @@ def __del__(self) -> None: else: if self.log: self.log.debug("Destroying zmq context for %s", self) - self.context.destroy() + self.context.destroy(linger=100) try: super_del = super().__del__ # type:ignore[misc] except AttributeError: @@ -154,7 +155,7 @@ async def _async_get_control_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[ """Get a message from the control channel""" return await ensure_async(self.control_channel.get_msg(*args, **kwargs)) - async def _async_wait_for_ready(self, timeout: t.Optional[float] = None) -> None: + async def _async_wait_for_ready(self, timeout: float | None = None) -> None: """Waits for a response when a client is blocked - Sets future time for timeout @@ -214,7 +215,7 @@ async def _async_wait_for_ready(self, timeout: t.Optional[float] = None) -> None break async def _async_recv_reply( - self, msg_id: str, timeout: t.Optional[float] = None, channel: str = "shell" + self, msg_id: str, timeout: float | None = None, channel: str = "shell" ) -> t.Dict[str, t.Any]: """Receive and return the reply for a given request""" if timeout is not None: @@ -333,6 +334,9 @@ def stop_channels(self) -> None: if self.control_channel.is_alive(): self.control_channel.stop() + if self._created_context and not self.context.closed: + self.context.destroy(linger=100) + @property def channels_running(self) -> bool: """Are any of the channels created and running?""" @@ -426,12 +430,12 @@ async def _async_execute_interactive( code: str, silent: bool = False, store_history: bool = True, - user_expressions: t.Optional[t.Dict[str, t.Any]] = None, - allow_stdin: t.Optional[bool] = None, + user_expressions: t.Dict[str, t.Any] | None = None, + allow_stdin: bool | None = None, stop_on_error: bool = True, - timeout: t.Optional[float] = None, - output_hook: t.Optional[t.Callable] = None, - stdin_hook: t.Optional[t.Callable] = None, + timeout: float | None = None, + output_hook: t.Callable | None = None, + stdin_hook: t.Callable | None = None, ) -> t.Dict[str, t.Any]: """Execute code in the kernel interactively @@ -582,8 +586,8 @@ def execute( code: str, silent: bool = False, store_history: bool = True, - user_expressions: t.Optional[t.Dict[str, t.Any]] = None, - allow_stdin: t.Optional[bool] = None, + user_expressions: t.Dict[str, t.Any] | None = None, + allow_stdin: bool | None = None, stop_on_error: bool = True, ) -> str: """Execute code in the kernel. @@ -644,7 +648,7 @@ def execute( self.shell_channel.send(msg) return msg["header"]["msg_id"] - def complete(self, code: str, cursor_pos: t.Optional[int] = None) -> str: + def complete(self, code: str, cursor_pos: int | None = None) -> str: """Tab complete text in the kernel's namespace. Parameters @@ -667,7 +671,7 @@ def complete(self, code: str, cursor_pos: t.Optional[int] = None) -> str: self.shell_channel.send(msg) return msg["header"]["msg_id"] - def inspect(self, code: str, cursor_pos: t.Optional[int] = None, detail_level: int = 0) -> str: + def inspect(self, code: str, cursor_pos: int | None = None, detail_level: int = 0) -> str: """Get metadata information about an object in the kernel's namespace. It is up to the kernel to determine the appropriate object to inspect. @@ -755,7 +759,7 @@ def kernel_info(self) -> str: self.shell_channel.send(msg) return msg["header"]["msg_id"] - def comm_info(self, target_name: t.Optional[str] = None) -> str: + def comm_info(self, target_name: str | None = None) -> str: """Request comm info Returns diff --git a/jupyter_client/clientabc.py b/jupyter_client/clientabc.py index d003fe17..c9d7a016 100644 --- a/jupyter_client/clientabc.py +++ b/jupyter_client/clientabc.py @@ -1,4 +1,5 @@ """Abstract base class for kernel clients""" + # ----------------------------------------------------------------------------- # Copyright (c) The Jupyter Development Team # diff --git a/jupyter_client/connect.py b/jupyter_client/connect.py index f5d54550..f4667545 100644 --- a/jupyter_client/connect.py +++ b/jupyter_client/connect.py @@ -3,6 +3,7 @@ The :class:`ConnectionFileMixin` class in this module encapsulates the logic related to writing and reading connections files. """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -293,7 +294,7 @@ def tunnel_to_kernel( else: password = getpass("SSH Password for %s: " % sshserver) - for lp, rp in zip(lports, rports): + for lp, rp in zip(lports, rports, strict=False): tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password) return tuple(lports) @@ -717,9 +718,9 @@ def return_port(self, port: int) -> None: __all__ = [ - "write_connection_file", - "find_connection_file", - "tunnel_to_kernel", "KernelConnectionInfo", "LocalPortCache", + "find_connection_file", + "tunnel_to_kernel", + "write_connection_file", ] diff --git a/jupyter_client/consoleapp.py b/jupyter_client/consoleapp.py index 434fdf01..3a7cf980 100644 --- a/jupyter_client/consoleapp.py +++ b/jupyter_client/consoleapp.py @@ -1,9 +1,10 @@ -""" A minimal application base mixin for all ZMQ based IPython frontends. +"""A minimal application base mixin for all ZMQ based IPython frontends. This is not a complete console app, as subprocess will not be able to receive input, there is no real readline support, among other limitations. This is a refactoring of what used to be the IPython/qt/console/qtconsoleapp.py """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import atexit diff --git a/jupyter_client/ioloop/manager.py b/jupyter_client/ioloop/manager.py index 5a6c8aec..5e446a24 100644 --- a/jupyter_client/ioloop/manager.py +++ b/jupyter_client/ioloop/manager.py @@ -1,4 +1,5 @@ """A kernel manager with a tornado IOLoop""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import typing as t diff --git a/jupyter_client/ioloop/restarter.py b/jupyter_client/ioloop/restarter.py index 64b50840..a21d78aa 100644 --- a/jupyter_client/ioloop/restarter.py +++ b/jupyter_client/ioloop/restarter.py @@ -3,6 +3,7 @@ This watches a kernel's state using KernelManager.is_alive and auto restarts the kernel if it dies. """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import time diff --git a/jupyter_client/jsonutil.py b/jupyter_client/jsonutil.py index 157fc4f4..ad6ba6c8 100644 --- a/jupyter_client/jsonutil.py +++ b/jupyter_client/jsonutil.py @@ -1,4 +1,5 @@ """Utilities to manipulate JSON objects.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import math @@ -9,7 +10,7 @@ from binascii import b2a_base64 from collections.abc import Iterable from datetime import date, datetime -from typing import Any, Optional, Union +from typing import Any, Union from dateutil.parser import isoparse as _dateutil_parse from dateutil.tz import tzlocal @@ -51,7 +52,7 @@ def _ensure_tzinfo(dt: datetime) -> datetime: return dt -def parse_date(s: Optional[str]) -> Optional[Union[str, datetime]]: +def parse_date(s: str | None) -> Union[str, datetime] | None: """parse an ISO8601 date string If it is None or not a valid ISO8601 timestamp, @@ -74,7 +75,7 @@ def extract_dates(obj: Any) -> Any: for k, v in obj.items(): new_obj[k] = extract_dates(v) obj = new_obj - elif isinstance(obj, (list, tuple)): + elif isinstance(obj, list | tuple): obj = [extract_dates(o) for o in obj] elif isinstance(obj, str): obj = parse_date(obj) @@ -87,7 +88,7 @@ def squash_dates(obj: Any) -> Any: obj = dict(obj) # don't clobber for k, v in obj.items(): obj[k] = squash_dates(v) - elif isinstance(obj, (list, tuple)): + elif isinstance(obj, list | tuple): obj = [squash_dates(o) for o in obj] elif isinstance(obj, datetime): obj = obj.isoformat() @@ -188,7 +189,7 @@ def json_clean(obj: Any) -> Any: out[str(k)] = json_clean(v) return out - if isinstance(obj, (datetime, date)): + if isinstance(obj, datetime | date): return obj.strftime(ISO8601) # we don't understand it, it's probably an unserializable object diff --git a/jupyter_client/kernelapp.py b/jupyter_client/kernelapp.py index 5d43c64e..13afdaba 100644 --- a/jupyter_client/kernelapp.py +++ b/jupyter_client/kernelapp.py @@ -1,4 +1,5 @@ """An application to launch a kernel by name in a local subprocess.""" + import os import signal import typing as t diff --git a/jupyter_client/kernelspec.py b/jupyter_client/kernelspec.py index b903ee6d..97bb005e 100644 --- a/jupyter_client/kernelspec.py +++ b/jupyter_client/kernelspec.py @@ -1,4 +1,5 @@ """Tools for managing kernel specs""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations diff --git a/jupyter_client/kernelspecapp.py b/jupyter_client/kernelspecapp.py index 186aa9cf..27a5a17b 100644 --- a/jupyter_client/kernelspecapp.py +++ b/jupyter_client/kernelspecapp.py @@ -1,4 +1,5 @@ """Apps for managing kernel specs.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations diff --git a/jupyter_client/launcher.py b/jupyter_client/launcher.py index 47350b86..06f0cb06 100644 --- a/jupyter_client/launcher.py +++ b/jupyter_client/launcher.py @@ -1,23 +1,24 @@ """Utilities for launching kernels""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os import sys import warnings from subprocess import PIPE, Popen -from typing import Any, Optional +from typing import Any from traitlets.log import get_logger def launch_kernel( cmd: list[str], - stdin: Optional[int] = None, - stdout: Optional[int] = None, - stderr: Optional[int] = None, - env: Optional[dict[str, str]] = None, + stdin: int | None = None, + stdout: int | None = None, + stderr: int | None = None, + env: dict[str, str] | None = None, independent: bool = False, - cwd: Optional[str] = None, + cwd: str | None = None, **kw: Any, ) -> Popen: """Launches a localhost kernel, binding to the specified ports. diff --git a/jupyter_client/localinterfaces.py b/jupyter_client/localinterfaces.py index 9f0a8608..42fda961 100644 --- a/jupyter_client/localinterfaces.py +++ b/jupyter_client/localinterfaces.py @@ -1,4 +1,5 @@ """Utilities for identifying local IP addresses.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -7,9 +8,9 @@ import re import socket import subprocess -from collections.abc import Iterable, Mapping, Sequence +from collections.abc import Callable, Iterable, Mapping, Sequence from subprocess import PIPE, Popen -from typing import Any, Callable +from typing import Any from warnings import warn LOCAL_IPS: list[str] = [] @@ -180,7 +181,7 @@ def _load_ips_psutil() -> None: def _load_ips_netifaces() -> None: """load ip addresses with netifaces""" - import netifaces # type: ignore[import-not-found] + import netifaces addr_dict: dict[str, list[str]] = {} diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py index 088acd6c..58ef0d0d 100644 --- a/jupyter_client/manager.py +++ b/jupyter_client/manager.py @@ -1,4 +1,5 @@ """Base class to manage a running kernel""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio @@ -104,7 +105,7 @@ class KernelManager(ConnectionFileMixin): This version starts kernels with Popen. """ - _ready: t.Optional[t.Union[Future, CFuture]] + _ready: t.Union[Future, CFuture] | None def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: """Initialize a kernel manager.""" @@ -152,7 +153,7 @@ def _client_class_changed(self, change: t.Dict[str, DottedObjectName]) -> None: # The kernel provisioner with which this KernelManager is communicating. # This will generally be a LocalProvisioner instance unless the kernelspec # indicates otherwise. - provisioner: t.Optional[KernelProvisionerBase] = None + provisioner: KernelProvisionerBase | None = None kernel_spec_manager: Instance = Instance(kernelspec.KernelSpecManager) @@ -187,10 +188,10 @@ def _kernel_name_changed(self, change: t.Dict[str, str]) -> None: if change["new"] == "python": self.kernel_name = kernelspec.NATIVE_KERNEL_NAME - _kernel_spec: t.Optional[kernelspec.KernelSpec] = None + _kernel_spec: kernelspec.KernelSpec | None = None @property - def kernel_spec(self) -> t.Optional[kernelspec.KernelSpec]: + def kernel_spec(self) -> kernelspec.KernelSpec | None: if self._kernel_spec is None and self.kernel_name != "": self._kernel_spec = self.kernel_spec_manager.get_kernel_spec(self.kernel_name) return self._kernel_spec @@ -300,7 +301,7 @@ def update_env(self, *, env: t.Dict[str, str]) -> None: ): self._launch_args["env"].update(env) # type: ignore [unreachable] - def format_kernel_cmd(self, extra_arguments: t.Optional[t.List[str]] = None) -> t.List[str]: + def format_kernel_cmd(self, extra_arguments: t.List[str] | None = None) -> t.List[str]: """Replace templated args (e.g. {connection_file})""" extra_arguments = extra_arguments or [] assert self.kernel_spec is not None @@ -390,7 +391,7 @@ async def _async_pre_start_kernel( self.kernel_id = self.kernel_id or kw.pop("kernel_id", str(uuid.uuid4())) # save kwargs for use in restart # assigning Traitlets Dicts to Dict make mypy unhappy but is ok - self._launch_args = kw.copy() # type:ignore [assignment] + self._launch_args = kw.copy() if self.provisioner is None: # will not be None on restarts self.provisioner = KPF.instance(parent=self.parent).create_provisioner_instance( self.kernel_id, @@ -456,7 +457,7 @@ async def _async_request_shutdown(self, restart: bool = False) -> None: async def _async_finish_shutdown( self, - waittime: t.Optional[float] = None, + waittime: float | None = None, pollinterval: float = 0.1, restart: bool = False, ) -> None: diff --git a/jupyter_client/managerabc.py b/jupyter_client/managerabc.py index c74ea1dc..97686fbd 100644 --- a/jupyter_client/managerabc.py +++ b/jupyter_client/managerabc.py @@ -1,4 +1,5 @@ """Abstract base class for kernel managers.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import abc diff --git a/jupyter_client/multikernelmanager.py b/jupyter_client/multikernelmanager.py index 44f20407..494b7903 100644 --- a/jupyter_client/multikernelmanager.py +++ b/jupyter_client/multikernelmanager.py @@ -1,4 +1,5 @@ """A kernel manager for multiple kernels""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -123,7 +124,7 @@ def __del__(self) -> None: if self._created_context and self.context and not self.context.closed: if self.log: self.log.debug("Destroying zmq context for %s", self) - self.context.destroy() + self.context.destroy(linger=1000) try: super_del = super().__del__ # type:ignore[misc] except AttributeError: diff --git a/jupyter_client/provisioning/factory.py b/jupyter_client/provisioning/factory.py index 53f21074..8632d185 100644 --- a/jupyter_client/provisioning/factory.py +++ b/jupyter_client/provisioning/factory.py @@ -1,16 +1,13 @@ """Kernel Provisioner Classes""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import glob -import sys -from os import getenv, path -from typing import Any # See compatibility note on `group` keyword in https://docs.python.org/3/library/importlib.metadata.html#entry-points -if sys.version_info < (3, 10): # pragma: no cover - from importlib_metadata import EntryPoint, entry_points # type:ignore[import-not-found] -else: # pragma: no cover - from importlib.metadata import EntryPoint, entry_points +from importlib.metadata import EntryPoint, entry_points +from os import getenv, path +from typing import Any from traitlets.config import SingletonConfigurable, Unicode, default @@ -196,5 +193,5 @@ def _get_provisioner(self, name: str) -> EntryPoint: return EntryPoint( "local-provisioner", "jupyter_client.provisioning", "LocalProvisioner" ) - - raise + err_message = "Was unable to find a provisioner" + raise RuntimeError(err_message) diff --git a/jupyter_client/provisioning/local_provisioner.py b/jupyter_client/provisioning/local_provisioner.py index 3cb57fd0..9f1086f1 100644 --- a/jupyter_client/provisioning/local_provisioner.py +++ b/jupyter_client/provisioning/local_provisioner.py @@ -1,11 +1,12 @@ """Kernel Provisioner Classes""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio import os import signal import sys -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any from ..connect import KernelConnectionInfo, LocalPortCache from ..launcher import launch_kernel @@ -13,7 +14,7 @@ from .provisioner_base import KernelProvisionerBase -class LocalProvisioner(KernelProvisionerBase): # type:ignore[misc] +class LocalProvisioner(KernelProvisionerBase): """ :class:`LocalProvisioner` is a concrete class of ABC :py:class:`KernelProvisionerBase` and is the out-of-box default implementation used when no kernel provisioner is @@ -36,14 +37,14 @@ class LocalProvisioner(KernelProvisionerBase): # type:ignore[misc] def has_process(self) -> bool: return self.process is not None - async def poll(self) -> Optional[int]: + async def poll(self) -> int | None: """Poll the provisioner.""" ret = 0 if self.process: ret = self.process.poll() # type:ignore[unreachable] return ret - async def wait(self) -> Optional[int]: + async def wait(self) -> int | None: """Wait for the provisioner process.""" ret = 0 if self.process: @@ -128,14 +129,19 @@ def _tolerate_no_process(os_error: OSError) -> None: # has already terminated. Ignore it. if sys.platform == "win32": if os_error.winerror != 5: - raise + err_message = f"Invalid Error, expecting error number to be 5, got {os_error}" + raise ValueError(err_message) + # On Unix, we may get an ESRCH error (or ProcessLookupError instance) if # the process has already terminated. Ignore it. else: from errno import ESRCH if not isinstance(os_error, ProcessLookupError) or os_error.errno != ESRCH: - raise + err_message = ( + f"Invalid Error, expecting ProcessLookupError or ESRCH, got {os_error}" + ) + raise ValueError(err_message) async def cleanup(self, restart: bool = False) -> None: """Clean up the resources used by the provisioner and optionally restart.""" diff --git a/jupyter_client/provisioning/provisioner_base.py b/jupyter_client/provisioning/provisioner_base.py index e5e33f67..8056a1ca 100644 --- a/jupyter_client/provisioning/provisioner_base.py +++ b/jupyter_client/provisioning/provisioner_base.py @@ -1,9 +1,10 @@ """Kernel Provisioner Classes""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os from abc import ABC, ABCMeta, abstractmethod -from typing import Any, Optional, Union +from typing import Any, Union from traitlets.config import Instance, LoggingConfigurable, Unicode @@ -14,9 +15,7 @@ class KernelProvisionerMeta(ABCMeta, type(LoggingConfigurable)): # type: ignore pass -class KernelProvisionerBase( # type:ignore[misc] - ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta -): +class KernelProvisionerBase(ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta): # type: ignore[metaclass] """ Abstract base class defining methods for KernelProvisioner classes. @@ -45,7 +44,7 @@ def has_process(self) -> bool: pass @abstractmethod - async def poll(self) -> Optional[int]: + async def poll(self) -> int | None: """ Checks if kernel process is still running. @@ -55,7 +54,7 @@ async def poll(self) -> Optional[int]: pass @abstractmethod - async def wait(self) -> Optional[int]: + async def wait(self) -> int | None: """ Waits for kernel process to terminate. diff --git a/jupyter_client/restarter.py b/jupyter_client/restarter.py index d41890f6..09bff06e 100644 --- a/jupyter_client/restarter.py +++ b/jupyter_client/restarter.py @@ -5,6 +5,7 @@ It is an incomplete base class, and must be subclassed. """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations diff --git a/jupyter_client/runapp.py b/jupyter_client/runapp.py index bea5d5cf..461eca8a 100644 --- a/jupyter_client/runapp.py +++ b/jupyter_client/runapp.py @@ -1,4 +1,5 @@ """A Jupyter console app to run files.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -37,14 +38,14 @@ frontend_flags = set(frontend_flags_dict.keys()) -class RunApp(JupyterApp, JupyterConsoleApp): # type:ignore[misc] +class RunApp(JupyterApp, JupyterConsoleApp): """An Jupyter Console app to run files.""" version = __version__ name = "jupyter run" description = """Run Jupyter kernel code.""" - flags = Dict(flags) # type:ignore[assignment] - aliases = Dict(aliases) # type:ignore[assignment] + flags = Dict(flags) + aliases = Dict(aliases) frontend_aliases = Any(frontend_aliases) frontend_flags = Any(frontend_flags) kernel_timeout = Float( diff --git a/jupyter_client/session.py b/jupyter_client/session.py index c387cd06..c58067ad 100644 --- a/jupyter_client/session.py +++ b/jupyter_client/session.py @@ -8,6 +8,7 @@ Sessions. * A Message object for convenience that allows attribute-access to the msg dict. """ + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations @@ -758,7 +759,7 @@ def send( content: dict[str, t.Any] | None = None, parent: dict[str, t.Any] | None = None, ident: bytes | list[bytes] | None = None, - buffers: list[bytes] | None = None, + buffers: list[bytes | memoryview[bytes]] | None = None, track: bool = False, header: dict[str, t.Any] | None = None, metadata: dict[str, t.Any] | None = None, @@ -811,10 +812,10 @@ def send( track = False if isinstance(stream, zmq.asyncio.Socket): - assert stream is not None # type:ignore[unreachable] + assert stream is not None stream = zmq.Socket.shadow(stream.underlying) - if isinstance(msg_or_type, (Message, dict)): + if isinstance(msg_or_type, Message | dict): # We got a Message or message dict, not a msg_type so don't # build a new Message. msg = msg_or_type @@ -841,16 +842,14 @@ def send( except TypeError as e: emsg = "Buffer objects must support the buffer protocol." raise TypeError(emsg) from e - # memoryview.contiguous is new in 3.3, - # just skip the check on Python 2 - if hasattr(view, "contiguous") and not view.contiguous: + if not view.contiguous: # zmq requires memoryviews to be contiguous raise ValueError("Buffer %i (%r) is not contiguous" % (idx, buf)) if self.adapt_version: msg = adapt(msg, self.adapt_version) to_send = self.serialize(msg, ident) - to_send.extend(buffers) + to_send.extend(buffers) # type: ignore[arg-type] longest = max([len(s) for s in to_send]) copy = longest < self.copy_threshold diff --git a/jupyter_client/ssh/forward.py b/jupyter_client/ssh/forward.py index dc9e5099..0fd3d4b2 100644 --- a/jupyter_client/ssh/forward.py +++ b/jupyter_client/ssh/forward.py @@ -4,6 +4,7 @@ forwarding (the openssh -L option) from a local port through a tunneled connection to a destination reachable from the SSH server machine. """ + # # This file is adapted from a paramiko demo, and thus licensed under LGPL 2.1. # Original Copyright (C) 2003-2007 Robey Pointer @@ -65,7 +66,7 @@ def handle(self): f"Connected! Tunnel open {self.request.getpeername()!r} -> {chan.getpeername()!r} -> {(self.chain_host, self.chain_port)!r}" ) while True: - r, w, x = select.select([self.request, chan], [], []) + r, _w, _x = select.select([self.request, chan], [], []) if self.request in r: data = self.request.recv(1024) if len(data) == 0: diff --git a/jupyter_client/ssh/tunnel.py b/jupyter_client/ssh/tunnel.py index 3b1b533c..5f4c7ca5 100644 --- a/jupyter_client/ssh/tunnel.py +++ b/jupyter_client/ssh/tunnel.py @@ -1,6 +1,7 @@ """Basic ssh tunnel utilities, and convenience functions for tunneling zeromq connections. """ + # Copyright (C) 2010-2011 IPython Development Team # Copyright (C) 2011- PyZMQ Developers # @@ -16,6 +17,7 @@ import warnings from getpass import getpass, getuser from multiprocessing import Process +from types import ModuleType from typing import Any, cast try: @@ -33,8 +35,9 @@ class SSHException(Exception): # type:ignore[no-redef] # noqa else: from .forward import forward_tunnel +pexpect: ModuleType | None try: - import pexpect # type: ignore[import-untyped] + import pexpect except ImportError: pexpect = None @@ -302,6 +305,7 @@ def openssh_tunnel( def _stop_tunnel(cmd: Any) -> None: + assert pexpect is not None pexpect.run(cmd) @@ -438,9 +442,9 @@ def _paramiko_tunnel( __all__ = [ - "tunnel_connection", - "ssh_tunnel", "openssh_tunnel", "paramiko_tunnel", + "ssh_tunnel", "try_passwordless_ssh", + "tunnel_connection", ] diff --git a/jupyter_client/threaded.py b/jupyter_client/threaded.py index b0bf5faf..a58f556e 100644 --- a/jupyter_client/threaded.py +++ b/jupyter_client/threaded.py @@ -1,13 +1,14 @@ -""" Defines a KernelClient that provides thread-safe sockets with async callbacks on message +"""Defines a KernelClient that provides thread-safe sockets with async callbacks on message replies. """ + import asyncio import atexit import time from concurrent.futures import Future from functools import partial from threading import Thread -from typing import Any, Optional +from typing import Any import zmq from tornado.ioloop import IOLoop @@ -35,9 +36,9 @@ class ThreadedZMQSocketChannel: def __init__( self, - socket: Optional[zmq.Socket], - session: Optional[Session], - loop: Optional[IOLoop], + socket: zmq.Socket | None, + session: Session | None, + loop: IOLoop | None, ) -> None: """Create a channel. @@ -143,7 +144,7 @@ def _handle_recv(self, msg_list: list) -> None: """ assert self.ioloop is not None assert self.session is not None - ident, smsg = self.session.feed_identities(msg_list) + _ident, smsg = self.session.feed_identities(msg_list) msg = self.session.deserialize(smsg) # let client inspect messages if self._inspect: @@ -262,8 +263,10 @@ async def assign_ioloop() -> None: self._start_future.set_exception(e) else: self._start_future.set_result(None) - - loop.run_until_complete(self._async_run()) + try: + loop.run_until_complete(self._async_run()) + finally: + loop.close() async def _async_run(self) -> None: """Run forever (until self._exiting is set)""" @@ -298,7 +301,7 @@ class ThreadedKernelClient(KernelClient): """A KernelClient that provides thread-safe sockets with async callbacks on message replies.""" @property - def ioloop(self) -> Optional[IOLoop]: # type:ignore[override] + def ioloop(self) -> IOLoop | None: # type:ignore[override] if self.ioloop_thread: return self.ioloop_thread.ioloop return None @@ -334,11 +337,11 @@ def stop_channels(self) -> None: if self.ioloop_thread and self.ioloop_thread.is_alive(): self.ioloop_thread.stop() - iopub_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[arg-type] - shell_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[arg-type] - stdin_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[arg-type] - hb_channel_class = Type(HBChannel) # type:ignore[arg-type] - control_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[arg-type] + iopub_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[assignment] + shell_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[assignment] + stdin_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[assignment] + hb_channel_class = Type(HBChannel) # type:ignore[assignment] + control_channel_class = Type(ThreadedZMQSocketChannel) # type:ignore[assignment] def is_alive(self) -> bool: """Is the kernel process still running?""" diff --git a/jupyter_client/utils.py b/jupyter_client/utils.py index d6cfefd2..08a139ec 100644 --- a/jupyter_client/utils.py +++ b/jupyter_client/utils.py @@ -3,6 +3,7 @@ - provides utility wrappers to run asynchronous functions in a blocking environment. - vendor functions from ipython_genutils that should be retired at some point. """ + from __future__ import annotations import os diff --git a/jupyter_client/win_interrupt.py b/jupyter_client/win_interrupt.py index 1ba2c3af..9b4fbfbc 100644 --- a/jupyter_client/win_interrupt.py +++ b/jupyter_client/win_interrupt.py @@ -3,6 +3,7 @@ The child needs to explicitly listen for this - see ipykernel.parentpoller.ParentPollerWindows for a Python implementation. """ + import ctypes from typing import Any diff --git a/pyproject.toml b/pyproject.toml index 2e9421a1..67ce34b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,13 +18,12 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3" ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ - "importlib_metadata>=4.8.3;python_version<\"3.10\"", - "jupyter_core>=4.12,!=5.0.*", + "jupyter_core>=5.1", "python-dateutil>=2.8.2", - "pyzmq>=23.0", - "tornado>=6.2", + "pyzmq>=25.0", + "tornado>=6.4.1", "traitlets>=5.3", ] @@ -52,8 +51,8 @@ test = [ "mypy", "paramiko; sys_platform == 'win32'", "pre-commit", - "pytest<8.2.0", - "pytest-jupyter[client]>=0.4.1", + "pytest", + "pytest-jupyter[client]>=0.6.2", "pytest-cov", "pytest-timeout", ] @@ -109,12 +108,12 @@ dependencies = ["pre-commit"] detached = true [tool.hatch.envs.lint.scripts] build = [ - "pre-commit run --all-files ruff", + "pre-commit run --all-files ruff-check", "pre-commit run --all-files ruff-format" ] [tool.pytest.ini_options] -minversion = "6.0" +minversion = "7.0" xfail_strict = true log_cli_level = "info" addopts = [ @@ -161,7 +160,7 @@ source = ["jupyter_client"] [tool.mypy] files = "jupyter_client" -python_version = "3.8" +python_version = "3.10" strict = true disallow_any_generics = false enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] @@ -243,6 +242,11 @@ ignore = [ "UP006", # non-pep604-annotation "UP007", + #UP031 Use format specifiers instead of percent format, + "UP031", + # `import` should be at the top-level of a file + "PLC0415" + ] unfixable = [ diff --git a/tests/problemkernel.py b/tests/problemkernel.py index a20cf708..e126011b 100644 --- a/tests/problemkernel.py +++ b/tests/problemkernel.py @@ -1,4 +1,5 @@ """Test kernel for signalling subprocesses""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os diff --git a/tests/signalkernel.py b/tests/signalkernel.py index 65fdb687..6f5ec1de 100644 --- a/tests/signalkernel.py +++ b/tests/signalkernel.py @@ -1,4 +1,5 @@ """Test kernel for signalling subprocesses""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 807e46ca..a496d880 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -1,4 +1,5 @@ """Tests for adapting Jupyter msg spec versions""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import copy @@ -101,7 +102,7 @@ def test_execute_reply(self): "payload": [{"source": "page", "text": "blah"}], }, ) - v4, v5 = self.adapt(msg) + _v4, v5 = self.adapt(msg) v5c = v5["content"] self.assertNotIn("user_variables", v5c) self.assertEqual(v5c["user_expressions"], {"a": 1, "a+a": 2}) diff --git a/tests/test_client.py b/tests/test_client.py index 53ae5253..3e6e1285 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,11 +1,10 @@ """Tests for the KernelClient""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os -import platform -import sys from threading import Event -from unittest import TestCase, mock +from unittest import TestCase import pytest from IPython.utils.capture import capture_output @@ -16,7 +15,7 @@ from jupyter_client.manager import KernelManager, start_new_async_kernel, start_new_kernel from jupyter_client.threaded import ThreadedKernelClient, ThreadedZMQSocketChannel -TIMEOUT = 30 +TIMEOUT = 60 pjoin = os.path.join @@ -123,13 +122,15 @@ def _check_reply(self, reply_type, reply): assert reply["header"]["msg_type"] == reply_type + "_reply" assert reply["parent_header"]["msg_type"] == reply_type + "_request" - @pytest.mark.skipif( - sys.platform != "linux" or platform.python_implementation().lower() == "pypy", - reason="only works with cpython on ubuntu in ci", - ) async def test_input_request(self, kc): - with mock.patch("builtins.input", return_value="test\n"): - reply = await kc.execute_interactive("a = input()", timeout=TIMEOUT) + def handle_stdin(msg): + kc.input("test") + + reply = await kc.execute_interactive( + "a = input()", + stdin_hook=handle_stdin, + timeout=TIMEOUT, + ) assert reply["content"]["status"] == "ok" async def test_output_hook(self, kc): diff --git a/tests/test_connect.py b/tests/test_connect.py index c02015a6..148973eb 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -1,4 +1,5 @@ """Tests for kernel connection utilities""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import json diff --git a/tests/test_consoleapp.py b/tests/test_consoleapp.py index 753ce9b1..37173b84 100644 --- a/tests/test_consoleapp.py +++ b/tests/test_consoleapp.py @@ -1,4 +1,5 @@ """Tests for the JupyterConsoleApp""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os diff --git a/tests/test_jsonutil.py b/tests/test_jsonutil.py index d7d9ae90..9f31dd6e 100644 --- a/tests/test_jsonutil.py +++ b/tests/test_jsonutil.py @@ -1,4 +1,5 @@ """Test suite for our JSON utilities.""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import datetime @@ -166,7 +167,7 @@ def test_json_default(): (True, None), (False, None), (None, None), - ({"key": b"\xFF"}, {"key": "/w=="}), + ({"key": b"\xff"}, {"key": "/w=="}), # Containers ([1, 2], None), ((1, 2), [1, 2]), diff --git a/tests/test_kernelmanager.py b/tests/test_kernelmanager.py index f2d749eb..f1881e19 100644 --- a/tests/test_kernelmanager.py +++ b/tests/test_kernelmanager.py @@ -1,4 +1,5 @@ """Tests for the KernelManager""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio @@ -7,6 +8,7 @@ import os import signal import sys +import sysconfig import time from subprocess import PIPE @@ -21,7 +23,10 @@ pjoin = os.path.join -TIMEOUT = 60 +TIMEOUT = 120 + + +is_freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) @pytest.fixture(params=["tcp", "ipc"]) @@ -123,9 +128,6 @@ class TestKernelManagerShutDownGracefully: @pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals") @pytest.mark.parametrize(*parameters) def test_signal_kernel_subprocesses(self, name, install, expected): - # ipykernel doesn't support 3.6 and this test uses async shutdown_request - if expected == _ShutdownStatus.ShutdownRequest and sys.version_info < (3, 7): - pytest.skip() install() km, kc = start_new_kernel(kernel_name=name) assert km._shutdown_status == _ShutdownStatus.Unset @@ -327,6 +329,7 @@ def test_start_sequence_kernels(self, config, install_kernel): self._run_signaltest_lifecycle(config) self._run_signaltest_lifecycle(config) + @pytest.mark.xfail(is_freethreaded, reason="Fail on free-threaded python") @pytest.mark.timeout(TIMEOUT + 10) def test_start_parallel_thread_kernels(self, config, install_kernel): if config.KernelManager.transport == "ipc": # FIXME @@ -339,14 +342,11 @@ def test_start_parallel_thread_kernels(self, config, install_kernel): future1.result() future2.result() + @pytest.mark.skipif(sys.version_info > (3, 14), reason="Zmq closing socket issues on 3.14+") @pytest.mark.timeout(TIMEOUT) - @pytest.mark.skipif( - (sys.platform == "darwin") and (sys.version_info >= (3, 6)) and (sys.version_info < (3, 8)), - reason='"Bad file descriptor" error', - ) def test_start_parallel_process_kernels(self, config, install_kernel): - if config.KernelManager.transport == "ipc": # FIXME - pytest.skip("IPC transport is currently not working for this test!") + if config.KernelManager.transport in ("ipc", "tcp"): # FIXME + pytest.skip("IPC/TCP transport is currently not working for this test!") self._run_signaltest_lifecycle(config) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread_executor: future1 = thread_executor.submit(self._run_signaltest_lifecycle, config) @@ -356,10 +356,6 @@ def test_start_parallel_process_kernels(self, config, install_kernel): future1.result() @pytest.mark.timeout(TIMEOUT) - @pytest.mark.skipif( - (sys.platform == "darwin") and (sys.version_info >= (3, 6)) and (sys.version_info < (3, 8)), - reason='"Bad file descriptor" error', - ) def test_start_sequence_process_kernels(self, config, install_kernel): if config.KernelManager.transport == "ipc": # FIXME pytest.skip("IPC transport is currently not working for this test!") @@ -407,6 +403,13 @@ def execute(cmd): km.shutdown_kernel() assert km.context.closed kc.stop_channels() + # close the event loop, if there is one + try: + loop = asyncio.get_event_loop() + except RuntimeError: + pass + else: + loop.close() class TestAsyncKernelManager: @@ -444,7 +447,7 @@ async def test_get_connect_info(self, async_km): ) assert keys == expected - @pytest.mark.timeout(10) + @pytest.mark.timeout(60) @pytest.mark.skipif(sys.platform == "win32", reason="Windows doesn't support signals") async def test_signal_kernel_subprocesses(self, install_kernel, jp_start_kernel): km, kc = await jp_start_kernel("signaltest") @@ -490,7 +493,7 @@ async def execute(cmd): # verify that subprocesses were interrupted assert reply["user_expressions"]["poll"] == [-signal.SIGINT] * N - @pytest.mark.timeout(10) + @pytest.mark.timeout(30) async def test_start_new_async_kernel(self, install_kernel, jp_start_kernel): km, kc = await jp_start_kernel("signaltest") is_alive = await km.is_alive() diff --git a/tests/test_kernelspec.py b/tests/test_kernelspec.py index 480d13ae..2903172d 100644 --- a/tests/test_kernelspec.py +++ b/tests/test_kernelspec.py @@ -1,4 +1,5 @@ """Tests for the KernelSpecManager""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import copy diff --git a/tests/test_kernelspecapp.py b/tests/test_kernelspecapp.py index 119b6fb6..07a3efe1 100644 --- a/tests/test_kernelspecapp.py +++ b/tests/test_kernelspecapp.py @@ -1,4 +1,5 @@ """Tests for the kernelspecapp""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os diff --git a/tests/test_manager.py b/tests/test_manager.py index 23c246af..bac6629c 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,4 +1,5 @@ """Tests for KernelManager""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import os diff --git a/tests/test_multikernelmanager.py b/tests/test_multikernelmanager.py index b3fc5797..d0b3ee32 100644 --- a/tests/test_multikernelmanager.py +++ b/tests/test_multikernelmanager.py @@ -1,14 +1,17 @@ """Tests for the notebook kernel and session manager.""" + import asyncio import concurrent.futures import os import sys +import sysconfig import uuid from asyncio import ensure_future from subprocess import PIPE from unittest import TestCase import pytest +import zmq from jupyter_core import paths from tornado.testing import AsyncTestCase, gen_test from traitlets.config.loader import Config @@ -26,7 +29,9 @@ skip_win32, ) -TIMEOUT = 30 +TIMEOUT = 90 + +is_freethreaded = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) async def now(awaitable): @@ -38,7 +43,12 @@ async def now(awaitable): class TestKernelManager(TestCase): + def tearDown(self): + zmq.Context.instance().destroy(linger=0) + return super().tearDown() + # static so picklable for multiprocessing on Windows + @staticmethod def _get_tcp_km(): c = Config() @@ -83,6 +93,7 @@ def _run_lifecycle(km, test_kid=None): km.shutdown_kernel(kid, now=True) assert kid not in km, f"{kid} not in {km}" kc.stop_channels() + km.context.destroy(linger=0) def _run_cinfo(self, km, transport, ip): kid = km.start_kernel(stdout=PIPE, stderr=PIPE) @@ -101,6 +112,7 @@ def _run_cinfo(self, km, transport, ip): stream = km.connect_hb(kid) stream.close() km.shutdown_kernel(kid, now=True) + km.context.destroy(linger=0) # static so picklable for multiprocessing on Windows @classmethod @@ -120,6 +132,7 @@ def test_shutdown_all(self): self.assertNotIn(kid, km) # shutdown again is okay, because we have no kernels km.shutdown_all() + km.context.destroy(linger=0) def test_tcp_cinfo(self): km = self._get_tcp_km() @@ -150,11 +163,12 @@ def test_start_sequence_ipc_kernels(self): def tcp_lifecycle_with_loop(self): # Ensure each thread has an event loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - self.test_tcp_lifecycle() - loop.close() + async def _task(): + return self.test_tcp_lifecycle() + + asyncio.run(_task()) + @pytest.mark.skipif(is_freethreaded, reason="Fail on free-threaded python") def test_start_parallel_thread_kernels(self): self.test_tcp_lifecycle() @@ -164,10 +178,6 @@ def test_start_parallel_thread_kernels(self): future1.result() future2.result() - @pytest.mark.skipif( - (sys.platform == "darwin") and (sys.version_info >= (3, 6)) and (sys.version_info < (3, 8)), - reason='"Bad file descriptor" error', - ) @pytest.mark.skipif( sys.platform == "linux", reason="Kernel refuses to start in process pool", @@ -249,7 +259,7 @@ def test_stream_on_recv(self): def record_activity(msg_list): nonlocal called """Record an IOPub message arriving from a kernel""" - idents, fed_msg_list = session.feed_identities(msg_list) + _idents, fed_msg_list = session.feed_identities(msg_list) msg = session.deserialize(fed_msg_list, content=False) msg_type = msg["header"]["msg_type"] @@ -264,12 +274,16 @@ def record_activity(msg_list): time.sleep(0.1) if called: break - + stream.close() client.stop_channels() km.shutdown_kernel(now=True) class TestAsyncKernelManager(AsyncTestCase): + def tearDown(self): + zmq.Context.instance().destroy(linger=0) + return super().tearDown() + # static so picklable for multiprocessing on Windows @staticmethod def _get_tcp_km(): @@ -321,6 +335,8 @@ async def _run_lifecycle(km, test_kid=None): assert isinstance(k, AsyncKernelManager) await km.shutdown_kernel(kid, now=True) assert kid not in km, f"{kid} not in {km}" + await km.shutdown_all() + km.context.destroy(linger=100) async def _run_cinfo(self, km, transport, ip): kid = await km.start_kernel(stdout=PIPE, stderr=PIPE) @@ -340,6 +356,7 @@ async def _run_cinfo(self, km, transport, ip): stream.close() await km.shutdown_kernel(kid, now=True) self.assertNotIn(kid, km) + km.context.destroy(linger=0) @gen_test async def test_tcp_lifecycle(self): @@ -391,6 +408,7 @@ async def test_shutdown_all_while_starting(self): self.assertNotIn(kid, km) # shutdown again is okay, because we have no kernels await km.shutdown_all() + km.context.destroy(linger=0) @gen_test async def test_use_pending_kernels(self): @@ -411,6 +429,7 @@ async def test_use_pending_kernels(self): k = km.get_kernel(kid) assert isinstance(k, AsyncKernelManager) await ensure_future(km.shutdown_kernel(kid, now=True)) + km.context.destroy(linger=0) # Wait for the kernel to shutdown await kernel.ready assert kid not in km, f"{kid} not in {km}" @@ -428,6 +447,7 @@ async def test_use_pending_kernels_early_restart(self): # Wait for the kernel to shutdown await kernel.ready assert kid not in km, f"{kid} not in {km}" + km.context.destroy(linger=0) @gen_test async def test_use_pending_kernels_early_shutdown(self): @@ -440,6 +460,7 @@ async def test_use_pending_kernels_early_shutdown(self): # Wait for the kernel to shutdown await kernel.ready assert kid not in km, f"{kid} not in {km}" + km.context.destroy(linger=0) @gen_test async def test_use_pending_kernels_early_interrupt(self): @@ -455,23 +476,27 @@ async def test_use_pending_kernels_early_interrupt(self): # Wait for the kernel to shutdown await kernel.ready assert kid not in km, f"{kid} not in {km}" + km.context.destroy(linger=0) @gen_test async def test_tcp_cinfo(self): km = self._get_tcp_km() await self._run_cinfo(km, "tcp", localhost()) + km.context.destroy(linger=0) @skip_win32 @gen_test async def test_ipc_lifecycle(self): km = self._get_ipc_km() await self._run_lifecycle(km) + km.context.destroy(linger=0) @skip_win32 @gen_test async def test_ipc_cinfo(self): km = self._get_ipc_km() await self._run_cinfo(km, "ipc", "test") + km.context.destroy(linger=0) @gen_test async def test_start_sequence_tcp_kernels(self): @@ -579,6 +604,7 @@ async def test_subclass_callables(self): mkm.get_kernel(kid).reset_counts() mkm.reset_counts() await mkm.shutdown_all(now=True) + mkm.context.destroy(linger=0) assert mkm.call_count("remove_kernel") == 1 assert mkm.call_count("_async_request_shutdown") == 0 assert mkm.call_count("_async_finish_shutdown") == 0 @@ -596,6 +622,7 @@ async def test_bad_kernelspec(self): ) with pytest.raises(FileNotFoundError): await ensure_future(km.start_kernel(kernel_name="bad", stdout=PIPE, stderr=PIPE)) + km.context.destroy(linger=0) @gen_test async def test_bad_kernelspec_pending(self): @@ -613,8 +640,9 @@ async def test_bad_kernelspec_pending(self): assert kernel_id in km.list_kernel_ids() await ensure_future(km.shutdown_kernel(kernel_id)) assert kernel_id not in km.list_kernel_ids() + km.context.destroy(linger=0) - @gen_test + @gen_test(timeout=TIMEOUT) async def test_stream_on_recv(self): mkm = self._get_tcp_km() kid = await mkm.start_kernel(stdout=PIPE, stderr=PIPE) @@ -628,7 +656,7 @@ async def test_stream_on_recv(self): def record_activity(msg_list): nonlocal called """Record an IOPub message arriving from a kernel""" - idents, fed_msg_list = session.feed_identities(msg_list) + _idents, fed_msg_list = session.feed_identities(msg_list) msg = session.deserialize(fed_msg_list, content=False) msg_type = msg["header"]["msg_type"] @@ -643,4 +671,6 @@ def record_activity(msg_list): await asyncio.sleep(0.1) client.stop_channels() + stream.close() await km.shutdown_kernel(now=True) + km.context.destroy(linger=0) diff --git a/tests/test_provisioning.py b/tests/test_provisioning.py index 02d4979b..87cd1036 100644 --- a/tests/test_provisioning.py +++ b/tests/test_provisioning.py @@ -1,4 +1,5 @@ """Test Provisioning""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio @@ -7,7 +8,7 @@ import signal import sys from subprocess import PIPE -from typing import Any, Optional +from typing import Any import pytest from jupyter_core import paths @@ -46,13 +47,13 @@ class CustomTestProvisioner(KernelProvisionerBase): # type:ignore def has_process(self) -> bool: return self.process is not None - async def poll(self) -> Optional[int]: + async def poll(self) -> int | None: ret = 0 if self.process: ret = self.process.poll() return ret - async def wait(self) -> Optional[int]: + async def wait(self) -> int | None: ret = 0 if self.process: while await self.poll() is None: @@ -134,7 +135,7 @@ class NewTestProvisioner(CustomTestProvisioner): # type:ignore pass -def build_kernelspec(name: str, provisioner: Optional[str] = None) -> None: +def build_kernelspec(name: str, provisioner: str | None = None) -> None: spec: dict = { "argv": [ sys.executable, diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 327957c3..d32ddc62 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -1,5 +1,5 @@ -"""Test the jupyter_client public API -""" +"""Test the jupyter_client public API""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import jupyter_client diff --git a/tests/test_restarter.py b/tests/test_restarter.py index 78997c3d..baaeca60 100644 --- a/tests/test_restarter.py +++ b/tests/test_restarter.py @@ -1,4 +1,5 @@ """Tests for the KernelManager""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import asyncio diff --git a/tests/test_session.py b/tests/test_session.py index f30f44f4..de817423 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,4 +1,5 @@ """test building messages with Session""" + # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import hmac @@ -51,7 +52,7 @@ def assertTrue(self, a): def test_msg(self, session): """message format""" msg = session.msg("execute") - thekeys = set("header parent_header metadata content msg_type msg_id".split()) + thekeys = {"header", "parent_header", "metadata", "content", "msg_type", "msg_id"} s = set(msg.keys()) self.assertEqual(s, thekeys) self.assertTrue(isinstance(msg["content"], dict)) diff --git a/tests/utils.py b/tests/utils.py index de43089a..66b62e7b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,5 @@ -"""Testing utils for jupyter_client tests +"""Testing utils for jupyter_client tests""" -""" import json import os import sys