From 4e955b98e072f061e172ae88d868b54c1a5e7172 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 1 Sep 2025 15:17:11 -0400 Subject: [PATCH 1/7] Ensure `isinstance` is unaffected by `sys.setprofile` on PyPy --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 38592935..434065c9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1986,7 +1986,7 @@ class _ConcatenateGenericAlias(list): __class__ = typing._GenericAlias def __init__(self, origin, args): - super().__init__(args) + list.__init__(self, args) self.__origin__ = origin self.__args__ = args @@ -2545,7 +2545,7 @@ def __typing_is_unpacked_typevartuple__(self): def __getitem__(self, args): if self.__typing_is_unpacked_typevartuple__: return args - return super().__getitem__(args) + return typing._GenericAlias.__getitem__(self, args) @_UnpackSpecialForm def Unpack(self, parameters): From 71b62ca08e53413b7f38bfad137898b26d4d5604 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 1 Sep 2025 15:17:28 -0400 Subject: [PATCH 2/7] Run PyPy tests under coverage --- .github/workflows/ci.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1059f458..4120c3e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,13 +67,11 @@ jobs: allow-prereleases: true - name: Install coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} run: | # Be wary that this does not install typing_extensions in the future pip install coverage - name: Test typing_extensions with coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} run: | # Be wary of running `pip install` here, since it becomes easy for us to # accidentally pick up typing_extensions as installed by a dependency @@ -82,18 +80,9 @@ jobs: # Run tests under coverage export COVERAGE_FILE=.coverage_${{ matrix.python-version }} python -m coverage run -m unittest test_typing_extensions.py - - name: Test typing_extensions no coverage on pypy - if: ${{ startsWith(matrix.python-version, 'pypy') }} - run: | - # Be wary of running `pip install` here, since it becomes easy for us to - # accidentally pick up typing_extensions as installed by a dependency - cd src - python --version # just to make sure we're running the right one - python -m unittest test_typing_extensions.py - name: Archive code coverage results id: archive-coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} uses: actions/upload-artifact@v4 with: name: .coverage_${{ matrix.python-version }} From 7b5c54da7cc1a7d0531bc71a2b97e68fae89f68b Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 1 Sep 2025 15:32:27 -0400 Subject: [PATCH 3/7] Add regression tests --- src/test_typing_extensions.py | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 551579dc..f7882a55 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6211,6 +6211,47 @@ def test_is_param_expr(self): self.assertTrue(typing._is_param_expr(concat)) self.assertTrue(typing._is_param_expr(typing_concat)) + def test_isinstance_results_unaffected_by_presence_of_tracing_function(self): + # See https://github.com/python/typing_extensions/issues/661 + + code = textwrap.dedent( + """\ + import sys, typing + + def trace_call(*args): + return trace_call + + def run(): + sys.modules.pop("typing_extensions", None) + from typing_extensions import Concatenate + return isinstance(Concatenate[...], typing._GenericAlias) + isinstance_result_1 = run() + sys.setprofile(trace_call) + isinstance_result_2 = run() + sys.stdout.write(f"{isinstance_result_1} {isinstance_result_2}") + """ + ) + + # Run this in an isolated process or it pollutes the environment + # and makes other tests fail: + try: + proc = subprocess.run( + [sys.executable, "-c", code], check=True, capture_output=True, text=True, + ) + except subprocess.CalledProcessError as exc: + print("stdout", exc.stdout, sep="\n") + print("stderr", exc.stderr, sep="\n") + raise + + # Sanity checks that assert the test is working as expected + self.assertIsInstance(proc.stdout, str) + result1, result2 = proc.stdout.split(" ") + self.assertIn(result1, {"True", "False"}) + self.assertIn(result2, {"True", "False"}) + + # The actual test: + self.assertEqual(result1, result2) + class TypeGuardTests(BaseTestCase): def test_basics(self): TypeGuard[int] # OK @@ -6633,6 +6674,46 @@ def test_type_var_inheritance(self): self.assertFalse(isinstance(Unpack[Ts], TypeVar)) self.assertFalse(isinstance(Unpack[Ts], typing.TypeVar)) + def test_isinstance_results_unaffected_by_presence_of_tracing_function(self): + # See https://github.com/python/typing_extensions/issues/661 + + code = textwrap.dedent( + """\ + import sys, typing + + def trace_call(*args): + return trace_call + + def run(): + sys.modules.pop("typing_extensions", None) + from typing_extensions import TypeVarTuple, Unpack + return isinstance(Unpack[TypeVarTuple("Ts")], typing.TypeVar) + isinstance_result_1 = run() + sys.setprofile(trace_call) + isinstance_result_2 = run() + sys.stdout.write(f"{isinstance_result_1} {isinstance_result_2}") + """ + ) + + # Run this in an isolated process or it pollutes the environment + # and makes other tests fail: + try: + proc = subprocess.run( + [sys.executable, "-c", code], check=True, capture_output=True, text=True, + ) + except subprocess.CalledProcessError as exc: + print("stdout", exc.stdout, sep="\n") + print("stderr", exc.stderr, sep="\n") + raise + + # Sanity checks that assert the test is working as expected + self.assertIsInstance(proc.stdout, str) + result1, result2 = proc.stdout.split(" ") + self.assertIn(result1, {"True", "False"}) + self.assertIn(result2, {"True", "False"}) + + # The actual test: + self.assertEqual(result1, result2) class TypeVarTupleTests(BaseTestCase): From 6f51d6365f71faa75b83b4d4fe9d85d5ebc19eea Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 1 Sep 2025 17:17:13 -0400 Subject: [PATCH 4/7] Add changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3356adbe..f9dd9399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Unreleased +- Fix incorrect behaviour on PyPy 3.9 and PyPy 3.10 that meant that the result of + calling `isinstance` with `typing_extensions.Concatenate[...]` or + `typing_extensions.Unpack[...]` as the first argument could have a different + result in some situations depending on whether or not a profiling function had been + set using `sys.setprofile`. Patch by Brian Schubert. - Raise `TypeError` when attempting to subclass `typing_extensions.ParamSpec` on Python 3.9. The `typing` implementation has always raised an error, and the `typing_extensions` implementation has raised an error on Python 3.10+ since From af837be5c4e0350c6e0c5ad388eb11691bb7da77 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Mon, 1 Sep 2025 17:20:14 -0400 Subject: [PATCH 5/7] phrasing --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9dd9399..de306190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Fix incorrect behaviour on PyPy 3.9 and PyPy 3.10 that meant that the result of +- Fix incorrect behaviour on PyPy 3.9 and PyPy 3.10 that meant that calling `isinstance` with `typing_extensions.Concatenate[...]` or `typing_extensions.Unpack[...]` as the first argument could have a different result in some situations depending on whether or not a profiling function had been From 04d7e202d1afe4dd158ba916d2dcc19562dbf7bb Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 9 Sep 2025 10:32:25 -0400 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Alex Waygood --- src/typing_extensions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 31b7f439..bd67a80a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1986,6 +1986,8 @@ class _ConcatenateGenericAlias(list): __class__ = typing._GenericAlias def __init__(self, origin, args): + # Cannot use `super().__init__` here because of the `__class__` assignment + # in the class body (https://github.com/python/typing_extensions/issues/661) list.__init__(self, args) self.__origin__ = origin self.__args__ = args @@ -2545,6 +2547,9 @@ def __typing_is_unpacked_typevartuple__(self): def __getitem__(self, args): if self.__typing_is_unpacked_typevartuple__: return args + # Cannot use `super().__getitem__` here because of the `__class__` assignment + # in the class body on Python <=3.11 + # (https://github.com/python/typing_extensions/issues/661) return typing._GenericAlias.__getitem__(self, args) @_UnpackSpecialForm From bb8a5bcbebf0c5252d8d1e4e0dfddda4d9987470 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 9 Sep 2025 11:09:39 -0400 Subject: [PATCH 7/7] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1d33119..733505a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,11 @@ # Unreleased -- Fix incorrect behaviour on PyPy 3.9 and PyPy 3.10 that meant that +- Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that calling `isinstance` with `typing_extensions.Concatenate[...]` or `typing_extensions.Unpack[...]` as the first argument could have a different result in some situations depending on whether or not a profiling function had been - set using `sys.setprofile`. Patch by Brian Schubert. + set using `sys.setprofile`. This affected both CPython and PyPy implementations. + Patch by Brian Schubert. - Fix `__init_subclass__()` behavior in the presence of multiple inheritance involving an `@deprecated`-decorated base class. Backport of CPython PR [#138210](https://github.com/python/cpython/pull/138210) by Brian Schubert.