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 2022-02-18 23:03:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-nbclient (Old)
 and      /work/SRC/openSUSE:Factory/.python-nbclient.new.1958 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-nbclient"

Fri Feb 18 23:03:02 2022 rev:17 rq:955826 version:0.5.11

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-nbclient/python-nbclient.changes  
2022-01-17 22:35:29.086300901 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-nbclient.new.1958/python-nbclient.changes    
    2022-02-18 23:03:37.889409616 +0100
@@ -1,0 +2,10 @@
+Thu Feb 17 16:33:39 UTC 2022 - Arun Persaud <a...@gmx.de>
+
+- update to version 0.5.11:
+  * Merged PRs
+    + Pin ipython<8 in tests #198 (@davidbrochart)
+    + Clear execution metadata, prefer msg header date when recording
+      times #195 (@kevin-bates)
+    + Client hooks #188 (@devintang3)
+
+-------------------------------------------------------------------

Old:
----
  nbclient-0.5.10.tar.gz

New:
----
  nbclient-0.5.11.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-nbclient.spec ++++++
--- /var/tmp/diff_new_pack.BcpbF7/_old  2022-02-18 23:03:38.445409572 +0100
+++ /var/tmp/diff_new_pack.BcpbF7/_new  2022-02-18 23:03:38.453409572 +0100
@@ -24,17 +24,15 @@
 %define psuffix %{nil}
 %bcond_with test
 %endif
-
 %if 0%{?suse_version} > 1500
 %bcond_without libalternatives
 %else
 %bcond_with libalternatives
 %endif
-
 %{?!python_module:%define python_module() python3-%{**}}
 %define         skip_python2 1
 Name:           python-nbclient%{psuffix}
-Version:        0.5.10
+Version:        0.5.11
 Release:        0
 Summary:        A client library for executing notebooks
 License:        BSD-3-Clause
@@ -48,14 +46,14 @@
 Requires:       python-nbformat >= 5.0
 Requires:       python-nest-asyncio
 Requires:       python-traitlets >= 4.2
+BuildArch:      noarch
 %if %{with libalternatives}
-Requires:       alts
 BuildRequires:  alts
+Requires:       alts
 %else
 Requires(post): update-alternatives
 Requires(postun):update-alternatives
 %endif
-BuildArch:      noarch
 %if %{with test}
 BuildRequires:  %{python_module ipykernel}
 BuildRequires:  %{python_module ipython}

++++++ nbclient-0.5.10.tar.gz -> nbclient-0.5.11.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/CHANGELOG.md 
new/nbclient-0.5.11/CHANGELOG.md
--- old/nbclient-0.5.10/CHANGELOG.md    2022-01-13 21:34:31.000000000 +0100
+++ new/nbclient-0.5.11/CHANGELOG.md    2022-02-14 20:49:07.000000000 +0100
@@ -2,6 +2,24 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 0.5.11
+
+([Full 
Changelog](https://github.com/jupyter/nbclient/compare/v0.5.10...050c7da89a98159e6361b1ad0dbefd215db5f816))
+
+### Merged PRs
+
+- Pin ipython<8 in tests [#198](https://github.com/jupyter/nbclient/pull/198) 
([@davidbrochart](https://github.com/davidbrochart))
+- Clear execution metadata, prefer msg header date when recording times 
[#195](https://github.com/jupyter/nbclient/pull/195) 
([@kevin-bates](https://github.com/kevin-bates))
+- Client hooks [#188](https://github.com/jupyter/nbclient/pull/188) 
([@devintang3](https://github.com/devintang3))
+
+### Contributors to this release
+
+([GitHub contributors page for this 
release](https://github.com/jupyter/nbclient/graphs/contributors?from=2022-01-13&to=2022-02-14&type=c))
+
+[@davidbrochart](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Adavidbrochart+updated%3A2022-01-13..2022-02-14&type=Issues)
 | 
[@devintang3](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Adevintang3+updated%3A2022-01-13..2022-02-14&type=Issues)
 | 
[@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Akevin-bates+updated%3A2022-01-13..2022-02-14&type=Issues)
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
 ## 0.5.10
 
 ([Full 
Changelog](https://github.com/jupyter/nbclient/compare/v0.5.9...e82c5d8d064ac1097f4e12f387b4c47ea5c576ff))
@@ -22,8 +40,6 @@
 
 
[@davidbrochart](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Adavidbrochart+updated%3A2021-11-19..2022-01-13&type=Issues)
 | 
[@frenzymadness](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Afrenzymadness+updated%3A2021-11-19..2022-01-13&type=Issues)
 | 
[@kianmeng](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Akianmeng+updated%3A2021-11-19..2022-01-13&type=Issues)
 | 
[@martinRenou](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3AmartinRenou+updated%3A2021-11-19..2022-01-13&type=Issues)
 | 
[@takluyver](https://github.com/search?q=repo%3Ajupyter%2Fnbclient+involves%3Atakluyver+updated%3A2021-11-19..2022-01-13&type=Issues)
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 0.5.9
 
 ([Full 
Changelog](https://github.com/jupyter/nbclient/compare/v0.5.8...0146681d7ffd62cbc675c8d1463a2b016a3d3008))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/PKG-INFO new/nbclient-0.5.11/PKG-INFO
--- old/nbclient-0.5.10/PKG-INFO        2022-01-13 21:35:03.114851200 +0100
+++ new/nbclient-0.5.11/PKG-INFO        2022-02-14 20:49:44.258455500 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: nbclient
-Version: 0.5.10
+Version: 0.5.11
 Summary: A client library for executing notebooks. Formerly nbconvert's 
ExecutePreprocessor.
 Home-page: https://jupyter.org
 Author: Jupyter Development Team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/docs/client.rst 
new/nbclient-0.5.11/docs/client.rst
--- old/nbclient-0.5.10/docs/client.rst 2022-01-13 21:34:31.000000000 +0100
+++ new/nbclient-0.5.11/docs/client.rst 2022-02-14 20:49:07.000000000 +0100
@@ -96,6 +96,36 @@
 maintain consistency: we can just run a notebook twice, specifying first
 "python2" and then "python3" as the kernel name.
 
+Hooks before and after notebook or cell execution
+-------------------------------------------------
+There are several configurable hooks that allow the user to execute code 
before and
+after a notebook or a cell is executed. Each one is configured with a function 
that will be called in its
+respective place in the execution pipeline.
+Each is described below:
+
+**Notebook-level hooks**: These hooks are called with a single extra parameter:
+
+- ``notebook=NotebookNode``: the current notebook being executed.
+
+Here is the available hooks:
+
+- ``on_notebook_start`` will run when the notebook client is initialized, 
before any execution has happened.
+- ``on_notebook_complete`` will run when the notebook client has finished 
executing, after kernel cleanup.
+- ``on_notebook_error`` will run when the notebook client has encountered an 
exception before kernel cleanup.
+
+**Cell-level hooks**: These hooks are called with two parameters:
+
+- ``cell=NotebookNode``: a reference to the current cell.
+- ``cell_index=int``: the index of the cell in the current notebook's list of 
cells.
+
+Here are the available hooks:
+
+- ``on_cell_start`` will run for all cell types before the cell is executed.
+- ``on_cell_execute`` will run right before the code cell is executed.
+- ``on_cell_complete`` will run after execution, if the cell is executed with 
no errors.
+- ``on_cell_error`` will run if there is an error during cell execution.
+
+
 Handling errors and exceptions
 ------------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient/_version.py 
new/nbclient-0.5.11/nbclient/_version.py
--- old/nbclient-0.5.10/nbclient/_version.py    2022-01-13 21:34:51.000000000 
+0100
+++ new/nbclient-0.5.11/nbclient/_version.py    2022-02-14 20:49:29.000000000 
+0100
@@ -1,7 +1,7 @@
 import re
 from typing import List, Union
 
-__version__ = "0.5.10"
+__version__ = "0.5.11"
 
 # 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.5.10/nbclient/client.py 
new/nbclient-0.5.11/nbclient/client.py
--- old/nbclient-0.5.10/nbclient/client.py      2022-01-13 21:34:31.000000000 
+0100
+++ new/nbclient-0.5.11/nbclient/client.py      2022-02-14 20:49:07.000000000 
+0100
@@ -9,12 +9,24 @@
 from queue import Empty
 from textwrap import dedent
 from time import monotonic
+from typing import Optional
 
 from jupyter_client import KernelManager
 from jupyter_client.client import KernelClient
 from nbformat import NotebookNode
 from nbformat.v4 import output_from_msg
-from traitlets import Any, Bool, 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 (
@@ -25,10 +37,25 @@
     DeadKernelError,
 )
 from .output_widget import OutputWidget
-from .util import ensure_async, run_sync
+from .util import ensure_async, run_hook, run_sync
 
 
-def timestamp() -> str:
+def timestamp(msg: Optional[Dict] = None) -> str:
+    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):
+            try:
+                # reformat datetime into expected format
+                formatted_time = datetime.datetime.strftime(
+                    msg_header['date'], '%Y-%m-%dT%H:%M:%S.%fZ'
+                )
+                if (
+                    formatted_time
+                ):  # docs indicate strftime may return empty string, so let's 
catch that too
+                    return formatted_time
+            except Exception:
+                pass  # fallback to a local time
+
     return datetime.datetime.utcnow().isoformat() + 'Z'
 
 
@@ -245,6 +272,87 @@
 
     kernel_manager_class: KernelManager = Type(config=True, help='The kernel 
manager class to use.')
 
+    on_notebook_start: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes after the kernel manager and kernel 
client are setup, and
+            cells are about to execute.
+            Called with kwargs `notebook`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_notebook_complete: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes after the kernel is cleaned up.
+            Called with kwargs `notebook`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_notebook_error: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes when the notebook encounters an error.
+            Called with kwargs `notebook`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_cell_start: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes before a cell is executed and before 
non-executing cells
+            are skipped.
+            Called with kwargs `cell` and `cell_index`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_cell_execute: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes just before a code cell is executed.
+            Called with kwargs `cell` and `cell_index`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_cell_complete: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes after a cell execution is complete. It is
+            called even when a cell results in a failure.
+            Called with kwargs `cell` and `cell_index`.
+            """
+        ),
+    ).tag(config=True)
+
+    on_cell_error: t.Optional[t.Callable] = Callable(
+        default_value=None,
+        allow_none=True,
+        help=dedent(
+            """
+            A callable which executes when a cell execution results in an 
error.
+            This is executed even if errors are suppressed with 
`cell_allows_errors`.
+            Called with kwargs `cell` and `cell_index`.
+            """
+        ),
+    ).tag(config=True)
+
     @default('kernel_manager_class')
     def _kernel_manager_class_default(self) -> KernelManager:
         """Use a dynamic default to avoid importing jupyter_client at 
startup"""
@@ -426,6 +534,7 @@
             await self._async_cleanup_kernel()
             raise
         self.kc.allow_stdin = False
+        await run_hook(self.on_notebook_start, notebook=self.nb)
         return self.kc
 
     start_new_kernel_client = run_sync(async_start_new_kernel_client)
@@ -497,10 +606,13 @@
             await self.async_start_new_kernel_client()
         try:
             yield
+        except RuntimeError as e:
+            await run_hook(self.on_notebook_error, notebook=self.nb)
+            raise e
         finally:
             if cleanup_kc:
                 await self._async_cleanup_kernel()
-
+            await run_hook(self.on_notebook_complete, notebook=self.nb)
             atexit.unregister(self._cleanup_kernel)
             try:
                 loop.remove_signal_handler(signal.SIGINT)
@@ -618,7 +730,7 @@
                 msg = await 
ensure_async(self.kc.shell_channel.get_msg(timeout=new_timeout))
                 if msg['parent_header'].get('msg_id') == msg_id:
                     if self.record_timing:
-                        cell['metadata']['execution']['shell.execute_reply'] = 
timestamp()
+                        cell['metadata']['execution']['shell.execute_reply'] = 
timestamp(msg)
                     try:
                         await asyncio.wait_for(task_poll_output_msg, 
self.iopub_timeout)
                     except (asyncio.TimeoutError, Empty):
@@ -729,7 +841,9 @@
             return True
         return False
 
-    def _check_raise_for_error(self, cell: NotebookNode, exec_reply: 
t.Optional[t.Dict]) -> None:
+    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
@@ -743,7 +857,7 @@
             or exec_reply_content.get('ename') in self.allow_error_names
             or "raises-exception" in cell.metadata.get("tags", [])
         )
-
+        await run_hook(self.on_cell_error, cell=cell, cell_index=cell_index)
         if not cell_allows_errors:
             raise CellExecutionError.from_cell_and_msg(cell, 
exec_reply_content)
 
@@ -788,6 +902,9 @@
             The cell which was just processed.
         """
         assert self.kc is not None
+
+        await run_hook(self.on_cell_start, cell=cell, cell_index=cell_index)
+
         if cell.cell_type != 'code' or not cell.source.strip():
             self.log.debug("Skipping non-executing cell %s", cell_index)
             return cell
@@ -796,7 +913,7 @@
             self.log.debug("Skipping tagged cell %s", cell_index)
             return cell
 
-        if self.record_timing and 'execution' not in cell['metadata']:
+        if self.record_timing:  # clear execution metadata prior to execution
             cell['metadata']['execution'] = {}
 
         self.log.debug("Executing cell:\n%s", cell.source)
@@ -805,11 +922,13 @@
             self.allow_errors or "raises-exception" in 
cell.metadata.get("tags", [])
         )
 
+        await run_hook(self.on_cell_execute, cell=cell, cell_index=cell_index)
         parent_msg_id = await ensure_async(
             self.kc.execute(
                 cell.source, store_history=store_history, stop_on_error=not 
cell_allows_errors
             )
         )
+        await run_hook(self.on_cell_complete, cell=cell, cell_index=cell_index)
         # We launched a code cell to execute
         self.code_cells_executed += 1
         exec_timeout = self._get_timeout(cell)
@@ -843,7 +962,7 @@
 
         if execution_count:
             cell['execution_count'] = execution_count
-        self._check_raise_for_error(cell, exec_reply)
+        await self._check_raise_for_error(cell, cell_index, exec_reply)
         self.nb['cells'][cell_index] = cell
         return cell
 
@@ -894,11 +1013,11 @@
         if self.record_timing:
             if msg_type == 'status':
                 if content['execution_state'] == 'idle':
-                    cell['metadata']['execution']['iopub.status.idle'] = 
timestamp()
+                    cell['metadata']['execution']['iopub.status.idle'] = 
timestamp(msg)
                 elif content['execution_state'] == 'busy':
-                    cell['metadata']['execution']['iopub.status.busy'] = 
timestamp()
+                    cell['metadata']['execution']['iopub.status.busy'] = 
timestamp(msg)
             elif msg_type == 'execute_input':
-                cell['metadata']['execution']['iopub.execute_input'] = 
timestamp()
+                cell['metadata']['execution']['iopub.execute_input'] = 
timestamp(msg)
 
         if msg_type == 'status':
             if content['execution_state'] == 'idle':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient/tests/test_client.py 
new/nbclient-0.5.11/nbclient/tests/test_client.py
--- old/nbclient-0.5.10/nbclient/tests/test_client.py   2022-01-13 
21:34:31.000000000 +0100
+++ new/nbclient-0.5.11/nbclient/tests/test_client.py   2022-02-14 
20:49:07.000000000 +0100
@@ -33,6 +33,16 @@
 # see: 
https://github.com/ipython/ipython/blob/master/docs/source/whatsnew/version8.rst#traceback-improvements
  # noqa
 ipython8_input_pat = re.compile(r'Input In \[\d+\],')
 
+hook_methods = [
+    "on_cell_start",
+    "on_cell_execute",
+    "on_cell_complete",
+    "on_cell_error",
+    "on_notebook_start",
+    "on_notebook_complete",
+    "on_notebook_error",
+]
+
 
 class AsyncMock(Mock):
     pass
@@ -741,6 +751,82 @@
         assert 'version_major' in wdata
         assert 'version_minor' in wdata
 
+    def test_execution_hook(self):
+        filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
+        with open(filename) as f:
+            input_nb = nbformat.read(f, 4)
+        hooks = [MagicMock() for i in range(7)]
+        executor = NotebookClient(input_nb)
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        executor.execute()
+        for hook in hooks[:3]:
+            hook.assert_called_once()
+        hooks[3].assert_not_called()
+        for hook in hooks[4:6]:
+            hook.assert_called_once()
+        hooks[6].assert_not_called()
+
+    def test_error_execution_hook_error(self):
+        filename = os.path.join(current_dir, 'files', 'Error.ipynb')
+        with open(filename) as f:
+            input_nb = nbformat.read(f, 4)
+        hooks = [MagicMock() for i in range(7)]
+        executor = NotebookClient(input_nb)
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        with pytest.raises(CellExecutionError):
+            executor.execute()
+        for hook in hooks[:5]:
+            hook.assert_called_once()
+        hooks[6].assert_not_called()
+
+    def test_error_notebook_hook(self):
+        filename = os.path.join(current_dir, 'files', 'Autokill.ipynb')
+        with open(filename) as f:
+            input_nb = nbformat.read(f, 4)
+        hooks = [MagicMock() for i in range(7)]
+        executor = NotebookClient(input_nb)
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        with pytest.raises(RuntimeError):
+            executor.execute()
+        for hook in hooks[:3]:
+            hook.assert_called_once()
+        hooks[3].assert_not_called()
+        for hook in hooks[4:]:
+            hook.assert_called_once()
+
+    def test_async_execution_hook(self):
+        filename = os.path.join(current_dir, 'files', 'HelloWorld.ipynb')
+        with open(filename) as f:
+            input_nb = nbformat.read(f, 4)
+        hooks = [AsyncMock() for i in range(7)]
+        executor = NotebookClient(input_nb)
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        executor.execute()
+        for hook in hooks[:3]:
+            hook.assert_called_once()
+        hooks[3].assert_not_called()
+        for hook in hooks[4:6]:
+            hook.assert_called_once()
+        hooks[6].assert_not_called()
+
+    def test_error_async_execution_hook(self):
+        filename = os.path.join(current_dir, 'files', 'Error.ipynb')
+        with open(filename) as f:
+            input_nb = nbformat.read(f, 4)
+        hooks = [AsyncMock() for i in range(7)]
+        executor = NotebookClient(input_nb)
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        with pytest.raises(CellExecutionError):
+            executor.execute().execute()
+        for hook in hooks[:5]:
+            hook.assert_called_once()
+        hooks[6].assert_not_called()
+
 
 class TestRunCell(NBClientTestsBase):
     """Contains test functions for NotebookClient.execute_cell"""
@@ -1524,3 +1610,92 @@
         assert message_mock.call_count == 0
         # Should also consume the message stream
         assert cell_mock.outputs == []
+
+    @prepare_cell_mocks()
+    def test_cell_hooks(self, executor, cell_mock, message_mock):
+        hooks = [MagicMock() for i in range(7)]
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        executor.execute_cell(cell_mock, 0)
+        for hook in hooks[:3]:
+            hook.assert_called_once_with(cell=cell_mock, cell_index=0)
+        for hook in hooks[4:]:
+            hook.assert_not_called()
+
+    @prepare_cell_mocks(
+        {
+            'msg_type': 'error',
+            'header': {'msg_type': 'error'},
+            'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': 
['Boom']},
+        },
+        reply_msg={
+            'msg_type': 'execute_reply',
+            'header': {'msg_type': 'execute_reply'},
+            # ERROR
+            'content': {'status': 'error'},
+        },
+    )
+    def test_error_cell_hooks(self, executor, cell_mock, message_mock):
+        hooks = [MagicMock() for i in range(7)]
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        with self.assertRaises(CellExecutionError):
+            executor.execute_cell(cell_mock, 0)
+        for hook in hooks[:4]:
+            hook.assert_called_once_with(cell=cell_mock, cell_index=0)
+        for hook in hooks[5:]:
+            hook.assert_not_called()
+
+    @prepare_cell_mocks(
+        reply_msg={
+            'msg_type': 'execute_reply',
+            'header': {'msg_type': 'execute_reply'},
+            # ERROR
+            'content': {'status': 'error'},
+        }
+    )
+    def test_non_code_cell_hooks(self, executor, cell_mock, message_mock):
+        cell_mock = NotebookNode(source='"foo" = "bar"', metadata={}, 
cell_type='raw', outputs=[])
+        hooks = [MagicMock() for i in range(7)]
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        executor.execute_cell(cell_mock, 0)
+        for hook in hooks[:1]:
+            hook.assert_called_once_with(cell=cell_mock, cell_index=0)
+        for hook in hooks[1:]:
+            hook.assert_not_called()
+
+    @prepare_cell_mocks()
+    def test_async_cell_hooks(self, executor, cell_mock, message_mock):
+        hooks = [AsyncMock() for i in range(7)]
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        executor.execute_cell(cell_mock, 0)
+        for hook in hooks[:3]:
+            hook.assert_called_once_with(cell=cell_mock, cell_index=0)
+        for hook in hooks[4:]:
+            hook.assert_not_called()
+
+    @prepare_cell_mocks(
+        {
+            'msg_type': 'error',
+            'header': {'msg_type': 'error'},
+            'content': {'ename': 'foo', 'evalue': 'bar', 'traceback': 
['Boom']},
+        },
+        reply_msg={
+            'msg_type': 'execute_reply',
+            'header': {'msg_type': 'execute_reply'},
+            # ERROR
+            'content': {'status': 'error'},
+        },
+    )
+    def test_error_async_cell_hooks(self, executor, cell_mock, message_mock):
+        hooks = [AsyncMock() for i in range(7)]
+        for executor_hook, hook in zip(hook_methods, hooks):
+            setattr(executor, executor_hook, hook)
+        with self.assertRaises(CellExecutionError):
+            executor.execute_cell(cell_mock, 0)
+        for hook in hooks[:4]:
+            hook.assert_called_once_with(cell=cell_mock, cell_index=0)
+        for hook in hooks[4:]:
+            hook.assert_not_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient/tests/test_util.py 
new/nbclient-0.5.11/nbclient/tests/test_util.py
--- old/nbclient-0.5.10/nbclient/tests/test_util.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/nbclient-0.5.11/nbclient/tests/test_util.py     2022-02-14 
20:49:07.000000000 +0100
@@ -0,0 +1,73 @@
+import asyncio
+from unittest.mock import MagicMock
+
+import pytest
+import tornado
+
+from nbclient.util import run_hook, run_sync
+
+
+@run_sync
+async def some_async_function():
+    await asyncio.sleep(0.01)
+    return 42
+
+
+def test_nested_asyncio_with_existing_ioloop():
+    ioloop = asyncio.new_event_loop()
+    try:
+        asyncio.set_event_loop(ioloop)
+        assert some_async_function() == 42
+        assert asyncio.get_event_loop() is ioloop
+    finally:
+        asyncio._set_running_loop(None)  # it seems nest_asyncio doesn't reset 
this
+
+
+def test_nested_asyncio_with_no_ioloop():
+    asyncio.set_event_loop(None)
+    try:
+        assert some_async_function() == 42
+    finally:
+        asyncio._set_running_loop(None)  # it seems nest_asyncio doesn't reset 
this
+
+
+def test_nested_asyncio_with_tornado():
+    # This tests if tornado accepts the pure-Python Futures, see
+    # https://github.com/tornadoweb/tornado/issues/2753
+    # https://github.com/erdewit/nest_asyncio/issues/23
+    asyncio.set_event_loop(asyncio.new_event_loop())
+    ioloop = tornado.ioloop.IOLoop.current()
+
+    async def some_async_function():
+        future = asyncio.ensure_future(asyncio.sleep(0.1))
+        # this future is a different future after nested-asyncio has patched
+        # the asyncio module, check if tornado likes it:
+        ioloop.add_future(future, lambda f: f.result())
+        await future
+        return 42
+
+    def some_sync_function():
+        return run_sync(some_async_function)()
+
+    async def run():
+        # calling some_async_function directly should work
+        assert await some_async_function() == 42
+        # but via a sync function (using nested-asyncio) can lead to issues:
+        # https://github.com/tornadoweb/tornado/issues/2753
+        assert some_sync_function() == 42
+
+    ioloop.run_sync(run)
+
+
+@pytest.mark.asyncio
+async def test_run_hook_sync():
+    some_sync_function = MagicMock()
+    await run_hook(some_sync_function)
+    assert some_sync_function.call_count == 1
+
+
+@pytest.mark.asyncio
+async def test_run_hook_async():
+    hook = MagicMock(return_value=some_async_function())
+    await run_hook(hook)
+    assert hook.call_count == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient/tests/util.py 
new/nbclient-0.5.11/nbclient/tests/util.py
--- old/nbclient-0.5.10/nbclient/tests/util.py  2022-01-13 21:34:31.000000000 
+0100
+++ new/nbclient-0.5.11/nbclient/tests/util.py  1970-01-01 01:00:00.000000000 
+0100
@@ -1,57 +0,0 @@
-import asyncio
-
-import tornado
-
-from nbclient.util import run_sync
-
-
-@run_sync
-async def some_async_function():
-    await asyncio.sleep(0.01)
-    return 42
-
-
-def test_nested_asyncio_with_existing_ioloop():
-    ioloop = asyncio.new_event_loop()
-    try:
-        asyncio.set_event_loop(ioloop)
-        assert some_async_function() == 42
-        assert asyncio.get_event_loop() is ioloop
-    finally:
-        asyncio._set_running_loop(None)  # it seems nest_asyncio doesn't reset 
this
-
-
-def test_nested_asyncio_with_no_ioloop():
-    asyncio.set_event_loop(None)
-    try:
-        assert some_async_function() == 42
-    finally:
-        asyncio._set_running_loop(None)  # it seems nest_asyncio doesn't reset 
this
-
-
-def test_nested_asyncio_with_tornado():
-    # This tests if tornado accepts the pure-Python Futures, see
-    # https://github.com/tornadoweb/tornado/issues/2753
-    # https://github.com/erdewit/nest_asyncio/issues/23
-    asyncio.set_event_loop(asyncio.new_event_loop())
-    ioloop = tornado.ioloop.IOLoop.current()
-
-    async def some_async_function():
-        future = asyncio.ensure_future(asyncio.sleep(0.1))
-        # this future is a different future after nested-asyncio has patched
-        # the asyncio module, check if tornado likes it:
-        ioloop.add_future(future, lambda f: f.result())
-        await future
-        return 42
-
-    def some_sync_function():
-        return run_sync(some_async_function)()
-
-    async def run():
-        # calling some_async_function directly should work
-        assert await some_async_function() == 42
-        # but via a sync function (using nested-asyncio) can lead to issues:
-        # https://github.com/tornadoweb/tornado/issues/2753
-        assert some_sync_function() == 42
-
-    ioloop.run_sync(run)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient/util.py 
new/nbclient-0.5.11/nbclient/util.py
--- old/nbclient-0.5.10/nbclient/util.py        2022-01-13 21:34:31.000000000 
+0100
+++ new/nbclient-0.5.11/nbclient/util.py        2022-02-14 20:49:07.000000000 
+0100
@@ -6,7 +6,7 @@
 import asyncio
 import inspect
 import sys
-from typing import Any, Awaitable, Callable, Union
+from typing import Any, Awaitable, Callable, Optional, Union
 
 
 def check_ipython() -> None:
@@ -102,3 +102,11 @@
         return result
     # obj doesn't need to be awaited
     return obj
+
+
+async def run_hook(hook: Optional[Callable], **kwargs) -> None:
+    if hook is None:
+        return
+    res = hook(**kwargs)
+    if inspect.isawaitable(res):
+        await res
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient.egg-info/PKG-INFO 
new/nbclient-0.5.11/nbclient.egg-info/PKG-INFO
--- old/nbclient-0.5.10/nbclient.egg-info/PKG-INFO      2022-01-13 
21:35:03.000000000 +0100
+++ new/nbclient-0.5.11/nbclient.egg-info/PKG-INFO      2022-02-14 
20:49:43.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: nbclient
-Version: 0.5.10
+Version: 0.5.11
 Summary: A client library for executing notebooks. Formerly nbconvert's 
ExecutePreprocessor.
 Home-page: https://jupyter.org
 Author: Jupyter Development Team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient.egg-info/SOURCES.txt 
new/nbclient-0.5.11/nbclient.egg-info/SOURCES.txt
--- old/nbclient-0.5.10/nbclient.egg-info/SOURCES.txt   2022-01-13 
21:35:03.000000000 +0100
+++ new/nbclient-0.5.11/nbclient.egg-info/SOURCES.txt   2022-02-14 
20:49:44.000000000 +0100
@@ -42,7 +42,7 @@
 nbclient/tests/conftest.py
 nbclient/tests/fake_kernelmanager.py
 nbclient/tests/test_client.py
-nbclient/tests/util.py
+nbclient/tests/test_util.py
 nbclient/tests/files/Autokill.ipynb
 nbclient/tests/files/Check History in Memory.ipynb
 nbclient/tests/files/Clear Output.ipynb
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient.egg-info/entry_points.txt 
new/nbclient-0.5.11/nbclient.egg-info/entry_points.txt
--- old/nbclient-0.5.10/nbclient.egg-info/entry_points.txt      2022-01-13 
21:35:03.000000000 +0100
+++ new/nbclient-0.5.11/nbclient.egg-info/entry_points.txt      2022-02-14 
20:49:44.000000000 +0100
@@ -1,3 +1,2 @@
 [console_scripts]
 jupyter-execute = nbclient.cli:main
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/nbclient.egg-info/requires.txt 
new/nbclient-0.5.11/nbclient.egg-info/requires.txt
--- old/nbclient-0.5.10/nbclient.egg-info/requires.txt  2022-01-13 
21:35:03.000000000 +0100
+++ new/nbclient-0.5.11/nbclient.egg-info/requires.txt  2022-02-14 
20:49:44.000000000 +0100
@@ -11,10 +11,11 @@
 myst-parser
 
 [test]
-ipython
+ipython<8.0.0
 ipykernel
 ipywidgets<8.0.0
 pytest>=4.1
+pytest-asyncio
 pytest-cov>=2.6.1
 check-manifest
 flake8
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/pyproject.toml 
new/nbclient-0.5.11/pyproject.toml
--- old/nbclient-0.5.10/pyproject.toml  2022-01-13 21:34:51.000000000 +0100
+++ new/nbclient-0.5.11/pyproject.toml  2022-02-14 20:49:29.000000000 +0100
@@ -53,7 +53,7 @@
 ignore = [".mailmap", "*.yml", "*.yaml"]
 
 [tool.tbump.version]
-current = "0.5.10"
+current = "0.5.11"
 regex = '''
   (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
   ((?P<channel>a|b|rc|.dev)(?P<release>\d+))?
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/nbclient-0.5.10/requirements-dev.txt 
new/nbclient-0.5.11/requirements-dev.txt
--- old/nbclient-0.5.10/requirements-dev.txt    2022-01-13 21:34:31.000000000 
+0100
+++ new/nbclient-0.5.11/requirements-dev.txt    2022-02-14 20:49:07.000000000 
+0100
@@ -1,7 +1,8 @@
-ipython
+ipython<8.0.0
 ipykernel
 ipywidgets<8.0.0
 pytest>=4.1
+pytest-asyncio
 pytest-cov>=2.6.1
 check-manifest
 flake8

Reply via email to