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 @@
 
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyter/nbclient/main?filepath=binder%2Frun_nbclient.ipynb)
 [![Build 
Status](https://github.com/jupyter/nbclient/workflows/CI/badge.svg)](https://github.com/jupyter/nbclient/actions)
 [![Documentation 
Status](https://readthedocs.org/projects/nbclient/badge/?version=latest)](https://nbclient.readthedocs.io/en/latest/?badge=latest)
-[![image](https://codecov.io/github/jupyter/nbclient/coverage.svg?branch=main)](https://codecov.io/github/jupyter/nbclient?branch=main)
+[![CodeCov](https://codecov.io/gh/jupyter/nbclient/coverage.svg?branch=main)](https://codecov.io/gh/jupyter/nbclient?branch=main)
 [![Python 
3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/)
 [![Python 
3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/)
 [![Python 
3.9](https://img.shields.io/badge/python-3.9-blue.svg)](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 @@
 
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyter/nbclient/main?filepath=binder%2Frun_nbclient.ipynb)
 [![Build 
Status](https://github.com/jupyter/nbclient/workflows/CI/badge.svg)](https://github.com/jupyter/nbclient/actions)
 [![Documentation 
Status](https://readthedocs.org/projects/nbclient/badge/?version=latest)](https://nbclient.readthedocs.io/en/latest/?badge=latest)
-[![image](https://codecov.io/github/jupyter/nbclient/coverage.svg?branch=main)](https://codecov.io/github/jupyter/nbclient?branch=main)
+[![CodeCov](https://codecov.io/gh/jupyter/nbclient/coverage.svg?branch=main)](https://codecov.io/gh/jupyter/nbclient?branch=main)
 [![Python 
3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/)
 [![Python 
3.8](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/)
 [![Python 
3.9](https://img.shields.io/badge/python-3.9-blue.svg)](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"]

Reply via email to