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 2024-01-21 23:07:29
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-ipykernel (Old)
 and      /work/SRC/openSUSE:Factory/.python-ipykernel.new.16006 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-ipykernel"

Sun Jan 21 23:07:29 2024 rev:45 rq:1140287 version:6.29.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-ipykernel/python-ipykernel.changes        
2023-12-21 23:38:47.074912191 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-ipykernel.new.16006/python-ipykernel.changes 
    2024-01-21 23:07:40.108001142 +0100
@@ -1,0 +2,17 @@
+Sun Jan 21 10:45:42 UTC 2024 - Ben Greiner <c...@bnavigator.de>
+
+- Update to 6.29.0
+  * Always set debugger to true in kernelspec #1191 (@ianthomas23)
+  * Revert "Enable ProactorEventLoop on windows for ipykernel"
+    #1194 (@blink1073)
+  * Make outputs go to correct cell when generated in
+    threads/asyncio #1186 (@krassowski)
+  * Pin pytest-asyncio to 0.23.2 #1189 (@ianthomas23)
+- Update to 6.28.0
+  * Enable ProactorEventLoop on windows for ipykernel #1184
+    (@NewUserHa)
+  * Adds a flag in debug_info for the copyToGlobals support #1099
+    (@brichet)
+  * Support python 3.12 #1185 (@blink1073)
+
+-------------------------------------------------------------------

Old:
----
  ipykernel-6.27.1.tar.gz

New:
----
  ipykernel-6.29.0.tar.gz

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

Other differences:
------------------
++++++ python-ipykernel.spec ++++++
--- /var/tmp/diff_new_pack.TDT0gs/_old  2024-01-21 23:07:40.676021846 +0100
+++ /var/tmp/diff_new_pack.TDT0gs/_new  2024-01-21 23:07:40.676021846 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-ipykernel
 #
-# Copyright (c) 2023 SUSE LLC
+# Copyright (c) 2024 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-ipykernel
-Version:        6.27.1
+Version:        6.29.0
 Release:        0
 Summary:        IPython Kernel for Jupyter
 License:        BSD-3-Clause
@@ -51,7 +51,7 @@
 BuildRequires:  %{python_module nest-asyncio}
 BuildRequires:  %{python_module packaging}
 BuildRequires:  %{python_module psutil}
-BuildRequires:  %{python_module pyzmq >= 20}
+BuildRequires:  %{python_module pyzmq >= 24}
 BuildRequires:  %{python_module tornado >= 6.1}
 BuildRequires:  %{python_module traitlets >= 5.1.0}
 BuildRequires:  %{python_module jupyter-core >= 5.1 or (%python-jupyter-core 
>= 4.12 with %python-jupyter-core < 5.0)}
@@ -64,7 +64,7 @@
 Requires:       python-nest-asyncio
 Requires:       python-packaging
 Requires:       python-psutil
-Requires:       python-pyzmq >= 20
+Requires:       python-pyzmq >= 24
 Requires:       python-tornado >= 6.1
 Requires:       python-traitlets >= 5.4.0
 Requires:       (python-jupyter-core >= 5.1 or (python-jupyter-core >= 4.12 
with python-jupyter-core < 5.0))
@@ -90,6 +90,7 @@
 %prep
 %autosetup -p1 -n ipykernel-%{version}
 sed -i -e 's/, "--color=yes"//' pyproject.toml
+sed -i -e '/ignore:.* current event loop:DeprecationWarning/ a \  
"ignore:pytest-asyncio detected an unclosed event loop:DeprecationWarning",' 
pyproject.toml
 
 %build
 %pyproject_wheel

++++++ ipykernel-6.27.1.tar.gz -> ipykernel-6.29.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/.github/dependabot.yml 
new/ipykernel-6.29.0/.github/dependabot.yml
--- old/ipykernel-6.27.1/.github/dependabot.yml 2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/.github/dependabot.yml 2020-02-02 01:00:00.000000000 
+0100
@@ -4,7 +4,15 @@
     directory: "/"
     schedule:
       interval: "weekly"
+    groups:
+      actions:
+        patterns:
+          - "*"
   - package-ecosystem: "pip"
     directory: "/"
     schedule:
       interval: "weekly"
+    groups:
+      actions:
+        patterns:
+          - "*"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/.github/workflows/ci.yml 
new/ipykernel-6.29.0/.github/workflows/ci.yml
--- old/ipykernel-6.27.1/.github/workflows/ci.yml       2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/.github/workflows/ci.yml       2020-02-02 
01:00:00.000000000 +0100
@@ -22,16 +22,16 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ["3.8", "3.11"]
+        python-version: ["3.8", "3.12"]
         include:
           - os: windows-latest
             python-version: "3.9"
           - os: ubuntu-latest
-            python-version: "pypy-3.8"
+            python-version: "pypy-3.9"
           - os: macos-latest
             python-version: "3.10"
           - os: ubuntu-latest
-            python-version: "3.8"
+            python-version: "3.11"
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -153,11 +153,11 @@
 
       - name: List installed packages
         run: |
-          hatch run test:list
+          hatch -v run test:list
 
       - name: Run the unit tests
         run: |
-          hatch run test:nowarn || hatch run test:nowarn --lf
+          hatch -v run test:nowarn || hatch run test:nowarn --lf
 
   test_prereleases:
     name: Test Prereleases
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/.github/workflows/downstream.yml 
new/ipykernel-6.29.0/.github/workflows/downstream.yml
--- old/ipykernel-6.27.1/.github/workflows/downstream.yml       2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/.github/workflows/downstream.yml       2020-02-02 
01:00:00.000000000 +0100
@@ -92,7 +92,7 @@
       - name: Checkout
         uses: actions/checkout@v4
       - name: Setup Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: "3.9"
           architecture: "x64"
@@ -124,7 +124,7 @@
       - name: Checkout
         uses: actions/checkout@v4
       - name: Setup Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: "3.9"
           architecture: "x64"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/.pre-commit-config.yaml 
new/ipykernel-6.29.0/.pre-commit-config.yaml
--- old/ipykernel-6.27.1/.pre-commit-config.yaml        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/.pre-commit-config.yaml        2020-02-02 
01:00:00.000000000 +0100
@@ -22,7 +22,7 @@
       - id: trailing-whitespace
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.27.1
+    rev: 0.27.3
     hooks:
       - id: check-github-workflows
 
@@ -34,13 +34,13 @@
           [mdformat-gfm, mdformat-frontmatter, mdformat-footnote]
 
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: "v3.1.0"
+    rev: "v4.0.0-alpha.8"
     hooks:
       - id: prettier
         types_or: [yaml, html, json]
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: "v1.7.0"
+    rev: "v1.8.0"
     hooks:
       - id: mypy
         files: ipykernel
@@ -74,7 +74,7 @@
       - id: rst-inline-touching-normal
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.1.6
+    rev: v0.1.9
     hooks:
       - id: ruff
         types_or: [python, jupyter]
@@ -83,7 +83,7 @@
         types_or: [python, jupyter]
 
   - repo: https://github.com/scientific-python/cookie
-    rev: "2023.11.17"
+    rev: "2023.12.21"
     hooks:
       - id: sp-repo-review
         additional_dependencies: ["repo-review[cli]"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/CHANGELOG.md 
new/ipykernel-6.29.0/CHANGELOG.md
--- old/ipykernel-6.27.1/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/CHANGELOG.md   2020-02-02 01:00:00.000000000 +0100
@@ -2,6 +2,58 @@
 
 <!-- <START NEW CHANGELOG ENTRY> -->
 
+## 6.29.0
+
+([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.28.0...84955484ec1636ee4c7611471d20df2016b5cb57))
+
+### Enhancements made
+
+- Always set debugger to true in kernelspec 
[#1191](https://github.com/ipython/ipykernel/pull/1191) 
([@ianthomas23](https://github.com/ianthomas23))
+
+### Bugs fixed
+
+- Revert "Enable `ProactorEventLoop` on windows for `ipykernel`" 
[#1194](https://github.com/ipython/ipykernel/pull/1194) 
([@blink1073](https://github.com/blink1073))
+- Make outputs go to correct cell when generated in threads/asyncio 
[#1186](https://github.com/ipython/ipykernel/pull/1186) 
([@krassowski](https://github.com/krassowski))
+
+### Maintenance and upkeep improvements
+
+- Pin pytest-asyncio to 0.23.2 
[#1189](https://github.com/ipython/ipykernel/pull/1189) 
([@ianthomas23](https://github.com/ianthomas23))
+- chore: update pre-commit hooks 
[#1187](https://github.com/ipython/ipykernel/pull/1187) 
([@pre-commit-ci](https://github.com/pre-commit-ci))
+
+### Contributors to this release
+
+([GitHub contributors page for this 
release](https://github.com/ipython/ipykernel/graphs/contributors?from=2023-12-26&to=2024-01-16&type=c))
+
+[@blink1073](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Ablink1073+updated%3A2023-12-26..2024-01-16&type=Issues)
 | 
[@ianthomas23](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Aianthomas23+updated%3A2023-12-26..2024-01-16&type=Issues)
 | 
[@krassowski](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Akrassowski+updated%3A2023-12-26..2024-01-16&type=Issues)
 | 
[@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Apre-commit-ci+updated%3A2023-12-26..2024-01-16&type=Issues)
+
+<!-- <END NEW CHANGELOG ENTRY> -->
+
+## 6.28.0
+
+([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.27.1...de45c7a49e197f0889f867f33f24cce322768a0e))
+
+### Enhancements made
+
+- Enable `ProactorEventLoop` on windows for `ipykernel` 
[#1184](https://github.com/ipython/ipykernel/pull/1184) 
([@NewUserHa](https://github.com/NewUserHa))
+- Adds a flag in debug_info for the copyToGlobals support 
[#1099](https://github.com/ipython/ipykernel/pull/1099) 
([@brichet](https://github.com/brichet))
+
+### Maintenance and upkeep improvements
+
+- Support python 3.12 [#1185](https://github.com/ipython/ipykernel/pull/1185) 
([@blink1073](https://github.com/blink1073))
+- Bump actions/setup-python from 4 to 5 
[#1181](https://github.com/ipython/ipykernel/pull/1181) 
([@dependabot](https://github.com/dependabot))
+- chore: update pre-commit hooks 
[#1179](https://github.com/ipython/ipykernel/pull/1179) 
([@pre-commit-ci](https://github.com/pre-commit-ci))
+- Refactor execute_request to reduce redundancy and improve consistency 
[#1177](https://github.com/ipython/ipykernel/pull/1177) 
([@jjvraw](https://github.com/jjvraw))
+
+### Documentation improvements
+
+- Update pytest commands in README 
[#1178](https://github.com/ipython/ipykernel/pull/1178) 
([@ianthomas23](https://github.com/ianthomas23))
+
+### Contributors to this release
+
+([GitHub contributors page for this 
release](https://github.com/ipython/ipykernel/graphs/contributors?from=2023-11-27&to=2023-12-26&type=c))
+
+[@blink1073](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Ablink1073+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@brichet](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Abrichet+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@dependabot](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Adependabot+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@ianthomas23](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Aianthomas23+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@jjvraw](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Ajjvraw+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@NewUserHa](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3ANewUserHa+updated%3A2023-11-27..2023-12-26&type=Issues)
 | 
[@pre-commit-ci](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Apre-commit-ci+updated%3A2023-11-27..2023-12-26&type=Issues)
+
 ## 6.27.1
 
 ([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.27.0...f9c517e868462d05d6854204c2ad0a244db1cd19))
@@ -16,8 +68,6 @@
 
 
[@blink1073](https://github.com/search?q=repo%3Aipython%2Fipykernel+involves%3Ablink1073+updated%3A2023-11-21..2023-11-27&type=Issues)
 
-<!-- <END NEW CHANGELOG ENTRY> -->
-
 ## 6.27.0
 
 ([Full 
Changelog](https://github.com/ipython/ipykernel/compare/v6.26.0...465d34483103d23f471a4795fe5fabb9cf7ac3f5))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/PKG-INFO 
new/ipykernel-6.29.0/PKG-INFO
--- old/ipykernel-6.27.1/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/PKG-INFO       2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: ipykernel
-Version: 6.27.1
+Version: 6.29.0
 Summary: IPython Kernel for Jupyter
 Project-URL: Homepage, https://ipython.org
 Author-email: IPython Development Team <ipython-...@scipy.org>
@@ -42,10 +42,6 @@
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
 Requires-Python: >=3.8
 Requires-Dist: appnope; platform_system == 'Darwin'
 Requires-Dist: comm>=0.1.1
@@ -57,7 +53,7 @@
 Requires-Dist: nest-asyncio
 Requires-Dist: packaging
 Requires-Dist: psutil
-Requires-Dist: pyzmq>=20
+Requires-Dist: pyzmq>=24
 Requires-Dist: tornado>=6.1
 Requires-Dist: traitlets>=5.4.0
 Provides-Extra: cov
@@ -82,7 +78,7 @@
 Requires-Dist: flaky; extra == 'test'
 Requires-Dist: ipyparallel; extra == 'test'
 Requires-Dist: pre-commit; extra == 'test'
-Requires-Dist: pytest-asyncio; extra == 'test'
+Requires-Dist: pytest-asyncio==0.23.2; extra == 'test'
 Requires-Dist: pytest-cov; extra == 'test'
 Requires-Dist: pytest-timeout; extra == 'test'
 Requires-Dist: pytest>=7.0; extra == 'test'
@@ -110,7 +106,7 @@
 and then from the root directory
 
 ```bash
-pytest ipykernel
+pytest
 ```
 
 ## Running tests with coverage
@@ -120,7 +116,7 @@
 and then from the root directory
 
 ```bash
-pytest ipykernel -vv -s --cov ipykernel --cov-branch --cov-report 
term-missing:skip-covered --durations 10
+pytest -vv -s --cov ipykernel --cov-branch --cov-report 
term-missing:skip-covered --durations 10
 ```
 
 ## About the IPython Development Team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/README.md 
new/ipykernel-6.29.0/README.md
--- old/ipykernel-6.27.1/README.md      2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/README.md      2020-02-02 01:00:00.000000000 +0100
@@ -20,7 +20,7 @@
 and then from the root directory
 
 ```bash
-pytest ipykernel
+pytest
 ```
 
 ## Running tests with coverage
@@ -30,7 +30,7 @@
 and then from the root directory
 
 ```bash
-pytest ipykernel -vv -s --cov ipykernel --cov-branch --cov-report 
term-missing:skip-covered --durations 10
+pytest -vv -s --cov ipykernel --cov-branch --cov-report 
term-missing:skip-covered --durations 10
 ```
 
 ## About the IPython Development Team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/ipykernel/_version.py 
new/ipykernel-6.29.0/ipykernel/_version.py
--- old/ipykernel-6.27.1/ipykernel/_version.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/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.27.1"
+__version__ = "6.29.0"
 
 # 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/ipykernel-6.27.1/ipykernel/debugger.py 
new/ipykernel-6.29.0/ipykernel/debugger.py
--- old/ipykernel-6.27.1/ipykernel/debugger.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/ipykernel/debugger.py  2020-02-02 01:00:00.000000000 
+0100
@@ -605,6 +605,7 @@
                 "stoppedThreads": list(self.stopped_threads),
                 "richRendering": True,
                 "exceptionPaths": ["Python Exceptions"],
+                "copyToGlobals": True,
             },
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/ipykernel/iostream.py 
new/ipykernel-6.29.0/ipykernel/iostream.py
--- old/ipykernel-6.27.1/ipykernel/iostream.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/ipykernel/iostream.py  2020-02-02 01:00:00.000000000 
+0100
@@ -5,6 +5,7 @@
 
 import asyncio
 import atexit
+import contextvars
 import io
 import os
 import sys
@@ -12,7 +13,7 @@
 import traceback
 import warnings
 from binascii import b2a_hex
-from collections import deque
+from collections import defaultdict, deque
 from io import StringIO, TextIOBase
 from threading import local
 from typing import Any, Callable, Deque, Dict, Optional
@@ -412,7 +413,7 @@
         name : str {'stderr', 'stdout'}
             the name of the standard stream to replace
         pipe : object
-            the pip object
+            the pipe object
         echo : bool
             whether to echo output
         watchfd : bool (default, True)
@@ -446,13 +447,19 @@
         self.pub_thread = pub_thread
         self.name = name
         self.topic = b"stream." + name.encode()
-        self.parent_header = {}
+        self._parent_header: contextvars.ContextVar[Dict[str, Any]] = 
contextvars.ContextVar(
+            "parent_header"
+        )
+        self._parent_header.set({})
+        self._thread_to_parent = {}
+        self._thread_to_parent_header = {}
+        self._parent_header_global = {}
         self._master_pid = os.getpid()
         self._flush_pending = False
         self._subprocess_flush_pending = False
         self._io_loop = pub_thread.io_loop
         self._buffer_lock = threading.RLock()
-        self._buffer = StringIO()
+        self._buffers = defaultdict(StringIO)
         self.echo = None
         self._isatty = bool(isatty)
         self._should_watch = False
@@ -495,6 +502,30 @@
                 msg = "echo argument must be a file-like object"
                 raise ValueError(msg)
 
+    @property
+    def parent_header(self):
+        try:
+            # asyncio-specific
+            return self._parent_header.get()
+        except LookupError:
+            try:
+                # thread-specific
+                identity = threading.current_thread().ident
+                # retrieve the outermost (oldest ancestor,
+                # discounting the kernel thread) thread identity
+                while identity in self._thread_to_parent:
+                    identity = self._thread_to_parent[identity]
+                # use the header of the oldest ancestor
+                return self._thread_to_parent_header[identity]
+            except KeyError:
+                # global (fallback)
+                return self._parent_header_global
+
+    @parent_header.setter
+    def parent_header(self, value):
+        self._parent_header_global = value
+        return self._parent_header.set(value)
+
     def isatty(self):
         """Return a bool indicating whether this is an 'interactive' stream.
 
@@ -598,28 +629,28 @@
                 if self.echo is not sys.__stderr__:
                     print(f"Flush failed: {e}", file=sys.__stderr__)
 
-        data = self._flush_buffer()
-        if data:
-            # FIXME: this disables Session's fork-safe check,
-            # since pub_thread is itself fork-safe.
-            # 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,
-                msg,
-                ident=self.topic,
-            )
+        for parent, data in self._flush_buffers():
+            if data:
+                # FIXME: this disables Session's fork-safe check,
+                # since pub_thread is itself fork-safe.
+                # 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=parent)
+
+                # 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,
+                    msg,
+                    ident=self.topic,
+                )
 
     def write(self, string: str) -> Optional[int]:  # type:ignore[override]
         """Write to current stream after encoding if necessary
@@ -630,6 +661,7 @@
             number of items from input parameter written to stream.
 
         """
+        parent = self.parent_header
 
         if not isinstance(string, str):
             msg = f"write() argument must be str, not {type(string)}"  # 
type:ignore[unreachable]
@@ -649,7 +681,7 @@
         is_child = not self._is_master_process()
         # only touch the buffer in the IO thread to avoid races
         with self._buffer_lock:
-            self._buffer.write(string)
+            self._buffers[frozenset(parent.items())].write(string)
         if is_child:
             # mp.Pool cannot be trusted to flush promptly (or ever),
             # and this helps.
@@ -675,19 +707,20 @@
         """Test whether the stream is writable."""
         return True
 
-    def _flush_buffer(self):
+    def _flush_buffers(self):
         """clear the current buffer and return the current buffer data."""
-        buf = self._rotate_buffer()
-        data = buf.getvalue()
-        buf.close()
-        return data
+        buffers = self._rotate_buffers()
+        for frozen_parent, buffer in buffers.items():
+            data = buffer.getvalue()
+            buffer.close()
+            yield dict(frozen_parent), data
 
-    def _rotate_buffer(self):
+    def _rotate_buffers(self):
         """Returns the current buffer and replaces it with an empty buffer."""
         with self._buffer_lock:
-            old_buffer = self._buffer
-            self._buffer = StringIO()
-        return old_buffer
+            old_buffers = self._buffers
+            self._buffers = defaultdict(StringIO)
+        return old_buffers
 
     @property
     def _hooks(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/ipykernel/ipkernel.py 
new/ipykernel-6.29.0/ipykernel/ipkernel.py
--- old/ipykernel-6.27.1/ipykernel/ipkernel.py  2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/ipykernel/ipkernel.py  2020-02-02 01:00:00.000000000 
+0100
@@ -2,6 +2,7 @@
 
 import asyncio
 import builtins
+import gc
 import getpass
 import os
 import signal
@@ -14,6 +15,7 @@
 import comm
 from IPython.core import release
 from IPython.utils.tokenutil import line_at_cursor, token_at_cursor
+from jupyter_client.session import extract_header
 from traitlets import Any, Bool, HasTraits, Instance, List, Type, observe, 
observe_compat
 from zmq.eventloop.zmqstream import ZMQStream
 
@@ -22,6 +24,7 @@
 from .compiler import XCachingCompiler
 from .debugger import Debugger, _is_debugpy_available
 from .eventloops import _use_appnope
+from .iostream import OutStream
 from .kernelbase import Kernel as KernelBase
 from .kernelbase import _accepts_parameters
 from .zmqshell import ZMQInteractiveShell
@@ -151,6 +154,14 @@
 
             appnope.nope()
 
+        self._new_threads_parent_header = {}
+        self._initialize_thread_hooks()
+
+        if hasattr(gc, "callbacks"):
+            # while `gc.callbacks` exists since Python 3.3, pypy does not
+            # implement it even as of 3.9.
+            gc.callbacks.append(self._clean_thread_parent_frames)
+
     help_links = List(
         [
             {
@@ -341,6 +352,12 @@
             # restore the previous sigint handler
             signal.signal(signal.SIGINT, save_sigint)
 
+    async def execute_request(self, stream, ident, parent):
+        """Override for cell output - cell reconciliation."""
+        parent_header = extract_header(parent)
+        self._associate_new_top_level_threads_with(parent_header)
+        await super().execute_request(stream, ident, parent)
+
     async def do_execute(
         self,
         code,
@@ -706,6 +723,83 @@
             self.shell.reset(False)
         return dict(status="ok")
 
+    def _associate_new_top_level_threads_with(self, parent_header):
+        """Store the parent header to associate it with new top-level 
threads"""
+        self._new_threads_parent_header = parent_header
+
+    def _initialize_thread_hooks(self):
+        """Store thread hierarchy and thread-parent_header associations."""
+        stdout = self._stdout
+        stderr = self._stderr
+        kernel_thread_ident = threading.get_ident()
+        kernel = self
+        _threading_Thread_run = threading.Thread.run
+        _threading_Thread__init__ = threading.Thread.__init__
+
+        def run_closure(self: threading.Thread):
+            """Wrap the `threading.Thread.start` to intercept thread identity.
+
+            This is needed because there is no "start" hook yet, but there
+            might be one in the future: https://bugs.python.org/issue14073
+
+            This is a no-op if the `self._stdout` and `self._stderr` are not
+            sub-classes of `OutStream`.
+            """
+
+            try:
+                parent = self._ipykernel_parent_thread_ident  # 
type:ignore[attr-defined]
+            except AttributeError:
+                return
+            for stream in [stdout, stderr]:
+                if isinstance(stream, OutStream):
+                    if parent == kernel_thread_ident:
+                        stream._thread_to_parent_header[
+                            self.ident
+                        ] = kernel._new_threads_parent_header
+                    else:
+                        stream._thread_to_parent[self.ident] = parent
+            _threading_Thread_run(self)
+
+        def init_closure(self: threading.Thread, *args, **kwargs):
+            _threading_Thread__init__(self, *args, **kwargs)
+            self._ipykernel_parent_thread_ident = threading.get_ident()  # 
type:ignore[attr-defined]
+
+        threading.Thread.__init__ = init_closure  # type:ignore[method-assign]
+        threading.Thread.run = run_closure  # type:ignore[method-assign]
+
+    def _clean_thread_parent_frames(
+        self, phase: t.Literal["start", "stop"], info: t.Dict[str, t.Any]
+    ):
+        """Clean parent frames of threads which are no longer running.
+        This is meant to be invoked by garbage collector callback hook.
+
+        The implementation enumerates the threads because there is no "exit" 
hook yet,
+        but there might be one in the future: 
https://bugs.python.org/issue14073
+
+        This is a no-op if the `self._stdout` and `self._stderr` are not
+        sub-classes of `OutStream`.
+        """
+        # Only run before the garbage collector starts
+        if phase != "start":
+            return
+        active_threads = {thread.ident for thread in threading.enumerate()}
+        for stream in [self._stdout, self._stderr]:
+            if isinstance(stream, OutStream):
+                thread_to_parent_header = stream._thread_to_parent_header
+                for identity in list(thread_to_parent_header.keys()):
+                    if identity not in active_threads:
+                        try:
+                            del thread_to_parent_header[identity]
+                        except KeyError:
+                            pass
+                thread_to_parent = stream._thread_to_parent
+                for identity in list(thread_to_parent.keys()):
+                    if identity not in active_threads:
+                        try:
+                            del thread_to_parent[identity]
+                        except KeyError:
+                            pass
+
 
 # This exists only for backwards compatibility - use IPythonKernel instead
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/ipykernel/kernelbase.py 
new/ipykernel-6.29.0/ipykernel/kernelbase.py
--- old/ipykernel-6.27.1/ipykernel/kernelbase.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/ipykernel/kernelbase.py        2020-02-02 
01:00:00.000000000 +0100
@@ -61,6 +61,7 @@
 from ipykernel.jsonutil import json_clean
 
 from ._version import kernel_protocol_version
+from .iostream import OutStream
 
 
 def _accepts_parameters(meth, param_names):
@@ -272,6 +273,13 @@
     def __init__(self, **kwargs):
         """Initialize the kernel."""
         super().__init__(**kwargs)
+
+        # Kernel application may swap stdout and stderr to OutStream,
+        # which is the case in `IPKernelApp.init_io`, hence `sys.stdout`
+        # can already by different from TextIO at initialization time.
+        self._stdout: OutStream | t.TextIO = sys.stdout
+        self._stderr: OutStream | t.TextIO = sys.stderr
+
         # Build dict of handlers for message types
         self.shell_handlers = {}
         for msg_type in self.msg_types:
@@ -283,6 +291,11 @@
 
         self.control_queue: Queue[t.Any] = Queue()
 
+        # Storing the accepted parameters for do_execute, used in 
execute_request
+        self._do_exec_accepted_params = _accepts_parameters(
+            self.do_execute, ["cell_meta", "cell_id"]
+        )
+
     def dispatch_control(self, msg):
         self.control_queue.put_nowait(msg)
 
@@ -724,6 +737,8 @@
             store_history = content.get("store_history", not silent)
             user_expressions = content.get("user_expressions", {})
             allow_stdin = content.get("allow_stdin", False)
+            cell_meta = parent.get("metadata", {})
+            cell_id = cell_meta.get("cellId")
         except Exception:
             self.log.error("Got bad msg: ")
             self.log.error("%s", parent)
@@ -739,12 +754,6 @@
             self.execution_count += 1
             self._publish_execute_input(code, parent, self.execution_count)
 
-        cell_meta = parent.get("metadata", {})
-        cell_id = cell_meta.get("cellId")
-
-        # Check which parameters do_execute can accept
-        accepts_params = _accepts_parameters(self.do_execute, ["cell_meta", 
"cell_id"])
-
         # Arguments based on the do_execute signature
         do_execute_args = {
             "code": code,
@@ -754,9 +763,9 @@
             "allow_stdin": allow_stdin,
         }
 
-        if accepts_params["cell_meta"]:
+        if self._do_exec_accepted_params["cell_meta"]:
             do_execute_args["cell_meta"] = cell_meta
-        if accepts_params["cell_id"]:
+        if self._do_exec_accepted_params["cell_id"]:
             do_execute_args["cell_id"] = cell_id
 
         # Call do_execute with the appropriate arguments
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/ipykernel/kernelspec.py 
new/ipykernel-6.29.0/ipykernel/kernelspec.py
--- old/ipykernel-6.27.1/ipykernel/kernelspec.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/ipykernel/kernelspec.py        2020-02-02 
01:00:00.000000000 +0100
@@ -66,7 +66,7 @@
         "argv": make_ipkernel_cmd(extra_arguments=extra_arguments),
         "display_name": "Python %i (ipykernel)" % sys.version_info[0],
         "language": "python",
-        "metadata": {"debugger": _is_debugpy_available},
+        "metadata": {"debugger": True},
     }
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/pyproject.toml 
new/ipykernel-6.29.0/pyproject.toml
--- old/ipykernel-6.27.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/ipykernel-6.29.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -17,10 +17,6 @@
     "License :: OSI Approved :: BSD License",
     "Programming Language :: Python",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.8",
-    "Programming Language :: Python :: 3.9",
-    "Programming Language :: Python :: 3.10",
-    "Programming Language :: Python :: 3.11",
 ]
 urls = {Homepage = "https://ipython.org"}
 requires-python = ">=3.8"
@@ -36,7 +32,7 @@
     "tornado>=6.1",
     "matplotlib-inline>=0.1",
     'appnope;platform_system=="Darwin"',
-    "pyzmq>=20",
+    "pyzmq>=24",
     "psutil",
     "packaging",
 ]
@@ -57,7 +53,7 @@
     "flaky",
     "ipyparallel",
     "pre-commit",
-    "pytest-asyncio",
+    "pytest-asyncio==0.23.2",
     "pytest-timeout"
 ]
 cov = [
@@ -175,6 +171,9 @@
   "ignore:unclosed event loop:ResourceWarning",
   "ignore:There is no current event loop:DeprecationWarning",
   "module:Jupyter is migrating its paths to use standard 
platformdirs:DeprecationWarning",
+
+  # Ignore datetime warning.
+  "ignore:datetime.datetime.utc:DeprecationWarning",
 ]
 
 [tool.coverage.report]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/tests/test_debugger.py 
new/ipykernel-6.29.0/tests/test_debugger.py
--- old/ipykernel-6.27.1/tests/test_debugger.py 2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/tests/test_debugger.py 2020-02-02 01:00:00.000000000 
+0100
@@ -6,8 +6,13 @@
 
 seq = 0
 
-# Skip if debugpy is not available
-pytest.importorskip("debugpy")
+# Tests support debugpy not being installed, in which case the tests don't do 
anything useful
+# functionally as the debug message replies are usually empty dictionaries, 
but they confirm that
+# ipykernel doesn't block, or segfault, or raise an exception.
+try:
+    import debugpy
+except ImportError:
+    debugpy = None
 
 
 def wait_for_debug_request(kernel, command, arguments=None, full_reply=False):
@@ -85,15 +90,21 @@
             "locale": "en",
         },
     )
-    assert reply["success"]
+    if debugpy:
+        assert reply["success"]
+    else:
+        assert reply == {}
 
 
 def test_attach_debug(kernel_with_debug):
     reply = wait_for_debug_request(
         kernel_with_debug, "evaluate", {"expression": "'a' + 'b'", "context": 
"repl"}
     )
-    assert reply["success"]
-    assert reply["body"]["result"] == ""
+    if debugpy:
+        assert reply["success"]
+        assert reply["body"]["result"] == ""
+    else:
+        assert reply == {}
 
 
 def test_set_breakpoints(kernel_with_debug):
@@ -104,7 +115,11 @@
 f(2, 3)"""
 
     r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
-    source = r["body"]["sourcePath"]
+    if debugpy:
+        source = r["body"]["sourcePath"]
+    else:
+        assert r == {}
+        source = "non-existent path"
 
     reply = wait_for_debug_request(
         kernel_with_debug,
@@ -115,20 +130,29 @@
             "sourceModified": False,
         },
     )
-    assert reply["success"]
-    assert len(reply["body"]["breakpoints"]) == 1
-    assert reply["body"]["breakpoints"][0]["verified"]
-    assert reply["body"]["breakpoints"][0]["source"]["path"] == source
+    if debugpy:
+        assert reply["success"]
+        assert len(reply["body"]["breakpoints"]) == 1
+        assert reply["body"]["breakpoints"][0]["verified"]
+        assert reply["body"]["breakpoints"][0]["source"]["path"] == source
+    else:
+        assert reply == {}
 
     r = wait_for_debug_request(kernel_with_debug, "debugInfo")
 
     def func(b):
         return b["source"]
 
-    assert source in map(func, r["body"]["breakpoints"])
+    if debugpy:
+        assert source in map(func, r["body"]["breakpoints"])
+    else:
+        assert r == {}
 
     r = wait_for_debug_request(kernel_with_debug, "configurationDone")
-    assert r["success"]
+    if debugpy:
+        assert r["success"]
+    else:
+        assert r == {}
 
 
 def test_stop_on_breakpoint(kernel_with_debug):
@@ -139,7 +163,11 @@
 f(2, 3)"""
 
     r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
-    source = r["body"]["sourcePath"]
+    if debugpy:
+        source = r["body"]["sourcePath"]
+    else:
+        assert r == {}
+        source = "some path"
 
     wait_for_debug_request(kernel_with_debug, "debugInfo")
 
@@ -157,6 +185,10 @@
 
     kernel_with_debug.execute(code)
 
+    if not debugpy:
+        # Cannot stop on breakpoint if debugpy not installed
+        return
+
     # Wait for stop on breakpoint
     msg: dict = {"msg_type": "", "content": {}}
     while msg.get("msg_type") != "debug_event" or msg["content"].get("event") 
!= "stopped":
@@ -175,7 +207,11 @@
 f(2, 3)"""
 
     r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
-    source = r["body"]["sourcePath"]
+    if debugpy:
+        source = r["body"]["sourcePath"]
+    else:
+        assert r == {}
+        source = "some path"
 
     wait_for_debug_request(kernel_with_debug, "debugInfo")
 
@@ -193,6 +229,10 @@
 
     kernel_with_debug.execute(code)
 
+    if not debugpy:
+        # Cannot stop on breakpoint if debugpy not installed
+        return
+
     # Wait for stop on breakpoint
     msg: dict = {"msg_type": "", "content": {}}
     while msg.get("msg_type") != "debug_event" or msg["content"].get("event") 
!= "stopped":
@@ -216,7 +256,10 @@
     def func(v):
         return v["name"]
 
-    assert var_name in list(map(func, r["body"]["variables"]))
+    if debugpy:
+        assert var_name in list(map(func, r["body"]["variables"]))
+    else:
+        assert r == {}
 
     reply = wait_for_debug_request(
         kernel_with_debug,
@@ -224,7 +267,10 @@
         {"variableName": var_name},
     )
 
-    assert reply["body"]["data"] == {"text/plain": f"'{value}'"}
+    if debugpy:
+        assert reply["body"]["data"] == {"text/plain": f"'{value}'"}
+    else:
+        assert reply == {}
 
 
 def test_rich_inspect_at_breakpoint(kernel_with_debug):
@@ -235,7 +281,11 @@
 f(2, 3)"""
 
     r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
-    source = r["body"]["sourcePath"]
+    if debugpy:
+        source = r["body"]["sourcePath"]
+    else:
+        assert r == {}
+        source = "some path"
 
     wait_for_debug_request(
         kernel_with_debug,
@@ -253,6 +303,10 @@
 
     kernel_with_debug.execute(code)
 
+    if not debugpy:
+        # Cannot stop on breakpoint if debugpy not installed
+        return
+
     # Wait for stop on breakpoint
     msg: dict = {"msg_type": "", "content": {}}
     while msg.get("msg_type") != "debug_event" or msg["content"].get("event") 
!= "stopped":
@@ -304,7 +358,11 @@
 
     # Init debugger and set breakpoint
     r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
-    source = r["body"]["sourcePath"]
+    if debugpy:
+        source = r["body"]["sourcePath"]
+    else:
+        assert r == {}
+        source = "some path"
 
     wait_for_debug_request(
         kernel_with_debug,
@@ -323,6 +381,10 @@
     # Execute code
     kernel_with_debug.execute(code)
 
+    if not debugpy:
+        # Cannot stop on breakpoint if debugpy not installed
+        return
+
     # Wait for stop on breakpoint
     msg: dict = {"msg_type": "", "content": {}}
     while msg.get("msg_type") != "debug_event" or msg["content"].get("event") 
!= "stopped":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ipykernel-6.27.1/tests/test_kernel.py 
new/ipykernel-6.29.0/tests/test_kernel.py
--- old/ipykernel-6.27.1/tests/test_kernel.py   2020-02-02 01:00:00.000000000 
+0100
+++ new/ipykernel-6.29.0/tests/test_kernel.py   2020-02-02 01:00:00.000000000 
+0100
@@ -58,6 +58,106 @@
         _check_master(kc, expected=True)
 
 
+def test_print_to_correct_cell_from_thread():
+    """should print to the cell that spawned the thread, not a subsequently 
run cell"""
+    iterations = 5
+    interval = 0.25
+    code = f"""\
+    from threading import Thread
+    from time import sleep
+
+    def thread_target():
+        for i in range({iterations}):
+            print(i, end='', flush=True)
+            sleep({interval})
+
+    Thread(target=thread_target).start()
+    """
+    with kernel() as kc:
+        thread_msg_id = kc.execute(code)
+        _ = kc.execute("pass")
+
+        received = 0
+        while received < iterations:
+            msg = kc.get_iopub_msg(timeout=interval * 2)
+            if msg["msg_type"] != "stream":
+                continue
+            content = msg["content"]
+            assert content["name"] == "stdout"
+            assert content["text"] == str(received)
+            # this is crucial as the parent header decides to which cell the 
output goes
+            assert msg["parent_header"]["msg_id"] == thread_msg_id
+            received += 1
+
+
+def test_print_to_correct_cell_from_child_thread():
+    """should print to the cell that spawned the thread, not a subsequently 
run cell"""
+    iterations = 5
+    interval = 0.25
+    code = f"""\
+    from threading import Thread
+    from time import sleep
+
+    def child_target():
+        for i in range({iterations}):
+            print(i, end='', flush=True)
+            sleep({interval})
+
+    def parent_target():
+        sleep({interval})
+        Thread(target=child_target).start()
+
+    Thread(target=parent_target).start()
+    """
+    with kernel() as kc:
+        thread_msg_id = kc.execute(code)
+        _ = kc.execute("pass")
+
+        received = 0
+        while received < iterations:
+            msg = kc.get_iopub_msg(timeout=interval * 2)
+            if msg["msg_type"] != "stream":
+                continue
+            content = msg["content"]
+            assert content["name"] == "stdout"
+            assert content["text"] == str(received)
+            # this is crucial as the parent header decides to which cell the 
output goes
+            assert msg["parent_header"]["msg_id"] == thread_msg_id
+            received += 1
+
+
+def test_print_to_correct_cell_from_asyncio():
+    """should print to the cell that scheduled the task, not a subsequently 
run cell"""
+    iterations = 5
+    interval = 0.25
+    code = f"""\
+    import asyncio
+
+    async def async_task():
+        for i in range({iterations}):
+            print(i, end='', flush=True)
+            await asyncio.sleep({interval})
+
+    loop = asyncio.get_event_loop()
+    loop.create_task(async_task());
+    """
+    with kernel() as kc:
+        thread_msg_id = kc.execute(code)
+        _ = kc.execute("pass")
+
+        received = 0
+        while received < iterations:
+            msg = kc.get_iopub_msg(timeout=interval * 2)
+            if msg["msg_type"] != "stream":
+                continue
+            content = msg["content"]
+            assert content["name"] == "stdout"
+            assert content["text"] == str(received)
+            # this is crucial as the parent header decides to which cell the 
output goes
+            assert msg["parent_header"]["msg_id"] == thread_msg_id
+            received += 1
+
+
 @pytest.mark.skip(reason="Currently don't capture during test as pytest does 
its own capturing")
 def test_capture_fd():
     """simple print statement in kernel"""

Reply via email to