From 29a8019506f582bdc10298eb8029c31cf642b002 Mon Sep 17 00:00:00 2001 From: Gregory Shklover Date: Mon, 1 Jul 2024 16:36:32 +0300 Subject: [PATCH 01/34] Fixed error accessing sys.stdout/sys.stderr when those are None (#1247) --- ipykernel/inprocess/ipkernel.py | 6 ++++-- ipykernel/kernelbase.py | 30 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/ipykernel/inprocess/ipkernel.py b/ipykernel/inprocess/ipkernel.py index 789b0bf8a..aecb3b102 100644 --- a/ipykernel/inprocess/ipkernel.py +++ b/ipykernel/inprocess/ipkernel.py @@ -91,8 +91,10 @@ def _abort_queues(self): def _input_request(self, prompt, ident, parent, password=False): # Flush output before making the request. self.raw_input_str = None - sys.stderr.flush() - sys.stdout.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() # Send the input request. content = json_clean(dict(prompt=prompt, password=password)) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 0c922f2bd..77113a7f9 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -333,8 +333,10 @@ async def process_control(self, msg): except Exception: self.log.error("Exception in control handler:", exc_info=True) # noqa: G201 - sys.stdout.flush() - sys.stderr.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() self._publish_status_and_flush("idle", "control", self.control_stream) def should_handle(self, stream, msg, idents): @@ -438,8 +440,10 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None): except Exception: self.log.debug("Unable to signal in post_handler_hook:", exc_info=True) - sys.stdout.flush() - sys.stderr.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() self._publish_status_and_flush("idle", "shell", stream) def pre_handler_hook(self): @@ -844,8 +848,10 @@ async def execute_request(self, stream, ident, parent): reply_content = await reply_content # Flush output before sending the reply. - sys.stdout.flush() - sys.stderr.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() # FIXME: on rare occasions, the flush doesn't seem to make it to the # clients... This seems to mitigate the problem, but we definitely need # to better understand what's going on. @@ -1242,8 +1248,10 @@ async def apply_request(self, stream, ident, parent): # pragma: no cover reply_content, result_buf = self.do_apply(content, bufs, msg_id, md) # flush i/o - sys.stdout.flush() - sys.stderr.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() md = self.finish_metadata(parent, md, reply_content) if not self.session: @@ -1441,8 +1449,10 @@ def raw_input(self, prompt=""): def _input_request(self, prompt, ident, parent, password=False): # Flush output before making the request. - sys.stderr.flush() - sys.stdout.flush() + if sys.stdout is not None: + sys.stdout.flush() + if sys.stderr is not None: + sys.stderr.flush() # flush the stdin socket, to purge stale replies while True: From eff15cf92d286e16a17cde538d1af95d59b08f0e Mon Sep 17 00:00:00 2001 From: bluss Date: Tue, 15 Oct 2024 11:25:38 +0200 Subject: [PATCH 02/34] Detect parent change in more cases on unix (#1271) --- ipykernel/kernelapp.py | 2 +- ipykernel/parentpoller.py | 28 +++++++++++++++++++++++++--- tests/test_parentpoller.py | 18 +++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index e3136a03c..dd426c757 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -220,7 +220,7 @@ def init_poller(self): # PID 1 (init) is special and will never go away, # only be reassigned. # Parent polling doesn't work if ppid == 1 to start with. - self.poller = ParentPollerUnix() + self.poller = ParentPollerUnix(parent_pid=self.parent_handle) def _try_bind_socket(self, s, port): iface = f"{self.transport}://{self.ip}" diff --git a/ipykernel/parentpoller.py b/ipykernel/parentpoller.py index a6d9c7538..895a785c7 100644 --- a/ipykernel/parentpoller.py +++ b/ipykernel/parentpoller.py @@ -22,9 +22,17 @@ class ParentPollerUnix(Thread): when the parent process no longer exists. """ - def __init__(self): - """Initialize the poller.""" + def __init__(self, parent_pid=0): + """Initialize the poller. + + Parameters + ---------- + parent_handle : int, optional + If provided, the program will terminate immediately when + process parent is no longer this original parent. + """ super().__init__() + self.parent_pid = parent_pid self.daemon = True def run(self): @@ -32,9 +40,23 @@ def run(self): # We cannot use os.waitpid because it works only for child processes. from errno import EINTR + # before start, check if the passed-in parent pid is valid + original_ppid = os.getppid() + if original_ppid != self.parent_pid: + self.parent_pid = 0 + + get_logger().debug( + "%s: poll for parent change with original parent pid=%d", + type(self).__name__, + self.parent_pid, + ) + while True: try: - if os.getppid() == 1: + ppid = os.getppid() + parent_is_init = not self.parent_pid and ppid == 1 + parent_has_changed = self.parent_pid and ppid != self.parent_pid + if parent_is_init or parent_has_changed: get_logger().warning("Parent appears to have exited, shutting down.") os._exit(1) time.sleep(1.0) diff --git a/tests/test_parentpoller.py b/tests/test_parentpoller.py index 97cd80440..716c9e8f5 100644 --- a/tests/test_parentpoller.py +++ b/tests/test_parentpoller.py @@ -9,7 +9,7 @@ @pytest.mark.skipif(os.name == "nt", reason="only works on posix") -def test_parent_poller_unix(): +def test_parent_poller_unix_to_pid1(): poller = ParentPollerUnix() with mock.patch("os.getppid", lambda: 1): # noqa: PT008 @@ -27,6 +27,22 @@ def mock_getppid(): poller.run() +@pytest.mark.skipif(os.name == "nt", reason="only works on posix") +def test_parent_poller_unix_reparent_not_pid1(): + parent_pid = 221 + parent_pids = iter([parent_pid, parent_pid - 1]) + + poller = ParentPollerUnix(parent_pid=parent_pid) + + with mock.patch("os.getppid", lambda: next(parent_pids)): # noqa: PT008 + + def exit_mock(*args): + sys.exit(1) + + with mock.patch("os._exit", exit_mock), pytest.raises(SystemExit): + poller.run() + + @pytest.mark.skipif(os.name != "nt", reason="only works on windows") def test_parent_poller_windows(): poller = ParentPollerWindows(interrupt_handle=1) From 0efd83e1faca7d4f0ba08f80d9423c3cc0196d51 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 22 Oct 2024 02:27:03 -0700 Subject: [PATCH 03/34] Try to add workflow to publish nightlies (#1276) --- .github/workflows/nightly.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/nightly.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 000000000..a60be2b40 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,32 @@ +name: nightly build and upload +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +defaults: + run: + shell: bash -eux {0} + +jobs: + build: + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Build + run: | + python -m build + - name: Upload wheel + uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 + with: + artifacts_path: dist + anaconda_nightly_upload_token: ${{secrets.UPLOAD_TOKEN}} From 32748451ad37aaf044fb010dd8c16c14ef5dab62 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 22 Oct 2024 11:29:17 +0200 Subject: [PATCH 04/34] install build --- .github/workflows/nightly.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a60be2b40..499f43562 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -24,6 +24,7 @@ jobs: - name: Build run: | + python -m pip install build python -m build - name: Upload wheel uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 From 1c14e83f4b82e8613c539d5f055a0344aa979b31 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 22 Oct 2024 06:01:30 -0700 Subject: [PATCH 05/34] Remove some potential dead-code. (#1273) --- examples/embedding/inprocess_qtconsole.py | 34 ++--------------------- ipykernel/kernelapp.py | 2 +- tests/test_eventloop.py | 22 +-------------- tests/test_kernel.py | 4 +-- 4 files changed, 6 insertions(+), 56 deletions(-) diff --git a/examples/embedding/inprocess_qtconsole.py b/examples/embedding/inprocess_qtconsole.py index 18ce28638..7a976a319 100644 --- a/examples/embedding/inprocess_qtconsole.py +++ b/examples/embedding/inprocess_qtconsole.py @@ -1,54 +1,24 @@ """An in-process qt console app.""" import os -import sys import tornado from IPython.lib import guisupport from qtconsole.inprocess import QtInProcessKernelManager from qtconsole.rich_ipython_widget import RichIPythonWidget +assert tornado.version_info >= (6, 1) + def print_process_id(): """Print the process id.""" print("Process ID is:", os.getpid()) -def init_asyncio_patch(): - """set default asyncio policy to be compatible with tornado - Tornado 6 (at least) is not compatible with the default - asyncio implementation on Windows - Pick the older SelectorEventLoopPolicy on Windows - if the known-incompatible default policy is in use. - do this as early as possible to make it a low priority and overridable - ref: https://github.com/tornadoweb/tornado/issues/2608 - FIXME: if/when tornado supports the defaults in asyncio, - remove and bump tornado requirement for py38 - """ - if ( - sys.platform.startswith("win") - and sys.version_info >= (3, 8) - and tornado.version_info < (6, 1) - ): - import asyncio - - try: - from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy - except ImportError: - pass - # not affected - else: - if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy: - # WindowsProactorEventLoopPolicy is not compatible with tornado 6 - # fallback to the pre-3.8 default of Selector - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - - def main(): """The main entry point.""" # Print the ID of the main process print_process_id() - init_asyncio_patch() app = guisupport.get_app_qt4() # Create an in-process kernel diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index dd426c757..64924c2c1 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -666,7 +666,7 @@ def _init_asyncio_patch(self): where asyncio.ProactorEventLoop supports add_reader and friends. """ - if sys.platform.startswith("win") and sys.version_info >= (3, 8): + if sys.platform.startswith("win"): import asyncio try: diff --git a/tests/test_eventloop.py b/tests/test_eventloop.py index 76cadd7be..fb19b829c 100644 --- a/tests/test_eventloop.py +++ b/tests/test_eventloop.py @@ -7,7 +7,6 @@ import time import pytest -import tornado from ipykernel.eventloops import ( enable_gui, @@ -16,7 +15,7 @@ loop_tk, ) -from .utils import execute, flush_channels, start_new_kernel +from .utils import flush_channels, start_new_kernel KC = KM = None @@ -61,25 +60,6 @@ def _setup_env(): """ -@pytest.mark.skipif(tornado.version_info < (5,), reason="only relevant on tornado 5") -def test_asyncio_interrupt(): - assert KM is not None - assert KC is not None - flush_channels(KC) - msg_id, content = execute("%gui asyncio", KC) - assert content["status"] == "ok", content - - flush_channels(KC) - msg_id, content = execute(async_code, KC) - assert content["status"] == "ok", content - - KM.interrupt_kernel() - - flush_channels(KC) - msg_id, content = execute(async_code, KC) - assert content["status"] == "ok" - - windows_skip = pytest.mark.skipif(os.name == "nt", reason="causing failures on windows") diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 88f02ae9a..89d5e390b 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -212,7 +212,7 @@ def test_sys_path_profile_dir(): @flaky(max_runs=3) @pytest.mark.skipif( - sys.platform == "win32" or (sys.platform == "darwin" and sys.version_info >= (3, 8)), + sys.platform == "win32" or (sys.platform == "darwin"), reason="subprocess prints fail on Windows and MacOS Python 3.8+", ) def test_subprocess_print(): @@ -267,7 +267,7 @@ def test_subprocess_noprint(): @flaky(max_runs=3) @pytest.mark.skipif( - sys.platform == "win32" or (sys.platform == "darwin" and sys.version_info >= (3, 8)), + (sys.platform == "win32") or (sys.platform == "darwin"), reason="subprocess prints fail on Windows and MacOS Python 3.8+", ) def test_subprocess_error(): From 1990ff56ee9923aaf68edf36d159ffd5618513fb Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 22 Oct 2024 14:49:37 +0200 Subject: [PATCH 06/34] remove deprecated ipyparallel methods no version of ipython parallel without these methods will work with ipykernel 7 anyway --- ipykernel/kernelbase.py | 106 ++++------------------------------------ 1 file changed, 9 insertions(+), 97 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 77113a7f9..1d1270029 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -50,7 +50,6 @@ Instance, Integer, List, - Set, Unicode, default, observe, @@ -240,9 +239,6 @@ def _parent_header(self): # by record_ports and used by connect_request. _recorded_ports = Dict() - # set of aborted msg_ids - aborted = Set() - # Track execution count here. For IPython, we override this to use the # execution count we store in the shell. execution_count = 0 @@ -258,14 +254,10 @@ def _parent_header(self): "shutdown_request", "is_complete_request", "interrupt_request", - # deprecated: - "apply_request", ] # add deprecated ipyparallel control messages control_msg_types = [ *msg_types, - "clear_request", - "abort_request", "debug_request", "usage_request", "create_subshell_request", @@ -343,13 +335,11 @@ def should_handle(self, stream, msg, idents): """Check whether a shell-channel message should be handled Allows subclasses to prevent handling of certain messages (e.g. aborted requests). + + .. versionchanged:: 7 + Subclass should_handle _may_ be async. + Base class implementation is not async. """ - msg_id = msg["header"]["msg_id"] - if msg_id in self.aborted: - # is it safe to assume a msg_id will not be resubmitted? - self.aborted.remove(msg_id) - self._send_abort_reply(stream, msg, idents) - return False return True async def dispatch_shell(self, msg, /, subshell_id: str | None = None): @@ -412,8 +402,12 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None): self.log.debug("\n*** MESSAGE TYPE:%s***", msg_type) self.log.debug(" Content: %s\n --->\n ", msg["content"]) - if not self.should_handle(stream, msg, idents): + should_handle: bool | t.Awaitable[bool] = self.should_handle(stream, msg, idents) + if inspect.isawaitable(should_handle): + should_handle = await should_handle + if not should_handle: self._publish_status_and_flush("idle", "shell", stream) + self.log.debug("Not handling %s:%s", msg_type, msg["header"].get("msg_id")) return handler = self.shell_handlers.get(msg_type, None) @@ -1228,88 +1222,6 @@ async def list_subshell_request(self, socket, ident, parent) -> None: self.session.send(socket, "list_subshell_reply", reply, parent, ident) - # --------------------------------------------------------------------------- - # Engine methods (DEPRECATED) - # --------------------------------------------------------------------------- - - async def apply_request(self, stream, ident, parent): # pragma: no cover - """Handle an apply request.""" - self.log.warning("apply_request is deprecated in kernel_base, moving to ipyparallel.") - try: - content = parent["content"] - bufs = parent["buffers"] - msg_id = parent["header"]["msg_id"] - except Exception: - self.log.error("Got bad msg: %s", parent, exc_info=True) # noqa: G201 - return - - md = self.init_metadata(parent) - - reply_content, result_buf = self.do_apply(content, bufs, msg_id, md) - - # flush i/o - if sys.stdout is not None: - sys.stdout.flush() - if sys.stderr is not None: - sys.stderr.flush() - - md = self.finish_metadata(parent, md, reply_content) - if not self.session: - return - self.session.send( - stream, - "apply_reply", - reply_content, - parent=parent, - ident=ident, - buffers=result_buf, - metadata=md, - ) - - def do_apply(self, content, bufs, msg_id, reply_metadata): - """DEPRECATED""" - raise NotImplementedError - - # --------------------------------------------------------------------------- - # Control messages (DEPRECATED) - # --------------------------------------------------------------------------- - - async def abort_request(self, stream, ident, parent): # pragma: no cover - """abort a specific msg by id""" - self.log.warning( - "abort_request is deprecated in kernel_base. It is only part of IPython parallel" - ) - msg_ids = parent["content"].get("msg_ids", None) - if isinstance(msg_ids, str): - msg_ids = [msg_ids] - if not msg_ids: - subshell_id = parent["header"].get("subshell_id") - self._abort_queues(subshell_id) - - for mid in msg_ids: - self.aborted.add(str(mid)) - - content = dict(status="ok") - if not self.session: - return - reply_msg = self.session.send( - stream, "abort_reply", content=content, parent=parent, ident=ident - ) - self.log.debug("%s", reply_msg) - - async def clear_request(self, stream, idents, parent): # pragma: no cover - """Clear our namespace.""" - self.log.warning( - "clear_request is deprecated in kernel_base. It is only part of IPython parallel" - ) - content = self.do_clear() - if self.session: - self.session.send(stream, "clear_reply", ident=idents, parent=parent, content=content) - - def do_clear(self): - """DEPRECATED since 4.0.3""" - raise NotImplementedError - # --------------------------------------------------------------------------- # Protected interface # --------------------------------------------------------------------------- From 1bc2277adce8666fa0f94756ff1d183a011c03bf Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 22 Oct 2024 15:18:15 +0200 Subject: [PATCH 07/34] update test_should_handle --- tests/test_kernel_direct.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_kernel_direct.py b/tests/test_kernel_direct.py index dfb8a70fe..c799052be 100644 --- a/tests/test_kernel_direct.py +++ b/tests/test_kernel_direct.py @@ -130,8 +130,7 @@ def __init__(self, bytes): def test_should_handle(kernel): msg = kernel.session.msg("debug_request", {}) - kernel.aborted.add(msg["header"]["msg_id"]) - assert not kernel.should_handle(kernel.control_stream, msg, []) + assert kernel.should_handle(kernel.control_stream, msg, []) is True async def test_dispatch_shell(kernel): From 22632d72c6db513e4388924ac157134f7b93a771 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 25 Oct 2024 15:15:33 +0200 Subject: [PATCH 08/34] update control channel message type comment --- ipykernel/kernelbase.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 1d1270029..fba660e6b 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -255,7 +255,9 @@ def _parent_header(self): "is_complete_request", "interrupt_request", ] - # add deprecated ipyparallel control messages + + # control channel accepts all shell messages + # and some of its own control_msg_types = [ *msg_types, "debug_request", From c775910994d3fe041dd5f608097d890c10a03996 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Mon, 28 Oct 2024 04:00:48 -0700 Subject: [PATCH 09/34] Add 20 min timeout dowstream ipyparallel (#1287) --- .github/workflows/downstream.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 42ea51994..82e9fe051 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -56,6 +56,7 @@ jobs: ipyparallel: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 From 9f82d322b09958dfdf666dea176253615f3b3711 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 20 Dec 2024 20:16:00 +0100 Subject: [PATCH 10/34] Fix test_print_to_correct_cell_from_child_thread (#1312) --- tests/test_kernel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_kernel.py b/tests/test_kernel.py index 89d5e390b..6d7864c32 100644 --- a/tests/test_kernel.py +++ b/tests/test_kernel.py @@ -106,7 +106,9 @@ def child_target(): def parent_target(): sleep({interval}) - Thread(target=child_target).start() + thread = Thread(target=child_target) + thread.start() + thread.join() Thread(target=parent_target).start() """ From 70a584846affceefc8de0c40dbf6f7d6dcfaac65 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 13 Feb 2025 11:28:04 +0100 Subject: [PATCH 11/34] Remove dead code (#1332) --- tests/_asyncio_utils.py | 17 ----------------- tests/test_eventloop.py | 6 ------ 2 files changed, 23 deletions(-) delete mode 100644 tests/_asyncio_utils.py diff --git a/tests/_asyncio_utils.py b/tests/_asyncio_utils.py deleted file mode 100644 index 43e3229fa..000000000 --- a/tests/_asyncio_utils.py +++ /dev/null @@ -1,17 +0,0 @@ -"""test utilities that use async/await syntax - -a separate file to avoid syntax errors on Python 2 -""" - -import asyncio - - -def async_func(): - """Simple async function to schedule a task on the current eventloop""" - loop = asyncio.get_event_loop() - assert loop.is_running() - - async def task(): - await asyncio.sleep(1) - - loop.create_task(task()) diff --git a/tests/test_eventloop.py b/tests/test_eventloop.py index fb19b829c..b18ceff5d 100644 --- a/tests/test_eventloop.py +++ b/tests/test_eventloop.py @@ -54,12 +54,6 @@ def _setup_env(): KM.shutdown_kernel(now=True) -async_code = """ -from tests._asyncio_utils import async_func -async_func() -""" - - windows_skip = pytest.mark.skipif(os.name == "nt", reason="causing failures on windows") From 5afd9893fa5a4ac91be65420cb99063bd2df7958 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 12 Feb 2025 05:48:01 -0800 Subject: [PATCH 12/34] Remove link to numfocus for funding. (#1320) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39246b492..89a8a32ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ dependencies = [ [project.urls] Homepage = "https://ipython.org" Documentation = "https://ipykernel.readthedocs.io" -Funding = "https://numfocus.org/donate" Source = "https://github.com/ipython/ipykernel" Tracker = "https://github.com/ipython/ipykernel/issues" From c190cf62abff473c467235ae04937e06e2cdb817 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 13 Feb 2025 11:32:03 +0100 Subject: [PATCH 13/34] minor code reformating valid ruff 0.9.6 (#1330) --- ipykernel/connect.py | 4 ++-- ipykernel/iostream.py | 12 +++++++----- tests/test_message_spec.py | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ipykernel/connect.py b/ipykernel/connect.py index 59d36452d..117484599 100644 --- a/ipykernel/connect.py +++ b/ipykernel/connect.py @@ -133,8 +133,8 @@ def connect_qtconsole( __all__ = [ - "write_connection_file", + "connect_qtconsole", "get_connection_file", "get_connection_info", - "connect_qtconsole", + "write_connection_file", ] diff --git a/ipykernel/iostream.py b/ipykernel/iostream.py index 9dc55242b..8c258bd76 100644 --- a/ipykernel/iostream.py +++ b/ipykernel/iostream.py @@ -466,11 +466,13 @@ def __init__( self._local = local() if ( - watchfd - and ( - (sys.platform.startswith("linux") or sys.platform.startswith("darwin")) - # Pytest set its own capture. Don't redirect from within pytest. - and ("PYTEST_CURRENT_TEST" not in os.environ) + ( + watchfd + and ( + (sys.platform.startswith("linux") or sys.platform.startswith("darwin")) + # Pytest set its own capture. Don't redirect from within pytest. + and ("PYTEST_CURRENT_TEST" not in os.environ) + ) ) # allow forcing watchfd (mainly for tests) or watchfd == "force" diff --git a/tests/test_message_spec.py b/tests/test_message_spec.py index 0459b342b..b35861cb3 100644 --- a/tests/test_message_spec.py +++ b/tests/test_message_spec.py @@ -33,7 +33,6 @@ def _setup_env(): class Reference(HasTraits): - """ Base class for message spec specification testing. From 5b3a84bfb1009b1dd0d91adc83f760cab68d8bdb Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 13 Feb 2025 12:32:39 +0100 Subject: [PATCH 14/34] Copy payloadpage.page from IPython (#1317) --- ipykernel/zmqshell.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 60682379d..9cdf2bc68 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -20,7 +20,7 @@ import warnings from pathlib import Path -from IPython.core import page, payloadpage +from IPython.core import page from IPython.core.autocall import ZMQExitAutocall from IPython.core.displaypub import DisplayPublisher from IPython.core.error import UsageError @@ -541,10 +541,38 @@ def init_environment(self): env["PAGER"] = "cat" env["GIT_PAGER"] = "cat" + def payloadpage_page(self, strg, start=0, screen_lines=0, pager_cmd=None): + """Print a string, piping through a pager. + + This version ignores the screen_lines and pager_cmd arguments and uses + IPython's payload system instead. + + Parameters + ---------- + strg : str or mime-dict + Text to page, or a mime-type keyed dict of already formatted data. + start : int + Starting line at which to place the display. + """ + + # Some routines may auto-compute start offsets incorrectly and pass a + # negative value. Offset to 0 for robustness. + start = max(0, start) + + data = strg if isinstance(strg, dict) else {"text/plain": strg} + + payload = dict( + source="page", + data=data, + start=start, + ) + assert self.payload_manager is not None + self.payload_manager.write_payload(payload) + def init_hooks(self): """Initialize hooks.""" super().init_hooks() - self.set_hook("show_in_pager", page.as_hook(payloadpage.page), 99) + self.set_hook("show_in_pager", page.as_hook(self.payloadpage_page), 99) def init_data_pub(self): """Delay datapub init until request, for deprecation warnings""" From 4ddb54724ea995d909d1be54676bd2d1c5271f95 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 13 Feb 2025 13:42:32 +0100 Subject: [PATCH 15/34] Try to force precommit-ci to send autoupdate PRs. (#1325) Co-authored-by: Min RK --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2ae0f758..4e5bdab8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ ci: - autoupdate_schedule: monthly autoupdate_commit_msg: "chore: update pre-commit hooks" + autoupdate_schedule: weekly repos: - repo: https://github.com/pre-commit/pre-commit-hooks From 6162765c626ae8e9cf1a475a8b974a9c4cbf208d Mon Sep 17 00:00:00 2001 From: Stephen Macke Date: Thu, 13 Feb 2025 23:23:09 -0800 Subject: [PATCH 16/34] make debugger class configurable (#1307) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- ipykernel/ipkernel.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py index 22ea82880..6eb2a164e 100644 --- a/ipykernel/ipkernel.py +++ b/ipykernel/ipkernel.py @@ -77,6 +77,12 @@ class IPythonKernel(KernelBase): shell = Instance("IPython.core.interactiveshell.InteractiveShellABC", allow_none=True) shell_class = Type(ZMQInteractiveShell) + # use fully-qualified name to ensure lazy import and prevent the issue from + # https://github.com/ipython/ipykernel/issues/1198 + debugger_class = Type("ipykernel.debugger.Debugger") + + compiler_class = Type(XCachingCompiler) + use_experimental_completions = Bool( True, help="Set this flag to False to deactivate the use of experimental IPython completion APIs.", @@ -114,11 +120,11 @@ def __init__(self, **kwargs): """Initialize the kernel.""" super().__init__(**kwargs) - from .debugger import Debugger, _is_debugpy_available + from .debugger import _is_debugpy_available # Initialize the Debugger if _is_debugpy_available: - self.debugger = Debugger( + self.debugger = self.debugger_class( self.log, self.debugpy_stream, self._publish_debug_event, @@ -134,7 +140,7 @@ def __init__(self, **kwargs): user_module=self.user_module, user_ns=self.user_ns, kernel=self, - compiler_class=XCachingCompiler, + compiler_class=self.compiler_class, ) self.shell.displayhook.session = self.session # type:ignore[attr-defined] From 76dbe3778546103dc88abd4f0463752947ed7799 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Fri, 14 Feb 2025 08:24:02 +0100 Subject: [PATCH 17/34] Remove downstream_check (#1318) --- .github/workflows/downstream.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 82e9fe051..269707ad1 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -148,20 +148,3 @@ jobs: run: | cd ${GITHUB_WORKSPACE}/../spyder-kernels xvfb-run --auto-servernum ${pythonLocation}/bin/python -m pytest -x -vv -s --full-trace --color=yes spyder_kernels - - downstream_check: # This job does nothing and is only used for the branch protection - if: always() - needs: - - nbclient - - ipywidgets - - jupyter_client - - ipyparallel - - jupyter_kernel_test - - spyder_kernels - - qtconsole - runs-on: ubuntu-latest - steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@release/v1 - with: - jobs: ${{ toJSON(needs) }} From 9b653d95c5303d88d7e4e3b6f7667a0a6ec69c29 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 14 Feb 2025 08:28:10 +0100 Subject: [PATCH 18/34] Remove implicit bind_kernel in `%qtconsole` (#1315) --- ipykernel/zmqshell.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 9cdf2bc68..8d4642e05 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -398,14 +398,6 @@ def qtconsole(self, arg_s): Useful for connecting a qtconsole to running notebooks, for better debugging. """ - - # %qtconsole should imply bind_kernel for engines: - # FIXME: move to ipyparallel Kernel subclass - if "ipyparallel" in sys.modules: - from ipyparallel import bind_kernel - - bind_kernel() - try: connect_qtconsole(argv=arg_split(arg_s, os.name == "posix")) except Exception as e: From 665dd2ce2ec31bf9bd0ab6fd92c115853ef2b65b Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Fri, 14 Feb 2025 07:31:15 +0000 Subject: [PATCH 19/34] Use supported_features=['debugger'] in kernel info reply (#1296) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- ipykernel/kernelbase.py | 15 ++++++++++----- tests/test_debugger.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index fba660e6b..d4b298826 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -976,18 +976,23 @@ async def connect_request(self, stream, ident, parent): @property def kernel_info(self): - info = { + from .debugger import _is_debugpy_available + + supported_features: list[str] = [] + if self._supports_kernel_subshells: + supported_features.append("kernel subshells") + if _is_debugpy_available: + supported_features.append("debugger") + + return { "protocol_version": kernel_protocol_version, "implementation": self.implementation, "implementation_version": self.implementation_version, "language_info": self.language_info, "banner": self.banner, "help_links": self.help_links, - "supported_features": [], + "supported_features": supported_features, } - if self._supports_kernel_subshells: - info["supported_features"] = ["kernel subshells"] - return info async def kernel_info_request(self, stream, ident, parent): """Handle a kernel info request.""" diff --git a/tests/test_debugger.py b/tests/test_debugger.py index fa64d2d09..4646fb49d 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -96,6 +96,17 @@ def test_debug_initialize(kernel): assert reply == {} +def test_supported_features(kernel_with_debug): + kernel_with_debug.kernel_info() + reply = kernel_with_debug.get_shell_msg(timeout=TIMEOUT) + supported_features = reply["content"]["supported_features"] + + if debugpy: + assert "debugger" in supported_features + else: + assert "debugger" not in supported_features + + def test_attach_debug(kernel_with_debug): reply = wait_for_debug_request( kernel_with_debug, "evaluate", {"expression": "'a' + 'b'", "context": "repl"} From 148afd62c91bd440e9bd83c1dadb4e8e6bd23f24 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Fri, 14 Feb 2025 08:40:34 +0100 Subject: [PATCH 20/34] Some formatting changes to prepare bumping ruff pre-commit. (#1329) --- examples/embedding/inprocess_qtconsole.py | 1 + examples/embedding/inprocess_terminal.py | 1 + examples/embedding/ipkernel_wxapp.py | 1 + hatch_build.py | 1 + ipykernel/__main__.py | 1 + ipykernel/compiler.py | 1 + ipykernel/connect.py | 4 ++-- ipykernel/datapub.py | 3 +-- ipykernel/debugger.py | 1 + ipykernel/embed.py | 3 +-- ipykernel/gui/gtk3embed.py | 3 +-- ipykernel/gui/gtkembed.py | 3 +-- ipykernel/heartbeat.py | 3 +-- ipykernel/inprocess/blocking.py | 3 ++- ipykernel/inprocess/constants.py | 3 +-- ipykernel/inprocess/socket.py | 2 +- ipykernel/log.py | 1 + ipykernel/trio_runner.py | 1 + tests/test_zmq_shell.py | 2 +- 19 files changed, 21 insertions(+), 17 deletions(-) diff --git a/examples/embedding/inprocess_qtconsole.py b/examples/embedding/inprocess_qtconsole.py index 7a976a319..f256ecfed 100644 --- a/examples/embedding/inprocess_qtconsole.py +++ b/examples/embedding/inprocess_qtconsole.py @@ -1,4 +1,5 @@ """An in-process qt console app.""" + import os import tornado diff --git a/examples/embedding/inprocess_terminal.py b/examples/embedding/inprocess_terminal.py index b644c94af..221a0c473 100644 --- a/examples/embedding/inprocess_terminal.py +++ b/examples/embedding/inprocess_terminal.py @@ -1,4 +1,5 @@ """An in-process terminal example.""" + import os import sys diff --git a/examples/embedding/ipkernel_wxapp.py b/examples/embedding/ipkernel_wxapp.py index f24ed9392..b36fcc312 100755 --- a/examples/embedding/ipkernel_wxapp.py +++ b/examples/embedding/ipkernel_wxapp.py @@ -16,6 +16,7 @@ Ref: Modified from wxPython source code wxPython/samples/simple/simple.py """ + # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- diff --git a/hatch_build.py b/hatch_build.py index 934348050..4dfdd1a22 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,4 +1,5 @@ """A custom hatch build hook for ipykernel.""" + import shutil import sys from pathlib import Path diff --git a/ipykernel/__main__.py b/ipykernel/__main__.py index a1050e32e..59c864405 100644 --- a/ipykernel/__main__.py +++ b/ipykernel/__main__.py @@ -1,4 +1,5 @@ """The cli entry point for ipykernel.""" + if __name__ == "__main__": from ipykernel import kernelapp as app diff --git a/ipykernel/compiler.py b/ipykernel/compiler.py index e42007ed6..6652e08ae 100644 --- a/ipykernel/compiler.py +++ b/ipykernel/compiler.py @@ -1,4 +1,5 @@ """Compiler helpers for the debugger.""" + import os import sys import tempfile diff --git a/ipykernel/connect.py b/ipykernel/connect.py index 117484599..1e1f16cdc 100644 --- a/ipykernel/connect.py +++ b/ipykernel/connect.py @@ -1,5 +1,5 @@ -"""Connection file-related utilities for the kernel -""" +"""Connection file-related utilities for the kernel""" + # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. from __future__ import annotations diff --git a/ipykernel/datapub.py b/ipykernel/datapub.py index cc19696db..b0ab2c7b2 100644 --- a/ipykernel/datapub.py +++ b/ipykernel/datapub.py @@ -1,8 +1,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -"""Publishing native (typically pickled) objects. -""" +"""Publishing native (typically pickled) objects.""" import warnings diff --git a/ipykernel/debugger.py b/ipykernel/debugger.py index 4d4d43dc3..64320e631 100644 --- a/ipykernel/debugger.py +++ b/ipykernel/debugger.py @@ -1,4 +1,5 @@ """Debugger implementation for the IPython kernel.""" + import os import re import sys diff --git a/ipykernel/embed.py b/ipykernel/embed.py index d2cbe60b4..5078f8ace 100644 --- a/ipykernel/embed.py +++ b/ipykernel/embed.py @@ -1,5 +1,4 @@ -"""Simple function for embedding an IPython kernel -""" +"""Simple function for embedding an IPython kernel""" # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- diff --git a/ipykernel/gui/gtk3embed.py b/ipykernel/gui/gtk3embed.py index 3317ecfe4..ab4ec2226 100644 --- a/ipykernel/gui/gtk3embed.py +++ b/ipykernel/gui/gtk3embed.py @@ -1,5 +1,4 @@ -"""GUI support for the IPython ZeroMQ kernel - GTK toolkit support. -""" +"""GUI support for the IPython ZeroMQ kernel - GTK toolkit support.""" # ----------------------------------------------------------------------------- # Copyright (C) 2010-2011 The IPython Development Team # diff --git a/ipykernel/gui/gtkembed.py b/ipykernel/gui/gtkembed.py index e87249ead..6f3b6d166 100644 --- a/ipykernel/gui/gtkembed.py +++ b/ipykernel/gui/gtkembed.py @@ -1,5 +1,4 @@ -"""GUI support for the IPython ZeroMQ kernel - GTK toolkit support. -""" +"""GUI support for the IPython ZeroMQ kernel - GTK toolkit support.""" # ----------------------------------------------------------------------------- # Copyright (C) 2010-2011 The IPython Development Team # diff --git a/ipykernel/heartbeat.py b/ipykernel/heartbeat.py index 38fea17f9..3291e0aa6 100644 --- a/ipykernel/heartbeat.py +++ b/ipykernel/heartbeat.py @@ -1,5 +1,4 @@ -"""The client and server for a basic ping-pong style heartbeat. -""" +"""The client and server for a basic ping-pong style heartbeat.""" # ----------------------------------------------------------------------------- # Copyright (C) 2008-2011 The IPython Development Team diff --git a/ipykernel/inprocess/blocking.py b/ipykernel/inprocess/blocking.py index c598a44b4..dcded4ce2 100644 --- a/ipykernel/inprocess/blocking.py +++ b/ipykernel/inprocess/blocking.py @@ -1,7 +1,8 @@ -""" Implements a fully blocking kernel client. +"""Implements a fully blocking kernel client. Useful for test suites and blocking terminal interfaces. """ + import sys # ----------------------------------------------------------------------------- diff --git a/ipykernel/inprocess/constants.py b/ipykernel/inprocess/constants.py index 6133c757d..16d572083 100644 --- a/ipykernel/inprocess/constants.py +++ b/ipykernel/inprocess/constants.py @@ -1,5 +1,4 @@ -"""Shared constants. -""" +"""Shared constants.""" # Because inprocess communication is not networked, we can use a common Session # key everywhere. This is not just the empty bytestring to avoid tripping diff --git a/ipykernel/inprocess/socket.py b/ipykernel/inprocess/socket.py index 2df72b5e1..2a2866cb5 100644 --- a/ipykernel/inprocess/socket.py +++ b/ipykernel/inprocess/socket.py @@ -1,4 +1,4 @@ -""" Defines a dummy socket implementing (part of) the zmq.Socket interface. """ +"""Defines a dummy socket implementing (part of) the zmq.Socket interface.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. diff --git a/ipykernel/log.py b/ipykernel/log.py index bbd4c445b..91a4e114e 100644 --- a/ipykernel/log.py +++ b/ipykernel/log.py @@ -1,4 +1,5 @@ """A PUB log handler.""" + import warnings from zmq.log.handlers import PUBHandler diff --git a/ipykernel/trio_runner.py b/ipykernel/trio_runner.py index a641feba8..6fb44107b 100644 --- a/ipykernel/trio_runner.py +++ b/ipykernel/trio_runner.py @@ -1,4 +1,5 @@ """A trio loop runner.""" + import builtins import logging import signal diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index dfd22dec0..8a8fe042b 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -1,4 +1,4 @@ -""" Tests for zmq shell / display publisher. """ +"""Tests for zmq shell / display publisher.""" # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. From 8cde51dca915dff1dd0d558816390a914a8d7b47 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Fri, 14 Feb 2025 08:41:05 +0100 Subject: [PATCH 21/34] Ignore or fix most of the remaining ruff 0.9.6 errors (#1331) --- ipykernel/datapub.py | 2 +- pyproject.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/ipykernel/datapub.py b/ipykernel/datapub.py index b0ab2c7b2..3bd107fef 100644 --- a/ipykernel/datapub.py +++ b/ipykernel/datapub.py @@ -29,7 +29,7 @@ class ZMQDataPublisher(Configurable): """A zmq data publisher.""" - topic = topic = CBytes(b"datapub") + topic = CBytes(b"datapub") session = Instance(Session, allow_none=True) pub_socket = Any(allow_none=True) parent_header = Dict({}) diff --git a/pyproject.toml b/pyproject.toml index 89a8a32ab..49d33b4b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -273,6 +273,11 @@ ignore = [ "G002", # `open()` should be replaced by `Path.open()` "PTH123", + # use `X | Y` for type annotations, this does not works for dynamic getting type hints on older python + "UP007", + "UP031", # Use format specifiers instead of percent format + "PT023", # Use `@pytest.mark.skip` over `@pytest.mark.skip()` + "PT001", # autofixable: Use `@pytest.fixture` over `@pytest.fixture()` ] unfixable = [ # Don't touch print statements From 1939a5a3de953bc3858e69ce7687927cc4e781b6 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Mon, 17 Feb 2025 20:15:22 +0100 Subject: [PATCH 22/34] Suggest to make implementations of some function always return awaitable (#1295) Co-authored-by: David Brochart --- ipykernel/kernelbase.py | 54 ++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 4 +++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index d4b298826..1e0447b60 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -61,6 +61,14 @@ from ._version import kernel_protocol_version from .iostream import OutStream +_AWAITABLE_MESSAGE: str = ( + "For consistency across implementations, it is recommended that `{func_name}`" + " either be a coroutine function (`async def`) or return an awaitable object" + " (like an `asyncio.Future`). It might become a requirement in the future." + " Coroutine functions and awaitables have been supported since" + " ipykernel 6.0 (2021). {target} does not seem to return an awaitable" +) + def _accepts_parameters(meth, param_names): parameters = inspect.signature(meth).parameters @@ -842,6 +850,12 @@ async def execute_request(self, stream, ident, parent): if inspect.isawaitable(reply_content): reply_content = await reply_content + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_execute", target=self.do_execute), + PendingDeprecationWarning, + stacklevel=1, + ) # Flush output before sending the reply. if sys.stdout is not None: @@ -898,6 +912,12 @@ async def complete_request(self, stream, ident, parent): matches = self.do_complete(code, cursor_pos) if inspect.isawaitable(matches): matches = await matches + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_complete", target=self.do_complete), + PendingDeprecationWarning, + stacklevel=1, + ) matches = json_clean(matches) self.session.send(stream, "complete_reply", matches, parent, ident) @@ -926,6 +946,12 @@ async def inspect_request(self, stream, ident, parent): ) if inspect.isawaitable(reply_content): reply_content = await reply_content + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_inspect", target=self.do_inspect), + PendingDeprecationWarning, + stacklevel=1, + ) # Before we send this object over, we scrub it for JSON usage reply_content = json_clean(reply_content) @@ -945,6 +971,12 @@ async def history_request(self, stream, ident, parent): reply_content = self.do_history(**content) if inspect.isawaitable(reply_content): reply_content = await reply_content + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_history", target=self.do_history), + PendingDeprecationWarning, + stacklevel=1, + ) reply_content = json_clean(reply_content) msg = self.session.send(stream, "history_reply", reply_content, parent, ident) @@ -1067,7 +1099,13 @@ async def shutdown_request(self, stream, ident, parent): content = self.do_shutdown(parent["content"]["restart"]) if inspect.isawaitable(content): content = await content - self.session.send(stream, "shutdown_reply", content, parent, ident=ident) + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_shutdown", target=self.do_shutdown), + PendingDeprecationWarning, + stacklevel=1, + ) + self.session.send(socket, "shutdown_reply", content, parent, ident=ident) # same content, but different msg_id for broadcasting on IOPub self._shutdown_message = self.session.msg("shutdown_reply", content, parent) @@ -1100,6 +1138,12 @@ async def is_complete_request(self, stream, ident, parent): reply_content = self.do_is_complete(code) if inspect.isawaitable(reply_content): reply_content = await reply_content + else: + warnings.warn( + _AWAITABLE_MESSAGE.format(func_name="do_is_complete", target=self.do_is_complete), + PendingDeprecationWarning, + stacklevel=1, + ) reply_content = json_clean(reply_content) reply_msg = self.session.send(stream, "is_complete_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) @@ -1116,6 +1160,14 @@ async def debug_request(self, stream, ident, parent): reply_content = self.do_debug_request(content) if inspect.isawaitable(reply_content): reply_content = await reply_content + else: + warnings.warn( + _AWAITABLE_MESSAGE.format( + func_name="do_debug_request", target=self.do_debug_request + ), + PendingDeprecationWarning, + stacklevel=1, + ) reply_content = json_clean(reply_content) reply_msg = self.session.send(stream, "debug_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) diff --git a/pyproject.toml b/pyproject.toml index 49d33b4b3..1ebe345fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -188,6 +188,10 @@ filterwarnings= [ # ignore unclosed sqlite in traits "ignore:unclosed database in Date: Tue, 18 Feb 2025 14:37:40 +0100 Subject: [PATCH 23/34] Enable ruff G002 and fix 6 occurences (#1341) --- ipykernel/kernelapp.py | 10 +++++----- ipykernel/kernelbase.py | 2 +- pyproject.toml | 2 -- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index 64924c2c1..c8a2c3a08 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -331,12 +331,12 @@ def init_sockets(self): self.shell_socket = context.socket(zmq.ROUTER) self.shell_socket.linger = 1000 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port) - self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port) + self.log.debug("shell ROUTER Channel on port: %i", self.shell_port) self.stdin_socket = context.socket(zmq.ROUTER) self.stdin_socket.linger = 1000 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port) - self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port) + self.log.debug("stdin ROUTER Channel on port: %i", self.stdin_port) if hasattr(zmq, "ROUTER_HANDOVER"): # set router-handover to workaround zeromq reconnect problems @@ -352,7 +352,7 @@ def init_control(self, context): self.control_socket = context.socket(zmq.ROUTER) self.control_socket.linger = 1000 self.control_port = self._bind_socket(self.control_socket, self.control_port) - self.log.debug("control ROUTER Channel on port: %i" % self.control_port) + self.log.debug("control ROUTER Channel on port: %i", self.control_port) self.debugpy_socket = context.socket(zmq.STREAM) self.debugpy_socket.linger = 1000 @@ -380,7 +380,7 @@ def init_iopub(self, context): self.iopub_socket = context.socket(zmq.PUB) self.iopub_socket.linger = 1000 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port) - self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port) + self.log.debug("iopub PUB Channel on port: %i", self.iopub_port) self.configure_tornado_logger() self.iopub_thread = IOPubThread(self.iopub_socket, pipe=True) self.iopub_thread.start() @@ -394,7 +394,7 @@ def init_heartbeat(self): hb_ctx = zmq.Context() self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port)) self.hb_port = self.heartbeat.port - self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port) + self.log.debug("Heartbeat REP Channel on port: %i", self.hb_port) self.heartbeat.start() def close(self): diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 1e0447b60..08301416b 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -1105,7 +1105,7 @@ async def shutdown_request(self, stream, ident, parent): PendingDeprecationWarning, stacklevel=1, ) - self.session.send(socket, "shutdown_reply", content, parent, ident=ident) + self.session.send(stream, "shutdown_reply", content, parent, ident=ident) # same content, but different msg_id for broadcasting on IOPub self._shutdown_message = self.session.msg("shutdown_reply", content, parent) diff --git a/pyproject.toml b/pyproject.toml index 1ebe345fa..406089874 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -273,8 +273,6 @@ ignore = [ "ARG001", # Unused method argument: "ARG002", - # Logging statement uses `%` - "G002", # `open()` should be replaced by `Path.open()` "PTH123", # use `X | Y` for type annotations, this does not works for dynamic getting type hints on older python From 9930732550423c30b34bdb5b94fdefca91856870 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Tue, 18 Feb 2025 14:41:25 +0100 Subject: [PATCH 24/34] Remove unused ignores lints. (#1342) --- pyproject.toml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 406089874..326b0960e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -255,18 +255,10 @@ ignore = [ "PLR", # Design related pylint codes # Allow non-abstract empty methods in abstract base classes "B027", - # Use of `assert` detected - "S101", - # Possible hardcoded password - "S105", "S106", - # `print` found - "T201", + "T201", # `print` found # Unnecessary `dict` call (rewrite as a literal) "C408", - # Use `contextlib.suppress(ValueError)` instead of try-except-pass - "SIM105", - # `try`-`except`-`pass` detected - "S110", + "SIM105", # Use `contextlib.suppress(ValueError)` instead of try-except-pass # Mutable class attributes should be annotated with `typing.ClassVar` "RUF012", # Unused function argument: @@ -275,21 +267,11 @@ ignore = [ "ARG002", # `open()` should be replaced by `Path.open()` "PTH123", - # use `X | Y` for type annotations, this does not works for dynamic getting type hints on older python - "UP007", + "UP007", # use `X | Y` for type annotations, this does not works for dynamic getting type hints on older python "UP031", # Use format specifiers instead of percent format "PT023", # Use `@pytest.mark.skip` over `@pytest.mark.skip()` "PT001", # autofixable: Use `@pytest.fixture` over `@pytest.fixture()` ] -unfixable = [ - # Don't touch print statements - "T201", - # Don't touch noqa lines - "RUF100", - # Don't touch imports - "F401", - "F403" -] [tool.ruff.lint.per-file-ignores] # B011 Do not call assert False since python -O removes these calls From 2c7cf9de200db5ccc5b1d22ef48c0d97f96d3152 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 19 Feb 2025 12:04:10 +0100 Subject: [PATCH 25/34] Refine deprecation error messages. (#1334) Knowing since when is useful. --- ipykernel/datapub.py | 4 ++-- ipykernel/log.py | 2 +- ipykernel/pickleutil.py | 2 +- ipykernel/serialize.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ipykernel/datapub.py b/ipykernel/datapub.py index 3bd107fef..e8cc501dc 100644 --- a/ipykernel/datapub.py +++ b/ipykernel/datapub.py @@ -20,7 +20,7 @@ from jupyter_client.session import Session, extract_header warnings.warn( - "ipykernel.datapub is deprecated. It has moved to ipyparallel.datapub", + "ipykernel.datapub is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.datapub", DeprecationWarning, stacklevel=2, ) @@ -73,7 +73,7 @@ def publish_data(data): The data to be published. Think of it as a namespace. """ warnings.warn( - "ipykernel.datapub is deprecated. It has moved to ipyparallel.datapub", + "ipykernel.datapub is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.datapub", DeprecationWarning, stacklevel=2, ) diff --git a/ipykernel/log.py b/ipykernel/log.py index 91a4e114e..c230065e8 100644 --- a/ipykernel/log.py +++ b/ipykernel/log.py @@ -5,7 +5,7 @@ from zmq.log.handlers import PUBHandler warnings.warn( - "ipykernel.log is deprecated. It has moved to ipyparallel.engine.log", + "ipykernel.log is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.engine.log", DeprecationWarning, stacklevel=2, ) diff --git a/ipykernel/pickleutil.py b/ipykernel/pickleutil.py index 4ffa5262e..ec299b1a1 100644 --- a/ipykernel/pickleutil.py +++ b/ipykernel/pickleutil.py @@ -18,7 +18,7 @@ from traitlets.utils.importstring import import_item warnings.warn( - "ipykernel.pickleutil is deprecated. It has moved to ipyparallel.", + "ipykernel.pickleutil is deprecated since IPykernel 4.3.0 (2016). It has moved to ipyparallel.", DeprecationWarning, stacklevel=2, ) diff --git a/ipykernel/serialize.py b/ipykernel/serialize.py index 22ba5396e..55247cd67 100644 --- a/ipykernel/serialize.py +++ b/ipykernel/serialize.py @@ -35,7 +35,7 @@ from jupyter_client.session import MAX_BYTES, MAX_ITEMS warnings.warn( - "ipykernel.serialize is deprecated. It has moved to ipyparallel.serialize", + "ipykernel.serialize is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.serialize", DeprecationWarning, stacklevel=2, ) From dc4524370b42190e70274c7e82a0f29aaf4b01a5 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 19 Feb 2025 12:05:09 +0100 Subject: [PATCH 26/34] Try to debug non-closed iopub socket (#1345) --- tests/test_connect.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_connect.py b/tests/test_connect.py index 118d94dee..127f9502d 100644 --- a/tests/test_connect.py +++ b/tests/test_connect.py @@ -18,6 +18,21 @@ from .utils import TemporaryWorkingDirectory + +@pytest.fixture(scope="module", autouse=True) +def _enable_tracemalloc(): + try: + import tracemalloc + except ModuleNotFoundError: + # pypy + tracemalloc = None + if tracemalloc is not None: + tracemalloc.start() + yield + if tracemalloc is not None: + tracemalloc.stop() + + sample_info: dict = { "ip": "1.2.3.4", "transport": "ipc", From 9af3995835882e4fffd62006dbb27369d44f940a Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 19 Feb 2025 14:12:35 +0100 Subject: [PATCH 27/34] Make our own mock kernel methods async (#1346) --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7fe026a03..16a936598 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,7 +121,7 @@ def __init__(self, *args, **kwargs): self.shell = MagicMock() super().__init__(*args, **kwargs) - def do_execute( + async def do_execute( self, code, silent, store_history=True, user_expressions=None, allow_stdin=False ): if not silent: From f16220b624beb2a02fc1fd06b2458a56d7c07200 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 20 Feb 2025 09:01:36 +0100 Subject: [PATCH 28/34] Try to reenable tests from downstream ipywidgets (#1350) --- .github/workflows/downstream.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 269707ad1..2aac74ee6 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -38,7 +38,7 @@ jobs: uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 with: package_name: ipywidgets - 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 + test_command: pytest -vv -raXxs -W default --durations 10 --color=yes jupyter_client: runs-on: ubuntu-latest From 1f1cd448e2034ee1aea4a2bb9d3d82bfc3857e83 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Thu, 20 Feb 2025 15:58:07 +0100 Subject: [PATCH 29/34] Check ignores warnings are still relevant. (#1340) --- pyproject.toml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 326b0960e..e6404d973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -167,7 +167,6 @@ filterwarnings= [ # Ignore our own warnings "ignore:The `stream` parameter of `getpass.getpass` will have no effect:UserWarning", - "ignore:has moved to ipyparallel:DeprecationWarning", # IPython warnings "ignore: `Completer.complete` is pending deprecation since IPython 6.0 and will be replaced by `Completer.completions`:PendingDeprecationWarning", @@ -175,17 +174,8 @@ filterwarnings= [ "ignore: backend2gui is deprecated since IPython 8.24, backends are managed in matplotlib and can be externally registered.:DeprecationWarning", # Ignore jupyter_client warnings - "ignore:unclosed Date: Fri, 28 Feb 2025 13:58:52 +0100 Subject: [PATCH 30/34] Remove deprecated modules since 4.3 (2016). (#1352) --- ipykernel/datapub.py | 83 ------- ipykernel/pickleutil.py | 485 --------------------------------------- ipykernel/serialize.py | 203 ---------------- tests/test_pickleutil.py | 78 ------- 4 files changed, 849 deletions(-) delete mode 100644 ipykernel/datapub.py delete mode 100644 ipykernel/pickleutil.py delete mode 100644 ipykernel/serialize.py delete mode 100644 tests/test_pickleutil.py diff --git a/ipykernel/datapub.py b/ipykernel/datapub.py deleted file mode 100644 index e8cc501dc..000000000 --- a/ipykernel/datapub.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - -"""Publishing native (typically pickled) objects.""" - -import warnings - -from traitlets import Any, CBytes, Dict, Instance -from traitlets.config import Configurable - -from ipykernel.jsonutil import json_clean - -try: - # available since ipyparallel 5.0.0 - from ipyparallel.serialize import serialize_object -except ImportError: - # Deprecated since ipykernel 4.3.0 - from ipykernel.serialize import serialize_object - -from jupyter_client.session import Session, extract_header - -warnings.warn( - "ipykernel.datapub is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.datapub", - DeprecationWarning, - stacklevel=2, -) - - -class ZMQDataPublisher(Configurable): - """A zmq data publisher.""" - - topic = CBytes(b"datapub") - session = Instance(Session, allow_none=True) - pub_socket = Any(allow_none=True) - parent_header = Dict({}) - - def set_parent(self, parent): - """Set the parent for outbound messages.""" - self.parent_header = extract_header(parent) - - def publish_data(self, data): - """publish a data_message on the IOPub channel - - Parameters - ---------- - data : dict - The data to be published. Think of it as a namespace. - """ - session = self.session - assert session is not None - buffers = serialize_object( - data, - buffer_threshold=session.buffer_threshold, - item_threshold=session.item_threshold, - ) - content = json_clean(dict(keys=list(data.keys()))) - session.send( - self.pub_socket, - "data_message", - content=content, - parent=self.parent_header, - buffers=buffers, - ident=self.topic, - ) - - -def publish_data(data): - """publish a data_message on the IOPub channel - - Parameters - ---------- - data : dict - The data to be published. Think of it as a namespace. - """ - warnings.warn( - "ipykernel.datapub is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.datapub", - DeprecationWarning, - stacklevel=2, - ) - - from ipykernel.zmqshell import ZMQInteractiveShell - - ZMQInteractiveShell.instance().data_pub.publish_data(data) diff --git a/ipykernel/pickleutil.py b/ipykernel/pickleutil.py deleted file mode 100644 index ec299b1a1..000000000 --- a/ipykernel/pickleutil.py +++ /dev/null @@ -1,485 +0,0 @@ -"""Pickle related utilities. Perhaps this should be called 'can'.""" - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. -import copy -import pickle -import sys -import typing -import warnings -from types import FunctionType - -# This registers a hook when it's imported -try: - from ipyparallel.serialize import codeutil # noqa: F401 -except ImportError: - pass -from traitlets.log import get_logger -from traitlets.utils.importstring import import_item - -warnings.warn( - "ipykernel.pickleutil is deprecated since IPykernel 4.3.0 (2016). It has moved to ipyparallel.", - DeprecationWarning, - stacklevel=2, -) - -buffer = memoryview -class_type = type - -PICKLE_PROTOCOL = pickle.DEFAULT_PROTOCOL - - -def _get_cell_type(a=None): - """the type of a closure cell doesn't seem to be importable, - so just create one - """ - - def inner(): - return a - - return type(inner.__closure__[0]) # type:ignore[index] - - -cell_type = _get_cell_type() - -# ------------------------------------------------------------------------------- -# Functions -# ------------------------------------------------------------------------------- - - -def interactive(f): - """decorator for making functions appear as interactively defined. - This results in the function being linked to the user_ns as globals() - instead of the module globals(). - """ - - # build new FunctionType, so it can have the right globals - # interactive functions never have closures, that's kind of the point - if isinstance(f, FunctionType): - mainmod = __import__("__main__") - f = FunctionType( - f.__code__, - mainmod.__dict__, - f.__name__, - f.__defaults__, - ) - # associate with __main__ for uncanning - f.__module__ = "__main__" - return f - - -def use_dill(): - """use dill to expand serialization support - - adds support for object methods and closures to serialization. - """ - # import dill causes most of the magic - import dill - - # dill doesn't work with cPickle, - # tell the two relevant modules to use plain pickle - - global pickle # noqa: PLW0603 - pickle = dill - - try: - from ipykernel import serialize - except ImportError: - pass - else: - serialize.pickle = dill # type:ignore[attr-defined] - - # disable special function handling, let dill take care of it - can_map.pop(FunctionType, None) - - -def use_cloudpickle(): - """use cloudpickle to expand serialization support - - adds support for object methods and closures to serialization. - """ - import cloudpickle - - global pickle # noqa: PLW0603 - pickle = cloudpickle - - try: - from ipykernel import serialize - except ImportError: - pass - else: - serialize.pickle = cloudpickle # type:ignore[attr-defined] - - # disable special function handling, let cloudpickle take care of it - can_map.pop(FunctionType, None) - - -# ------------------------------------------------------------------------------- -# Classes -# ------------------------------------------------------------------------------- - - -class CannedObject: - """A canned object.""" - - def __init__(self, obj, keys=None, hook=None): - """can an object for safe pickling - - Parameters - ---------- - obj - The object to be canned - keys : list (optional) - list of attribute names that will be explicitly canned / uncanned - hook : callable (optional) - An optional extra callable, - which can do additional processing of the uncanned object. - - Notes - ----- - large data may be offloaded into the buffers list, - used for zero-copy transfers. - """ - self.keys = keys or [] - self.obj = copy.copy(obj) - self.hook = can(hook) - for key in keys: - setattr(self.obj, key, can(getattr(obj, key))) - - self.buffers = [] - - def get_object(self, g=None): - """Get an object.""" - if g is None: - g = {} - obj = self.obj - for key in self.keys: - setattr(obj, key, uncan(getattr(obj, key), g)) - - if self.hook: - self.hook = uncan(self.hook, g) - self.hook(obj, g) - return self.obj - - -class Reference(CannedObject): - """object for wrapping a remote reference by name.""" - - def __init__(self, name): - """Initialize the reference.""" - if not isinstance(name, str): - raise TypeError("illegal name: %r" % name) - self.name = name - self.buffers = [] - - def __repr__(self): - """Get the string repr of the reference.""" - return "" % self.name - - def get_object(self, g=None): - """Get an object in the reference.""" - if g is None: - g = {} - - return eval(self.name, g) - - -class CannedCell(CannedObject): - """Can a closure cell""" - - def __init__(self, cell): - """Initialize the canned cell.""" - self.cell_contents = can(cell.cell_contents) - - def get_object(self, g=None): - """Get an object in the cell.""" - cell_contents = uncan(self.cell_contents, g) - - def inner(): - """Inner function.""" - return cell_contents - - return inner.__closure__[0] # type:ignore[index] - - -class CannedFunction(CannedObject): - """Can a function.""" - - def __init__(self, f): - """Initialize the can""" - self._check_type(f) - self.code = f.__code__ - self.defaults: typing.Optional[list[typing.Any]] - if f.__defaults__: - self.defaults = [can(fd) for fd in f.__defaults__] - else: - self.defaults = None - - self.closure: typing.Any - closure = f.__closure__ - if closure: - self.closure = tuple(can(cell) for cell in closure) - else: - self.closure = None - - self.module = f.__module__ or "__main__" - self.__name__ = f.__name__ - self.buffers = [] - - def _check_type(self, obj): - assert isinstance(obj, FunctionType), "Not a function type" - - def get_object(self, g=None): - """Get an object out of the can.""" - # try to load function back into its module: - if not self.module.startswith("__"): - __import__(self.module) - g = sys.modules[self.module].__dict__ - - if g is None: - g = {} - defaults = tuple(uncan(cfd, g) for cfd in self.defaults) if self.defaults else None - closure = tuple(uncan(cell, g) for cell in self.closure) if self.closure else None - return FunctionType(self.code, g, self.__name__, defaults, closure) - - -class CannedClass(CannedObject): - """A canned class object.""" - - def __init__(self, cls): - """Initialize the can.""" - self._check_type(cls) - self.name = cls.__name__ - self.old_style = not isinstance(cls, type) - self._canned_dict = {} - for k, v in cls.__dict__.items(): - if k not in ("__weakref__", "__dict__"): - self._canned_dict[k] = can(v) - mro = [] if self.old_style else cls.mro() - - self.parents = [can(c) for c in mro[1:]] - self.buffers = [] - - def _check_type(self, obj): - assert isinstance(obj, class_type), "Not a class type" - - def get_object(self, g=None): - """Get an object from the can.""" - parents = tuple(uncan(p, g) for p in self.parents) - return type(self.name, parents, uncan_dict(self._canned_dict, g=g)) - - -class CannedArray(CannedObject): - """A canned numpy array.""" - - def __init__(self, obj): - """Initialize the can.""" - from numpy import ascontiguousarray - - self.shape = obj.shape - self.dtype = obj.dtype.descr if obj.dtype.fields else obj.dtype.str - self.pickled = False - if sum(obj.shape) == 0: - self.pickled = True - elif obj.dtype == "O": - # can't handle object dtype with buffer approach - self.pickled = True - elif obj.dtype.fields and any(dt == "O" for dt, sz in obj.dtype.fields.values()): - self.pickled = True - if self.pickled: - # just pickle it - self.buffers = [pickle.dumps(obj, PICKLE_PROTOCOL)] - else: - # ensure contiguous - obj = ascontiguousarray(obj, dtype=None) - self.buffers = [buffer(obj)] - - def get_object(self, g=None): - """Get the object.""" - from numpy import frombuffer - - data = self.buffers[0] - if self.pickled: - # we just pickled it - return pickle.loads(data) - return frombuffer(data, dtype=self.dtype).reshape(self.shape) - - -class CannedBytes(CannedObject): - """A canned bytes object.""" - - @staticmethod - def wrap(buf: typing.Union[memoryview, bytes, typing.SupportsBytes]) -> bytes: - """Cast a buffer or memoryview object to bytes""" - if isinstance(buf, memoryview): - return buf.tobytes() - if not isinstance(buf, bytes): - return bytes(buf) - return buf - - def __init__(self, obj): - """Initialize the can.""" - self.buffers = [obj] - - def get_object(self, g=None): - """Get the canned object.""" - data = self.buffers[0] - return self.wrap(data) - - -class CannedBuffer(CannedBytes): - """A canned buffer.""" - - wrap = buffer # type:ignore[assignment] - - -class CannedMemoryView(CannedBytes): - """A canned memory view.""" - - wrap = memoryview # type:ignore[assignment] - - -# ------------------------------------------------------------------------------- -# Functions -# ------------------------------------------------------------------------------- - - -def _import_mapping(mapping, original=None): - """import any string-keys in a type mapping""" - log = get_logger() - log.debug("Importing canning map") - for key, _ in list(mapping.items()): - if isinstance(key, str): - try: - cls = import_item(key) - except Exception: - if original and key not in original: - # only message on user-added classes - log.error("canning class not importable: %r", key, exc_info=True) # noqa: G201 - mapping.pop(key) - else: - mapping[cls] = mapping.pop(key) - - -def istype(obj, check): - """like isinstance(obj, check), but strict - - This won't catch subclasses. - """ - if isinstance(check, tuple): - return any(type(obj) is cls for cls in check) - return type(obj) is check - - -def can(obj): - """prepare an object for pickling""" - - import_needed = False - - for cls, canner in can_map.items(): - if isinstance(cls, str): - import_needed = True - break - if istype(obj, cls): - return canner(obj) - - if import_needed: - # perform can_map imports, then try again - # this will usually only happen once - _import_mapping(can_map, _original_can_map) - return can(obj) - - return obj - - -def can_class(obj): - """Can a class object.""" - if isinstance(obj, class_type) and obj.__module__ == "__main__": - return CannedClass(obj) - return obj - - -def can_dict(obj): - """can the *values* of a dict""" - if istype(obj, dict): - newobj = {} - for k, v in obj.items(): - newobj[k] = can(v) - return newobj - return obj - - -sequence_types = (list, tuple, set) - - -def can_sequence(obj): - """can the elements of a sequence""" - if istype(obj, sequence_types): - t = type(obj) - return t([can(i) for i in obj]) - return obj - - -def uncan(obj, g=None): - """invert canning""" - - import_needed = False - for cls, uncanner in uncan_map.items(): - if isinstance(cls, str): - import_needed = True - break - if isinstance(obj, cls): - return uncanner(obj, g) - - if import_needed: - # perform uncan_map imports, then try again - # this will usually only happen once - _import_mapping(uncan_map, _original_uncan_map) - return uncan(obj, g) - - return obj - - -def uncan_dict(obj, g=None): - """Uncan a dict object.""" - if istype(obj, dict): - newobj = {} - for k, v in obj.items(): - newobj[k] = uncan(v, g) - return newobj - return obj - - -def uncan_sequence(obj, g=None): - """Uncan a sequence.""" - if istype(obj, sequence_types): - t = type(obj) - return t([uncan(i, g) for i in obj]) - return obj - - -# ------------------------------------------------------------------------------- -# API dictionaries -# ------------------------------------------------------------------------------- - -# These dicts can be extended for custom serialization of new objects - -can_map = { - "numpy.ndarray": CannedArray, - FunctionType: CannedFunction, - bytes: CannedBytes, - memoryview: CannedMemoryView, - cell_type: CannedCell, - class_type: can_class, -} -if buffer is not memoryview: - can_map[buffer] = CannedBuffer - -uncan_map: dict[type, typing.Any] = { - CannedObject: lambda obj, g: obj.get_object(g), - dict: uncan_dict, -} - -# for use in _import_mapping: -_original_can_map = can_map.copy() -_original_uncan_map = uncan_map.copy() diff --git a/ipykernel/serialize.py b/ipykernel/serialize.py deleted file mode 100644 index 55247cd67..000000000 --- a/ipykernel/serialize.py +++ /dev/null @@ -1,203 +0,0 @@ -"""serialization utilities for apply messages""" - -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - -import pickle -import warnings -from itertools import chain - -try: - # available since ipyparallel 5.0.0 - from ipyparallel.serialize.canning import ( - CannedObject, - can, - can_sequence, - istype, - sequence_types, - uncan, - uncan_sequence, - ) - from ipyparallel.serialize.serialize import PICKLE_PROTOCOL -except ImportError: - # Deprecated since ipykernel 4.3.0 - from ipykernel.pickleutil import ( - PICKLE_PROTOCOL, - CannedObject, - can, - can_sequence, - istype, - sequence_types, - uncan, - uncan_sequence, - ) - -from jupyter_client.session import MAX_BYTES, MAX_ITEMS - -warnings.warn( - "ipykernel.serialize is deprecated since ipykernel 4.3.0 (2016). It has moved to ipyparallel.serialize", - DeprecationWarning, - stacklevel=2, -) - -# ----------------------------------------------------------------------------- -# Serialization Functions -# ----------------------------------------------------------------------------- - - -def _extract_buffers(obj, threshold=MAX_BYTES): - """extract buffers larger than a certain threshold""" - buffers = [] - if isinstance(obj, CannedObject) and obj.buffers: - for i, buf in enumerate(obj.buffers): - if len(buf) > threshold: - # buffer larger than threshold, prevent pickling - obj.buffers[i] = None - buffers.append(buf) - # buffer too small for separate send, coerce to bytes - # because pickling buffer objects just results in broken pointers - elif isinstance(buf, memoryview): - obj.buffers[i] = buf.tobytes() - return buffers - - -def _restore_buffers(obj, buffers): - """restore buffers extracted by""" - if isinstance(obj, CannedObject) and obj.buffers: - for i, buf in enumerate(obj.buffers): - if buf is None: - obj.buffers[i] = buffers.pop(0) - - -def serialize_object(obj, buffer_threshold=MAX_BYTES, item_threshold=MAX_ITEMS): - """Serialize an object into a list of sendable buffers. - - Parameters - ---------- - obj : object - The object to be serialized - buffer_threshold : int - The threshold (in bytes) for pulling out data buffers - to avoid pickling them. - item_threshold : int - The maximum number of items over which canning will iterate. - Containers (lists, dicts) larger than this will be pickled without - introspection. - - Returns - ------- - [bufs] : list of buffers representing the serialized object. - """ - buffers = [] - if istype(obj, sequence_types) and len(obj) < item_threshold: - cobj = can_sequence(obj) - for c in cobj: - buffers.extend(_extract_buffers(c, buffer_threshold)) - elif istype(obj, dict) and len(obj) < item_threshold: - cobj = {} - for k in sorted(obj): - c = can(obj[k]) - buffers.extend(_extract_buffers(c, buffer_threshold)) - cobj[k] = c - else: - cobj = can(obj) - buffers.extend(_extract_buffers(cobj, buffer_threshold)) - - buffers.insert(0, pickle.dumps(cobj, PICKLE_PROTOCOL)) - return buffers - - -def deserialize_object(buffers, g=None): - """reconstruct an object serialized by serialize_object from data buffers. - - Parameters - ---------- - buffers : list of buffers/bytes - g : globals to be used when uncanning - - Returns - ------- - (newobj, bufs) : unpacked object, and the list of remaining unused buffers. - """ - bufs = list(buffers) - pobj = bufs.pop(0) - canned = pickle.loads(pobj) - if istype(canned, sequence_types) and len(canned) < MAX_ITEMS: - for c in canned: - _restore_buffers(c, bufs) - newobj = uncan_sequence(canned, g) - elif istype(canned, dict) and len(canned) < MAX_ITEMS: - newobj = {} - for k in sorted(canned): - c = canned[k] - _restore_buffers(c, bufs) - newobj[k] = uncan(c, g) - else: - _restore_buffers(canned, bufs) - newobj = uncan(canned, g) - - return newobj, bufs - - -def pack_apply_message(f, args, kwargs, buffer_threshold=MAX_BYTES, item_threshold=MAX_ITEMS): - """pack up a function, args, and kwargs to be sent over the wire - - Each element of args/kwargs will be canned for special treatment, - but inspection will not go any deeper than that. - - Any object whose data is larger than `threshold` will not have their data copied - (only numpy arrays and bytes/buffers support zero-copy) - - Message will be a list of bytes/buffers of the format: - - [ cf, pinfo, , ] - - With length at least two + len(args) + len(kwargs) - """ - - arg_bufs = list( - chain.from_iterable(serialize_object(arg, buffer_threshold, item_threshold) for arg in args) - ) - - kw_keys = sorted(kwargs.keys()) - kwarg_bufs = list( - chain.from_iterable( - serialize_object(kwargs[key], buffer_threshold, item_threshold) for key in kw_keys - ) - ) - - info = dict(nargs=len(args), narg_bufs=len(arg_bufs), kw_keys=kw_keys) - - msg = [pickle.dumps(can(f), PICKLE_PROTOCOL)] - msg.append(pickle.dumps(info, PICKLE_PROTOCOL)) - msg.extend(arg_bufs) - msg.extend(kwarg_bufs) - - return msg - - -def unpack_apply_message(bufs, g=None, copy=True): - """unpack f,args,kwargs from buffers packed by pack_apply_message() - Returns: original f,args,kwargs""" - bufs = list(bufs) # allow us to pop - assert len(bufs) >= 2, "not enough buffers!" - pf = bufs.pop(0) - f = uncan(pickle.loads(pf), g) - pinfo = bufs.pop(0) - info = pickle.loads(pinfo) - arg_bufs, kwarg_bufs = bufs[: info["narg_bufs"]], bufs[info["narg_bufs"] :] - - args_list = [] - for _ in range(info["nargs"]): - arg, arg_bufs = deserialize_object(arg_bufs, g) - args_list.append(arg) - args = tuple(args_list) - assert not arg_bufs, "Shouldn't be any arg bufs left over" - - kwargs = {} - for key in info["kw_keys"]: - kwarg, kwarg_bufs = deserialize_object(kwarg_bufs, g) - kwargs[key] = kwarg - assert not kwarg_bufs, "Shouldn't be any kwarg bufs left over" - - return f, args, kwargs diff --git a/tests/test_pickleutil.py b/tests/test_pickleutil.py deleted file mode 100644 index c48eadf77..000000000 --- a/tests/test_pickleutil.py +++ /dev/null @@ -1,78 +0,0 @@ -import pickle -import warnings - -with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from ipykernel.pickleutil import can, uncan - - -def interactive(f): - f.__module__ = "__main__" - return f - - -def dumps(obj): - return pickle.dumps(can(obj)) - - -def loads(obj): - return uncan(pickle.loads(obj)) - - -def test_no_closure(): - @interactive - def foo(): - a = 5 - return a - - pfoo = dumps(foo) - bar = loads(pfoo) - assert foo() == bar() - - -def test_generator_closure(): - # this only creates a closure on Python 3 - @interactive - def foo(): - i = "i" - r = [i for j in (1, 2)] - return r - - pfoo = dumps(foo) - bar = loads(pfoo) - assert foo() == bar() - - -def test_nested_closure(): - @interactive - def foo(): - i = "i" - - def g(): - return i - - return g() - - pfoo = dumps(foo) - bar = loads(pfoo) - assert foo() == bar() - - -def test_closure(): - i = "i" - - @interactive - def foo(): - return i - - pfoo = dumps(foo) - bar = loads(pfoo) - assert foo() == bar() - - -def test_uncan_bytes_buffer(): - data = b"data" - canned = can(data) - canned.buffers = [memoryview(buf) for buf in canned.buffers] - out = uncan(canned) - assert out == data From 9c9fbc65a58896f0b668f62d238e4c83defe30d7 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 6 Mar 2025 20:37:15 +0100 Subject: [PATCH 31/34] Fix OutStream using _fid before being defined (#1373) --- ipykernel/iostream.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ipykernel/iostream.py b/ipykernel/iostream.py index 8c258bd76..7e8301149 100644 --- a/ipykernel/iostream.py +++ b/ipykernel/iostream.py @@ -383,6 +383,9 @@ def _watch_pipe_fd(self): """ + if self._fid is None: + return + try: bts = os.read(self._fid, PIPE_BUFFER_SIZE) while bts and self._should_watch: @@ -434,6 +437,7 @@ def __init__( ) # This is necessary for compatibility with Python built-in streams self.session = session + self._fid = None if not isinstance(pub_thread, IOPubThread): # Backward-compat: given socket, not thread. Wrap in a thread. warnings.warn( From 50d4db06ee40cc860c79598e8fec1d4bf87a520b Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Fri, 21 Mar 2025 15:27:32 +0100 Subject: [PATCH 32/34] TQDM workaround due to unresponsive maintainer (#1363) Co-authored-by: Min RK --- ipykernel/zmqshell.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 8d4642e05..a5975fd6d 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -468,6 +468,18 @@ def subshell(self, arg_s): class ZMQInteractiveShell(InteractiveShell): """A subclass of InteractiveShell for ZMQ.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # tqdm has an incorrect detection of ZMQInteractiveShell when launch via + # a scheduler that bypass IPKernelApp Think of JupyterHub cluster + # spawners and co. as of end of Feb 2025, the maintainer has been + # unresponsive for 5 months, to our fix, so we implement a workaround. I + # don't like it but we have few other choices. + # See https://github.com/tqdm/tqdm/pull/1628 + if "IPKernelApp" not in self.config: + self.config.IPKernelApp.tqdm = "dummy value for https://github.com/tqdm/tqdm/pull/1628" + displayhook_class = Type(ZMQShellDisplayHook) display_pub_class = Type(ZMQDisplayPublisher) data_pub_class = Any() # type:ignore[assignment] From 524040a1d9ebd0e6734c37f5173df73d8fbbbb0b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:20:47 +0200 Subject: [PATCH 33/34] chore: update pre-commit hooks (#1388) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- ipykernel/_version.py | 1 + ipykernel/ipkernel.py | 6 +++--- ipykernel/kernelbase.py | 2 +- ipykernel/shellchannel.py | 1 + ipykernel/subshell_manager.py | 1 + ipykernel/thread.py | 1 + 7 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e5bdab8d..7d50407a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,7 +74,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.11.4 hooks: - id: ruff types_or: [python, jupyter] diff --git a/ipykernel/_version.py b/ipykernel/_version.py index 33313973b..6b7e6d02a 100644 --- a/ipykernel/_version.py +++ b/ipykernel/_version.py @@ -1,6 +1,7 @@ """ store the current version info of the server. """ + import re # Version string must appear intact for hatch versioning diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py index 6eb2a164e..8da776025 100644 --- a/ipykernel/ipkernel.py +++ b/ipykernel/ipkernel.py @@ -780,9 +780,9 @@ def run_closure(self: threading.Thread): for stream in [stdout, stderr]: if isinstance(stream, OutStream): if parent == kernel_thread_ident: - stream._thread_to_parent_header[ - self.ident - ] = kernel._new_threads_parent_header + stream._thread_to_parent_header[self.ident] = ( + kernel._new_threads_parent_header + ) else: stream._thread_to_parent[self.ident] = parent _threading_Thread_run(self) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 08301416b..da16f2a55 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -72,7 +72,7 @@ def _accepts_parameters(meth, param_names): parameters = inspect.signature(meth).parameters - accepts = {param: False for param in param_names} + accepts = dict.fromkeys(param_names, False) for param in param_names: param_spec = parameters.get(param) diff --git a/ipykernel/shellchannel.py b/ipykernel/shellchannel.py index 77a02f11a..12102e870 100644 --- a/ipykernel/shellchannel.py +++ b/ipykernel/shellchannel.py @@ -1,4 +1,5 @@ """A thread for a shell channel.""" + from __future__ import annotations from typing import Any diff --git a/ipykernel/subshell_manager.py b/ipykernel/subshell_manager.py index be9dd758e..21f0a5af8 100644 --- a/ipykernel/subshell_manager.py +++ b/ipykernel/subshell_manager.py @@ -1,4 +1,5 @@ """Manager of subshells in a kernel.""" + from __future__ import annotations import json diff --git a/ipykernel/thread.py b/ipykernel/thread.py index 13eb781b7..3e1d4f07a 100644 --- a/ipykernel/thread.py +++ b/ipykernel/thread.py @@ -1,4 +1,5 @@ """Base class for threads.""" + from threading import Thread from tornado.ioloop import IOLoop From 558859c46892e8efa6269101a7c26c7a9310684c Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Mon, 14 Jul 2025 16:43:13 +0100 Subject: [PATCH 34/34] Bump mypy to 1.16.1 --- .pre-commit-config.yaml | 2 +- ipykernel/comm/comm.py | 2 +- ipykernel/inprocess/blocking.py | 7 +++--- ipykernel/inprocess/client.py | 40 ++++++++++++++++++++------------- ipykernel/inprocess/ipkernel.py | 7 +++--- ipykernel/ipkernel.py | 4 ++-- ipykernel/kernelapp.py | 8 +++---- ipykernel/zmqshell.py | 29 ++++++++++++------------ 8 files changed, 56 insertions(+), 43 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d50407a2..6f852f711 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: types_or: [yaml, html, json] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.8.0" + rev: "v1.16.1" hooks: - id: mypy files: ipykernel diff --git a/ipykernel/comm/comm.py b/ipykernel/comm/comm.py index 1747d4ce7..a1b659e9a 100644 --- a/ipykernel/comm/comm.py +++ b/ipykernel/comm/comm.py @@ -49,7 +49,7 @@ def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys): class Comm(BaseComm, traitlets.config.LoggingConfigurable): """Class for communicating between a Frontend and a Kernel""" - kernel = Instance("ipykernel.kernelbase.Kernel", allow_none=True) # type:ignore[assignment] + kernel = Instance("ipykernel.kernelbase.Kernel", allow_none=True) comm_id = Unicode() primary = Bool(True, help="Am I the primary or secondary Comm?") diff --git a/ipykernel/inprocess/blocking.py b/ipykernel/inprocess/blocking.py index dcded4ce2..3c2991990 100644 --- a/ipykernel/inprocess/blocking.py +++ b/ipykernel/inprocess/blocking.py @@ -69,6 +69,7 @@ def call_handlers(self, msg): _raw_input = self.client.kernel._sys_raw_input prompt = msg["content"]["prompt"] print(prompt, end="", file=sys.__stdout__) + assert sys.__stdout__ is not None sys.__stdout__.flush() self.client.input(_raw_input()) @@ -77,9 +78,9 @@ class BlockingInProcessKernelClient(InProcessKernelClient): """A blocking in-process kernel client.""" # The classes to use for the various channels. - shell_channel_class = Type(BlockingInProcessChannel) # type:ignore[arg-type] - iopub_channel_class = Type(BlockingInProcessChannel) # type:ignore[arg-type] - stdin_channel_class = Type(BlockingInProcessStdInChannel) # type:ignore[arg-type] + shell_channel_class = Type(BlockingInProcessChannel) + iopub_channel_class = Type(BlockingInProcessChannel) + stdin_channel_class = Type(BlockingInProcessStdInChannel) def wait_for_ready(self): """Wait for kernel info reply on shell channel.""" diff --git a/ipykernel/inprocess/client.py b/ipykernel/inprocess/client.py index 6250302d5..ffe044826 100644 --- a/ipykernel/inprocess/client.py +++ b/ipykernel/inprocess/client.py @@ -10,8 +10,10 @@ # ----------------------------------------------------------------------------- # Imports # ----------------------------------------------------------------------------- +from __future__ import annotations import asyncio +from typing import Any from jupyter_client.client import KernelClient from jupyter_client.clientabc import KernelClientABC @@ -39,11 +41,11 @@ class InProcessKernelClient(KernelClient): """ # The classes to use for the various channels. - shell_channel_class = Type(InProcessChannel) # type:ignore[arg-type] - iopub_channel_class = Type(InProcessChannel) # type:ignore[arg-type] - stdin_channel_class = Type(InProcessChannel) # type:ignore[arg-type] - control_channel_class = Type(InProcessChannel) # type:ignore[arg-type] - hb_channel_class = Type(InProcessHBChannel) # type:ignore[arg-type] + shell_channel_class = Type(InProcessChannel) # type:ignore[assignment] + iopub_channel_class = Type(InProcessChannel) # type:ignore[assignment] + stdin_channel_class = Type(InProcessChannel) # type:ignore[assignment] + control_channel_class = Type(InProcessChannel) # type:ignore[assignment] + hb_channel_class = Type(InProcessHBChannel) # type:ignore[assignment] kernel = Instance("ipykernel.inprocess.ipkernel.InProcessKernel", allow_none=True) @@ -57,9 +59,9 @@ def _default_blocking_class(self): return BlockingInProcessKernelClient - def get_connection_info(self): + def get_connection_info(self, session: bool = False) -> dict[str, int | str | bytes]: """Get the connection info for the client.""" - d = super().get_connection_info() + d = super().get_connection_info(session=session) d["kernel"] = self.kernel # type:ignore[assignment] return d @@ -72,39 +74,45 @@ def start_channels(self, *args, **kwargs): @property def shell_channel(self): if self._shell_channel is None: - self._shell_channel = self.shell_channel_class(self) # type:ignore[abstract,call-arg] + self._shell_channel = self.shell_channel_class(self) return self._shell_channel @property def iopub_channel(self): if self._iopub_channel is None: - self._iopub_channel = self.iopub_channel_class(self) # type:ignore[abstract,call-arg] + self._iopub_channel = self.iopub_channel_class(self) return self._iopub_channel @property def stdin_channel(self): if self._stdin_channel is None: - self._stdin_channel = self.stdin_channel_class(self) # type:ignore[abstract,call-arg] + self._stdin_channel = self.stdin_channel_class(self) return self._stdin_channel @property def control_channel(self): if self._control_channel is None: - self._control_channel = self.control_channel_class(self) # type:ignore[abstract,call-arg] + self._control_channel = self.control_channel_class(self) return self._control_channel @property def hb_channel(self): if self._hb_channel is None: - self._hb_channel = self.hb_channel_class(self) # type:ignore[abstract,call-arg] + self._hb_channel = self.hb_channel_class(self) return self._hb_channel # Methods for sending specific messages # ------------------------------------- def execute( - self, code, silent=False, store_history=True, user_expressions=None, allow_stdin=None - ): + self, + code: str, + silent: bool = False, + store_history: bool = True, + user_expressions: dict[str, Any] | None = None, + allow_stdin: bool | None = None, + stop_on_error: bool = True, + ) -> str: """Execute code on the client.""" if allow_stdin is None: allow_stdin = self.allow_stdin @@ -117,7 +125,9 @@ def execute( ) msg = self.session.msg("execute_request", content) self._dispatch_to_kernel(msg) - return msg["header"]["msg_id"] + res = msg["header"]["msg_id"] + assert isinstance(res, str) + return res def complete(self, code, cursor_pos=None): """Get code completion.""" diff --git a/ipykernel/inprocess/ipkernel.py b/ipykernel/inprocess/ipkernel.py index aecb3b102..62c15036b 100644 --- a/ipykernel/inprocess/ipkernel.py +++ b/ipykernel/inprocess/ipkernel.py @@ -2,6 +2,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import logging import sys @@ -51,7 +52,7 @@ class InProcessKernel(IPythonKernel): _underlying_iopub_socket = Instance(DummySocket, ()) iopub_thread: IOPubThread = Instance(IOPubThread) # type:ignore[assignment] - shell_stream = Instance(DummySocket, ()) # type:ignore[arg-type] + shell_stream = Instance(DummySocket, ()) # type:ignore[assignment] @default("iopub_thread") def _default_iopub_thread(self): @@ -65,7 +66,7 @@ def _default_iopub_thread(self): def _default_iopub_socket(self): return self.iopub_thread.background_socket - stdin_socket = Instance(DummySocket, ()) # type:ignore[assignment] + stdin_socket = Instance(DummySocket, ()) def __init__(self, **traits): """Initialize the kernel.""" @@ -85,7 +86,7 @@ def start(self): if self.shell: self.shell.exit_now = False - def _abort_queues(self): + def _abort_queues(self, subshell_id: str | None = ...): """The in-process kernel doesn't abort requests.""" def _input_request(self, prompt, ident, parent, password=False): diff --git a/ipykernel/ipkernel.py b/ipykernel/ipkernel.py index 8da776025..3e3927cc5 100644 --- a/ipykernel/ipkernel.py +++ b/ipykernel/ipkernel.py @@ -231,7 +231,7 @@ def dispatch_debugpy(self, msg): self.debugger.tcp_client.receive_dap_frame(frame) @property - def banner(self): + def banner(self): # type:ignore[override] if self.shell: return self.shell.banner return None @@ -455,7 +455,7 @@ async def run_cell(*args, **kwargs): if threading.current_thread() == threading.main_thread() else self._dummy_context_manager ) - with cm(coro_future): # type:ignore[operator] + with cm(coro_future): res = None try: res = await coro_future diff --git a/ipykernel/kernelapp.py b/ipykernel/kernelapp.py index c8a2c3a08..86b275a82 100644 --- a/ipykernel/kernelapp.py +++ b/ipykernel/kernelapp.py @@ -118,9 +118,9 @@ class IPKernelApp(BaseIPythonApplication, InteractiveShellApp, ConnectionFileMix """The IPYKernel application class.""" name = "ipython-kernel" - aliases = Dict(kernel_aliases) # type:ignore[assignment] - flags = Dict(kernel_flags) # type:ignore[assignment] - classes = [IPythonKernel, ZMQInteractiveShell, ProfileDir, Session] + aliases = Dict(kernel_aliases) + flags = Dict(kernel_flags) + classes = [IPythonKernel, ZMQInteractiveShell, ProfileDir, Session] # type:ignore[assignment] # the kernel class, as an importstring kernel_class = Type( "ipykernel.ipkernel.IPythonKernel", @@ -261,7 +261,7 @@ def _bind_socket(self, s, port): raise return None - def write_connection_file(self): + def write_connection_file(self, **kwargs: Any) -> None: """write connection info to JSON file""" cf = self.abs_connection_file connection_info = dict( diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index a5975fd6d..4883b376b 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -17,6 +17,7 @@ import os import sys import threading +import typing import warnings from pathlib import Path @@ -78,12 +79,16 @@ def _hooks(self): self._thread_local.hooks = [] return self._thread_local.hooks - def publish( + # Feb: 2025 IPython has a deprecated, `source` parameter, marked for removal that + # triggers typing errors. + def publish( # type:ignore[override] self, data, metadata=None, + *, transient=None, update=False, + **kwargs, ): """Publish a display-data message @@ -125,7 +130,7 @@ def publish( for hook in self._hooks: msg = hook(msg) if msg is None: - return # type:ignore[unreachable] + return self.session.send( self.pub_socket, @@ -153,7 +158,7 @@ def clear_output(self, wait=False): for hook in self._hooks: msg = hook(msg) if msg is None: - return # type:ignore[unreachable] + return self.session.send( self.pub_socket, @@ -482,7 +487,7 @@ def __init__(self, *args, **kwargs): displayhook_class = Type(ZMQShellDisplayHook) display_pub_class = Type(ZMQDisplayPublisher) - data_pub_class = Any() # type:ignore[assignment] + data_pub_class = Any() kernel = Any() parent_header = Any() @@ -520,7 +525,7 @@ def _update_exit_now(self, change): # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no # interactive input being read; we provide event loop support in ipkernel - def enable_gui(self, gui): + def enable_gui(self, gui: typing.Any = None) -> None: """Enable a given guil.""" from .eventloops import enable_gui as real_enable_gui @@ -590,7 +595,7 @@ def data_pub(self): stacklevel=2, ) - self._data_pub = self.data_pub_class(parent=self) # type:ignore[has-type] + self._data_pub = self.data_pub_class(parent=self) self._data_pub.session = self.display_pub.session # type:ignore[attr-defined] self._data_pub.pub_socket = self.display_pub.pub_socket # type:ignore[attr-defined] return self._data_pub @@ -660,14 +665,10 @@ def set_parent(self, parent): self.display_pub.set_parent(parent) # type:ignore[attr-defined] if hasattr(self, "_data_pub"): self.data_pub.set_parent(parent) - try: - sys.stdout.set_parent(parent) # type:ignore[attr-defined] - except AttributeError: - pass - try: - sys.stderr.set_parent(parent) # type:ignore[attr-defined] - except AttributeError: - pass + if hasattr(sys.stdout, "set_parent"): + sys.stdout.set_parent(parent) + if hasattr(sys.stderr, "set_parent"): + sys.stderr.set_parent(parent) def get_parent(self): """Get the parent header."""