Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-ipykernel for 
openSUSE:Factory checked in at 2023-06-04 00:12:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ipykernel (Old)
 and      /work/SRC/openSUSE:Factory/.python-ipykernel.new.15902 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ipykernel"

Sun Jun  4 00:12:32 2023 rev:37 rq:1090502 version:6.23.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ipykernel/python-ipykernel.changes        
2023-04-04 21:27:51.331725423 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-ipykernel.new.15902/python-ipykernel.changes 
    2023-06-04 00:12:42.273570604 +0200
@@ -1,0 +2,11 @@
+Fri Jun  2 12:42:05 UTC 2023 - Ben Greiner <c...@bnavigator.de>
+
+- Update to 6.23.1
+  * Avoid echoing onto a captured FD #1111 (@minrk)
+- Release 6.23.0
+  * Support control<>iopub messages to e.g. unblock comm_msg from
+    command execution #1114 (@tkrabel-db)
+  * Add outstream hook similar to display publisher #1110
+    (@maartenbreddels)
+
+-------------------------------------------------------------------

Old:
----
  ipykernel-6.22.0.tar.gz

New:
----
  ipykernel-6.23.1.tar.gz

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

Other differences:
------------------
++++++ python-ipykernel.spec ++++++
--- /var/tmp/diff_new_pack.RJ7VWM/_old  2023-06-04 00:12:42.933574550 +0200
+++ /var/tmp/diff_new_pack.RJ7VWM/_new  2023-06-04 00:12:42.937574573 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-ipykernel
-Version:        6.22.0
+Version:        6.23.1
 Release:        0
 Summary:        IPython Kernel for Jupyter
 License:        BSD-3-Clause

++++++ ipykernel-6.22.0.tar.gz -> ipykernel-6.23.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/.github/workflows/ci.yml 
new/ipykernel-6.23.1/.github/workflows/ci.yml
--- old/ipykernel-6.22.0/.github/workflows/ci.yml       2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/.github/workflows/ci.yml       2020-02-02 
01:00:00.000000000 +0100
@@ -57,17 +57,24 @@
         run: |
           hatch run cov:nowarn || hatch run test:nowarn --lf
 
-      - name: Coverage
-        run: |
-          pip install codecov coverage[toml]
-          codecov
-
       - name: Check Launcher
         run: |
           pip install .
           cd $HOME
           python -m ipykernel_launcher --help
 
+      - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1
+
+  coverage:
+    runs-on: ubuntu-latest
+    needs:
+      - build
+    steps:
+      - uses: actions/checkout@v3
+      - uses: jupyterlab/maintainer-tools/.github/actions/report-coverage@v1
+        with:
+          fail_under: 80
+
   test_lint:
     name: Test Lint
     runs-on: ubuntu-latest
@@ -143,6 +150,11 @@
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
         with:
           dependency_type: minimum
+
+      - name: List installed packages
+        run: |
+          hatch run test:list
+
       - name: Run the unit tests
         run: |
           hatch run test:nowarn || hatch run test:nowarn --lf
@@ -190,7 +202,7 @@
   tests_check: # This job does nothing and is only used for the branch 
protection
     if: always()
     needs:
-      - build
+      - coverage
       - test_docs
       - test_without_debugpy
       - test_miniumum_versions
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/.pre-commit-config.yaml 
new/ipykernel-6.23.1/.pre-commit-config.yaml
--- old/ipykernel-6.22.0/.pre-commit-config.yaml        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/.pre-commit-config.yaml        2020-02-02 
01:00:00.000000000 +0100
@@ -21,7 +21,7 @@
       - id: trailing-whitespace
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.21.0
+    rev: 0.22.0
     hooks:
       - id: check-github-workflows
 
@@ -31,12 +31,12 @@
       - id: mdformat
 
   - repo: https://github.com/psf/black
-    rev: 23.1.0
+    rev: 23.3.0
     hooks:
       - id: black
 
   - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.0.254
+    rev: v0.0.263
     hooks:
       - id: ruff
         args: ["--fix"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/.readthedocs.yaml 
new/ipykernel-6.23.1/.readthedocs.yaml
--- old/ipykernel-6.22.0/.readthedocs.yaml      2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/.readthedocs.yaml      2020-02-02 01:00:00.000000000 
+0100
@@ -1,8 +1,14 @@
 version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
 sphinx:
   configuration: docs/conf.py
+
 python:
-  version: 3.8
   install:
     # install itself with pip install .
     - method: pip
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/CHANGELOG.md 
new/ipykernel-6.23.1/CHANGELOG.md
--- old/ipykernel-6.22.0/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
@@ -2,6 +2,45 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 6.23.1
+
+([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.23.0...d63c33afb9872f2781997b2428d7e9e0c1d23d41))
+
+### Bugs fixed
+
+- Avoid echoing onto a captured FD 
[#1111](https://github.com/ipython/ipykernel/pull/1111) 
([@minrk](https://github.com/minrk))
+
+### Maintenance and upkeep improvements
+
+- update readthedocs env to 3.11 
[#1117](https://github.com/ipython/ipykernel/pull/1117) 
([@minrk](https://github.com/minrk))
+
+### Contributors to this release
+
+([GitHub contributors page for this 
release](https://github.com/ipython/ipykernel/graphs/contributors?from=2023-05-08&to=2023-05-15&type=c))
+
+[@minrk](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Aminrk+updated%3A2023-05-08..2023-05-15&type=Issues)
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
+## 6.23.0
+
+([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.22.0...3dd6dc9712ff6eb0a53cf79969dcefa0ba1b086e))
+
+### Enhancements made
+
+- Support control\<>iopub messages to e.g. unblock comm_msg from command 
execution  [#1114](https://github.com/ipython/ipykernel/pull/1114) 
([@tkrabel-db](https://github.com/tkrabel-db))
+- Add outstream hook similar to display publisher 
[#1110](https://github.com/ipython/ipykernel/pull/1110) 
([@maartenbreddels](https://github.com/maartenbreddels))
+
+### Maintenance and upkeep improvements
+
+- Use local coverage [#1109](https://github.com/ipython/ipykernel/pull/1109) 
([@blink1073](https://github.com/blink1073))
+
+### Contributors to this release
+
+([GitHub contributors page for this 
release](https://github.com/ipython/ipykernel/graphs/contributors?from=2023-03-20&to=2023-05-08&type=c))
+
+[@blink1073](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Ablink1073+updated%3A2023-03-20..2023-05-08&type=Issues)
 | 
[@maartenbreddels](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Amaartenbreddels+updated%3A2023-03-20..2023-05-08&type=Issues)
 | 
[@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Apre-commit-ci+updated%3A2023-03-20..2023-05-08&type=Issues)
 | 
[@tkrabel-db](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Atkrabel-db+updated%3A2023-03-20..2023-05-08&type=Issues)
+
 ## 6.22.0
 
 ([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.21.3...e2972d763b5357d4e1cb9b5355593583ca6d5657))
@@ -18,8 +57,6 @@
 
 
[@martinRenou](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3AmartinRenou+updated%3A2023-03-06..2023-03-20&type=Issues)
 | 
[@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Apre-commit-ci+updated%3A2023-03-06..2023-03-20&type=Issues)
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 6.21.3
 
 ([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.21.2...e46f75b93c388886f4b6ba32182e29c3cc486984))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/PKG-INFO 
new/ipykernel-6.23.1/PKG-INFO
--- old/ipykernel-6.22.0/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: ipykernel
-Version: 6.22.0
+Version: 6.23.1
 Summary: IPython Kernel for Jupyter
 Project-URL: Homepage, https://ipython.org
 Author-email: IPython Development Team <ipython-...@scipy.org>
@@ -91,7 +91,6 @@
 # IPython Kernel for Jupyter
 
 [![Build 
Status](https://github.com/ipython/ipykernel/actions/workflows/ci.yml/badge.svg?query=branch%3Amain++)](https://github.com/ipython/ipykernel/actions/workflows/ci.yml/badge.svg?query=branch%3Amain++)
-[![codecov](https://codecov.io/gh/ipython/ipykernel/branch/main/graph/badge.svg?token=SyksDOcIJa)](https://codecov.io/gh/ipython/ipykernel)
 [![Documentation 
Status](https://readthedocs.org/projects/ipykernel/badge/?version=latest)](http://ipykernel.readthedocs.io/en/latest/?badge=latest)
 
 This package provides the IPython kernel for Jupyter.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/README.md 
new/ipykernel-6.23.1/README.md
--- old/ipykernel-6.22.0/README.md      2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/README.md      2020-02-02 01:00:00.000000000 +0100
@@ -1,7 +1,6 @@
 # IPython Kernel for Jupyter
 
 [![Build 
Status](https://github.com/ipython/ipykernel/actions/workflows/ci.yml/badge.svg?query=branch%3Amain++)](https://github.com/ipython/ipykernel/actions/workflows/ci.yml/badge.svg?query=branch%3Amain++)
-[![codecov](https://codecov.io/gh/ipython/ipykernel/branch/main/graph/badge.svg?token=SyksDOcIJa)](https://codecov.io/gh/ipython/ipykernel)
 [![Documentation 
Status](https://readthedocs.org/projects/ipykernel/badge/?version=latest)](http://ipykernel.readthedocs.io/en/latest/?badge=latest)
 
 This package provides the IPython kernel for Jupyter.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/codecov.yml 
new/ipykernel-6.23.1/codecov.yml
--- old/ipykernel-6.22.0/codecov.yml    2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/codecov.yml    1970-01-01 01:00:00.000000000 +0100
@@ -1,9 +0,0 @@
-coverage:
-  status:
-    project:
-      default:
-        target: auto
-        threshold: 1
-    patch:
-      default:
-        target: 0%
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/_version.py 
new/ipykernel-6.23.1/ipykernel/_version.py
--- old/ipykernel-6.22.0/ipykernel/_version.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/_version.py  2020-02-02 01:00:00.000000000 
+0100
@@ -5,7 +5,7 @@
 from typing import List
 
 # Version string must appear intact for hatch versioning
-__version__ = "6.22.0"
+__version__ = "6.23.1"
 
 # Build up version_info tuple for backwards compatibility
 pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
@@ -17,4 +17,4 @@
 version_info = tuple(parts)
 
 kernel_protocol_version_info = (5, 3)
-kernel_protocol_version = "%s.%s" % kernel_protocol_version_info
+kernel_protocol_version = "{}.{}".format(*kernel_protocol_version_info)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/comm/comm.py 
new/ipykernel-6.23.1/ipykernel/comm/comm.py
--- old/ipykernel-6.22.0/ipykernel/comm/comm.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/comm/comm.py 2020-02-02 01:00:00.000000000 
+0100
@@ -3,6 +3,7 @@
 # Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
 
+import threading
 import uuid
 from typing import Optional
 from warnings import warn
@@ -11,6 +12,7 @@
 import traitlets.config
 from traitlets import Bool, Bytes, Instance, Unicode, default
 
+from ipykernel.control import CONTROL_THREAD_NAME
 from ipykernel.jsonutil import json_clean
 from ipykernel.kernelbase import Kernel
 
@@ -30,6 +32,11 @@
         metadata = {} if metadata is None else metadata
         content = json_clean(dict(data=data, comm_id=self.comm_id, **keys))
 
+        if threading.current_thread().name == CONTROL_THREAD_NAME:
+            channel_from_which_to_get_parent_header = "control"
+        else:
+            channel_from_which_to_get_parent_header = "shell"
+
         if self.kernel is None:
             self.kernel = Kernel.instance()
 
@@ -38,7 +45,7 @@
             msg_type,
             content,
             metadata=json_clean(metadata),
-            parent=self.kernel.get_parent("shell"),
+            
parent=self.kernel.get_parent(channel_from_which_to_get_parent_header),
             ident=self.topic,
             buffers=buffers,
         )
@@ -80,6 +87,7 @@
                 "The `ipykernel.comm.Comm` class has been deprecated. Please 
use the `comm` module instead."
                 "For creating comms, use the function `from comm import 
create_comm`.",
                 DeprecationWarning,
+                stacklevel=2,
             )
 
         # Handle differing arguments between base classes.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/connect.py 
new/ipykernel-6.23.1/ipykernel/connect.py
--- old/ipykernel-6.22.0/ipykernel/connect.py   2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/connect.py   2020-02-02 01:00:00.000000000 
+0100
@@ -117,7 +117,7 @@
     kwargs["start_new_session"] = True
 
     return Popen(
-        [sys.executable, "-c", cmd, "--existing", cf, *argv],
+        [sys.executable, "-c", cmd, "--existing", cf, *argv],  # noqa
         stdout=PIPE,
         stderr=PIPE,
         close_fds=(sys.platform != "win32"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/control.py 
new/ipykernel-6.23.1/ipykernel/control.py
--- old/ipykernel-6.22.0/ipykernel/control.py   2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/control.py   2020-02-02 01:00:00.000000000 
+0100
@@ -3,20 +3,22 @@
 
 from tornado.ioloop import IOLoop
 
+CONTROL_THREAD_NAME = "Control"
+
 
 class ControlThread(Thread):
     """A thread for a control channel."""
 
     def __init__(self, **kwargs):
         """Initialize the thread."""
-        Thread.__init__(self, name="Control", **kwargs)
+        Thread.__init__(self, name=CONTROL_THREAD_NAME, **kwargs)
         self.io_loop = IOLoop(make_current=False)
         self.pydev_do_not_trace = True
         self.is_pydev_daemon_thread = True
 
     def run(self):
         """Run the thread."""
-        self.name = "Control"
+        self.name = CONTROL_THREAD_NAME
         try:
             self.io_loop.start()
         finally:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/gui/gtk3embed.py 
new/ipykernel-6.23.1/ipykernel/gui/gtk3embed.py
--- old/ipykernel-6.22.0/ipykernel/gui/gtk3embed.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/gui/gtk3embed.py     2020-02-02 
01:00:00.000000000 +0100
@@ -14,7 +14,9 @@
 import sys
 import warnings
 
-warnings.warn("The Gtk3 event loop for ipykernel is deprecated", 
category=DeprecationWarning)
+warnings.warn(
+    "The Gtk3 event loop for ipykernel is deprecated", 
category=DeprecationWarning, stacklevel=2
+)
 
 # Third-party
 import gi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/gui/gtkembed.py 
new/ipykernel-6.23.1/ipykernel/gui/gtkembed.py
--- old/ipykernel-6.22.0/ipykernel/gui/gtkembed.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/gui/gtkembed.py      2020-02-02 
01:00:00.000000000 +0100
@@ -14,7 +14,9 @@
 import sys
 import warnings
 
-warnings.warn("The Gtk3 event loop for ipykernel is deprecated", 
category=DeprecationWarning)
+warnings.warn(
+    "The Gtk3 event loop for ipykernel is deprecated", 
category=DeprecationWarning, stacklevel=2
+)
 
 # Third-party
 import gobject
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/iostream.py 
new/ipykernel-6.23.1/ipykernel/iostream.py
--- old/ipykernel-6.22.0/ipykernel/iostream.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/iostream.py  2020-02-02 01:00:00.000000000 
+0100
@@ -13,6 +13,7 @@
 from binascii import b2a_hex
 from collections import deque
 from io import StringIO, TextIOBase
+from threading import local
 from typing import Any, Callable, Deque, Optional
 from weakref import WeakSet
 
@@ -134,7 +135,8 @@
         except zmq.ZMQError as e:
             warnings.warn(
                 "Couldn't bind IOPub Pipe to 127.0.0.1: %s" % e
-                + "\nsubprocess output will be unavailable."
+                + "\nsubprocess output will be unavailable.",
+                stacklevel=2,
             )
             self._pipe_flag = False
             pipe_in.close()
@@ -362,7 +364,7 @@
         echo : bool
             whether to echo output
         watchfd : bool (default, True)
-            Watch the file descripttor corresponding to the replaced stream.
+            Watch the file descriptor corresponding to the replaced stream.
             This is useful if you know some underlying code will write directly
             the file descriptor by its number. It will spawn a watching thread,
             that will swap the give file descriptor for a pipe, read from the
@@ -402,22 +404,43 @@
         self.echo = None
         self._isatty = bool(isatty)
         self._should_watch = False
+        self._local = local()
 
         if (
             watchfd
-            and (sys.platform.startswith("linux") or 
sys.platform.startswith("darwin"))
-            and ("PYTEST_CURRENT_TEST" not in os.environ)
+            and (
+                (sys.platform.startswith("linux") or 
sys.platform.startswith("darwin"))
+                # Pytest set its own capture. Don't redirect from within 
pytest.
+                and ("PYTEST_CURRENT_TEST" not in os.environ)
+            )
+            # allow forcing watchfd (mainly for tests)
+            or watchfd == "force"
         ):
-            # Pytest set its own capture. Dont redirect from within pytest.
-
             self._should_watch = True
             self._setup_stream_redirects(name)
 
         if echo:
             if hasattr(echo, "read") and hasattr(echo, "write"):
+                # make sure we aren't trying to echo on the FD we're watching!
+                # that would cause an infinite loop, always echoing on itself
+                if self._should_watch:
+                    try:
+                        echo_fd = echo.fileno()
+                    except Exception:
+                        echo_fd = None
+
+                    if echo_fd is not None and echo_fd == 
self._original_stdstream_fd:
+                        # echo on the _copy_ we made during
+                        # this is the actual terminal FD now
+                        echo = io.TextIOWrapper(
+                            io.FileIO(
+                                self._original_stdstream_copy,
+                                "w",
+                            )
+                        )
                 self.echo = echo
             else:
-                msg = "echo argument must be a file like object"
+                msg = "echo argument must be a file-like object"
                 raise ValueError(msg)
 
     def isatty(self):
@@ -430,7 +453,7 @@
 
     def _setup_stream_redirects(self, name):
         pr, pw = os.pipe()
-        fno = getattr(sys, name).fileno()
+        fno = self._original_stdstream_fd = getattr(sys, name).fileno()
         self._original_stdstream_copy = os.dup(fno)
         os.dup2(pw, fno)
 
@@ -452,7 +475,13 @@
         """Close the stream."""
         if self._should_watch:
             self._should_watch = False
+            # thread won't wake unless there's something to read
+            # writing something after _should_watch will not be echoed
+            os.write(self._original_stdstream_fd, b'\0')
             self.watch_fd_thread.join()
+            # restore original FDs
+            os.dup2(self._original_stdstream_copy, self._original_stdstream_fd)
+            os.close(self._original_stdstream_copy)
         if self._exc:
             etype, value, tb = self._exc
             traceback.print_exception(etype, value, tb)
@@ -524,11 +553,19 @@
             # There should be a better way to do this.
             self.session.pid = os.getpid()
             content = {"name": self.name, "text": data}
+            msg = self.session.msg("stream", content, 
parent=self.parent_header)
+
+            # Each transform either returns a new
+            # message or None. If None is returned,
+            # the message has been 'used' and we return.
+            for hook in self._hooks:
+                msg = hook(msg)
+                if msg is None:
+                    return
+
             self.session.send(
                 self.pub_thread,
-                "stream",
-                content=content,
-                parent=self.parent_header,
+                msg,
                 ident=self.topic,
             )
 
@@ -600,3 +637,49 @@
             old_buffer = self._buffer
             self._buffer = StringIO()
         return old_buffer
+
+    @property
+    def _hooks(self):
+        if not hasattr(self._local, "hooks"):
+            # create new list for a new thread
+            self._local.hooks = []
+        return self._local.hooks
+
+    def register_hook(self, hook):
+        """
+        Registers a hook with the thread-local storage.
+
+        Parameters
+        ----------
+        hook : Any callable object
+
+        Returns
+        -------
+        Either a publishable message, or `None`.
+        The hook callable must return a message from
+        the __call__ method if they still require the
+        `session.send` method to be called after transformation.
+        Returning `None` will halt that execution path, and
+        session.send will not be called.
+        """
+        self._hooks.append(hook)
+
+    def unregister_hook(self, hook):
+        """
+        Un-registers a hook with the thread-local storage.
+
+        Parameters
+        ----------
+        hook : Any callable object which has previously been
+            registered as a hook.
+
+        Returns
+        -------
+        bool - `True` if the hook was removed, `False` if it wasn't
+            found.
+        """
+        try:
+            self._hooks.remove(hook)
+            return True
+        except ValueError:
+            return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/ipkernel.py 
new/ipykernel-6.23.1/ipykernel/ipkernel.py
--- old/ipykernel-6.22.0/ipykernel/ipkernel.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/ipkernel.py  2020-02-02 01:00:00.000000000 
+0100
@@ -704,5 +704,6 @@
         warnings.warn(
             "Kernel is a deprecated alias of ipykernel.ipkernel.IPythonKernel",
             DeprecationWarning,
+            stacklevel=2,
         )
         super().__init__(*args, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/kernelspec.py 
new/ipykernel-6.23.1/ipykernel/kernelspec.py
--- old/ipykernel-6.22.0/ipykernel/kernelspec.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/kernelspec.py        2020-02-02 
01:00:00.000000000 +0100
@@ -227,7 +227,7 @@
         )
         opts = parser.parse_args(self.argv)
         if opts.env:
-            opts.env = {k: v for (k, v) in opts.env}
+            opts.env = dict(opts.env)
         try:
             dest = install(
                 user=opts.user,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/parentpoller.py 
new/ipykernel-6.23.1/ipykernel/parentpoller.py
--- old/ipykernel-6.22.0/ipykernel/parentpoller.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/parentpoller.py      2020-02-02 
01:00:00.000000000 +0100
@@ -115,6 +115,7 @@
                     """Parent poll failed.  If the frontend dies,
                 the kernel may be left running.  Please let us know
                 about your system (bitness, Python, etc.) at
-                ipython-...@scipy.org"""
+                ipython-...@scipy.org""",
+                    stacklevel=2,
                 )
                 return
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/pickleutil.py 
new/ipykernel-6.23.1/ipykernel/pickleutil.py
--- old/ipykernel-6.22.0/ipykernel/pickleutil.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/pickleutil.py        2020-02-02 
01:00:00.000000000 +0100
@@ -300,7 +300,7 @@
         data = self.buffers[0]
         if self.pickled:
             # we just pickled it
-            return pickle.loads(data)
+            return pickle.loads(data)  # noqa
         else:
             return frombuffer(data, dtype=self.dtype).reshape(self.shape)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/pylab/backend_inline.py 
new/ipykernel-6.23.1/ipykernel/pylab/backend_inline.py
--- old/ipykernel-6.22.0/ipykernel/pylab/backend_inline.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/pylab/backend_inline.py      2020-02-02 
01:00:00.000000000 +0100
@@ -11,4 +11,5 @@
     "`ipykernel.pylab.backend_inline` is deprecated, directly "
     "use `matplotlib_inline.backend_inline`",
     DeprecationWarning,
+    stacklevel=2,
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/pylab/config.py 
new/ipykernel-6.23.1/ipykernel/pylab/config.py
--- old/ipykernel-6.22.0/ipykernel/pylab/config.py      2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/pylab/config.py      2020-02-02 
01:00:00.000000000 +0100
@@ -10,4 +10,5 @@
 warnings.warn(
     "`ipykernel.pylab.config` is deprecated, directly use 
`matplotlib_inline.config`",
     DeprecationWarning,
+    stacklevel=2,
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/serialize.py 
new/ipykernel-6.23.1/ipykernel/serialize.py
--- old/ipykernel-6.22.0/ipykernel/serialize.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/serialize.py 2020-02-02 01:00:00.000000000 
+0100
@@ -122,7 +122,7 @@
     """
     bufs = list(buffers)
     pobj = bufs.pop(0)
-    canned = pickle.loads(pobj)
+    canned = pickle.loads(pobj)  # noqa
     if istype(canned, sequence_types) and len(canned) < MAX_ITEMS:
         for c in canned:
             _restore_buffers(c, bufs)
@@ -183,9 +183,9 @@
     bufs = list(bufs)  # allow us to pop
     assert len(bufs) >= 2, "not enough buffers!"
     pf = bufs.pop(0)
-    f = uncan(pickle.loads(pf), g)
+    f = uncan(pickle.loads(pf), g)  # noqa
     pinfo = bufs.pop(0)
-    info = pickle.loads(pinfo)
+    info = pickle.loads(pinfo)  # noqa
     arg_bufs, kwarg_bufs = bufs[: info["narg_bufs"]], bufs[info["narg_bufs"] :]
 
     args_list = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/tests/test_io.py 
new/ipykernel-6.23.1/ipykernel/tests/test_io.py
--- old/ipykernel-6.22.0/ipykernel/tests/test_io.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/tests/test_io.py     2020-02-02 
01:00:00.000000000 +0100
@@ -1,7 +1,12 @@
 """Test IO capturing functionality"""
 
 import io
+import os
+import subprocess
+import sys
+import time
 import warnings
+from unittest import mock
 
 import pytest
 import zmq
@@ -10,20 +15,28 @@
 from ipykernel.iostream import MASTER, BackgroundSocket, IOPubThread, OutStream
 
 
-def test_io_api():
-    """Test that wrapped stdout has the same API as a normal TextIO object"""
-    session = Session()
+@pytest.fixture
+def ctx():
     ctx = zmq.Context()
-    pub = ctx.socket(zmq.PUB)
-    thread = IOPubThread(pub)
-    thread.start()
+    yield ctx
+    ctx.destroy()
 
-    stream = OutStream(session, thread, "stdout")
 
-    # cleanup unused zmq objects before we start testing
-    thread.stop()
-    thread.close()
-    ctx.term()
+@pytest.fixture
+def iopub_thread(ctx):
+    with ctx.socket(zmq.PUB) as pub:
+        thread = IOPubThread(pub)
+        thread.start()
+
+        yield thread
+        thread.stop()
+        thread.close()
+
+
+def test_io_api(iopub_thread):
+    """Test that wrapped stdout has the same API as a normal TextIO object"""
+    session = Session()
+    stream = OutStream(session, iopub_thread, "stdout")
 
     assert stream.errors is None
     assert not stream.isatty()
@@ -43,28 +56,21 @@
         stream.write(b"")  # type:ignore
 
 
-def test_io_isatty():
+def test_io_isatty(iopub_thread):
     session = Session()
-    ctx = zmq.Context()
-    pub = ctx.socket(zmq.PUB)
-    thread = IOPubThread(pub)
-    thread.start()
-
-    stream = OutStream(session, thread, "stdout", isatty=True)
+    stream = OutStream(session, iopub_thread, "stdout", isatty=True)
     assert stream.isatty()
 
 
-def test_io_thread():
-    ctx = zmq.Context()
-    pub = ctx.socket(zmq.PUB)
-    thread = IOPubThread(pub)
+def test_io_thread(iopub_thread):
+    thread = iopub_thread
     thread._setup_pipe_in()
     msg = [thread._pipe_uuid, b"a"]
     thread._handle_pipe_msg(msg)
     ctx1, pipe = thread._setup_pipe_out()
     pipe.close()
     thread._pipe_in.close()
-    thread._check_mp_mode = lambda: MASTER  # type:ignore
+    thread._check_mp_mode = lambda: MASTER
     thread._really_send([b"hi"])
     ctx1.destroy()
     thread.close()
@@ -72,40 +78,139 @@
     thread._really_send(None)
 
 
-def test_background_socket():
-    ctx = zmq.Context()
-    pub = ctx.socket(zmq.PUB)
-    thread = IOPubThread(pub)
-    sock = BackgroundSocket(thread)
+def test_background_socket(iopub_thread):
+    sock = BackgroundSocket(iopub_thread)
     assert sock.__class__ == BackgroundSocket
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", DeprecationWarning)
         sock.linger = 101
-        assert thread.socket.linger == 101
-    assert sock.io_thread == thread
+        assert iopub_thread.socket.linger == 101
+    assert sock.io_thread == iopub_thread
     sock.send(b"hi")
 
 
-def test_outstream():
+def test_outstream(iopub_thread):
     session = Session()
-    ctx = zmq.Context()
-    pub = ctx.socket(zmq.PUB)
-    thread = IOPubThread(pub)
-    thread.start()
-
+    pub = iopub_thread.socket
     with warnings.catch_warnings():
         warnings.simplefilter("ignore", DeprecationWarning)
         stream = OutStream(session, pub, "stdout")
-        stream = OutStream(session, thread, "stdout", pipe=object())
+        stream.close()
+        stream = OutStream(session, iopub_thread, "stdout", pipe=object())
+        stream.close()
 
-        stream = OutStream(session, thread, "stdout", watchfd=False)
+        stream = OutStream(session, iopub_thread, "stdout", watchfd=False)
         stream.close()
 
-    stream = OutStream(session, thread, "stdout", isatty=True, 
echo=io.StringIO())
-    with pytest.raises(io.UnsupportedOperation):
-        stream.fileno()
-    stream._watch_pipe_fd()
-    stream.flush()
-    stream.write("hi")
-    stream.writelines(["ab", "cd"])
-    assert stream.writable()
+    stream = OutStream(session, iopub_thread, "stdout", isatty=True, 
echo=io.StringIO())
+
+    with stream:
+        with pytest.raises(io.UnsupportedOperation):
+            stream.fileno()
+        stream._watch_pipe_fd()
+        stream.flush()
+        stream.write("hi")
+        stream.writelines(["ab", "cd"])
+        assert stream.writable()
+
+
+def subprocess_test_echo_watch():
+    # handshake Pub subscription
+    session = Session(key=b'abc')
+
+    # use PUSH socket to avoid subscription issues
+    with zmq.Context() as ctx, ctx.socket(zmq.PUSH) as pub:
+        pub.connect(os.environ["IOPUB_URL"])
+        iopub_thread = IOPubThread(pub)
+        iopub_thread.start()
+        stdout_fd = sys.stdout.fileno()
+        sys.stdout.flush()
+        stream = OutStream(
+            session,
+            iopub_thread,
+            "stdout",
+            isatty=True,
+            echo=sys.stdout,
+            watchfd="force",
+        )
+        save_stdout = sys.stdout
+        with stream, mock.patch.object(sys, "stdout", stream):
+            # write to low-level FD
+            os.write(stdout_fd, b"fd\n")
+            # print (writes to stream)
+            print("print\n", end="")
+            sys.stdout.flush()
+            # write to unwrapped __stdout__ (should also go to original FD)
+            sys.__stdout__.write("__stdout__\n")
+            sys.__stdout__.flush()
+            # write to original sys.stdout (should be the same as __stdout__)
+            save_stdout.write("stdout\n")
+            save_stdout.flush()
+            # is there another way to flush on the FD?
+            fd_file = os.fdopen(stdout_fd, "w")
+            fd_file.flush()
+            # we don't have a sync flush on _reading_ from the watched pipe
+            time.sleep(1)
+            stream.flush()
+        iopub_thread.stop()
+        iopub_thread.close()
+
+
+@pytest.mark.skipif(sys.platform.startswith("win"), reason="Windows")
+def test_echo_watch(ctx):
+    """Test echo on underlying FD while capturing the same FD
+
+    Test runs in a subprocess to avoid messing with pytest output capturing.
+    """
+    s = ctx.socket(zmq.PULL)
+    port = s.bind_to_random_port("tcp://127.0.0.1")
+    url = f"tcp://127.0.0.1:{port}"
+    session = Session(key=b'abc')
+    messages = []
+    stdout_chunks = []
+    with s:
+        env = dict(os.environ)
+        env["IOPUB_URL"] = url
+        env["PYTHONUNBUFFERED"] = "1"
+        env.pop("PYTEST_CURRENT_TEST", None)
+        p = subprocess.run(
+            [
+                sys.executable,
+                "-c",
+                f"import {__name__}; {__name__}.subprocess_test_echo_watch()",
+            ],
+            env=env,
+            capture_output=True,
+            text=True,
+            timeout=10,
+        )
+        print(f"{p.stdout=}")
+        print(f"{p.stderr}=", file=sys.stderr)
+        assert p.returncode == 0
+        while s.poll(timeout=100):
+            ident, msg = session.recv(s)
+            assert msg is not None  # for type narrowing
+            if msg["header"]["msg_type"] == "stream" and 
msg["content"]["name"] == "stdout":
+                stdout_chunks.append(msg["content"]["text"])
+
+    # check outputs
+    # use sets of lines to ignore ordering issues with
+    # async flush and watchfd thread
+
+    # Check the stream output forwarded over zmq
+    zmq_stdout = "".join(stdout_chunks)
+    assert set(zmq_stdout.strip().splitlines()) == {
+        "fd",
+        "print",
+        "stdout",
+        "__stdout__",
+    }
+
+    # Check what was written to the process stdout (kernel terminal)
+    # just check that each output source went to the terminal
+    assert set(p.stdout.strip().splitlines()) == {
+        "fd",
+        "print",
+        "stdout",
+        "__stdout__",
+    }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/tests/test_pickleutil.py 
new/ipykernel-6.23.1/ipykernel/tests/test_pickleutil.py
--- old/ipykernel-6.22.0/ipykernel/tests/test_pickleutil.py     2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/tests/test_pickleutil.py     2020-02-02 
01:00:00.000000000 +0100
@@ -16,7 +16,7 @@
 
 
 def loads(obj):
-    return uncan(pickle.loads(obj))
+    return uncan(pickle.loads(obj))  # noqa
 
 
 def test_no_closure():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/trio_runner.py 
new/ipykernel-6.23.1/ipykernel/trio_runner.py
--- old/ipykernel-6.22.0/ipykernel/trio_runner.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/ipykernel/trio_runner.py       2020-02-02 
01:00:00.000000000 +0100
@@ -22,7 +22,7 @@
         kernel.shell.set_trio_runner(self)
         kernel.shell.run_line_magic("autoawait", "trio")
         kernel.shell.magics_manager.magics["line"]["autoawait"] = lambda _: 
warnings.warn(
-            "Autoawait isn't allowed in Trio background loop mode."
+            "Autoawait isn't allowed in Trio background loop mode.", 
stacklevel=2
         )
         self._interrupted = False
         bg_thread = threading.Thread(target=io_loop.start, daemon=True, 
name="TornadoBackground")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel/zmqshell.py 
new/ipykernel-6.23.1/ipykernel/zmqshell.py
--- old/ipykernel-6.22.0/ipykernel/zmqshell.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel/zmqshell.py  2020-02-02 01:00:00.000000000 
+0100
@@ -363,7 +363,7 @@
             connection_file = get_connection_file()
             info = get_connection_info(unpack=False)
         except Exception as e:
-            warnings.warn("Could not get connection info: %r" % e)
+            warnings.warn("Could not get connection info: %r" % e, 
stacklevel=2)
             return
 
         # if it's in the default dir, truncate to basename
@@ -399,7 +399,7 @@
         try:
             connect_qtconsole(argv=arg_split(arg_s, os.name == "posix"))
         except Exception as e:
-            warnings.warn("Could not start qtconsole: %r" % e)
+            warnings.warn("Could not start qtconsole: %r" % e, stacklevel=2)
             return
 
     @line_magic
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/ipykernel_launcher.py 
new/ipykernel-6.23.1/ipykernel_launcher.py
--- old/ipykernel-6.22.0/ipykernel_launcher.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.23.1/ipykernel_launcher.py  2020-02-02 01:00:00.000000000 
+0100
@@ -9,7 +9,7 @@
 if __name__ == "__main__":
     # Remove the CWD from sys.path while we load stuff.
     # This is added back by InteractiveShellApp.init_path()
-    if sys.path[0] == "":
+    if sys.path[0] == "":  # noqa
         del sys.path[0]
 
     from ipykernel import kernelapp as app
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.22.0/pyproject.toml 
new/ipykernel-6.23.1/pyproject.toml
--- old/ipykernel-6.22.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.23.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -91,6 +91,7 @@
 [tool.hatch.envs.test]
 features = ["test"]
 [tool.hatch.envs.test.scripts]
+list = "python -m pip freeze"
 test = "python -m pytest -vv {args}"
 nowarn = "test -W default {args}"
 
@@ -116,7 +117,7 @@
 test = "mypy --install-types --non-interactive {args:.}"
 
 [tool.hatch.envs.lint]
-dependencies = ["black==23.1.0", "mdformat>0.7", "ruff==0.0.254"]
+dependencies = ["black==23.3.0", "mdformat>0.7", "ruff==0.0.263"]
 detached = true
 [tool.hatch.envs.lint.scripts]
 style = [
@@ -191,6 +192,8 @@
 ]
 
 [tool.coverage.run]
+relative_files = true
+source = ["ipykernel"]
 omit = [
   "ipykernel/tests/*",
   "ipykernel/datapub.py",
@@ -296,7 +299,11 @@
 # PLR2004 Magic value used in comparison
 # PLW0603 Using the global statement to update ...
 # PLW2901 `for` loop variable ...
-"ipykernel/tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", 
"F841", "EM101", "EM102", "EM103", "PLR2004", "PLW0603", "PLW2901"]
+# PLC1901 `stderr == ""` can be simplified to `not stderr` as an empty string 
is falsey
+# B018 Found useless expression. Either assign it to a variable or remove it.
+# S603 `subprocess` call: check for execution of untrusted input
+"ipykernel/tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", 
"F841", "EM101",
+    "EM102", "EM103", "PLR2004", "PLW0603", "PLW2901", "PLC1901", "B018", 
"S603"]
 
 [tool.interrogate]
 ignore-init-module=true

Reply via email to