Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-nbclient for openSUSE:Factory checked in at 2023-04-24 22:31:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-nbclient (Old) and /work/SRC/openSUSE:Factory/.python-nbclient.new.1533 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-nbclient" Mon Apr 24 22:31:07 2023 rev:27 rq:1082321 version:0.7.3 Changes: -------- --- /work/SRC/openSUSE:Factory/python-nbclient/python-nbclient.changes 2023-01-08 21:25:52.659367636 +0100 +++ /work/SRC/openSUSE:Factory/.python-nbclient.new.1533/python-nbclient.changes 2023-04-24 22:31:14.251519297 +0200 @@ -1,0 +2,6 @@ +Sun Apr 23 17:56:28 UTC 2023 - Ben Greiner <c...@bnavigator.de> + +- Update to 0.7.3 + * Add coalesce_streams #279 (@davidbrochart) + +------------------------------------------------------------------- Old: ---- nbclient-0.7.2.tar.gz New: ---- nbclient-0.7.3.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-nbclient.spec ++++++ --- /var/tmp/diff_new_pack.QOqM2a/_old 2023-04-24 22:31:14.835522769 +0200 +++ /var/tmp/diff_new_pack.QOqM2a/_new 2023-04-24 22:31:14.843522816 +0200 @@ -31,7 +31,7 @@ %endif Name: python-nbclient%{psuffix} -Version: 0.7.2 +Version: 0.7.3 Release: 0 Summary: A client library for executing notebooks License: BSD-3-Clause @@ -55,6 +55,7 @@ Requires(postun):update-alternatives %endif %if %{with test} +BuildRequires: %{python_module flaky} BuildRequires: %{python_module ipykernel} BuildRequires: %{python_module ipython} BuildRequires: %{python_module ipywidgets} ++++++ nbclient-0.7.2.tar.gz -> nbclient-0.7.3.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/LICENSE new/nbclient-0.7.3/LICENSE --- old/nbclient-0.7.2/LICENSE 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/LICENSE 2020-02-02 01:00:00.000000000 +0100 @@ -1,59 +1,30 @@ -# Licensing terms +BSD 3-Clause License -This project is licensed under the terms of the Modified BSD License -(also known as New or Revised or 3-Clause BSD), as follows: - -- Copyright (c) 2020-, Jupyter Development Team +Copyright (c) 2020-, Jupyter Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of the Jupyter Development Team nor the names of its -contributors may be used to endorse or promote products derived from this -software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -## About the Jupyter Development Team - -The Jupyter Development Team is the set of all contributors to the Jupyter project. -This includes all of the Jupyter subprojects. - -The core team that coordinates development on GitHub can be found here: -https://github.com/jupyter/. - -## Our Copyright Policy - -Jupyter uses a shared copyright model. Each contributor maintains copyright -over their contributions to Jupyter. But, it is important to note that these -contributions are typically only changes to the repositories. Thus, the Jupyter -source code, in its entirety is not the copyright of any single person or -institution. Instead, it is the collective copyright of the entire Jupyter -Development Team. If individual contributors want to maintain a record of what -changes/contributions they have specific copyright on, they should indicate -their copyright in the commit message of the change, when they commit the -change to one of the Jupyter repositories. - -With this in mind, the following banner should be used in any source code file -to indicate the copyright and license terms: - - # Copyright (c) Jupyter Development Team. - # Distributed under the terms of the Modified BSD License. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/PKG-INFO new/nbclient-0.7.3/PKG-INFO --- old/nbclient-0.7.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: nbclient -Version: 0.7.2 +Version: 0.7.3 Summary: A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor. Project-URL: Documentation, https://nbclient.readthedocs.io Project-URL: Funding, https://numfocus.org/ @@ -8,65 +8,36 @@ Project-URL: Source, https://github.com/jupyter/nbclient Project-URL: Tracker, https://github.com/jupyter/nbclient/issues Author-email: Jupyter Development Team <jupy...@googlegroups.com> -License: # Licensing terms +License: BSD 3-Clause License - This project is licensed under the terms of the Modified BSD License - (also known as New or Revised or 3-Clause BSD), as follows: - - - Copyright (c) 2020-, Jupyter Development Team + Copyright (c) 2020-, Jupyter Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - Neither the name of the Jupyter Development Team nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - ## About the Jupyter Development Team - - The Jupyter Development Team is the set of all contributors to the Jupyter project. - This includes all of the Jupyter subprojects. - - The core team that coordinates development on GitHub can be found here: - https://github.com/jupyter/. - - ## Our Copyright Policy - - Jupyter uses a shared copyright model. Each contributor maintains copyright - over their contributions to Jupyter. But, it is important to note that these - contributions are typically only changes to the repositories. Thus, the Jupyter - source code, in its entirety is not the copyright of any single person or - institution. Instead, it is the collective copyright of the entire Jupyter - Development Team. If individual contributors want to maintain a record of what - changes/contributions they have specific copyright on, they should indicate - their copyright in the commit message of the change, when they commit the - change to one of the Jupyter repositories. - - With this in mind, the following banner should be used in any source code file - to indicate the copyright and license terms: - - # Copyright (c) Jupyter Development Team. - # Distributed under the terms of the Modified BSD License. License-File: LICENSE Keywords: executor,jupyter,notebook,pipeline Classifier: Intended Audience :: Developers @@ -95,7 +66,9 @@ Requires-Dist: nbclient[test]; extra == 'docs' Requires-Dist: sphinx-book-theme; extra == 'docs' Requires-Dist: sphinx>=1.7; extra == 'docs' +Requires-Dist: sphinxcontrib-spelling; extra == 'docs' Provides-Extra: test +Requires-Dist: flaky; extra == 'test' Requires-Dist: ipykernel; extra == 'test' Requires-Dist: ipython; extra == 'test' Requires-Dist: ipywidgets; extra == 'test' @@ -110,7 +83,7 @@ [](https://mybinder.org/v2/gh/jupyter/nbclient/main?filepath=binder%2Frun_nbclient.ipynb) [](https://github.com/jupyter/nbclient/actions) [](https://nbclient.readthedocs.io/en/latest/?badge=latest) -[](https://codecov.io/github/jupyter/nbclient?branch=main) +[](https://codecov.io/gh/jupyter/nbclient?branch=main) [](https://www.python.org/downloads/release/python-370/) [](https://www.python.org/downloads/release/python-380/) [](https://www.python.org/downloads/release/python-390/) @@ -162,3 +135,31 @@ for starting, managing and communicating with Jupyter kernels. While, nbclient allows notebooks to be run in different execution contexts. + +## About the Jupyter Development Team + +The Jupyter Development Team is the set of all contributors to the Jupyter project. +This includes all of the Jupyter subprojects. + +The core team that coordinates development on GitHub can be found here: +https://github.com/jupyter/. + +## Our Copyright Policy + +Jupyter uses a shared copyright model. Each contributor maintains copyright +over their contributions to Jupyter. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Jupyter +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Jupyter +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Jupyter repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +``` +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/README.md new/nbclient-0.7.3/README.md --- old/nbclient-0.7.2/README.md 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/README.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,7 +1,7 @@ [](https://mybinder.org/v2/gh/jupyter/nbclient/main?filepath=binder%2Frun_nbclient.ipynb) [](https://github.com/jupyter/nbclient/actions) [](https://nbclient.readthedocs.io/en/latest/?badge=latest) -[](https://codecov.io/github/jupyter/nbclient?branch=main) +[](https://codecov.io/gh/jupyter/nbclient?branch=main) [](https://www.python.org/downloads/release/python-370/) [](https://www.python.org/downloads/release/python-380/) [](https://www.python.org/downloads/release/python-390/) @@ -53,3 +53,31 @@ for starting, managing and communicating with Jupyter kernels. While, nbclient allows notebooks to be run in different execution contexts. + +## About the Jupyter Development Team + +The Jupyter Development Team is the set of all contributors to the Jupyter project. +This includes all of the Jupyter subprojects. + +The core team that coordinates development on GitHub can be found here: +https://github.com/jupyter/. + +## Our Copyright Policy + +Jupyter uses a shared copyright model. Each contributor maintains copyright +over their contributions to Jupyter. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Jupyter +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Jupyter +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Jupyter repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +``` +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/__init__.py new/nbclient-0.7.3/nbclient/__init__.py --- old/nbclient-0.7.2/nbclient/__init__.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/__init__.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,8 +1,7 @@ import subprocess import sys -from ._version import __version__ # noqa -from ._version import version_info # noqa +from ._version import __version__, version_info # noqa # noqa from .client import NotebookClient, execute # noqa: F401 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/_version.py new/nbclient-0.7.3/nbclient/_version.py --- old/nbclient-0.7.2/nbclient/_version.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/_version.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,7 +1,8 @@ +"""Version info.""" import re from typing import List, Union -__version__ = "0.7.2" +__version__ = "0.7.3" # Build up version_info tuple for backwards compatibility pattern = r'(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/cli.py new/nbclient-0.7.3/nbclient/cli.py --- old/nbclient-0.7.2/nbclient/cli.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/cli.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,4 @@ +"""nbclient cli.""" import logging import pathlib import sys @@ -12,13 +13,13 @@ from .client import NotebookClient -nbclient_aliases = { +nbclient_aliases: dict = { 'timeout': 'NbClientApp.timeout', 'startup_timeout': 'NbClientApp.startup_timeout', 'kernel_name': 'NbClientApp.kernel_name', } -nbclient_flags = { +nbclient_flags: dict = { 'allow-errors': ( { 'NbClientApp': { @@ -99,6 +100,7 @@ @catch_config_error def initialize(self, argv=None): + """Initialize the app.""" super().initialize(argv) # Get notebooks to run @@ -106,13 +108,13 @@ # If there are none, throw an error if not self.notebooks: - print(f"{self.name}: error: expected path to notebook") sys.exit(-1) # Loop and run them one by one [self.run_notebook(path) for path in self.notebooks] def get_notebooks(self): + """Get the notebooks for the app.""" # If notebooks were provided from the command line, use those if self.extra_args: notebooks = self.extra_args @@ -124,6 +126,7 @@ return notebooks def run_notebook(self, notebook_path): + """Run a notebook by path.""" # Log it self.log.info(f"Executing {notebook_path}") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/client.py new/nbclient-0.7.3/nbclient/client.py --- old/nbclient-0.7.2/nbclient/client.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/client.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,8 +1,10 @@ +"""nbclient implementation.""" import asyncio import atexit import base64 import collections import datetime +import re import signal import typing as t from contextlib import asynccontextmanager, contextmanager @@ -14,18 +16,7 @@ from jupyter_client.client import KernelClient from nbformat import NotebookNode from nbformat.v4 import output_from_msg -from traitlets import ( - Any, - Bool, - Callable, - Dict, - Enum, - Integer, - List, - Type, - Unicode, - default, -) +from traitlets import Any, Bool, Callable, Dict, Enum, Integer, List, Type, Unicode, default from traitlets.config.configurable import LoggingConfigurable from .exceptions import ( @@ -38,8 +29,12 @@ from .output_widget import OutputWidget from .util import ensure_async, run_hook, run_sync +_RGX_CARRIAGERETURN = re.compile(r".*\r(?=[^\n])") +_RGX_BACKSPACE = re.compile(r"[^\n]\b") + def timestamp(msg: t.Optional[t.Dict] = None) -> str: + """Get the timestamp for a message.""" if msg and 'header' in msg: # The test mocks don't provide a header, so tolerate that msg_header = msg['header'] if 'date' in msg_header and isinstance(msg_header['date'], datetime.datetime): @@ -52,7 +47,7 @@ formatted_time ): # docs indicate strftime may return empty string, so let's catch that too return formatted_time - except Exception: + except Exception: # noqa pass # fallback to a local time return datetime.datetime.utcnow().isoformat() + 'Z' @@ -435,6 +430,14 @@ ) ) + coalesce_streams = Bool( + help=dedent( + """ + Merge all stream outputs with shared names into single streams. + """ + ) + ) + def __init__(self, nb: NotebookNode, km: t.Optional[KernelManager] = None, **kw: t.Any) -> None: """Initializes the execution manager. @@ -556,13 +559,11 @@ try: self.kc = self.km.client() await ensure_async(self.kc.start_channels()) # type:ignore[func-returns-value] - await ensure_async( - self.kc.wait_for_ready(timeout=self.startup_timeout) # type:ignore[attr-defined] - ) + await ensure_async(self.kc.wait_for_ready(timeout=self.startup_timeout)) except Exception as e: self.log.error( "Error occurred while starting new kernel client for kernel {}: {}".format( - self.km.kernel_id, str(e) + getattr(self.km, 'kernel_id', None), str(e) ) ) await self._async_cleanup_kernel() @@ -626,7 +627,8 @@ atexit.register(self._cleanup_kernel) def on_signal(): - asyncio.ensure_future(self._async_cleanup_kernel()) + """Handle signals.""" + self._async_cleanup_kernel_future = asyncio.ensure_future(self._async_cleanup_kernel()) atexit.unregister(self._cleanup_kernel) loop = asyncio.get_event_loop() @@ -710,6 +712,7 @@ execute = run_sync(async_execute) def set_widgets_metadata(self) -> None: + """Set with widget metadata.""" if self.widget_state: self.nb.metadata.widgets = { 'application/vnd.jupyter.widget-state+json': { @@ -759,7 +762,6 @@ task_poll_output_msg: asyncio.Future, task_poll_kernel_alive: asyncio.Future, ) -> t.Dict: - msg: t.Dict assert self.kc is not None new_timeout: t.Optional[float] = None @@ -784,7 +786,7 @@ task_poll_kernel_alive.cancel() raise CellTimeoutError.error_from_timeout_and_cell( "Timeout waiting for IOPub output", self.iopub_timeout, cell - ) + ) from None else: self.log.warning("Timeout waiting for IOPub output") task_poll_kernel_alive.cancel() @@ -802,7 +804,6 @@ async def _async_poll_output_msg( self, parent_msg_id: str, cell: NotebookNode, cell_index: int ) -> None: - assert self.kc is not None while True: msg = await ensure_async(self.kc.iopub_channel.get_msg(timeout=None)) @@ -837,7 +838,6 @@ async def _async_handle_timeout( self, timeout: int, cell: t.Optional[NotebookNode] = None ) -> t.Union[None, t.Dict]: - self.log.error("Timeout waiting for execute reply (%is)." % timeout) if self.interrupt_on_timeout: self.log.error("Interrupting kernel") @@ -862,7 +862,7 @@ async def async_wait_for_reply( self, msg_id: str, cell: t.Optional[NotebookNode] = None ) -> t.Optional[t.Dict]: - + """Wait for a message reply.""" assert self.kc is not None # wait for finish, with timeout timeout = self._get_timeout(cell) @@ -895,7 +895,6 @@ async def _check_raise_for_error( self, cell: NotebookNode, cell_index: int, exec_reply: t.Optional[t.Dict] ) -> None: - if exec_reply is None: return None @@ -1003,7 +1002,7 @@ except asyncio.CancelledError: # can only be cancelled by task_poll_kernel_alive when the kernel is dead task_poll_output_msg.cancel() - raise DeadKernelError("Kernel died") + raise DeadKernelError("Kernel died") from None except Exception as e: # Best effort to cancel request if it hasn't been resolved try: @@ -1019,6 +1018,44 @@ self.on_cell_executed, cell=cell, cell_index=cell_index, execute_reply=exec_reply ) await self._check_raise_for_error(cell, cell_index, exec_reply) + + if self.coalesce_streams and cell.outputs: + new_outputs = [] + streams: dict[str, NotebookNode] = {} + for output in cell.outputs: + if output["output_type"] == "stream": + if output["name"] in streams: + streams[output["name"]]["text"] += output["text"] + else: + new_outputs.append(output) + streams[output["name"]] = output + else: + new_outputs.append(output) + + # process \r and \b characters + for output in streams.values(): + old = output["text"] + while len(output["text"]) < len(old): + old = output["text"] + # Cancel out anything-but-newline followed by backspace + output["text"] = _RGX_BACKSPACE.sub("", output["text"]) + # Replace all carriage returns not followed by newline + output["text"] = _RGX_CARRIAGERETURN.sub("", output["text"]) + + # We also want to ensure stdout and stderr are always in the same consecutive order, + # because they are asynchronous, so order isn't guaranteed. + for i, output in enumerate(new_outputs): + if output["output_type"] == "stream" and output["name"] == "stderr": + if ( + len(new_outputs) >= i + 2 + and new_outputs[i + 1]["output_type"] == "stream" + and new_outputs[i + 1]["name"] == "stdout" + ): + stdout = new_outputs.pop(i + 1) + new_outputs.insert(i, stdout) + + cell.outputs = new_outputs + self.nb['cells'][cell_index] = cell return cell @@ -1091,6 +1128,7 @@ def output( self, outs: t.List, msg: t.Dict, display_id: str, cell_index: int ) -> t.Optional[NotebookNode]: + """Handle output.""" msg_type = msg['msg_type'] out: t.Optional[NotebookNode] = None @@ -1127,7 +1165,7 @@ return out def clear_output(self, outs: t.List, msg: t.Dict, cell_index: int) -> None: - + """Clear output.""" content = msg['content'] parent_msg_id = msg['parent_header'].get('msg_id') @@ -1147,13 +1185,13 @@ self.clear_display_id_mapping(cell_index) def clear_display_id_mapping(self, cell_index: int) -> None: - + """Clear a display id mapping for a cell.""" for _, cell_map in self._display_id_map.items(): if cell_index in cell_map: cell_map[cell_index] = [] def handle_comm_msg(self, outs: t.List, msg: t.Dict, cell_index: int) -> None: - + """Handle a comm message.""" content = msg['content'] data = content['data'] if self.store_widget_state and 'state' in data: # ignore custom msg'es @@ -1225,6 +1263,7 @@ assert removed_hook == hook def on_comm_open_jupyter_widget(self, msg: t.Dict) -> t.Optional[t.Any]: + """Handle a jupyter widget comm open.""" content = msg['content'] data = content['data'] state = data['state'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/exceptions.py new/nbclient-0.7.3/nbclient/exceptions.py --- old/nbclient-0.7.2/nbclient/exceptions.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/exceptions.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,9 +1,10 @@ +"""Exceptions for nbclient.""" from typing import Dict from nbformat import NotebookNode -class CellControlSignal(Exception): +class CellControlSignal(Exception): # noqa """ A custom exception used to indicate that the exception is used for cell control actions (not the best model, but it's needed to cover existing @@ -22,6 +23,7 @@ def error_from_timeout_and_cell( cls, msg: str, timeout: int, cell: NotebookNode ) -> "CellTimeoutError": + """Create an error from a timeout on a cell.""" if cell and cell.source: src_by_lines = cell.source.strip().split("\n") src = ( @@ -35,6 +37,8 @@ class DeadKernelError(RuntimeError): + """A dead kernel error.""" + pass @@ -58,21 +62,25 @@ """ def __init__(self, traceback: str, ename: str, evalue: str) -> None: + """Initialize the error.""" super().__init__(traceback) self.traceback = traceback self.ename = ename self.evalue = evalue def __reduce__(self) -> tuple: + """Reduce implementation.""" return type(self), (self.traceback, self.ename, self.evalue) def __str__(self) -> str: + """Str repr.""" s = self.__unicode__() if not isinstance(s, str): s = s.encode('utf8', 'replace') return s def __unicode__(self) -> str: + """Unicode repr.""" return self.traceback @classmethod diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/output_widget.py new/nbclient-0.7.3/nbclient/output_widget.py --- old/nbclient-0.7.2/nbclient/output_widget.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/output_widget.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,4 @@ +"""An output widget mimic.""" from typing import Any, Dict, List, Optional from jupyter_client.client import KernelClient @@ -12,7 +13,7 @@ def __init__( self, comm_id: str, state: Dict[str, Any], kernel_client: KernelClient, executor: Any ) -> None: - + """Initialize the widget.""" self.comm_id: str = comm_id self.state: Dict[str, Any] = state self.kernel_client: KernelClient = kernel_client @@ -22,7 +23,7 @@ self.clear_before_next_output: bool = False def clear_output(self, outs: List, msg: Dict, cell_index: int) -> None: - + """Clear output.""" self.parent_header = msg['parent_header'] content = msg['content'] if content.get('wait'): @@ -36,6 +37,7 @@ self.executor.widget_state[self.comm_id]['outputs'] = self.outputs def sync_state(self) -> None: + """Sync state.""" state = {'outputs': self.outputs} msg = {'method': 'update', 'state': state, 'buffer_paths': []} self.send(msg) @@ -46,7 +48,7 @@ data: Optional[Dict] = None, metadata: Optional[Dict] = None, buffers: Optional[List] = None, - **keys: Any + **keys: Any, ) -> None: """Helper for sending a comm message on IOPub""" data = {} if data is None else data @@ -63,11 +65,11 @@ metadata: Optional[Dict] = None, buffers: Optional[List] = None, ) -> None: - + """Send a comm message.""" self._publish_msg('comm_msg', data=data, metadata=metadata, buffers=buffers) def output(self, outs: List, msg: Dict, display_id: str, cell_index: int) -> None: - + """Handle output.""" if self.clear_before_next_output: self.outputs = [] self.clear_before_next_output = False @@ -93,6 +95,7 @@ self.executor.widget_state[self.comm_id]['outputs'] = self.outputs def set_state(self, state: Dict) -> None: + """Set the state.""" if 'msg_id' in state: msg_id = state.get('msg_id') if msg_id: @@ -103,9 +106,11 @@ self.msg_id = msg_id def handle_msg(self, msg: Dict) -> None: + """Handle a message.""" content = msg['content'] comm_id = content['comm_id'] - assert comm_id == self.comm_id + if comm_id != self.comm_id: + raise AssertionError('Mismatched comm id') data = content['data'] if 'state' in data: self.set_state(data['state']) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/tests/test_client.py new/nbclient-0.7.3/nbclient/tests/test_client.py --- old/nbclient-0.7.2/nbclient/tests/test_client.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/tests/test_client.py 2020-02-02 01:00:00.000000000 +0100 @@ -15,6 +15,7 @@ import nbformat import pytest import xmltodict +from flaky import flaky # type:ignore from jupyter_client import KernelClient, KernelManager from jupyter_client._version import version_info from jupyter_client.kernelspec import KernelSpecManager @@ -23,8 +24,9 @@ from testpath import modified_env from traitlets import TraitError -from .. import NotebookClient, execute -from ..exceptions import CellExecutionError +from nbclient import NotebookClient, execute +from nbclient.exceptions import CellExecutionError + from .base import NBClientTestsBase addr_pat = re.compile(r'0x[0-9a-f]{7,9}') @@ -311,25 +313,30 @@ @pytest.mark.parametrize( ["input_name", "opts"], [ - ("Other Comms.ipynb", dict(kernel_name="python")), - ("Clear Output.ipynb", dict(kernel_name="python")), - ("Empty Cell.ipynb", dict(kernel_name="python")), - ("Factorials.ipynb", dict(kernel_name="python")), - ("HelloWorld.ipynb", dict(kernel_name="python")), - ("Inline Image.ipynb", dict(kernel_name="python")), + ("Other Comms.ipynb", {"kernel_name": "python"}), + ("Clear Output.ipynb", {"kernel_name": "python"}), + ("Empty Cell.ipynb", {"kernel_name": "python"}), + ("Factorials.ipynb", {"kernel_name": "python"}), + ("HelloWorld.ipynb", {"kernel_name": "python"}), + ("Inline Image.ipynb", {"kernel_name": "python"}), ( "Interrupt.ipynb", - dict(kernel_name="python", timeout=1, interrupt_on_timeout=True, allow_errors=True), + { + "kernel_name": "python", + "timeout": 1, + "interrupt_on_timeout": True, + "allow_errors": True, + }, ), - ("JupyterWidgets.ipynb", dict(kernel_name="python")), - ("Skip Exceptions with Cell Tags.ipynb", dict(kernel_name="python")), - ("Skip Exceptions.ipynb", dict(kernel_name="python", allow_errors=True)), - ("Skip Execution with Cell Tag.ipynb", dict(kernel_name="python")), - ("SVG.ipynb", dict(kernel_name="python")), - ("Unicode.ipynb", dict(kernel_name="python")), - ("UnicodePy3.ipynb", dict(kernel_name="python")), - ("update-display-id.ipynb", dict(kernel_name="python")), - ("Check History in Memory.ipynb", dict(kernel_name="python")), + ("JupyterWidgets.ipynb", {"kernel_name": "python"}), + ("Skip Exceptions with Cell Tags.ipynb", {"kernel_name": "python"}), + ("Skip Exceptions.ipynb", {"kernel_name": "python", "allow_errors": True}), + ("Skip Execution with Cell Tag.ipynb", {"kernel_name": "python"}), + ("SVG.ipynb", {"kernel_name": "python"}), + ("Unicode.ipynb", {"kernel_name": "python"}), + ("UnicodePy3.ipynb", {"kernel_name": "python"}), + ("update-display-id.ipynb", {"kernel_name": "python"}), + ("Check History in Memory.ipynb", {"kernel_name": "python"}), ], ) def test_run_all_notebooks(input_name, opts): @@ -345,7 +352,7 @@ The two notebooks spawned here use the filesystem to check that the other notebook wrote to the filesystem.""" - opts = dict(kernel_name="python") + opts = {"kernel_name": "python"} input_name = "Parallel Execute {label}.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = notebook_resources() @@ -371,7 +378,7 @@ Specifically, many IPython kernels when run simultaneously would encounter errors due to using the same SQLite history database. """ - opts = dict(kernel_name="python", timeout=5) + opts = {"kernel_name": "python", "timeout": 5} input_name = "HelloWorld.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = NBClientTestsBase().build_resources() @@ -397,7 +404,7 @@ The two notebooks spawned here use the filesystem to check that the other notebook wrote to the filesystem.""" - opts = dict(kernel_name="python") + opts = {"kernel_name": "python"} input_name = "Parallel Execute {label}.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = notebook_resources() @@ -423,7 +430,7 @@ Specifically, many IPython kernels when run simultaneously would encounter errors due to using the same SQLite history database. """ - opts = dict(kernel_name="python", timeout=5) + opts = {"kernel_name": "python", "timeout": 5} input_name = "HelloWorld.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = NBClientTestsBase().build_resources() @@ -446,7 +453,7 @@ """Compare the execution timing information stored in the cell with the actual time it took to run the cell. Also check for the cell timing string format.""" - opts = dict(kernel_name="python") + opts = {"kernel_name": "python"} input_name = "Sleep1s.ipynb" input_file = os.path.join(current_dir, "files", input_name) res = notebook_resources() @@ -596,7 +603,7 @@ filename = os.path.join(current_dir, 'files', 'Disable Stdin.ipynb') res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) - input_nb, output_nb = run_notebook(filename, dict(allow_errors=True), res) + input_nb, output_nb = run_notebook(filename, {"allow_errors": True}, res) # We need to special-case this particular notebook, because the # traceback contains machine-specific stuff like where IPython @@ -619,7 +626,7 @@ res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(TimeoutError) as err: - run_notebook(filename, dict(timeout=1), res) + run_notebook(filename, {"timeout": 1}, res) self.assertEqual( str(err.value.args[0]), """A cell timed out while it was being executed, after 1 seconds. @@ -641,7 +648,7 @@ return 10 with pytest.raises(TimeoutError): - run_notebook(filename, dict(timeout_func=timeout_func), res) + run_notebook(filename, {"timeout_func": timeout_func}, res) def test_sync_kernel_manager(self): nb = nbformat.v4.new_notebook() # Certainly has no language_info. @@ -654,6 +661,7 @@ assert info_msg is not None assert 'name' in info_msg["content"]["language_info"] + @flaky def test_kernel_death_after_timeout(self): """Check that an error is raised when the kernel is_alive is false after a cell timed out""" filename = os.path.join(current_dir, 'files', 'Interrupt.ipynb') @@ -671,7 +679,7 @@ async def is_alive(): return False - km.is_alive = is_alive + km.is_alive = is_alive # type:ignore # Will be a RuntimeError, TimeoutError, or subclass DeadKernelError # depending # on if jupyter_client or nbconvert catches the dead client first @@ -699,7 +707,7 @@ res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(CellExecutionError) as exc: - run_notebook(filename, dict(allow_errors=False), res) + run_notebook(filename, {"allow_errors": False}, res) self.assertIsInstance(str(exc.value), str) assert "# üñîçøâé" in str(exc.value) @@ -712,7 +720,7 @@ res = self.build_resources() res['metadata']['path'] = os.path.dirname(filename) with pytest.raises(CellExecutionError) as exc: - run_notebook(filename, dict(force_raise_errors=True), res) + run_notebook(filename, {"force_raise_errors": True}, res) self.assertIsInstance(str(exc.value), str) assert "# üñîçøâé" in str(exc.value) @@ -826,7 +834,7 @@ def test_widgets(self): """Runs a test notebook with widgets and checks the widget state is saved.""" input_file = os.path.join(current_dir, 'files', 'JupyterWidgets.ipynb') - opts = dict(kernel_name="python") + opts = {"kernel_name": "python"} res = self.build_resources() res['metadata']['path'] = os.path.dirname(input_file) input_nb, output_nb = run_notebook(input_file, opts, res) @@ -1822,3 +1830,34 @@ hooks["on_notebook_start"].assert_not_called() hooks["on_notebook_complete"].assert_not_called() hooks["on_notebook_error"].assert_not_called() + + @prepare_cell_mocks( + { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo1'}, + }, + { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar1'}, + }, + { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stdout', 'text': 'foo2'}, + }, + { + 'msg_type': 'stream', + 'header': {'msg_type': 'stream'}, + 'content': {'name': 'stderr', 'text': 'bar2'}, + }, + ) + def test_coalesce_streams(self, executor, cell_mock, message_mock): + executor.coalesce_streams = True + executor.execute_cell(cell_mock, 0) + + assert cell_mock.outputs == [ + {'output_type': 'stream', 'name': 'stdout', 'text': 'foo1foo2'}, + {'output_type': 'stream', 'name': 'stderr', 'text': 'bar1bar2'}, + ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/tests/test_util.py new/nbclient-0.7.3/nbclient/tests/test_util.py --- old/nbclient-0.7.2/nbclient/tests/test_util.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/tests/test_util.py 2020-02-02 01:00:00.000000000 +0100 @@ -33,12 +33,12 @@ # This tests if tornado accepts the pure-Python Futures, see # https://github.com/tornadoweb/tornado/issues/2753 asyncio.set_event_loop(asyncio.new_event_loop()) - ioloop = tornado.ioloop.IOLoop.current() # type: ignore + ioloop = tornado.ioloop.IOLoop.current() async def some_async_function(): future: asyncio.Future = asyncio.ensure_future(asyncio.sleep(0.1)) # the asyncio module, check if tornado likes it: - ioloop.add_future(future, lambda f: f.result()) + ioloop.add_future(future, lambda f: f.result()) # type:ignore await future return 42 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/nbclient/util.py new/nbclient-0.7.3/nbclient/util.py --- old/nbclient-0.7.2/nbclient/util.py 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/nbclient/util.py 2020-02-02 01:00:00.000000000 +0100 @@ -10,6 +10,7 @@ async def run_hook(hook: Optional[Callable], **kwargs: Any) -> None: + """Run a hook callback.""" if hook is None: return res = hook(**kwargs) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/nbclient-0.7.2/pyproject.toml new/nbclient-0.7.3/pyproject.toml --- old/nbclient-0.7.2/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/nbclient-0.7.3/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -44,6 +44,7 @@ [project.optional-dependencies] test = [ + "flaky", "ipykernel", "ipython", "ipywidgets", @@ -60,6 +61,7 @@ "moto", "myst-parser", "sphinx-book-theme", + "sphinxcontrib_spelling", "sphinx>=1.7", "nbclient[test]", ] @@ -103,18 +105,39 @@ test = "python -m pytest -vv --cov nbclient --cov-branch --cov-report term-missing:skip-covered {args}" nowarn = "test -W default {args}" +[tool.hatch.envs.typing] +features = ["test"] +dependencies = ["mypy>=0.990"] +[tool.hatch.envs.typing.scripts] +test = "mypy --install-types --non-interactive {args:nbclient}" + +[tool.hatch.envs.lint] +dependencies = [ + "black[jupyter]==22.10.0", + "mdformat>0.7", + "mdformat-gfm>=0.3.5", + "ruff==0.0.254" +] +detached = true +[tool.hatch.envs.lint.scripts] +style = [ + "ruff {args:.}", + "black --check --diff {args:.}", + "mdformat --check {args:docs *.md}" +] +fmt = [ + "black {args:.}", + "ruff --fix {args:.}", + "mdformat {args:docs *.md}" +] + [tool.black] line-length = 100 +target-version = ["py37"] include = "\\.pyi?$" exclude = "/(\n \\.git\n | \\.hg\n | \\.mypy_cache\n | \\.tox\n | \\.venv\n | _build\n | buck-out\n | build\n | dist\n\n # The following are specific to Black, you probably don't want those.\n | blib2to3\n | tests/data\n | profiling\n)/\n" skip-string-normalization = true -[tool.isort] -profile = "black" -known_first_party = [ - "nbclient", -] - [tool.pytest.ini_options] addopts = "-raXs --durations 10 --color=yes --doctest-modules" asyncio_mode = "auto" @@ -159,21 +182,34 @@ ] ignore_missing_imports = true -[tool.flake8] -ignore = "E501, W503, E402" -builtins = "c, get_config" -exclude = [ - ".cache", - ".github", - "docs", -] -enable-extensions = "G" -extend-ignore = ["G001", "G002", "G004", "G200", "G201", "G202", - # black adds spaces around ':' - "E203" -] -per-file-ignores = [ - # B011: Do not call assert False since python -O removes these calls - # F841 local variable 'foo' is assigned to but never used - "nbclient/tests/*: B011, F841" -] + +[tool.ruff] +target-version = "py37" +line-length = 100 +select = [ + "A", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T", + "UP", "W", "YTT", +] +ignore = [ +# Q000 Single quotes found but double quotes preferred +"Q000", +# FBT001 Boolean positional arg in function definition +"FBT001", "FBT002", "FBT003", +# C901 `async_setup_kernel` is too complex (12) +"C901", +] + +[tool.ruff.per-file-ignores] +# S101 Use of `assert` detected +"nbclient/tests/*" = ["S101"] +"nbclient/client.py" = ["S101"] + +[tool.interrogate] +ignore-init-module=true +ignore-private=true +ignore-semiprivate=true +ignore-property-decorators=true +ignore-nested-functions=true +ignore-nested-classes=true +fail-under=100 +exclude = ["*/tests", "docs"]