From 5d20e9eed31de88667542ba5a6f66e6dc439b681 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 28 Sep 2023 14:35:35 +0100 Subject: [PATCH 01/75] Make `NewType.__call__` params positional-only (#288) This is really minor, but it means that the signature of `typing_extensions.NewType.__call__` exactly matches that of `typing.NewType.__call__` on all Python versions we support: ```pycon Python 3.8.16 (default, Mar 2 2023, 03:18:16) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 Type "help", "copyright", "credits" or "license" for more information. >>> from typing import NewType >>> x = NewType("x", int) >>> x(obj=42) Traceback (most recent call last): File "", line 1, in TypeError: new_type() got an unexpected keyword argument 'obj' ``` ```pycon Python 3.10.8 | packaged by conda-forge | (main, Nov 24 2022, 14:07:00) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> x = NewType("x", int) Traceback (most recent call last): File "", line 1, in NameError: name 'NewType' is not defined >>> from typing import NewType >>> x = NewType("x", int) >>> x(obj=42) Traceback (most recent call last): File "", line 1, in TypeError: NewType.__call__() got an unexpected keyword argument 'obj' ``` --- CHANGELOG.md | 6 ++++++ src/typing_extensions.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58f0487d..3ecde9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Release 4.9.0 (???) + +- All parameters on `NewType.__call__` are now positional-only. This means that + the signature of `typing_extensions.NewType.__call__` now exactly matches the + signature of `typing.NewType.__call__`. Patch by Alex Waygood. + # Release 4.8.0 (September 17, 2023) No changes since 4.8.0rc1. diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c96bf90f..58706dc9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2600,7 +2600,7 @@ def name_by_id(user_id: UserId) -> str: num = UserId(5) + 1 # type: int """ - def __call__(self, obj): + def __call__(self, obj, /): return obj def __init__(self, name, tp): From b6318ab4b4777a38b468eef7aae97061c283eb8d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 3 Oct 2023 19:35:05 -0700 Subject: [PATCH 02/75] Add .readthedocs.yaml (#289) --- .readthedocs.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..83cd53cb --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,13 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + From 04f98954ba63a5e8a09c12171be24785298276b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 18 Oct 2023 20:26:19 +0200 Subject: [PATCH 03/75] Fix tests on Python 3.13 (#291) This is a followup for d95cc228ea96feec105592a9902e5b2d6cc048a9 The removed block raises TypeError on Python 3.13+. The TypeError is already asserted in test_keywords_syntax_raises_on_3_13. For older Pythons, the DeprecationWarning and __name__ + __annotations__ value are already asserted in test_typeddict_special_keyword_names. --- src/test_typing_extensions.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 97717bce..7d8e2553 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3383,11 +3383,6 @@ def test_typeddict_create_errors(self): with self.assertRaises(TypeError): TypedDict('Emp', [('name', str)], None) - with self.assertWarns(DeprecationWarning): - Emp = TypedDict('Emp', name=str, id=int) - self.assertEqual(Emp.__name__, 'Emp') - self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) - def test_typeddict_errors(self): Emp = TypedDict('Emp', {'name': str, 'id': int}) if sys.version_info >= (3, 13): From fda0c15e3b5ed05703420cfb4d0974edb5e39c46 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Oct 2023 21:39:22 +0300 Subject: [PATCH 04/75] Run tests on Python 3.13 (#292) * Run pydantic's tests on py312; bump PyPy to PyPy3.10 for typeguard's tests Pydantic started testing with py312 in https://github.com/pydantic/pydantic/commit/ea7bf54c2e46d03aedf76b1f661d88971688c7cf Typeguard bumped the version of PyPy they test with in CI in https://github.com/agronholm/typeguard/commit/9aa873a95d703130894fa28cb511ce04f1b6fd9f Co-authored-by: Alex Waygood --- .github/workflows/ci.yml | 5 +++-- .github/workflows/package.yml | 8 ++++---- .github/workflows/third_party.yml | 33 ++++++++++++++++--------------- tox.ini | 2 +- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e221022c..1c3d990e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,7 @@ jobs: - "3.11" - "3.11.0" - "3.12" + - "3.13" - "pypy3.8" - "pypy3.9" - "pypy3.10" @@ -58,7 +59,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 @@ -82,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index ad2deee1..622861bc 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -20,10 +20,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 @@ -50,10 +50,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3 diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index b318e333..fcde71ca 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -41,12 +41,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Checkout pydantic - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: pydantic/pydantic - name: Edit pydantic pyproject.toml @@ -54,13 +54,14 @@ jobs: # as a requirement unless we do this run: sed -i 's/^requires-python = .*/requires-python = ">=3.8"/' pyproject.toml - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup pdm for pydantic tests uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} + allow-python-prereleases: true - name: Add local version of typing_extensions as a dependency run: pdm add ./typing-extensions-latest - name: Install pydantic test dependencies @@ -90,12 +91,12 @@ jobs: timeout-minutes: 60 steps: - name: Checkout typing_inspect - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ilevkivskyi/typing_inspect path: typing_inspect - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python @@ -133,12 +134,12 @@ jobs: timeout-minutes: 60 steps: - name: Check out pyanalyze - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: quora/pyanalyze path: pyanalyze - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python @@ -172,17 +173,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Check out typeguard - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: agronholm/typeguard path: typeguard - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python @@ -221,12 +222,12 @@ jobs: timeout-minutes: 60 steps: - name: Check out typed-argument-parser - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: swansonk14/typed-argument-parser path: typed-argument-parser - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python @@ -272,12 +273,12 @@ jobs: timeout-minutes: 60 steps: - name: Checkout mypy for stubtest and mypyc tests - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: python/mypy path: mypy - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python @@ -319,11 +320,11 @@ jobs: timeout-minutes: 60 steps: - name: Checkout cattrs - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: python-attrs/cattrs - name: Checkout typing_extensions - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: typing-extensions-latest - name: Setup Python diff --git a/tox.ini b/tox.ini index 5bed0225..5be7adb8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = True -envlist = py38, py39, py310, py311, py312 +envlist = py38, py39, py310, py311, py312, py313 [testenv] changedir = src From 9de9fd613913faee5db317f827f5eec3755d8a92 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 29 Oct 2023 14:40:41 +0000 Subject: [PATCH 05/75] Raise if a non-`str` is passed as the first parameter to `@deprecated` (#296) --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 15 +++++++++++++++ src/typing_extensions.py | 5 +++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ecde9cf..43838db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - All parameters on `NewType.__call__` are now positional-only. This means that the signature of `typing_extensions.NewType.__call__` now exactly matches the signature of `typing.NewType.__call__`. Patch by Alex Waygood. +- `typing.deprecated` now gives a better error message if you pass a non-`str` + argument to the `msg` parameter. Patch by Alex Waygood. # Release 4.8.0 (September 17, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7d8e2553..151d44c6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -480,6 +480,21 @@ def d(): warnings.simplefilter("error") d() + def test_only_strings_allowed(self): + with self.assertRaisesRegex( + TypeError, + "Expected an object of type str for 'msg', not 'type'" + ): + @deprecated + class Foo: ... + + with self.assertRaisesRegex( + TypeError, + "Expected an object of type str for 'msg', not 'function'" + ): + @deprecated + def foo(): ... + class AnyTests(BaseTestCase): def test_can_subclass(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 58706dc9..34affa97 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2331,6 +2331,11 @@ def g(x: str) -> int: ... See PEP 702 for details. """ + if not isinstance(msg, str): + raise TypeError( + f"Expected an object of type str for 'msg', not {type(msg).__name__!r}" + ) + def decorator(arg: _T, /) -> _T: if category is None: arg.__deprecated__ = msg From fc9acbdbfaf039fa98e59a6c5c1caa59efc2bb31 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Tue, 31 Oct 2023 18:05:50 +0300 Subject: [PATCH 06/75] gh-110686: Fix pattern matching with `runtime_checkable` protocols (#290) --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 37 +++++++++++++++++++++++++++++++++-- src/typing_extensions.py | 7 ++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43838db0..16775856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ signature of `typing.NewType.__call__`. Patch by Alex Waygood. - `typing.deprecated` now gives a better error message if you pass a non-`str` argument to the `msg` parameter. Patch by Alex Waygood. +- Exclude `__match_args__` from `Protocol` members, + this is a backport of https://github.com/python/cpython/pull/110683 # Release 4.8.0 (September 17, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 151d44c6..ffc84266 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2521,6 +2521,39 @@ class Bad: pass self.assertNotIsInstance(Other(), Concrete) self.assertIsInstance(NT(1, 2), Position) + def test_runtime_checkable_with_match_args(self): + @runtime_checkable + class P_regular(Protocol): + x: int + y: int + + @runtime_checkable + class P_match(Protocol): + __match_args__ = ("x", "y") + x: int + y: int + + class Regular: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + class WithMatch: + __match_args__ = ("x", "y", "z") + def __init__(self, x: int, y: int, z: int): + self.x = x + self.y = y + self.z = z + + class Nope: ... + + self.assertIsInstance(Regular(1, 2), P_regular) + self.assertIsInstance(Regular(1, 2), P_match) + self.assertIsInstance(WithMatch(1, 2, 3), P_regular) + self.assertIsInstance(WithMatch(1, 2, 3), P_match) + self.assertNotIsInstance(Nope(), P_regular) + self.assertNotIsInstance(Nope(), P_match) + def test_protocols_isinstance_init(self): T = TypeVar('T') @runtime_checkable @@ -5062,12 +5095,12 @@ def test_typing_extensions_defers_when_possible(self): exclude |= {'final', 'Any', 'NewType'} if sys.version_info < (3, 12): exclude |= { - 'Protocol', 'SupportsAbs', 'SupportsBytes', + 'SupportsAbs', 'SupportsBytes', 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', 'SupportsRound', 'Unpack', } if sys.version_info < (3, 13): - exclude |= {'NamedTuple', 'TypedDict', 'is_typeddict'} + exclude |= {'NamedTuple', 'Protocol', 'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 34affa97..78ae1635 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -473,6 +473,7 @@ def clear_overloads(): "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", "__subclasshook__", "__orig_class__", "__init__", "__new__", "__protocol_attrs__", "__callable_proto_members_only__", + "__match_args__", } if sys.version_info >= (3, 9): @@ -503,9 +504,9 @@ def _caller(depth=2): return None -# The performance of runtime-checkable protocols is significantly improved on Python 3.12, -# so we backport the 3.12 version of Protocol to Python <=3.11 -if sys.version_info >= (3, 12): +# `__match_args__` attribute was removed from protocol members in 3.13, +# we want to backport this change to older Python versions. +if sys.version_info >= (3, 13): Protocol = typing.Protocol else: def _allow_reckless_class_checks(depth=3): From f9f257c6baa557658004bbe019bd38fcff3fb2b5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 4 Nov 2023 14:04:47 -0700 Subject: [PATCH 07/75] Fix deprecating a mixin; warn when inheriting from a deprecated class (#294) Co-authored-by: Alex Waygood --- CHANGELOG.md | 4 +- doc/index.rst | 5 ++ src/test_typing_extensions.py | 87 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 30 ++++++++++-- 4 files changed, 122 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16775856..2893d27a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ - All parameters on `NewType.__call__` are now positional-only. This means that the signature of `typing_extensions.NewType.__call__` now exactly matches the signature of `typing.NewType.__call__`. Patch by Alex Waygood. -- `typing.deprecated` now gives a better error message if you pass a non-`str` +- Fix bug with using `@deprecated` on a mixin class. Inheriting from a + deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra. +- `@deprecated` now gives a better error message if you pass a non-`str` argument to the `msg` parameter. Patch by Alex Waygood. - Exclude `__match_args__` from `Protocol` members, this is a backport of https://github.com/python/cpython/pull/110683 diff --git a/doc/index.rst b/doc/index.rst index 28b795a3..39885861 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -553,6 +553,11 @@ Decorators .. versionadded:: 4.5.0 + .. versionchanged:: 4.9.0 + + Inheriting from a deprecated class now also raises a runtime + :py:exc:`DeprecationWarning`. + .. decorator:: final See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ffc84266..702ba541 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -418,6 +418,93 @@ def __new__(cls, x): self.assertEqual(instance.x, 42) self.assertTrue(new_called) + def test_mixin_class(self): + @deprecated("Mixin will go away soon") + class Mixin: + pass + + class Base: + def __init__(self, a) -> None: + self.a = a + + with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"): + class Child(Base, Mixin): + pass + + instance = Child(42) + self.assertEqual(instance.a, 42) + + def test_existing_init_subclass(self): + @deprecated("C will go away soon") + class C: + def __init_subclass__(cls) -> None: + cls.inited = True + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + C() + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + class D(C): + pass + + self.assertTrue(D.inited) + self.assertIsInstance(D(), D) # no deprecation + + def test_existing_init_subclass_in_base(self): + class Base: + def __init_subclass__(cls, x) -> None: + cls.inited = x + + @deprecated("C will go away soon") + class C(Base, x=42): + pass + + self.assertEqual(C.inited, 42) + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + C() + + with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"): + class D(C, x=3): + pass + + self.assertEqual(D.inited, 3) + + def test_init_subclass_has_correct_cls(self): + init_subclass_saw = None + + @deprecated("Base will go away soon") + class Base: + def __init_subclass__(cls) -> None: + nonlocal init_subclass_saw + init_subclass_saw = cls + + self.assertIsNone(init_subclass_saw) + + with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): + class C(Base): + pass + + self.assertIs(init_subclass_saw, C) + + def test_init_subclass_with_explicit_classmethod(self): + init_subclass_saw = None + + @deprecated("Base will go away soon") + class Base: + @classmethod + def __init_subclass__(cls) -> None: + nonlocal init_subclass_saw + init_subclass_saw = cls + + self.assertIsNone(init_subclass_saw) + + with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): + class C(Base): + pass + + self.assertIs(init_subclass_saw, C) + def test_function(self): @deprecated("b will go away soon") def b(): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 78ae1635..c8c6853b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2343,21 +2343,45 @@ def decorator(arg: _T, /) -> _T: return arg elif isinstance(arg, type): original_new = arg.__new__ - has_init = arg.__init__ is not object.__init__ @functools.wraps(original_new) def __new__(cls, *args, **kwargs): - warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + if cls is arg: + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) if original_new is not object.__new__: return original_new(cls, *args, **kwargs) # Mirrors a similar check in object.__new__. - elif not has_init and (args or kwargs): + elif cls.__init__ is object.__init__ and (args or kwargs): raise TypeError(f"{cls.__name__}() takes no arguments") else: return original_new(cls) arg.__new__ = staticmethod(__new__) + + original_init_subclass = arg.__init_subclass__ + # We need slightly different behavior if __init_subclass__ + # is a bound method (likely if it was implemented in Python) + if isinstance(original_init_subclass, _types.MethodType): + original_init_subclass = original_init_subclass.__func__ + + @functools.wraps(original_init_subclass) + def __init_subclass__(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return original_init_subclass(*args, **kwargs) + + arg.__init_subclass__ = classmethod(__init_subclass__) + # Or otherwise, which likely means it's a builtin such as + # object's implementation of __init_subclass__. + else: + @functools.wraps(original_init_subclass) + def __init_subclass__(*args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + return original_init_subclass(*args, **kwargs) + + arg.__init_subclass__ = __init_subclass__ + arg.__deprecated__ = __new__.__deprecated__ = msg + __init_subclass__.__deprecated__ = msg return arg elif callable(arg): @functools.wraps(arg) From 7af82f97686df8da7fbf0d9871f3f942d9254449 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 23:19:02 -0800 Subject: [PATCH 08/75] @deprecated: will be in warnings, not typing (#298) --- doc/index.rst | 2 +- src/typing_extensions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 39885861..3bbe2fc8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -549,7 +549,7 @@ Decorators .. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1) - See :pep:`702`. Experimental; not yet part of the standard library. + See :pep:`702`. In the :mod:`warnings` module since Python 3.13. .. versionadded:: 4.5.0 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c8c6853b..fc656de8 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2282,8 +2282,8 @@ def method(self) -> None: return arg -if hasattr(typing, "deprecated"): - deprecated = typing.deprecated +if hasattr(warnings, "deprecated"): + deprecated = warnings.deprecated else: _T = typing.TypeVar("_T") From 4f91502281d748671c7c1dfa26726111853f1342 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Nov 2023 14:31:20 +0000 Subject: [PATCH 09/75] Backport recent change to `NamedTuple` classes regarding `__set_name__` (#303) --- CHANGELOG.md | 4 + src/test_typing_extensions.py | 135 ++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 30 +++++++- 3 files changed, 166 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2893d27a..1ff6a1ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ argument to the `msg` parameter. Patch by Alex Waygood. - Exclude `__match_args__` from `Protocol` members, this is a backport of https://github.com/python/cpython/pull/110683 +- When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__` + is called on all objects that define `__set_name__` and exist in the values + of the `NamedTuple` class's class dictionary. Patch by Alex Waygood, + backporting https://github.com/python/cpython/pull/111876. # Release 4.8.0 (September 17, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 702ba541..b54c5926 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -56,6 +56,11 @@ # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters +skip_if_early_py313_alpha = skipIf( + sys.version_info[:4] == (3, 13, 0, 'alpha') and sys.version_info.serial < 3, + "Bugfixes will be released in 3.13.0a3" +) + ANN_MODULE_SOURCE = '''\ from typing import Optional from functools import wraps @@ -5548,6 +5553,136 @@ class GenericNamedTuple(NamedTuple, Generic[T]): self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) + @skip_if_early_py313_alpha + def test_setname_called_on_values_in_class_dictionary(self): + class Vanilla: + def __set_name__(self, owner, name): + self.name = name + + class Foo(NamedTuple): + attr = Vanilla() + + foo = Foo() + self.assertEqual(len(foo), 0) + self.assertNotIn('attr', Foo._fields) + self.assertIsInstance(foo.attr, Vanilla) + self.assertEqual(foo.attr.name, "attr") + + class Bar(NamedTuple): + attr: Vanilla = Vanilla() + + bar = Bar() + self.assertEqual(len(bar), 1) + self.assertIn('attr', Bar._fields) + self.assertIsInstance(bar.attr, Vanilla) + self.assertEqual(bar.attr.name, "attr") + + @skipIf( + TYPING_3_12_0, + "__set_name__ behaviour changed on py312+ to use BaseException.add_note()" + ) + def test_setname_raises_the_same_as_on_other_classes_py311_minus(self): + class CustomException(BaseException): pass + + class Annoying: + def __set_name__(self, owner, name): + raise CustomException + + annoying = Annoying() + + with self.assertRaises(RuntimeError) as cm: + class NormalClass: + attr = annoying + normal_exception = cm.exception + + with self.assertRaises(RuntimeError) as cm: + class NamedTupleClass(NamedTuple): + attr = annoying + namedtuple_exception = cm.exception + + expected_note = ( + "Error calling __set_name__ on 'Annoying' instance " + "'attr' in 'NamedTupleClass'" + ) + + self.assertIs(type(namedtuple_exception), RuntimeError) + self.assertIs(type(namedtuple_exception), type(normal_exception)) + self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args)) + self.assertEqual( + namedtuple_exception.args[0], + normal_exception.args[0].replace("NormalClass", "NamedTupleClass") + ) + + self.assertIs(type(namedtuple_exception.__cause__), CustomException) + self.assertIs( + type(namedtuple_exception.__cause__), type(normal_exception.__cause__) + ) + self.assertEqual( + namedtuple_exception.__cause__.args, normal_exception.__cause__.args + ) + + @skipUnless( + TYPING_3_12_0, + "__set_name__ behaviour changed on py312+ to use BaseException.add_note()" + ) + @skip_if_early_py313_alpha + def test_setname_raises_the_same_as_on_other_classes_py312_plus(self): + class CustomException(BaseException): pass + + class Annoying: + def __set_name__(self, owner, name): + raise CustomException + + annoying = Annoying() + + with self.assertRaises(CustomException) as cm: + class NormalClass: + attr = annoying + normal_exception = cm.exception + + with self.assertRaises(CustomException) as cm: + class NamedTupleClass(NamedTuple): + attr = annoying + namedtuple_exception = cm.exception + + expected_note = ( + "Error calling __set_name__ on 'Annoying' instance " + "'attr' in 'NamedTupleClass'" + ) + + self.assertIs(type(namedtuple_exception), CustomException) + self.assertIs(type(namedtuple_exception), type(normal_exception)) + self.assertEqual(namedtuple_exception.args, normal_exception.args) + + self.assertEqual(len(namedtuple_exception.__notes__), 1) + self.assertEqual( + len(namedtuple_exception.__notes__), len(normal_exception.__notes__) + ) + + self.assertEqual(namedtuple_exception.__notes__[0], expected_note) + self.assertEqual( + namedtuple_exception.__notes__[0], + normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass") + ) + + @skip_if_early_py313_alpha + def test_strange_errors_when_accessing_set_name_itself(self): + class CustomException(Exception): pass + + class Meta(type): + def __getattribute__(self, attr): + if attr == "__set_name__": + raise CustomException + return object.__getattribute__(self, attr) + + class VeryAnnoying(metaclass=Meta): pass + + very_annoying = VeryAnnoying() + + with self.assertRaises(CustomException): + class Foo(NamedTuple): + attr = very_annoying + class TypeVarTests(BaseTestCase): def test_basic_plain(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index fc656de8..8730173b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2467,11 +2467,35 @@ def __new__(cls, typename, bases, ns): class_getitem = typing.Generic.__class_getitem__.__func__ nm_tpl.__class_getitem__ = classmethod(class_getitem) # update from user namespace without overriding special namedtuple attributes - for key in ns: + for key, val in ns.items(): if key in _prohibited_namedtuple_fields: raise AttributeError("Cannot overwrite NamedTuple attribute " + key) - elif key not in _special_namedtuple_fields and key not in nm_tpl._fields: - setattr(nm_tpl, key, ns[key]) + elif key not in _special_namedtuple_fields: + if key not in nm_tpl._fields: + setattr(nm_tpl, key, ns[key]) + try: + set_name = type(val).__set_name__ + except AttributeError: + pass + else: + try: + set_name(val, nm_tpl, key) + except BaseException as e: + msg = ( + f"Error calling __set_name__ on {type(val).__name__!r} " + f"instance {key!r} in {typename!r}" + ) + # BaseException.add_note() existed on py311, + # but the __set_name__ machinery didn't start + # using add_note() until py312. + # Making sure exceptions are raised in the same way + # as in "normal" classes seems most important here. + if sys.version_info >= (3, 12): + e.add_note(msg) + raise + else: + raise RuntimeError(msg) from e + if typing.Generic in bases: nm_tpl.__init_subclass__() return nm_tpl From e4d9d8bcb674a78ce3beb288c465d8e3648b534f Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Nov 2023 17:28:51 +0000 Subject: [PATCH 10/75] fix typo in `override()` docstring (#305) backport of https://github.com/python/cpython/commit/12c7e9d573de57343cf018fb4e67521aba46c90f --- src/typing_extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 8730173b..f85b0aa1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2252,7 +2252,7 @@ def override(arg: _F, /) -> _F: Usage: class Base: - def method(self) -> None: ... + def method(self) -> None: pass class Child(Base): From 18ae2b323d5199071cc51eef342bc0ac98e1edf1 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Nov 2023 17:39:02 +0000 Subject: [PATCH 11/75] Backport recent improvements to the error message when trying to call `issubclass()` against a protocol with non-method members (#304) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 17 +++++++++++++++++ src/typing_extensions.py | 7 ++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff6a1ef..43569fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ is called on all objects that define `__set_name__` and exist in the values of the `NamedTuple` class's class dictionary. Patch by Alex Waygood, backporting https://github.com/python/cpython/pull/111876. +- Improve the error message when trying to call `issubclass()` against a + `Protocol` that has non-method members. Patch by Alex Waygood (backporting + https://github.com/python/cpython/pull/112344, by Randolph Scholz). # Release 4.8.0 (September 17, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b54c5926..f4c211fc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3446,6 +3446,23 @@ def method(self) -> None: ... self.assertIsInstance(Foo(), ProtocolWithMixedMembers) self.assertNotIsInstance(42, ProtocolWithMixedMembers) + @skip_if_early_py313_alpha + def test_protocol_issubclass_error_message(self): + class Vec2D(Protocol): + x: float + y: float + + def square_norm(self) -> float: + return self.x ** 2 + self.y ** 2 + + self.assertEqual(Vec2D.__protocol_attrs__, {'x', 'y', 'square_norm'}) + expected_error_message = ( + "Protocols with non-method members don't support issubclass()." + " Non-method members: 'x', 'y'." + ) + with self.assertRaisesRegex(TypeError, re.escape(expected_error_message)): + issubclass(int, Vec2D) + class Point2DGeneric(Generic[T], TypedDict): a: T diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f85b0aa1..5e4932bc 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -570,8 +570,13 @@ def __subclasscheck__(cls, other): not cls.__callable_proto_members_only__ and cls.__dict__.get("__subclasshook__") is _proto_hook ): + non_method_attrs = sorted( + attr for attr in cls.__protocol_attrs__ + if not callable(getattr(cls, attr, None)) + ) raise TypeError( - "Protocols with non-method members don't support issubclass()" + "Protocols with non-method members don't support issubclass()." + f" Non-method members: {str(non_method_attrs)[1:-1]}." ) if not getattr(cls, '_is_runtime_protocol', False): raise TypeError( From db6f9b4a0e1c18c6269691691e72e6b80a247ebd Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Nov 2023 09:39:28 -0800 Subject: [PATCH 12/75] Update @deprecated implementation (#302) Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 12 +++++- src/typing_extensions.py | 69 ++++++++++++++++++++++------------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43569fae..d85aae3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra. - `@deprecated` now gives a better error message if you pass a non-`str` argument to the `msg` parameter. Patch by Alex Waygood. +- `@deprecated` is now implemented as a class for better introspectability. + Patch by Jelle Zijlstra. - Exclude `__match_args__` from `Protocol` members, this is a backport of https://github.com/python/cpython/pull/110683 - When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f4c211fc..562704e9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -575,18 +575,26 @@ def d(): def test_only_strings_allowed(self): with self.assertRaisesRegex( TypeError, - "Expected an object of type str for 'msg', not 'type'" + "Expected an object of type str for 'message', not 'type'" ): @deprecated class Foo: ... with self.assertRaisesRegex( TypeError, - "Expected an object of type str for 'msg', not 'function'" + "Expected an object of type str for 'message', not 'function'" ): @deprecated def foo(): ... + def test_no_retained_references_to_wrapper_instance(self): + @deprecated('depr') + def d(): pass + + self.assertFalse(any( + isinstance(cell.cell_contents, deprecated) for cell in d.__closure__ + )) + class AnyTests(BaseTestCase): def test_can_subclass(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5e4932bc..a875ec97 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2292,15 +2292,12 @@ def method(self) -> None: else: _T = typing.TypeVar("_T") - def deprecated( - msg: str, - /, - *, - category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, - stacklevel: int = 1, - ) -> typing.Callable[[_T], _T]: + class deprecated: """Indicate that a class, function or overload is deprecated. + When this decorator is applied to an object, the type checker + will generate a diagnostic on usage of the deprecated object. + Usage: @deprecated("Use B instead") @@ -2317,36 +2314,56 @@ def g(x: int) -> int: ... @overload def g(x: str) -> int: ... - When this decorator is applied to an object, the type checker - will generate a diagnostic on usage of the deprecated object. - - The warning specified by ``category`` will be emitted on use - of deprecated objects. For functions, that happens on calls; - for classes, on instantiation. If the ``category`` is ``None``, - no warning is emitted. The ``stacklevel`` determines where the + The warning specified by *category* will be emitted at runtime + on use of deprecated objects. For functions, that happens on calls; + for classes, on instantiation and on creation of subclasses. + If the *category* is ``None``, no warning is emitted at runtime. + The *stacklevel* determines where the warning is emitted. If it is ``1`` (the default), the warning is emitted at the direct caller of the deprecated object; if it is higher, it is emitted further up the stack. + Static type checker behavior is not affected by the *category* + and *stacklevel* arguments. - The decorator sets the ``__deprecated__`` - attribute on the decorated object to the deprecation message - passed to the decorator. If applied to an overload, the decorator + The deprecation message passed to the decorator is saved in the + ``__deprecated__`` attribute on the decorated object. + If applied to an overload, the decorator must be after the ``@overload`` decorator for the attribute to exist on the overload as returned by ``get_overloads()``. See PEP 702 for details. """ - if not isinstance(msg, str): - raise TypeError( - f"Expected an object of type str for 'msg', not {type(msg).__name__!r}" - ) - - def decorator(arg: _T, /) -> _T: + def __init__( + self, + message: str, + /, + *, + category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, + stacklevel: int = 1, + ) -> None: + if not isinstance(message, str): + raise TypeError( + "Expected an object of type str for 'message', not " + f"{type(message).__name__!r}" + ) + self.message = message + self.category = category + self.stacklevel = stacklevel + + def __call__(self, arg: _T, /) -> _T: + # Make sure the inner functions created below don't + # retain a reference to self. + msg = self.message + category = self.category + stacklevel = self.stacklevel if category is None: arg.__deprecated__ = msg return arg elif isinstance(arg, type): + import functools + from types import MethodType + original_new = arg.__new__ @functools.wraps(original_new) @@ -2366,7 +2383,7 @@ def __new__(cls, *args, **kwargs): original_init_subclass = arg.__init_subclass__ # We need slightly different behavior if __init_subclass__ # is a bound method (likely if it was implemented in Python) - if isinstance(original_init_subclass, _types.MethodType): + if isinstance(original_init_subclass, MethodType): original_init_subclass = original_init_subclass.__func__ @functools.wraps(original_init_subclass) @@ -2389,6 +2406,8 @@ def __init_subclass__(*args, **kwargs): __init_subclass__.__deprecated__ = msg return arg elif callable(arg): + import functools + @functools.wraps(arg) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) @@ -2402,8 +2421,6 @@ def wrapper(*args, **kwargs): f"a class or callable, not {arg!r}" ) - return decorator - # We have to do some monkey patching to deal with the dual nature of # Unpack/TypeVarTuple: From 0b0166d649cebcb48e7e208ae5da36cfab5965fe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Nov 2023 09:45:37 -0800 Subject: [PATCH 13/75] Add support for PEP 705 (#284) Co-authored-by: Alice Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 + doc/index.rst | 29 ++++++++- src/test_typing_extensions.py | 60 ++++++++++++++++-- src/typing_extensions.py | 113 +++++++++++++++++++++++++++++----- 4 files changed, 183 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d85aae3c..36c735d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Release 4.9.0 (???) +- Add support for PEP 705, adding `typing_extensions.ReadOnly`. Patch + by Jelle Zijlstra. - All parameters on `NewType.__call__` are now positional-only. This means that the signature of `typing_extensions.NewType.__call__` now exactly matches the signature of `typing.NewType.__call__`. Patch by Alex Waygood. diff --git a/doc/index.rst b/doc/index.rst index 3bbe2fc8..76ba1a50 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -318,6 +318,12 @@ Special typing primitives present in a protocol class's :py:term:`method resolution order`. See :issue:`245` for some examples. +.. data:: ReadOnly + + See :pep:`705`. Indicates that a :class:`TypedDict` item may not be modified. + + .. versionadded:: 4.9.0 + .. data:: Required See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11. @@ -344,7 +350,7 @@ Special typing primitives See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. -.. class:: TypedDict +.. class:: TypedDict(dict, total=True) See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8. @@ -366,6 +372,23 @@ Special typing primitives raises a :py:exc:`DeprecationWarning` when this syntax is used in Python 3.12 or lower and fails with a :py:exc:`TypeError` in Python 3.13 and higher. + ``typing_extensions`` supports the experimental :data:`ReadOnly` qualifier + proposed by :pep:`705`. It is reflected in the following attributes:: + + .. attribute:: __readonly_keys__ + + A :py:class:`frozenset` containing the names of all read-only keys. Keys + are read-only if they carry the :data:`ReadOnly` qualifier. + + .. versionadded:: 4.9.0 + + .. attribute:: __mutable_keys__ + + A :py:class:`frozenset` containing the names of all mutable keys. Keys + are mutable if they do not carry the :data:`ReadOnly` qualifier. + + .. versionadded:: 4.9.0 + .. versionchanged:: 4.3.0 Added support for generic ``TypedDict``\ s. @@ -394,6 +417,10 @@ Special typing primitives disallowed in Python 3.15. To create a TypedDict class with 0 fields, use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``. + .. versionchanged:: 4.9.0 + + Support for the :data:`ReadOnly` qualifier was added. + .. class:: TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False, default=...) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 562704e9..77876b7f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -31,7 +31,7 @@ import typing_extensions from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard -from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired +from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired, ReadOnly from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases @@ -3550,10 +3550,7 @@ def test_typeddict_create_errors(self): def test_typeddict_errors(self): Emp = TypedDict('Emp', {'name': str, 'id': int}) - if sys.version_info >= (3, 13): - self.assertEqual(TypedDict.__module__, 'typing') - else: - self.assertEqual(TypedDict.__module__, 'typing_extensions') + self.assertEqual(TypedDict.__module__, 'typing_extensions') jim = Emp(name='Jim', id=1) with self.assertRaises(TypeError): isinstance({}, Emp) @@ -4077,6 +4074,55 @@ class T4(TypedDict, Generic[S]): pass self.assertEqual(klass.__optional_keys__, set()) self.assertIsInstance(klass(), dict) + def test_readonly_inheritance(self): + class Base1(TypedDict): + a: ReadOnly[int] + + class Child1(Base1): + b: str + + self.assertEqual(Child1.__readonly_keys__, frozenset({'a'})) + self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) + + class Base2(TypedDict): + a: ReadOnly[int] + + class Child2(Base2): + b: str + + self.assertEqual(Child1.__readonly_keys__, frozenset({'a'})) + self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) + + def test_cannot_make_mutable_key_readonly(self): + class Base(TypedDict): + a: int + + with self.assertRaises(TypeError): + class Child(Base): + a: ReadOnly[int] + + def test_can_make_readonly_key_mutable(self): + class Base(TypedDict): + a: ReadOnly[int] + + class Child(Base): + a: int + + self.assertEqual(Child.__readonly_keys__, frozenset()) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + + def test_combine_qualifiers(self): + class AllTheThings(TypedDict): + a: Annotated[Required[ReadOnly[int]], "why not"] + b: Required[Annotated[ReadOnly[int], "why not"]] + c: ReadOnly[NotRequired[Annotated[int, "why not"]]] + d: NotRequired[Annotated[int, "why not"]] + + self.assertEqual(AllTheThings.__required_keys__, frozenset({'a', 'b'})) + self.assertEqual(AllTheThings.__optional_keys__, frozenset({'c', 'd'})) + self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'})) + self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'})) + class AnnotatedTests(BaseTestCase): @@ -5217,7 +5263,9 @@ def test_typing_extensions_defers_when_possible(self): 'SupportsRound', 'Unpack', } if sys.version_info < (3, 13): - exclude |= {'NamedTuple', 'Protocol', 'TypedDict', 'is_typeddict'} + exclude |= {'NamedTuple', 'Protocol'} + if not hasattr(typing, 'ReadOnly'): + exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a875ec97..1666e96b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -86,6 +86,7 @@ 'TYPE_CHECKING', 'Never', 'NoReturn', + 'ReadOnly', 'Required', 'NotRequired', @@ -773,7 +774,7 @@ def inner(func): return inner -if sys.version_info >= (3, 13): +if hasattr(typing, "ReadOnly"): # The standard library TypedDict in Python 3.8 does not store runtime information # about which (if any) keys are optional. See https://bugs.python.org/issue38834 # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" @@ -784,6 +785,7 @@ def inner(func): # Aaaand on 3.12 we add __orig_bases__ to TypedDict # to enable better runtime introspection. # On 3.13 we deprecate some odd ways of creating TypedDicts. + # PEP 705 proposes adding the ReadOnly[] qualifier. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict @@ -791,8 +793,29 @@ def inner(func): # 3.10.0 and later _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters + def _get_typeddict_qualifiers(annotation_type): + while True: + annotation_origin = get_origin(annotation_type) + if annotation_origin is Annotated: + annotation_args = get_args(annotation_type) + if annotation_args: + annotation_type = annotation_args[0] + else: + break + elif annotation_origin is Required: + yield Required + annotation_type, = get_args(annotation_type) + elif annotation_origin is NotRequired: + yield NotRequired + annotation_type, = get_args(annotation_type) + elif annotation_origin is ReadOnly: + yield ReadOnly + annotation_type, = get_args(annotation_type) + else: + break + class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, total=True): + def __new__(cls, name, bases, ns, *, total=True): """Create new typed dict class object. This method is called when TypedDict is subclassed, @@ -835,33 +858,46 @@ def __new__(cls, name, bases, ns, total=True): } required_keys = set() optional_keys = set() + readonly_keys = set() + mutable_keys = set() for base in bases: - annotations.update(base.__dict__.get('__annotations__', {})) - required_keys.update(base.__dict__.get('__required_keys__', ())) - optional_keys.update(base.__dict__.get('__optional_keys__', ())) + base_dict = base.__dict__ + + annotations.update(base_dict.get('__annotations__', {})) + required_keys.update(base_dict.get('__required_keys__', ())) + optional_keys.update(base_dict.get('__optional_keys__', ())) + readonly_keys.update(base_dict.get('__readonly_keys__', ())) + mutable_keys.update(base_dict.get('__mutable_keys__', ())) annotations.update(own_annotations) for annotation_key, annotation_type in own_annotations.items(): - annotation_origin = get_origin(annotation_type) - if annotation_origin is Annotated: - annotation_args = get_args(annotation_type) - if annotation_args: - annotation_type = annotation_args[0] - annotation_origin = get_origin(annotation_type) - - if annotation_origin is Required: + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + + if Required in qualifiers: required_keys.add(annotation_key) - elif annotation_origin is NotRequired: + elif NotRequired in qualifiers: optional_keys.add(annotation_key) elif total: required_keys.add(annotation_key) else: optional_keys.add(annotation_key) + if ReadOnly in qualifiers: + if annotation_key in mutable_keys: + raise TypeError( + f"Cannot override mutable key {annotation_key!r}" + " with read-only key" + ) + readonly_keys.add(annotation_key) + else: + mutable_keys.add(annotation_key) + readonly_keys.discard(annotation_key) tp_dict.__annotations__ = annotations tp_dict.__required_keys__ = frozenset(required_keys) tp_dict.__optional_keys__ = frozenset(optional_keys) + tp_dict.__readonly_keys__ = frozenset(readonly_keys) + tp_dict.__mutable_keys__ = frozenset(mutable_keys) if not hasattr(tp_dict, '__total__'): tp_dict.__total__ = total return tp_dict @@ -942,6 +978,8 @@ class Point2D(TypedDict): raise TypeError("TypedDict takes either a dict or keyword arguments," " but not both") if kwargs: + if sys.version_info >= (3, 13): + raise TypeError("TypedDict takes no keyword arguments") warnings.warn( "The kwargs-based syntax for TypedDict definitions is deprecated " "in Python 3.11, will be removed in Python 3.13, and may not be " @@ -1930,6 +1968,53 @@ class Movie(TypedDict): """) +if hasattr(typing, 'ReadOnly'): + ReadOnly = typing.ReadOnly +elif sys.version_info[:2] >= (3, 9): # 3.9-3.12 + @_ExtensionsSpecialForm + def ReadOnly(self, parameters): + """A special typing construct to mark an item of a TypedDict as read-only. + + For example: + + class Movie(TypedDict): + title: ReadOnly[str] + year: int + + def mutate_movie(m: Movie) -> None: + m["year"] = 1992 # allowed + m["title"] = "The Matrix" # typechecker error + + There is no runtime checking for this property. + """ + item = typing._type_check(parameters, f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + +else: # 3.8 + class _ReadOnlyForm(_ExtensionsSpecialForm, _root=True): + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only a single type.') + return typing._GenericAlias(self, (item,)) + + ReadOnly = _ReadOnlyForm( + 'ReadOnly', + doc="""A special typing construct to mark a key of a TypedDict as read-only. + + For example: + + class Movie(TypedDict): + title: ReadOnly[str] + year: int + + def mutate_movie(m: Movie) -> None: + m["year"] = 1992 # allowed + m["title"] = "The Matrix" # typechecker error + + There is no runtime checking for this propery. + """) + + _UNPACK_DOC = """\ Type unpack operator. From daa793141c3d504ce0a1d19ef032ea83466ba5c2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Nov 2023 18:07:36 +0000 Subject: [PATCH 14/75] Run typed-argument-parser tests on 3.12 in the daily workflow (#307) They declared support for Python 3.12 in https://github.com/swansonk14/typed-argument-parser/commit/0789b251e58892ca0fb6c18ade046c8a960c3268 --- .github/workflows/third_party.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index fcde71ca..3d7de82c 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -217,7 +217,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From f82d6367f3ff8f16b6291de06394ec6b9318bfc3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Nov 2023 10:13:59 -0800 Subject: [PATCH 15/75] Prepare release 4.9.0rc1 (#306) --- CHANGELOG.md | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c735d4..c5a8aebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Release 4.9.0 (???) +# Release 4.9.0rc1 (November 29, 2023) - Add support for PEP 705, adding `typing_extensions.ReadOnly`. Patch by Jelle Zijlstra. @@ -11,8 +11,8 @@ argument to the `msg` parameter. Patch by Alex Waygood. - `@deprecated` is now implemented as a class for better introspectability. Patch by Jelle Zijlstra. -- Exclude `__match_args__` from `Protocol` members, - this is a backport of https://github.com/python/cpython/pull/110683 +- Exclude `__match_args__` from `Protocol` members. + Backport of https://github.com/python/cpython/pull/110683 by Nikita Sobolev. - When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__` is called on all objects that define `__set_name__` and exist in the values of the `NamedTuple` class's class dictionary. Patch by Alex Waygood, diff --git a/pyproject.toml b/pyproject.toml index b71e6d01..7570b039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.8.0" +version = "4.9.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From fc461d6faf4585849b561f2e4cbb06e9db095307 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 17:11:53 -0800 Subject: [PATCH 16/75] Release 4.9.0 (#313) --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5a8aebc..fedc2a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Release 4.9.0 (December 9, 2023) + +This feature release adds `typing_extensions.ReadOnly`, as specified +by PEP 705, and makes various other improvements, especially to +`@typing_extensions.deprecated()`. + +There are no changes since 4.9.0rc1. + # Release 4.9.0rc1 (November 29, 2023) - Add support for PEP 705, adding `typing_extensions.ReadOnly`. Patch diff --git a/pyproject.toml b/pyproject.toml index 7570b039..5bea3e9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.9.0rc1" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From d6dc4f157e731b2475141aae0c2586ed0243b686 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Dec 2023 17:20:30 -0800 Subject: [PATCH 17/75] Fix readthedocs config (#314) The folder is called docs, not doc. Also use 3.12 while I'm here. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 83cd53cb..60419be8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,8 +6,8 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" sphinx: - configuration: docs/conf.py + configuration: doc/conf.py From f84880d60b1d5f7b4ceaab563e7eeb6021f4ca13 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 13 Jan 2024 08:35:48 -0800 Subject: [PATCH 18/75] third-party tests: skip cattrs on pypy (#321) It's broken for reasons unrelated to typing-extensions. See #320. --- .github/workflows/third_party.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 3d7de82c..b086299d 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -315,7 +315,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 004b893ddce2a5743d9a4de3a97ef5c48882d384 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 20 Jan 2024 17:56:16 +0000 Subject: [PATCH 19/75] Backport recent improvements to the implementation of `Protocol` (#324) --- CHANGELOG.md | 11 ++++ src/test_typing_extensions.py | 61 ++++++++++++++++++-- src/typing_extensions.py | 101 +++++++++++++++++++++++++++------- 3 files changed, 146 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fedc2a3f..be1c16d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Unreleased + +- Speedup `issubclass()` checks against simple runtime-checkable protocols by + around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex + Waygood). +- Fix a regression in the implementation of protocols where `typing.Protocol` + classes that were not marked as `@runtime_checkable` would be unnecessarily + introspected, potentially causing exceptions to be raised if the protocol had + problematic members. Patch by Alex Waygood, backporting + https://github.com/python/cpython/pull/113401. + # Release 4.9.0 (December 9, 2023) This feature release adds `typing_extensions.ReadOnly`, as specified diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 77876b7f..58dc1851 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2817,8 +2817,8 @@ def meth(self): pass # noqa: B027 self.assertNotIn("__protocol_attrs__", vars(NonP)) self.assertNotIn("__protocol_attrs__", vars(NonPR)) - self.assertNotIn("__callable_proto_members_only__", vars(NonP)) - self.assertNotIn("__callable_proto_members_only__", vars(NonPR)) + self.assertNotIn("__non_callable_proto_members__", vars(NonP)) + self.assertNotIn("__non_callable_proto_members__", vars(NonPR)) acceptable_extra_attrs = { '_is_protocol', '_is_runtime_protocol', '__parameters__', @@ -2891,11 +2891,26 @@ def __subclasshook__(cls, other): @skip_if_py312b1 def test_issubclass_fails_correctly(self): @runtime_checkable - class P(Protocol): + class NonCallableMembers(Protocol): x = 1 + + class NotRuntimeCheckable(Protocol): + def callable_member(self) -> int: ... + + @runtime_checkable + class RuntimeCheckable(Protocol): + def callable_member(self) -> int: ... + class C: pass - with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"): - issubclass(C(), P) + + # These three all exercise different code paths, + # but should result in the same error message: + for protocol in NonCallableMembers, NotRuntimeCheckable, RuntimeCheckable: + with self.subTest(proto_name=protocol.__name__): + with self.assertRaisesRegex( + TypeError, r"issubclass\(\) arg 1 must be a class" + ): + issubclass(C(), protocol) def test_defining_generic_protocols(self): T = TypeVar('T') @@ -3456,6 +3471,7 @@ def method(self) -> None: ... @skip_if_early_py313_alpha def test_protocol_issubclass_error_message(self): + @runtime_checkable class Vec2D(Protocol): x: float y: float @@ -3471,6 +3487,39 @@ def square_norm(self) -> float: with self.assertRaisesRegex(TypeError, re.escape(expected_error_message)): issubclass(int, Vec2D) + def test_nonruntime_protocol_interaction_with_evil_classproperty(self): + class classproperty: + def __get__(self, instance, type): + raise RuntimeError("NO") + + class Commentable(Protocol): + evil = classproperty() + + # recognised as a protocol attr, + # but not actually accessed by the protocol metaclass + # (which would raise RuntimeError) for non-runtime protocols. + # See gh-113320 + self.assertEqual(get_protocol_members(Commentable), {"evil"}) + + def test_runtime_protocol_interaction_with_evil_classproperty(self): + class CustomError(Exception): pass + + class classproperty: + def __get__(self, instance, type): + raise CustomError + + with self.assertRaises(TypeError) as cm: + @runtime_checkable + class Commentable(Protocol): + evil = classproperty() + + exc = cm.exception + self.assertEqual( + exc.args[0], + "Failed to determine whether protocol member 'evil' is a method member" + ) + self.assertIs(type(exc.__cause__), CustomError) + class Point2DGeneric(Generic[T], TypedDict): a: T @@ -5263,7 +5312,7 @@ def test_typing_extensions_defers_when_possible(self): 'SupportsRound', 'Unpack', } if sys.version_info < (3, 13): - exclude |= {'NamedTuple', 'Protocol'} + exclude |= {'NamedTuple', 'Protocol', 'runtime_checkable'} if not hasattr(typing, 'ReadOnly'): exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1666e96b..4007594c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -473,7 +473,7 @@ def clear_overloads(): "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__", "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", "__subclasshook__", "__orig_class__", "__init__", "__new__", - "__protocol_attrs__", "__callable_proto_members_only__", + "__protocol_attrs__", "__non_callable_proto_members__", "__match_args__", } @@ -521,6 +521,22 @@ def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') + def _type_check_issubclass_arg_1(arg): + """Raise TypeError if `arg` is not an instance of `type` + in `issubclass(arg, )`. + + In most cases, this is verified by type.__subclasscheck__. + Checking it again unnecessarily would slow down issubclass() checks, + so, we don't perform this check unless we absolutely have to. + + For various error paths, however, + we want to ensure that *this* error message is shown to the user + where relevant, rather than a typing.py-specific error message. + """ + if not isinstance(arg, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') + # Inheriting from typing._ProtocolMeta isn't actually desirable, # but is necessary to allow typing.Protocol and typing_extensions.Protocol # to mix without getting TypeErrors about "metaclass conflict" @@ -551,11 +567,6 @@ def __init__(cls, *args, **kwargs): abc.ABCMeta.__init__(cls, *args, **kwargs) if getattr(cls, "_is_protocol", False): cls.__protocol_attrs__ = _get_protocol_attrs(cls) - # PEP 544 prohibits using issubclass() - # with protocols that have non-method members. - cls.__callable_proto_members_only__ = all( - callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ - ) def __subclasscheck__(cls, other): if cls is Protocol: @@ -564,26 +575,23 @@ def __subclasscheck__(cls, other): getattr(cls, '_is_protocol', False) and not _allow_reckless_class_checks() ): - if not isinstance(other, type): - # Same error message as for issubclass(1, int). - raise TypeError('issubclass() arg 1 must be a class') + if not getattr(cls, '_is_runtime_protocol', False): + _type_check_issubclass_arg_1(other) + raise TypeError( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) if ( - not cls.__callable_proto_members_only__ + # this attribute is set by @runtime_checkable: + cls.__non_callable_proto_members__ and cls.__dict__.get("__subclasshook__") is _proto_hook ): - non_method_attrs = sorted( - attr for attr in cls.__protocol_attrs__ - if not callable(getattr(cls, attr, None)) - ) + _type_check_issubclass_arg_1(other) + non_method_attrs = sorted(cls.__non_callable_proto_members__) raise TypeError( "Protocols with non-method members don't support issubclass()." f" Non-method members: {str(non_method_attrs)[1:-1]}." ) - if not getattr(cls, '_is_runtime_protocol', False): - raise TypeError( - "Instance and class checks can only be used with " - "@runtime_checkable protocols" - ) return abc.ABCMeta.__subclasscheck__(cls, other) def __instancecheck__(cls, instance): @@ -610,7 +618,8 @@ def __instancecheck__(cls, instance): val = inspect.getattr_static(instance, attr) except AttributeError: break - if val is None and callable(getattr(cls, attr, None)): + # this attribute is set by @runtime_checkable: + if val is None and attr not in cls.__non_callable_proto_members__: break else: return True @@ -678,8 +687,58 @@ def __init_subclass__(cls, *args, **kwargs): cls.__init__ = _no_init +if sys.version_info >= (3, 13): + runtime_checkable = typing.runtime_checkable +else: + def runtime_checkable(cls): + """Mark a protocol class as a runtime protocol. + + Such protocol can be used with isinstance() and issubclass(). + Raise TypeError if applied to a non-protocol class. + This allows a simple-minded structural check very similar to + one trick ponies in collections.abc such as Iterable. + + For example:: + + @runtime_checkable + class Closable(Protocol): + def close(self): ... + + assert isinstance(open('/some/file'), Closable) + + Warning: this will check only the presence of the required methods, + not their type signatures! + """ + if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False): + raise TypeError('@runtime_checkable can be only applied to protocol classes,' + ' got %r' % cls) + cls._is_runtime_protocol = True + + # Only execute the following block if it's a typing_extensions.Protocol class. + # typing.Protocol classes don't need it. + if isinstance(cls, _ProtocolMeta): + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + # See gh-113320 for why we compute this attribute here, + # rather than in `_ProtocolMeta.__init__` + cls.__non_callable_proto_members__ = set() + for attr in cls.__protocol_attrs__: + try: + is_callable = callable(getattr(cls, attr, None)) + except Exception as e: + raise TypeError( + f"Failed to determine whether protocol member {attr!r} " + "is a method member" + ) from e + else: + if not is_callable: + cls.__non_callable_proto_members__.add(attr) + + return cls + + # The "runtime" alias exists for backwards compatibility. -runtime = runtime_checkable = typing.runtime_checkable +runtime = runtime_checkable # Our version of runtime-checkable protocols is faster on Python 3.8-3.11 From 69b48c377a2a1286c57059e66c47d386374c46c2 Mon Sep 17 00:00:00 2001 From: James Morris <6653392+J-M0@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:14:24 -0500 Subject: [PATCH 20/75] Fix display of TypedDict.__readonly_keys__ (#328) --- doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 76ba1a50..4985762e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -373,7 +373,7 @@ Special typing primitives or lower and fails with a :py:exc:`TypeError` in Python 3.13 and higher. ``typing_extensions`` supports the experimental :data:`ReadOnly` qualifier - proposed by :pep:`705`. It is reflected in the following attributes:: + proposed by :pep:`705`. It is reflected in the following attributes: .. attribute:: __readonly_keys__ From 05ffab5fda8510249ec10bc138c72678e3f6d2d9 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 13 Feb 2024 11:28:16 +0100 Subject: [PATCH 21/75] Catch a deprecation warning on Python 3.13 (#331) --- src/test_typing_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 58dc1851..6fd3b324 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3553,7 +3553,8 @@ def test_basics_functional_syntax(self): @skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13") def test_keywords_syntax_raises_on_3_13(self): with self.assertRaises(TypeError): - Emp = TypedDict('Emp', name=str, id=int) + with self.assertWarns(DeprecationWarning): + Emp = TypedDict('Emp', name=str, id=int) @skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs") def test_basics_keywords_syntax(self): From ff530f50e2e1440e870dfecd2f59a5ae9d2a4244 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 13 Feb 2024 11:43:48 +0100 Subject: [PATCH 22/75] Update GitHub Actions versions (#332) --- .github/workflows/ci.yml | 6 +++--- .github/workflows/package.yml | 4 ++-- .github/workflows/third_party.yml | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c3d990e..1dc21e06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3" cache: "pip" @@ -122,7 +122,7 @@ jobs: issues: write steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 622861bc..6b55f10e 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3 @@ -53,7 +53,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3 diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index b086299d..92ce3676 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -100,7 +100,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install typing_inspect test dependencies @@ -143,7 +143,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -187,7 +187,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -231,7 +231,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Configure git for typed-argument-parser tests @@ -282,7 +282,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -328,7 +328,7 @@ jobs: with: path: typing-extensions-latest - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install pdm for cattrs @@ -377,7 +377,7 @@ jobs: issues: write steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From d6c50f585c386490d38ad6b8ce5543aed6e633a2 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 14 Feb 2024 08:52:43 -0800 Subject: [PATCH 23/75] Drop runtime error in PEP 705 implementation (#333) See https://discuss.python.org/t/pep-705-read-only-typeddict-items/37867/10 --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 7 +++---- src/typing_extensions.py | 5 ----- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be1c16d6..e052710c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased +- Drop runtime error when a mutable `TypedDict` key overrides a read-only + one. Type checkers should still flag this as an error. Patch by Jelle + Zijlstra. - Speedup `issubclass()` checks against simple runtime-checkable protocols by around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex Waygood). diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 6fd3b324..2e8fe7b3 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4143,13 +4143,12 @@ class Child2(Base2): self.assertEqual(Child1.__readonly_keys__, frozenset({'a'})) self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) - def test_cannot_make_mutable_key_readonly(self): + def test_make_mutable_key_readonly(self): class Base(TypedDict): a: int - with self.assertRaises(TypeError): - class Child(Base): - a: ReadOnly[int] + class Child(Base): + a: ReadOnly[int] # type checker error, but allowed at runtime def test_can_make_readonly_key_mutable(self): class Base(TypedDict): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4007594c..a016e07b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -942,11 +942,6 @@ def __new__(cls, name, bases, ns, *, total=True): else: optional_keys.add(annotation_key) if ReadOnly in qualifiers: - if annotation_key in mutable_keys: - raise TypeError( - f"Cannot override mutable key {annotation_key!r}" - " with read-only key" - ) readonly_keys.add(annotation_key) else: mutable_keys.add(annotation_key) From 566e01e7a798abfcf88849814918fd8413b8d18b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 15 Feb 2024 20:21:55 -0800 Subject: [PATCH 24/75] Add support for TypeIs (PEP 742) (#330) * Add support for TypeNarrower (PEP 742) * Use TypeIs --- CHANGELOG.md | 2 + doc/index.rst | 6 +++ src/test_typing_extensions.py | 46 ++++++++++++++++- src/typing_extensions.py | 93 +++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e052710c..8be5ebd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch + by Jelle Zijlstra. - Drop runtime error when a mutable `TypedDict` key overrides a read-only one. Type checkers should still flag this as an error. Patch by Jelle Zijlstra. diff --git a/doc/index.rst b/doc/index.rst index 4985762e..b1e2477b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -350,6 +350,12 @@ Special typing primitives See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. +.. data:: TypeIs + + See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing. + + .. versionadded:: 4.10.0 + .. class:: TypedDict(dict, total=True) See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2e8fe7b3..12677fd0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -36,7 +36,7 @@ from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload -from typing_extensions import NamedTuple +from typing_extensions import NamedTuple, TypeIs from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol from typing_extensions import Doc from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated @@ -4774,6 +4774,50 @@ def test_no_isinstance(self): issubclass(int, TypeGuard) +class TypeIsTests(BaseTestCase): + def test_basics(self): + TypeIs[int] # OK + self.assertEqual(TypeIs[int], TypeIs[int]) + + def foo(arg) -> TypeIs[int]: ... + self.assertEqual(gth(foo), {'return': TypeIs[int]}) + + def test_repr(self): + if hasattr(typing, 'TypeIs'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs') + cv = TypeIs[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]') + cv = TypeIs[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]') + cv = TypeIs[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(TypeIs)): + pass + with self.assertRaises(TypeError): + class C(type(TypeIs[int])): + pass + + def test_cannot_init(self): + with self.assertRaises(TypeError): + TypeIs() + with self.assertRaises(TypeError): + type(TypeIs)() + with self.assertRaises(TypeError): + type(TypeIs[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeIs[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeIs) + + class LiteralStringTests(BaseTestCase): def test_basics(self): class Foo: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a016e07b..3ef09259 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -83,6 +83,7 @@ 'TypeAlias', 'TypeAliasType', 'TypeGuard', + 'TypeIs', 'TYPE_CHECKING', 'Never', 'NoReturn', @@ -1822,6 +1823,98 @@ def is_str(val: Union[str, float]): PEP 647 (User-Defined Type Guards). """) +# 3.13+ +if hasattr(typing, 'TypeIs'): + TypeIs = typing.TypeIs +# 3.9 +elif sys.version_info[:2] >= (3, 9): + @_ExtensionsSpecialForm + def TypeIs(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeIs[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeIs`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the intersection of the type inside ``TypeGuard`` and the argument's + previously known type. + + For example:: + + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: + if is_awaitable(val): + assert_type(val, Awaitable[int]) + else: + assert_type(val, int) + + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeIs). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeIsForm(_ExtensionsSpecialForm, _root=True): + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only a single type') + return typing._GenericAlias(self, (item,)) + + TypeIs = _TypeIsForm( + 'TypeIs', + doc="""Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeIs`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeIs[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeIs`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the intersection of the type inside ``TypeGuard`` and the argument's + previously known type. + + For example:: + + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: + return hasattr(val, '__await__') + + def f(val: Union[int, Awaitable[int]]) -> int: + if is_awaitable(val): + assert_type(val, Awaitable[int]) + else: + assert_type(val, int) + + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeIs). + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True): From 9f040ab8c6f859e8ce956331b496e6a98a33e6f6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 15 Feb 2024 20:38:14 -0800 Subject: [PATCH 25/75] Fix changelog entry and __mutable_keys__ tracking for PEP 705 (#334) --- CHANGELOG.md | 2 +- src/test_typing_extensions.py | 6 ++++++ src/typing_extensions.py | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be5ebd6..02416f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ - Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch by Jelle Zijlstra. -- Drop runtime error when a mutable `TypedDict` key overrides a read-only +- Drop runtime error when a read-only `TypedDict` item overrides a mutable one. Type checkers should still flag this as an error. Patch by Jelle Zijlstra. - Speedup `issubclass()` checks against simple runtime-checkable protocols by diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 12677fd0..53d905e0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4147,9 +4147,15 @@ def test_make_mutable_key_readonly(self): class Base(TypedDict): a: int + self.assertEqual(Base.__readonly_keys__, frozenset()) + self.assertEqual(Base.__mutable_keys__, frozenset({'a'})) + class Child(Base): a: ReadOnly[int] # type checker error, but allowed at runtime + self.assertEqual(Child.__readonly_keys__, frozenset({'a'})) + self.assertEqual(Child.__mutable_keys__, frozenset()) + def test_can_make_readonly_key_mutable(self): class Base(TypedDict): a: ReadOnly[int] diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3ef09259..f39d4c7f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -943,6 +943,7 @@ def __new__(cls, name, bases, ns, *, total=True): else: optional_keys.add(annotation_key) if ReadOnly in qualifiers: + mutable_keys.discard(annotation_key) readonly_keys.add(annotation_key) else: mutable_keys.add(annotation_key) From b7bf949d669dbe19537f7608e00f7b8368fdfb39 Mon Sep 17 00:00:00 2001 From: Zixuan Li <39874143+PIG208@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:26:50 -0500 Subject: [PATCH 26/75] Add support for PEP 728 (#329) Signed-off-by: Zixuan James Li --- CHANGELOG.md | 2 + doc/index.rst | 37 ++++++++ src/test_typing_extensions.py | 154 ++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 32 ++++++- 4 files changed, 222 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02416f46..cd38e40f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add support for PEP 728, supporting the `closed` keyword argument and the + special `__extra_items__` key for TypedDict. Patch by Zixuan James Li. - Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch by Jelle Zijlstra. - Drop runtime error when a read-only `TypedDict` item overrides a mutable diff --git a/doc/index.rst b/doc/index.rst index b1e2477b..4bd8c702 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -394,6 +394,38 @@ Special typing primitives are mutable if they do not carry the :data:`ReadOnly` qualifier. .. versionadded:: 4.9.0 + + The experimental ``closed`` keyword argument and the special key + ``__extra_items__`` proposed in :pep:`728` are supported. + + When ``closed`` is unspecified or ``closed=False`` is given, + ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a + special key that does not show up in ``__readonly_keys__``, + ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or + ``__annotations__``. + + For runtime introspection, two attributes can be looked at: + + .. attribute:: __closed__ + + A boolean flag indicating whether the current ``TypedDict`` is + considered closed. This is not inherited by the ``TypedDict``'s + subclasses. + + .. versionadded:: 4.10.0 + + .. attribute:: __extra_items__ + + The type annotation of the extra items allowed on the ``TypedDict``. + This attribute defaults to ``None`` on a TypedDict that has itself and + all its bases non-closed. This default is different from ``type(None)`` + that represents ``__extra_items__: None`` defined on a closed + ``TypedDict``. + + If ``__extra_items__`` is not defined or inherited on a closed + ``TypedDict``, this defaults to ``Never``. + + .. versionadded:: 4.10.0 .. versionchanged:: 4.3.0 @@ -427,6 +459,11 @@ Special typing primitives Support for the :data:`ReadOnly` qualifier was added. + .. versionchanged:: 4.10.0 + + The keyword argument ``closed`` and the special key ``__extra_items__`` + when ``closed=True`` is given were supported. + .. class:: TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False, default=...) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 53d905e0..79c1b881 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -52,6 +52,9 @@ # 3.12 changes the representation of Unpack[] (PEP 692) TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) +# 3.13 drops support for the keyword argument syntax of TypedDict +TYPING_3_13_0 = sys.version_info[:3] >= (3, 13, 0) + # https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10 # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters @@ -3820,6 +3823,24 @@ class ChildWithInlineAndOptional(Untotal, Inline): {'inline': bool, 'untotal': str, 'child': bool}, ) + class Closed(TypedDict, closed=True): + __extra_items__: None + + class Unclosed(TypedDict, closed=False): + ... + + class ChildUnclosed(Closed, Unclosed): + ... + + self.assertFalse(ChildUnclosed.__closed__) + self.assertEqual(ChildUnclosed.__extra_items__, type(None)) + + class ChildClosed(Unclosed, Closed): + ... + + self.assertFalse(ChildClosed.__closed__) + self.assertEqual(ChildClosed.__extra_items__, type(None)) + wrong_bases = [ (One, Regular), (Regular, One), @@ -4178,6 +4199,139 @@ class AllTheThings(TypedDict): self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'})) self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'})) + def test_extra_keys_non_readonly(self): + class Base(TypedDict, closed=True): + __extra_items__: str + + class Child(Base): + a: NotRequired[int] + + self.assertEqual(Child.__required_keys__, frozenset({})) + self.assertEqual(Child.__optional_keys__, frozenset({'a'})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + + def test_extra_keys_readonly(self): + class Base(TypedDict, closed=True): + __extra_items__: ReadOnly[str] + + class Child(Base): + a: NotRequired[str] + + self.assertEqual(Child.__required_keys__, frozenset({})) + self.assertEqual(Child.__optional_keys__, frozenset({'a'})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + + def test_extra_key_required(self): + with self.assertRaisesRegex( + TypeError, + "Special key __extra_items__ does not support Required" + ): + TypedDict("A", {"__extra_items__": Required[int]}, closed=True) + + with self.assertRaisesRegex( + TypeError, + "Special key __extra_items__ does not support NotRequired" + ): + TypedDict("A", {"__extra_items__": NotRequired[int]}, closed=True) + + def test_regular_extra_items(self): + class ExtraReadOnly(TypedDict): + __extra_items__: ReadOnly[str] + + self.assertEqual(ExtraReadOnly.__required_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraReadOnly.__optional_keys__, frozenset({})) + self.assertEqual(ExtraReadOnly.__readonly_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraReadOnly.__mutable_keys__, frozenset({})) + self.assertEqual(ExtraReadOnly.__extra_items__, None) + self.assertFalse(ExtraReadOnly.__closed__) + + class ExtraRequired(TypedDict): + __extra_items__: Required[str] + + self.assertEqual(ExtraRequired.__required_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraRequired.__optional_keys__, frozenset({})) + self.assertEqual(ExtraRequired.__readonly_keys__, frozenset({})) + self.assertEqual(ExtraRequired.__mutable_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraRequired.__extra_items__, None) + self.assertFalse(ExtraRequired.__closed__) + + class ExtraNotRequired(TypedDict): + __extra_items__: NotRequired[str] + + self.assertEqual(ExtraNotRequired.__required_keys__, frozenset({})) + self.assertEqual(ExtraNotRequired.__optional_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraNotRequired.__readonly_keys__, frozenset({})) + self.assertEqual(ExtraNotRequired.__mutable_keys__, frozenset({'__extra_items__'})) + self.assertEqual(ExtraNotRequired.__extra_items__, None) + self.assertFalse(ExtraNotRequired.__closed__) + + def test_closed_inheritance(self): + class Base(TypedDict, closed=True): + __extra_items__: ReadOnly[Union[str, None]] + + self.assertEqual(Base.__required_keys__, frozenset({})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({})) + self.assertEqual(Base.__annotations__, {}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertTrue(Base.__closed__) + + class Child(Base): + a: int + __extra_items__: int + + self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(Child.__annotations__, {"__extra_items__": int, "a": int}) + self.assertEqual(Child.__extra_items__, ReadOnly[Union[str, None]]) + self.assertFalse(Child.__closed__) + + class GrandChild(Child, closed=True): + __extra_items__: str + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(GrandChild.__annotations__, {"__extra_items__": int, "a": int}) + self.assertEqual(GrandChild.__extra_items__, str) + self.assertTrue(GrandChild.__closed__) + + def test_implicit_extra_items(self): + class Base(TypedDict): + a: int + + self.assertEqual(Base.__extra_items__, None) + self.assertFalse(Base.__closed__) + + class ChildA(Base, closed=True): + ... + + self.assertEqual(ChildA.__extra_items__, Never) + self.assertTrue(ChildA.__closed__) + + class ChildB(Base, closed=True): + __extra_items__: None + + self.assertEqual(ChildB.__extra_items__, type(None)) + self.assertTrue(ChildB.__closed__) + + @skipIf( + TYPING_3_13_0, + "The keyword argument alternative to define a " + "TypedDict type using the functional syntax is no longer supported" + ) + def test_backwards_compatibility(self): + with self.assertWarns(DeprecationWarning): + TD = TypedDict("TD", closed=int) + self.assertFalse(TD.__closed__) + self.assertEqual(TD.__annotations__, {"closed": int}) + class AnnotatedTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f39d4c7f..f3132ea4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -875,7 +875,7 @@ def _get_typeddict_qualifiers(annotation_type): break class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, *, total=True): + def __new__(cls, name, bases, ns, *, total=True, closed=False): """Create new typed dict class object. This method is called when TypedDict is subclassed, @@ -920,6 +920,7 @@ def __new__(cls, name, bases, ns, *, total=True): optional_keys = set() readonly_keys = set() mutable_keys = set() + extra_items_type = None for base in bases: base_dict = base.__dict__ @@ -929,6 +930,26 @@ def __new__(cls, name, bases, ns, *, total=True): optional_keys.update(base_dict.get('__optional_keys__', ())) readonly_keys.update(base_dict.get('__readonly_keys__', ())) mutable_keys.update(base_dict.get('__mutable_keys__', ())) + base_extra_items_type = base_dict.get('__extra_items__', None) + if base_extra_items_type is not None: + extra_items_type = base_extra_items_type + + if closed and extra_items_type is None: + extra_items_type = Never + if closed and "__extra_items__" in own_annotations: + annotation_type = own_annotations.pop("__extra_items__") + qualifiers = set(_get_typeddict_qualifiers(annotation_type)) + if Required in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "Required" + ) + if NotRequired in qualifiers: + raise TypeError( + "Special key __extra_items__ does not support " + "NotRequired" + ) + extra_items_type = annotation_type annotations.update(own_annotations) for annotation_key, annotation_type in own_annotations.items(): @@ -956,6 +977,8 @@ def __new__(cls, name, bases, ns, *, total=True): tp_dict.__mutable_keys__ = frozenset(mutable_keys) if not hasattr(tp_dict, '__total__'): tp_dict.__total__ = total + tp_dict.__closed__ = closed + tp_dict.__extra_items__ = extra_items_type return tp_dict __call__ = dict # static method @@ -969,7 +992,7 @@ def __subclasscheck__(cls, other): _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) @_ensure_subclassable(lambda bases: (_TypedDict,)) - def TypedDict(typename, fields=_marker, /, *, total=True, **kwargs): + def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -1029,6 +1052,9 @@ class Point2D(TypedDict): "using the functional syntax, pass an empty dictionary, e.g. " ) + example + "." warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) + if closed is not False and closed is not True: + kwargs["closed"] = closed + closed = False fields = kwargs elif kwargs: raise TypeError("TypedDict takes either a dict or keyword arguments," @@ -1050,7 +1076,7 @@ class Point2D(TypedDict): # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed) td.__orig_bases__ = (TypedDict,) return td From 06b23e3f05fd0f929dbaea17ae51621dcc8434ab Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 17 Feb 2024 18:56:17 -0800 Subject: [PATCH 27/75] Release 4.10.0rc1 (#340) --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd38e40f..23715b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Release 4.10.0rc1 (February 17, 2024) - Add support for PEP 728, supporting the `closed` keyword argument and the special `__extra_items__` key for TypedDict. Patch by Zixuan James Li. diff --git a/pyproject.toml b/pyproject.toml index 5bea3e9b..6351ba23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.9.0" +version = "4.10.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From ed81f2b2043f60b0c159914e264e127f5d0b4cda Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 25 Feb 2024 14:08:26 -0800 Subject: [PATCH 28/75] Prepare release 4.10.0 (#343) --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23715b6d..07fc328d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Release 4.10.0 (February 24, 2024) + +This feature release adds support for PEP 728 (TypedDict with extra +items) and PEP 742 (``TypeIs``). + +There are no changes since 4.10.0rc1. + # Release 4.10.0rc1 (February 17, 2024) - Add support for PEP 728, supporting the `closed` keyword argument and the diff --git a/pyproject.toml b/pyproject.toml index 6351ba23..e0ef3432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.10.0rc1" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 2d742164d717a39f40aca46aeb4a3b5188c5bda3 Mon Sep 17 00:00:00 2001 From: arthur-tacca Date: Fri, 1 Mar 2024 22:53:24 +0000 Subject: [PATCH 29/75] Add module name to doc source (to allow intersphinx usage) (#346) --- doc/conf.py | 17 ++++++++++++++++- doc/index.rst | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 7984bc22..40d3c6b7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -5,6 +5,8 @@ import os.path import sys +from sphinx.writers.html5 import HTML5Translator +from docutils.nodes import Element sys.path.insert(0, os.path.abspath('.')) @@ -26,9 +28,22 @@ intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} +add_module_names = False # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'alabaster' -html_static_path = ['_static'] + + +class MyTranslator(HTML5Translator): + """Adds a link target to name without `typing_extensions.` prefix.""" + def visit_desc_signature(self, node: Element) -> None: + desc_name = node.get("fullname") + if desc_name: + self.body.append(f'') + super().visit_desc_signature(node) + + +def setup(app): + app.set_translator('html', MyTranslator) diff --git a/doc/index.rst b/doc/index.rst index 4bd8c702..63082ddd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,3 +1,4 @@ +.. module:: typing_extensions Welcome to typing_extensions's documentation! ============================================= From c3dc681a298fae6f2aa3e937e20a32a446ecb58c Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 6 Mar 2024 16:35:04 +0300 Subject: [PATCH 30/75] Make sure that `ReadOnly` is removed when using `get_type_hints(include_extra=False)` (#349) --- CHANGELOG.md | 4 ++++ doc/index.rst | 5 +++++ src/test_typing_extensions.py | 14 ++++++++++++++ src/typing_extensions.py | 6 +++--- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fc328d..a545e25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Release 4.11.0 (WIP) + +- When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. + # Release 4.10.0 (February 24, 2024) This feature release adds support for PEP 728 (TypedDict with extra diff --git a/doc/index.rst b/doc/index.rst index 63082ddd..bdf94c75 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -760,6 +760,11 @@ Functions Interaction with :data:`Required` and :data:`NotRequired`. + .. versionchanged:: 4.11.0 + + When ``include_extra=False``, ``get_type_hints()`` now strips + :data:`ReadOnly` from the annotation. + .. function:: is_protocol(tp) Determine if a type is a :class:`Protocol`. This works with protocols diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 79c1b881..d48880ff 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4199,6 +4199,20 @@ class AllTheThings(TypedDict): self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'})) self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'})) + self.assertEqual( + get_type_hints(AllTheThings, include_extras=False), + {'a': int, 'b': int, 'c': int, 'd': int}, + ) + self.assertEqual( + get_type_hints(AllTheThings, include_extras=True), + { + 'a': Annotated[Required[ReadOnly[int]], 'why not'], + 'b': Required[Annotated[ReadOnly[int], 'why not']], + 'c': ReadOnly[NotRequired[Annotated[int, 'why not']]], + 'd': NotRequired[Annotated[int, 'why not']], + }, + ) + def test_extra_keys_non_readonly(self): class Base(TypedDict, closed=True): __extra_items__: str diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f3132ea4..4499c616 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1122,15 +1122,15 @@ def greet(name: str) -> None: return val -if hasattr(typing, "Required"): # 3.11+ +if hasattr(typing, "ReadOnly"): # 3.13+ get_type_hints = typing.get_type_hints -else: # <=3.10 +else: # <=3.13 # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" if isinstance(t, _AnnotatedAlias): return _strip_extras(t.__origin__) - if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired): + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): return _strip_extras(t.__args__[0]) if isinstance(t, typing._GenericAlias): stripped_args = tuple(_strip_extras(a) for a in t.__args__) From 3304a5f0045fc81ccc10c9c9fd238d378d020d94 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:07:57 -0800 Subject: [PATCH 31/75] Stabilise third party tests (#348) Use uv to test with the state of PyPI as of the commit we are testing --- .github/workflows/third_party.yml | 51 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 92ce3676..cee1fe21 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -103,12 +103,16 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typing_inspect test dependencies - run: pip install -r typing_inspect/test-requirements.txt + run: | + cd typing_inspect + uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typing_inspect tests run: | cd typing_inspect @@ -147,12 +151,16 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install pyanalyze test requirements - run: pip install ./pyanalyze[tests] + run: | + cd pyanalyze + uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run pyanalyze tests run: | cd pyanalyze @@ -191,12 +199,16 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typeguard test requirements - run: pip install -e ./typeguard[test] + run: | + cd typeguard + uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typeguard tests run: | cd typeguard @@ -234,6 +246,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Configure git for typed-argument-parser tests # typed-argument parser does this in their CI, # and the tests fail unless we do this @@ -242,12 +256,13 @@ jobs: git config --global user.name "Your Name" - name: Install typed-argument-parser test requirements run: | - pip install -e ./typed-argument-parser - pip install pytest + cd typed-argument-parser + uv pip install --system "typed-argument-parser @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system pytest --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typed-argument-parser tests run: | cd typed-argument-parser @@ -286,15 +301,17 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install mypy test requirements run: | cd mypy - pip install -r test-requirements.txt - pip install -e . + uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system -e . - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run stubtest & mypyc tests run: | cd mypy From 4fdc09ddb54be26580f68e26443a422c6024364c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 7 Mar 2024 15:57:21 +0000 Subject: [PATCH 32/75] Third-party tests: don't run pydantic tests on pypy (#351) they keep segfaulting and it's nothing to do with us --- .github/workflows/third_party.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index cee1fe21..a0feeefc 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -41,7 +41,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"] + # PyPy is deliberately omitted here, + # since pydantic's tests intermittently segfault on PyPy, + # and it's nothing to do with typing_extensions + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 9d1689ede041302d85f41292bf25a9d13bf16a7b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 13:38:44 -0700 Subject: [PATCH 33/75] Fix indentation in TypedDict docs (#352) --- doc/index.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index bdf94c75..f9097a41 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -396,37 +396,37 @@ Special typing primitives .. versionadded:: 4.9.0 - The experimental ``closed`` keyword argument and the special key - ``__extra_items__`` proposed in :pep:`728` are supported. + The experimental ``closed`` keyword argument and the special key + ``__extra_items__`` proposed in :pep:`728` are supported. - When ``closed`` is unspecified or ``closed=False`` is given, - ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a - special key that does not show up in ``__readonly_keys__``, - ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or - ``__annotations__``. + When ``closed`` is unspecified or ``closed=False`` is given, + ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a + special key that does not show up in ``__readonly_keys__``, + ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or + ``__annotations__``. - For runtime introspection, two attributes can be looked at: + For runtime introspection, two attributes can be looked at: - .. attribute:: __closed__ + .. attribute:: __closed__ - A boolean flag indicating whether the current ``TypedDict`` is - considered closed. This is not inherited by the ``TypedDict``'s - subclasses. + A boolean flag indicating whether the current ``TypedDict`` is + considered closed. This is not inherited by the ``TypedDict``'s + subclasses. - .. versionadded:: 4.10.0 + .. versionadded:: 4.10.0 - .. attribute:: __extra_items__ + .. attribute:: __extra_items__ - The type annotation of the extra items allowed on the ``TypedDict``. - This attribute defaults to ``None`` on a TypedDict that has itself and - all its bases non-closed. This default is different from ``type(None)`` - that represents ``__extra_items__: None`` defined on a closed - ``TypedDict``. + The type annotation of the extra items allowed on the ``TypedDict``. + This attribute defaults to ``None`` on a TypedDict that has itself and + all its bases non-closed. This default is different from ``type(None)`` + that represents ``__extra_items__: None`` defined on a closed + ``TypedDict``. - If ``__extra_items__`` is not defined or inherited on a closed - ``TypedDict``, this defaults to ``Never``. + If ``__extra_items__`` is not defined or inherited on a closed + ``TypedDict``, this defaults to ``Never``. - .. versionadded:: 4.10.0 + .. versionadded:: 4.10.0 .. versionchanged:: 4.3.0 From d409ec98e3889462e59c85a4b34f9f83ce40bf2c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 18:44:13 -0700 Subject: [PATCH 34/75] Run CPython test suite in our CI (#353) --- .github/workflows/ci.yml | 8 ++++++++ CHANGELOG.md | 4 +++- src/test_typing_extensions.py | 4 ++-- src/typing_extensions.py | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dc21e06..1174ce36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,14 @@ jobs: cd src python -m unittest test_typing_extensions.py + - name: Test CPython typing test suite + run: | + # Run the typing test suite from CPython with typing_extensions installed, + # because we monkeypatch typing under some circumstances. + python -c 'import typing_extensions; import test.__main__' test_typing -v + # Test suite fails on PyPy even without typing_extensions + if: !startsWith(matrix.python-version, 'pypy') + linting: name: Lint diff --git a/CHANGELOG.md b/CHANGELOG.md index a545e25b..ce9d3f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -# Release 4.11.0 (WIP) +# Unreleased +- Fix minor discrepancy between error messages produced by `typing` + and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra. - When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. # Release 4.10.0 (February 24, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d48880ff..8940145e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3262,7 +3262,7 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) self.assertTrue(MemoizedFunc._is_protocol) - things = "arguments" if sys.version_info >= (3, 11) else "parameters" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" # A bug was fixed in 3.11.1 # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) @@ -5711,7 +5711,7 @@ class Y(Generic[T], NamedTuple): self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - things = "arguments" if sys.version_info >= (3, 11) else "parameters" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4499c616..94218334 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -164,7 +164,8 @@ def _check_generic(cls, parameters, elen=_marker): num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): return - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things} for {cls};" f" actual {alen}, expected {elen}") From d34c389d3d1f8cce006dfd1200e203551c16418c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 20:53:23 -0700 Subject: [PATCH 35/75] Try to fix GH actions syntax (#355) --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1174ce36..e9d69774 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,12 +75,13 @@ jobs: python -m unittest test_typing_extensions.py - name: Test CPython typing test suite + # Test suite fails on PyPy even without typing_extensions + if: ${{ !startsWith(matrix.python-version, 'pypy') }} run: | + cd src # Run the typing test suite from CPython with typing_extensions installed, # because we monkeypatch typing under some circumstances. python -c 'import typing_extensions; import test.__main__' test_typing -v - # Test suite fails on PyPy even without typing_extensions - if: !startsWith(matrix.python-version, 'pypy') linting: name: Lint From 8170fc7744ca1c2ca4911ce22095c907f7f58f8b Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Tue, 12 Mar 2024 22:36:21 +0000 Subject: [PATCH 36/75] Fix runtime behaviour of PEP 696 (#293) Co-authored-by: Jelle Zijlstra Co-authored-by: James Hilton-Balfe Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 22 +++- src/typing_extensions.py | 187 ++++++++++++++++++++++++++-------- 3 files changed, 166 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9d3f0f..4ac948a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Fix the runtime behavior of type parameters with defaults (PEP 696). + Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra. - When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8940145e..03d3afda 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5712,7 +5712,6 @@ class Y(Generic[T], NamedTuple): self.assertEqual(a.x, 3) things = "arguments" if sys.version_info >= (3, 10) else "parameters" - with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] @@ -6215,6 +6214,27 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + def test_erroneous_generic(self): + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) + T = TypeVar('T') + + with self.assertRaises(TypeError): + Test = Generic[DefaultStrT, T] + + def test_need_more_params(self): + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) + T = typing_extensions.TypeVar('T') + U = typing_extensions.TypeVar('U') + + class A(Generic[T, U, DefaultStrT]): ... + A[int, bool] + A[int, bool, str] + + with self.assertRaises( + TypeError, msg="Too few arguments for .+; actual 1, expected at least 2" + ): + Test = A[int] + def test_pickle(self): global U, U_co, U_contra, U_default # pickle wants to reference the class by name U = typing_extensions.TypeVar('U') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 94218334..09fcfd87 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -147,28 +147,6 @@ def __repr__(self): _marker = _Sentinel() -def _check_generic(cls, parameters, elen=_marker): - """Check correct count for parameters of a generic cls (internal helper). - This gives a nice error message in case of count mismatch. - """ - if not elen: - raise TypeError(f"{cls} is not a generic class") - if elen is _marker: - if not hasattr(cls, "__parameters__") or not cls.__parameters__: - raise TypeError(f"{cls} is not a generic class") - elen = len(cls.__parameters__) - alen = len(parameters) - if alen != elen: - if hasattr(cls, "__parameters__"): - parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] - num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) - if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): - return - things = "arguments" if sys.version_info >= (3, 10) else "parameters" - raise TypeError(f"Too {'many' if alen > elen else 'few'} {things} for {cls};" - f" actual {alen}, expected {elen}") - - if sys.version_info >= (3, 10): def _should_collect_from_parameters(t): return isinstance( @@ -182,27 +160,6 @@ def _should_collect_from_parameters(t): return isinstance(t, typing._GenericAlias) and not t._special -def _collect_type_vars(types, typevar_types=None): - """Collect all type variable contained in types in order of - first appearance (lexicographic order). For example:: - - _collect_type_vars((T, List[S, T])) == (T, S) - """ - if typevar_types is None: - typevar_types = typing.TypeVar - tvars = [] - for t in types: - if ( - isinstance(t, typevar_types) and - t not in tvars and - not _is_unpack(t) - ): - tvars.append(t) - if _should_collect_from_parameters(t): - tvars.extend([t for t in t.__parameters__ if t not in tvars]) - return tuple(tvars) - - NoReturn = typing.NoReturn # Some unconstrained type variables. These are used by the container types. @@ -2690,9 +2647,151 @@ def wrapper(*args, **kwargs): # counting generic parameters, so that when we subscript a generic, # the runtime doesn't try to substitute the Unpack with the subscripted type. if not hasattr(typing, "TypeVarTuple"): + def _check_generic(cls, parameters, elen=_marker): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + if elen is _marker: + if not hasattr(cls, "__parameters__") or not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + elen = len(cls.__parameters__) + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): + return + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return + + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}" + f" for {cls}; actual {alen}, expected {expect_val}") +else: + # Python 3.11+ + + def _check_generic(cls, parameters, elen): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return + + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" + f" for {cls}; actual {alen}, expected {expect_val}") + +typing._check_generic = _check_generic + +# Python 3.11+ _collect_type_vars was renamed to _collect_parameters +if hasattr(typing, '_collect_type_vars'): + def _collect_type_vars(types, typevar_types=None): + """Collect all type variable contained in types in order of + first appearance (lexicographic order). For example:: + + _collect_type_vars((T, List[S, T])) == (T, S) + """ + if typevar_types is None: + typevar_types = typing.TypeVar + tvars = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in types: + if ( + isinstance(t, typevar_types) and + t not in tvars and + not _is_unpack(t) + ): + if getattr(t, '__default__', None) is not None: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + tvars.append(t) + if _should_collect_from_parameters(t): + tvars.extend([t for t in t.__parameters__ if t not in tvars]) + return tuple(tvars) + typing._collect_type_vars = _collect_type_vars - typing._check_generic = _check_generic +else: + def _collect_parameters(args): + """Collect all type variables and parameter specifications in args + in order of first appearance (lexicographic order). + + For example:: + + assert _collect_parameters((T, Callable[P, T])) == (T, P) + """ + parameters = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in args: + if isinstance(t, type): + # We don't want __parameters__ descriptor of a bare Python class. + pass + elif isinstance(t, tuple): + # `t` might be a tuple, when `ParamSpec` is substituted with + # `[T, int]`, or `[int, *Ts]`, etc. + for x in t: + for collected in _collect_parameters([x]): + if collected not in parameters: + parameters.append(collected) + elif hasattr(t, '__typing_subst__'): + if t not in parameters: + if getattr(t, '__default__', None) is not None: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + parameters.append(t) + else: + for x in getattr(t, '__parameters__', ()): + if x not in parameters: + parameters.append(x) + + return tuple(parameters) + typing._collect_parameters = _collect_parameters # Backport typing.NamedTuple as it exists in Python 3.13. # In 3.11, the ability to define generic `NamedTuple`s was supported. From 10648b6149e3b98cfb7d842684859318f01e940d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 14 Mar 2024 00:17:44 -0700 Subject: [PATCH 37/75] Fix tests on 3.13.0a5 (#358) --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 2 +- src/typing_extensions.py | 9 +++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac948a0..02f221d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Fix tests on 3.13.0a5. Patch by Jelle Zijlstra. - Fix the runtime behavior of type parameters with defaults (PEP 696). Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 03d3afda..27488550 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5531,7 +5531,7 @@ def test_typing_extensions_defers_when_possible(self): } if sys.version_info < (3, 13): exclude |= {'NamedTuple', 'Protocol', 'runtime_checkable'} - if not hasattr(typing, 'ReadOnly'): + if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 09fcfd87..9ccd519c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -792,7 +792,11 @@ def inner(func): return inner -if hasattr(typing, "ReadOnly"): +# Update this to something like >=3.13.0b1 if and when +# PEP 728 is implemented in CPython +_PEP_728_IMPLEMENTED = False + +if _PEP_728_IMPLEMENTED: # The standard library TypedDict in Python 3.8 does not store runtime information # about which (if any) keys are optional. See https://bugs.python.org/issue38834 # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" @@ -803,7 +807,8 @@ def inner(func): # Aaaand on 3.12 we add __orig_bases__ to TypedDict # to enable better runtime introspection. # On 3.13 we deprecate some odd ways of creating TypedDicts. - # PEP 705 proposes adding the ReadOnly[] qualifier. + # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier. + # PEP 728 (still pending) makes more changes. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict From 94bec447d6f7b9d3625ef0e688a0b0f9e487e951 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 24 Mar 2024 08:02:37 -0600 Subject: [PATCH 38/75] Prepare release 4.11.0rc1 (#362) --- CHANGELOG.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f221d7..52bf2ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -# Unreleased +# Release 4.11.0rc1 (March 24, 2024) -- Fix tests on 3.13.0a5. Patch by Jelle Zijlstra. +- Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra. - Fix the runtime behavior of type parameters with defaults (PEP 696). Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` diff --git a/pyproject.toml b/pyproject.toml index e0ef3432..2ecfbd3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.10.0" +version = "4.11.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From d4d929d44bd984350e2d17726362295f588eaace Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 5 Apr 2024 08:33:24 -0400 Subject: [PATCH 39/75] Prepare release 4.11.0 (#363) --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52bf2ab3..4cf71773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Release 4.11.0 (April 5, 2024) + +This feature release provides improvements to various recently +added features, most importantly type parameter defaults (PEP 696). + +There are no changes since 4.11.0rc1. + # Release 4.11.0rc1 (March 24, 2024) - Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra. diff --git a/pyproject.toml b/pyproject.toml index 2ecfbd3a..4b1a7601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.11.0rc1" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From b009fe04a68b4b42fb063d34d92c7d343aceb480 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 8 Apr 2024 13:22:50 +0200 Subject: [PATCH 40/75] Add dependabot for GitHub Actions (#365) --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5c563144 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + groups: + actions: + patterns: + - "*" From 66e0a1f6de7ec62b8e4eec3fe25e0663792be013 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:32:10 +0200 Subject: [PATCH 41/75] Bump pdm-project/setup-pdm from 3 to 4 in the actions group (#366) Bumps the actions group with 1 update: [pdm-project/setup-pdm](https://github.com/pdm-project/setup-pdm). Updates `pdm-project/setup-pdm` from 3 to 4 - [Release notes](https://github.com/pdm-project/setup-pdm/releases) - [Commits](https://github.com/pdm-project/setup-pdm/compare/v3...v4) --- updated-dependencies: - dependency-name: pdm-project/setup-pdm dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/third_party.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index a0feeefc..8424d8fe 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -61,7 +61,7 @@ jobs: with: path: typing-extensions-latest - name: Setup pdm for pydantic tests - uses: pdm-project/setup-pdm@v3 + uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} allow-python-prereleases: true From a1697b4936fc854355c0c539d7d1b9f3d05013b9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 10 Apr 2024 21:01:56 +0100 Subject: [PATCH 42/75] Make the daily test not fail tonight on Python 3.13 (#367) it's about to fail because of https://github.com/python/cpython/pull/115913 --- CHANGELOG.md | 7 +++++++ src/typing_extensions.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf71773..6d0b024e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Unreleased + +- Fix tests on Python 3.13.0a6. 3.13.0a6 adds a new + `__static_attributes__` attribute to all classes in Python, + which broke some assumptions made by the implementation of + `typing_extenions.Protocol`. + # Release 4.11.0 (April 5, 2024) This feature release provides improvements to various recently diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9ccd519c..3d441bc2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -442,6 +442,9 @@ def clear_overloads(): if sys.version_info >= (3, 12): _EXCLUDED_ATTRS.add("__type_params__") +if sys.version_info >= (3, 13): + _EXCLUDED_ATTRS.add("__static_attributes__") + _EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS) From d0a654c86e048a1b7800ffd0dd149d83213e06b5 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:08:05 -0700 Subject: [PATCH 43/75] Fix changelog typo (#368) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0b024e..d58e37f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ - Fix tests on Python 3.13.0a6. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of - `typing_extenions.Protocol`. + `typing_extensions.Protocol`. # Release 4.11.0 (April 5, 2024) From 2a7945bfa106f9bac74909aea9d2e1e99cc0cb73 Mon Sep 17 00:00:00 2001 From: Hashem Date: Sat, 20 Apr 2024 19:42:44 -0400 Subject: [PATCH 44/75] Backport assert_never change to include repr of value (#371) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 13 +++++++++++++ src/typing_extensions.py | 11 ++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d58e37f2..1f0dde20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of `typing_extensions.Protocol`. +- At runtime, `assert_never` now includes the repr of the argument + in the `AssertionError`. Patch by Hashem, backporting of the original + fix https://github.com/python/cpython/pull/91720 by Jelle Zijlstra. # Release 4.11.0 (April 5, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 27488550..1d0ff5f5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -283,6 +283,19 @@ def test_exception(self): with self.assertRaises(AssertionError): assert_never(None) + value = "some value" + with self.assertRaisesRegex(AssertionError, value): + assert_never(value) + + # Make sure a huge value doesn't get printed in its entirety + huge_value = "a" * 10000 + with self.assertRaises(AssertionError) as cm: + assert_never(huge_value) + self.assertLess( + len(cm.exception.args[0]), + typing_extensions._ASSERT_NEVER_REPR_MAX_LENGTH * 2, + ) + class OverrideTests(BaseTestCase): def test_override(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3d441bc2..e1427ee4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2355,6 +2355,12 @@ def reveal_type(obj: T, /) -> T: return obj +if hasattr(typing, "_ASSERT_NEVER_REPR_MAX_LENGTH"): # 3.11+ + _ASSERT_NEVER_REPR_MAX_LENGTH = typing._ASSERT_NEVER_REPR_MAX_LENGTH +else: # <=3.10 + _ASSERT_NEVER_REPR_MAX_LENGTH = 100 + + if hasattr(typing, "assert_never"): # 3.11+ assert_never = typing.assert_never else: # <=3.10 @@ -2378,7 +2384,10 @@ def int_or_str(arg: int | str) -> None: At runtime, this throws an exception when called. """ - raise AssertionError("Expected code to be unreachable") + value = repr(arg) + if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH: + value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...' + raise AssertionError(f"Expected code to be unreachable, but got: {value}") if sys.version_info >= (3, 12): # 3.12+ From c79c561ec62a09e5a62d14c12ccae4d7cb273da0 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 21 Apr 2024 01:09:38 +0100 Subject: [PATCH 45/75] Fix using `typing_extensions.runtime_checkable` in combination with `typing.Protocol` on 3.12.2+ (#373) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 12 ++++++++++++ src/typing_extensions.py | 11 ++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f0dde20..3bb322f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of `typing_extensions.Protocol`. +- Fix `AttributeError` when using `typing_extensions.runtime_checkable` + in combination with `typing.Protocol` on Python 3.12.2 or newer. + Patch by Alex Waygood. - At runtime, `assert_never` now includes the repr of the argument in the `AssertionError`. Patch by Hashem, backporting of the original fix https://github.com/python/cpython/pull/91720 by Jelle Zijlstra. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 1d0ff5f5..a2fe368b 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3536,6 +3536,18 @@ class Commentable(Protocol): ) self.assertIs(type(exc.__cause__), CustomError) + def test_extensions_runtimecheckable_on_typing_Protocol(self): + @runtime_checkable + class Functor(typing.Protocol): + def foo(self) -> None: ... + + self.assertNotIsSubclass(object, Functor) + + class Bar: + def foo(self): pass + + self.assertIsSubclass(Bar, Functor) + class Point2DGeneric(Generic[T], TypedDict): a: T diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e1427ee4..885c2d1f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -676,9 +676,14 @@ def close(self): ... ' got %r' % cls) cls._is_runtime_protocol = True - # Only execute the following block if it's a typing_extensions.Protocol class. - # typing.Protocol classes don't need it. - if isinstance(cls, _ProtocolMeta): + # typing.Protocol classes on <=3.11 break if we execute this block, + # because typing.Protocol classes on <=3.11 don't have a + # `__protocol_attrs__` attribute, and this block relies on the + # `__protocol_attrs__` attribute. Meanwhile, typing.Protocol classes on 3.12.2+ + # break if we *don't* execute this block, because *they* assume that all + # protocol classes have a `__non_callable_proto_members__` attribute + # (which this block sets) + if isinstance(cls, _ProtocolMeta) or sys.version_info >= (3, 12, 2): # PEP 544 prohibits using issubclass() # with protocols that have non-method members. # See gh-113320 for why we compute this attribute here, From 9a62a452312f947d0e9fa2d881eaeaf6d2084130 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 9 May 2024 14:33:35 +0100 Subject: [PATCH 46/75] gitignore `.python-version` (#375) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0ad58f48..ee36fe77 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ venv*/ .tox/ .venv*/ .vscode/ +.python-version *.swp *.pyc From 132685bbb25b1e670ac130297aef788ae190d904 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 9 May 2024 16:03:28 +0100 Subject: [PATCH 47/75] Fix `Protocol` tests on Python 3.13.0b1 (#376) --- CHANGELOG.md | 5 +++-- src/typing_extensions.py | 21 +++------------------ 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb322f6..b2379816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # Unreleased -- Fix tests on Python 3.13.0a6. 3.13.0a6 adds a new +- Fix tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of - `typing_extensions.Protocol`. + `typing_extensions.Protocol`. Similarly, 3.13.0b1 adds the new + `__firstlineno__` attribute to all classes. - Fix `AttributeError` when using `typing_extensions.runtime_checkable` in combination with `typing.Protocol` on Python 3.12.2 or newer. Patch by Alex Waygood. diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 885c2d1f..9eb45ce2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -427,26 +427,11 @@ def clear_overloads(): } -_EXCLUDED_ATTRS = { - "__abstractmethods__", "__annotations__", "__weakref__", "_is_protocol", - "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__", - "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", - "__subclasshook__", "__orig_class__", "__init__", "__new__", - "__protocol_attrs__", "__non_callable_proto_members__", - "__match_args__", +_EXCLUDED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | { + "__match_args__", "__protocol_attrs__", "__non_callable_proto_members__", + "__final__", } -if sys.version_info >= (3, 9): - _EXCLUDED_ATTRS.add("__class_getitem__") - -if sys.version_info >= (3, 12): - _EXCLUDED_ATTRS.add("__type_params__") - -if sys.version_info >= (3, 13): - _EXCLUDED_ATTRS.add("__static_attributes__") - -_EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS) - def _get_protocol_attrs(cls): attrs = set() From 348f5441aa588052f0ef0579b02aa65902763743 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 10 May 2024 13:58:07 +0100 Subject: [PATCH 48/75] Fix most tests on Python 3.13 (#378) --- CHANGELOG.md | 13 +++- doc/index.rst | 56 ++++++++++++++-- src/test_typing_extensions.py | 47 +++++++++++--- src/typing_extensions.py | 118 ++++++++++++++++++++++------------ 4 files changed, 179 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2379816..bbad4264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Unreleased -- Fix tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new +- Backport the `typing.NoDefault` sentinel object from Python 3.13. + TypeVars, ParamSpecs and TypeVarTuples without default values now have + their `__default__` attribute set to this sentinel value. +- TypeVars, ParamSpecs and TypeVarTuples now have a `has_default()` + method, matching `typing.TypeVar`, `typing.ParamSpec` and + `typing.TypeVarTuple` on Python 3.13+. +- TypeVars, ParamSpecs and TypeVarTuples with `default=None` passed to + their constructors now have their `__default__` attribute set to `None` + at runtime rather than `types.NoneType`. +- Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python + 3.13.0b1 and newer. +- Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of `typing_extensions.Protocol`. Similarly, 3.13.0b1 adds the new diff --git a/doc/index.rst b/doc/index.rst index f9097a41..3486ae74 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -253,13 +253,19 @@ Special typing primitives The improvements from Python 3.10 and 3.11 were backported. +.. data:: NoDefault + + See :py:class:`typing.NoDefault`. In ``typing`` since 3.13.0. + + .. versionadded:: 4.12.0 + .. data:: NotRequired See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. .. versionadded:: 4.0.0 -.. class:: ParamSpec(name, *, default=...) +.. class:: ParamSpec(name, *, default=NoDefault) See :py:class:`typing.ParamSpec` and :pep:`612`. In ``typing`` since 3.10. @@ -284,6 +290,20 @@ Special typing primitives Passing an ellipsis literal (``...``) to *default* now works on Python 3.10 and lower. + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + ParamSpecs now have a ``has_default()`` method, for compatibility + with :py:class:`typing.ParamSpec` on Python 3.13+. + .. class:: ParamSpecArgs .. class:: ParamSpecKwargs @@ -395,7 +415,7 @@ Special typing primitives are mutable if they do not carry the :data:`ReadOnly` qualifier. .. versionadded:: 4.9.0 - + The experimental ``closed`` keyword argument and the special key ``__extra_items__`` proposed in :pep:`728` are supported. @@ -466,7 +486,7 @@ Special typing primitives when ``closed=True`` is given were supported. .. class:: TypeVar(name, *constraints, bound=None, covariant=False, - contravariant=False, infer_variance=False, default=...) + contravariant=False, infer_variance=False, default=NoDefault) See :py:class:`typing.TypeVar`. @@ -484,7 +504,21 @@ Special typing primitives The implementation was changed for compatibility with Python 3.12. -.. class:: TypeVarTuple(name, *, default=...) + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + TypeVars now have a ``has_default()`` method, for compatibility + with :py:class:`typing.TypeVar` on Python 3.13+. + +.. class:: TypeVarTuple(name, *, default=NoDefault) See :py:class:`typing.TypeVarTuple` and :pep:`646`. In ``typing`` since 3.11. @@ -501,6 +535,20 @@ Special typing primitives The implementation was changed for compatibility with Python 3.12. + .. versionchanged:: 4.12.0 + + The :attr:`!__default__` attribute is now set to ``None`` if + ``default=None`` is passed, and to :data:`NoDefault` if no value is passed. + + Previously, passing ``None`` would result in :attr:`!__default__` being set + to :py:class:`types.NoneType`, and passing no value for the parameter would + result in :attr:`!__default__` being set to ``None``. + + .. versionchanged:: 4.12.0 + + TypeVarTuples now have a ``has_default()`` method, for compatibility + with :py:class:`typing.TypeVarTuple` on Python 3.13+. + .. data:: Unpack See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a2fe368b..c7c2f0d5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -38,7 +38,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple, TypeIs from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol -from typing_extensions import Doc +from typing_extensions import Doc, NoDefault from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated # Flags used to mark tests that only apply after a specific @@ -59,9 +59,9 @@ # versions, but not all HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters -skip_if_early_py313_alpha = skipIf( - sys.version_info[:4] == (3, 13, 0, 'alpha') and sys.version_info.serial < 3, - "Bugfixes will be released in 3.13.0a3" +skip_if_py313_beta_1 = skipIf( + sys.version_info[:5] == (3, 13, 0, 'beta', 1), + "Bugfixes will be released in 3.13.0b2" ) ANN_MODULE_SOURCE = '''\ @@ -3485,7 +3485,6 @@ def method(self) -> None: ... self.assertIsInstance(Foo(), ProtocolWithMixedMembers) self.assertNotIsInstance(42, ProtocolWithMixedMembers) - @skip_if_early_py313_alpha def test_protocol_issubclass_error_message(self): @runtime_checkable class Vec2D(Protocol): @@ -5917,7 +5916,6 @@ class GenericNamedTuple(NamedTuple, Generic[T]): self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) - @skip_if_early_py313_alpha def test_setname_called_on_values_in_class_dictionary(self): class Vanilla: def __set_name__(self, owner, name): @@ -5989,7 +5987,6 @@ class NamedTupleClass(NamedTuple): TYPING_3_12_0, "__set_name__ behaviour changed on py312+ to use BaseException.add_note()" ) - @skip_if_early_py313_alpha def test_setname_raises_the_same_as_on_other_classes_py312_plus(self): class CustomException(BaseException): pass @@ -6029,7 +6026,6 @@ class NamedTupleClass(NamedTuple): normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass") ) - @skip_if_early_py313_alpha def test_strange_errors_when_accessing_set_name_itself(self): class CustomException(Exception): pass @@ -6207,12 +6203,15 @@ class A(Generic[T]): ... def test_typevar_none(self): U = typing_extensions.TypeVar('U') U_None = typing_extensions.TypeVar('U_None', default=None) - self.assertEqual(U.__default__, None) - self.assertEqual(U_None.__default__, type(None)) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertEqual(U_None.__default__, None) + self.assertTrue(U_None.has_default()) def test_paramspec(self): P = ParamSpec('P', default=(str, int)) self.assertEqual(P.__default__, (str, int)) + self.assertTrue(P.has_default()) self.assertIsInstance(P, ParamSpec) if hasattr(typing, "ParamSpec"): self.assertIsInstance(P, typing.ParamSpec) @@ -6225,11 +6224,13 @@ class A(Generic[P]): ... P_default = ParamSpec('P_default', default=...) self.assertIs(P_default.__default__, ...) + self.assertTrue(P_default.has_default()) def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) self.assertIsInstance(Ts, TypeVarTuple) + self.assertTrue(Ts.has_default()) if hasattr(typing, "TypeVarTuple"): self.assertIsInstance(Ts, typing.TypeVarTuple) typing_Ts = typing.TypeVarTuple('Ts') @@ -6276,6 +6277,32 @@ def test_pickle(self): self.assertEqual(z.__default__, typevar.__default__) +class NoDefaultTests(BaseTestCase): + @skip_if_py313_beta_1 + def test_pickling(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + s = pickle.dumps(NoDefault, proto) + loaded = pickle.loads(s) + self.assertIs(NoDefault, loaded) + + def test_constructor(self): + self.assertIs(NoDefault, type(NoDefault)()) + with self.assertRaises(TypeError): + type(NoDefault)(1) + + def test_repr(self): + self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault') + + def test_no_call(self): + with self.assertRaises(TypeError): + NoDefault() + + @skip_if_py313_beta_1 + def test_immutable(self): + with self.assertRaises(AttributeError): + NoDefault.foo = 'bar' + + class TypeVarInferVarianceTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9eb45ce2..70a31193 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -116,6 +116,7 @@ 'MutableMapping', 'MutableSequence', 'MutableSet', + 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -134,6 +135,7 @@ # for backward compatibility PEP_560 = True GenericMeta = type +_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. @@ -1355,17 +1357,37 @@ def TypeAlias(self, parameters): ) +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType: + __slots__ = () + + def __new__(cls): + return globals().get("NoDefault") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoDefault" + + def __reduce__(self): + return "NoDefault" + + NoDefault = NoDefaultType() + del NoDefaultType + + def _set_default(type_param, default): + type_param.has_default = lambda: default is not NoDefault if isinstance(default, (tuple, list)): type_param.__default__ = tuple((typing._type_check(d, "Default must be a type") for d in default)) - elif default != _marker: + elif default in (None, NoDefault): + type_param.__default__ = default + else: if isinstance(type_param, ParamSpec) and default is ...: # ... not valid <3.11 type_param.__default__ = default else: type_param.__default__ = typing._type_check(default, "Default must be a type") - else: - type_param.__default__ = None def _set_module(typevarlike): @@ -1388,32 +1410,35 @@ def __instancecheck__(cls, __instance: Any) -> bool: return isinstance(__instance, cls._backported_typevarlike) -# Add default and infer_variance parameters from PEP 696 and 695 -class TypeVar(metaclass=_TypeVarLikeMeta): - """Type variable.""" +if _PEP_696_IMPLEMENTED: + from typing import TypeVar +else: + # Add default and infer_variance parameters from PEP 696 and 695 + class TypeVar(metaclass=_TypeVarLikeMeta): + """Type variable.""" - _backported_typevarlike = typing.TypeVar + _backported_typevarlike = typing.TypeVar - def __new__(cls, name, *constraints, bound=None, - covariant=False, contravariant=False, - default=_marker, infer_variance=False): - if hasattr(typing, "TypeAliasType"): - # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar - typevar = typing.TypeVar(name, *constraints, bound=bound, - covariant=covariant, contravariant=contravariant, - infer_variance=infer_variance) - else: - typevar = typing.TypeVar(name, *constraints, bound=bound, - covariant=covariant, contravariant=contravariant) - if infer_variance and (covariant or contravariant): - raise ValueError("Variance cannot be specified with infer_variance.") - typevar.__infer_variance__ = infer_variance - _set_default(typevar, default) - _set_module(typevar) - return typevar + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=NoDefault, infer_variance=False): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant, + infer_variance=infer_variance) + else: + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant) + if infer_variance and (covariant or contravariant): + raise ValueError("Variance cannot be specified with infer_variance.") + typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) + _set_module(typevar) + return typevar - def __init_subclass__(cls) -> None: - raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") # Python 3.10+ has PEP 612 @@ -1478,8 +1503,12 @@ def __eq__(self, other): return NotImplemented return self.__origin__ == other.__origin__ + +if _PEP_696_IMPLEMENTED: + from typing import ParamSpec + # 3.10+ -if hasattr(typing, 'ParamSpec'): +elif hasattr(typing, 'ParamSpec'): # Add default parameter - PEP 696 class ParamSpec(metaclass=_TypeVarLikeMeta): @@ -1489,7 +1518,7 @@ class ParamSpec(metaclass=_TypeVarLikeMeta): def __new__(cls, name, *, bound=None, covariant=False, contravariant=False, - infer_variance=False, default=_marker): + infer_variance=False, default=NoDefault): if hasattr(typing, "TypeAliasType"): # PEP 695 implemented, can pass infer_variance to typing.TypeVar paramspec = typing.ParamSpec(name, bound=bound, @@ -1572,7 +1601,7 @@ def kwargs(self): return ParamSpecKwargs(self) def __init__(self, name, *, bound=None, covariant=False, contravariant=False, - infer_variance=False, default=_marker): + infer_variance=False, default=NoDefault): super().__init__([self]) self.__name__ = name self.__covariant__ = bool(covariant) @@ -2226,7 +2255,10 @@ def _is_unpack(obj): return isinstance(obj, _UnpackAlias) -if hasattr(typing, "TypeVarTuple"): # 3.11+ +if _PEP_696_IMPLEMENTED: + from typing import TypeVarTuple + +elif hasattr(typing, "TypeVarTuple"): # 3.11+ # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): @@ -2234,7 +2266,7 @@ class TypeVarTuple(metaclass=_TypeVarLikeMeta): _backported_typevarlike = typing.TypeVarTuple - def __new__(cls, name, *, default=_marker): + def __new__(cls, name, *, default=NoDefault): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) _set_module(tvt) @@ -2294,7 +2326,7 @@ def get_shape(self) -> Tuple[*Ts]: def __iter__(self): yield self.__unpacked__ - def __init__(self, name, *, default=_marker): + def __init__(self, name, *, default=NoDefault): self.__name__ = name _DefaultMixin.__init__(self, default) @@ -2679,11 +2711,14 @@ def _check_generic(cls, parameters, elen=_marker): if alen < elen: # since we validate TypeVarLike default in _collect_type_vars # or _collect_parameters we can safely check parameters[alen] - if getattr(parameters[alen], '__default__', None) is not None: + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): return - num_default_tv = sum(getattr(p, '__default__', None) - is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) elen -= num_default_tv @@ -2713,11 +2748,14 @@ def _check_generic(cls, parameters, elen): if alen < elen: # since we validate TypeVarLike default in _collect_type_vars # or _collect_parameters we can safely check parameters[alen] - if getattr(parameters[alen], '__default__', None) is not None: + if ( + getattr(parameters[alen], '__default__', NoDefault) + is not NoDefault + ): return - num_default_tv = sum(getattr(p, '__default__', None) - is not None for p in parameters) + num_default_tv = sum(getattr(p, '__default__', NoDefault) + is not NoDefault for p in parameters) elen -= num_default_tv @@ -2747,7 +2785,7 @@ def _collect_type_vars(types, typevar_types=None): t not in tvars and not _is_unpack(t) ): - if getattr(t, '__default__', None) is not None: + if getattr(t, '__default__', NoDefault) is not NoDefault: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2784,7 +2822,7 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - if getattr(t, '__default__', None) is not None: + if getattr(t, '__default__', NoDefault) is not NoDefault: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' From 781e9960fe74676d642617081736756c5572f8fb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 10 May 2024 16:22:04 +0100 Subject: [PATCH 49/75] Remove some unnecessary monkeypatching on Python 3.13+ (#379) --- src/typing_extensions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 70a31193..ec145c0a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2764,7 +2764,8 @@ def _check_generic(cls, parameters, elen): raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" f" for {cls}; actual {alen}, expected {expect_val}") -typing._check_generic = _check_generic +if not _PEP_696_IMPLEMENTED: + typing._check_generic = _check_generic # Python 3.11+ _collect_type_vars was renamed to _collect_parameters if hasattr(typing, '_collect_type_vars'): @@ -2836,7 +2837,8 @@ def _collect_parameters(args): return tuple(parameters) - typing._collect_parameters = _collect_parameters + if not _PEP_696_IMPLEMENTED: + typing._collect_parameters = _collect_parameters # Backport typing.NamedTuple as it exists in Python 3.13. # In 3.11, the ability to define generic `NamedTuple`s was supported. From 12e901ed0cb8a826726357f8020cf3837d961e15 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 12 May 2024 01:46:37 +0100 Subject: [PATCH 50/75] Backport parameter defaults for `(Async)Generator` and `(Async)ContextManager` (#382) --- CHANGELOG.md | 9 ++++ doc/index.rst | 23 ++++++++- src/test_typing_extensions.py | 51 ++++++++++++++------ src/typing_extensions.py | 87 +++++++++++++++++++++++++++++++++-- 4 files changed, 151 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbad4264..5f1b8b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ - At runtime, `assert_never` now includes the repr of the argument in the `AssertionError`. Patch by Hashem, backporting of the original fix https://github.com/python/cpython/pull/91720 by Jelle Zijlstra. +- The second and third parameters of `typing_extensions.Generator`, + and the second parameter of `typing_extensions.AsyncGenerator`, + now default to `None`. This matches the behaviour of `typing.Generator` + and `typing.AsyncGenerator` on Python 3.13+. +- `typing.ContextManager` and `typing.AsyncContextManager` now have an + optional second parameter, which defaults to `Optional[bool]`. The new + parameter signifies the return type of the `__(a)exit__` method, + matching `typing.ContextManager` and `typing.AsyncContextManager` on + Python 3.13+. # Release 4.11.0 (April 5, 2024) diff --git a/doc/index.rst b/doc/index.rst index 3486ae74..0b4c4bda 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -885,8 +885,8 @@ Annotation metadata Pure aliases ~~~~~~~~~~~~ -These are simply re-exported from the :mod:`typing` module on all supported -versions of Python. They are listed here for completeness. +Most of these are simply re-exported from the :mod:`typing` module on all supported +versions of Python, but all are listed here for completeness. .. class:: AbstractSet @@ -904,10 +904,19 @@ versions of Python. They are listed here for completeness. See :py:class:`typing.AsyncContextManager`. In ``typing`` since 3.5.4 and 3.6.2. + .. versionchanged:: 4.12.0 + + ``AsyncContextManager`` now has an optional second parameter, defaulting to + ``Optional[bool]``, signifying the return type of the ``__aexit__`` method. + .. class:: AsyncGenerator See :py:class:`typing.AsyncGenerator`. In ``typing`` since 3.6.1. + .. versionchanged:: 4.12.0 + + The second type parameter is now optional (it defaults to ``None``). + .. class:: AsyncIterable See :py:class:`typing.AsyncIterable`. In ``typing`` since 3.5.2. @@ -956,6 +965,11 @@ versions of Python. They are listed here for completeness. See :py:class:`typing.ContextManager`. In ``typing`` since 3.5.4. + .. versionchanged:: 4.12.0 + + ``AsyncContextManager`` now has an optional second parameter, defaulting to + ``Optional[bool]``, signifying the return type of the ``__aexit__`` method. + .. class:: Coroutine See :py:class:`typing.Coroutine`. In ``typing`` since 3.5.3. @@ -996,6 +1010,11 @@ versions of Python. They are listed here for completeness. .. versionadded:: 4.7.0 + .. versionchanged:: 4.12.0 + + The second type and third type parameters are now optional + (they both default to ``None``). + .. class:: Generic See :py:class:`typing.Generic`. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c7c2f0d5..ad353735 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -41,6 +41,8 @@ from typing_extensions import Doc, NoDefault from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated +NoneType = type(None) + # Flags used to mark tests that only apply after a specific # version of the typing module. TYPING_3_9_0 = sys.version_info[:3] >= (3, 9, 0) @@ -1626,6 +1628,17 @@ async def g(): yield 0 self.assertNotIsInstance(type(g), G) self.assertNotIsInstance(g, G) + def test_generator_default(self): + g1 = typing_extensions.Generator[int] + g2 = typing_extensions.Generator[int, None, None] + self.assertEqual(get_args(g1), (int, type(None), type(None))) + self.assertEqual(get_args(g1), get_args(g2)) + + g3 = typing_extensions.Generator[int, float] + g4 = typing_extensions.Generator[int, float, None] + self.assertEqual(get_args(g3), (int, float, type(None))) + self.assertEqual(get_args(g3), get_args(g4)) + class OtherABCTests(BaseTestCase): @@ -1638,6 +1651,12 @@ def manager(): self.assertIsInstance(cm, typing_extensions.ContextManager) self.assertNotIsInstance(42, typing_extensions.ContextManager) + def test_contextmanager_type_params(self): + cm1 = typing_extensions.ContextManager[int] + self.assertEqual(get_args(cm1), (int, typing.Optional[bool])) + cm2 = typing_extensions.ContextManager[int, None] + self.assertEqual(get_args(cm2), (int, NoneType)) + def test_async_contextmanager(self): class NotACM: pass @@ -1649,11 +1668,20 @@ def manager(): cm = manager() self.assertNotIsInstance(cm, typing_extensions.AsyncContextManager) - self.assertEqual(typing_extensions.AsyncContextManager[int].__args__, (int,)) + self.assertEqual( + typing_extensions.AsyncContextManager[int].__args__, + (int, typing.Optional[bool]) + ) with self.assertRaises(TypeError): isinstance(42, typing_extensions.AsyncContextManager[int]) with self.assertRaises(TypeError): - typing_extensions.AsyncContextManager[int, str] + typing_extensions.AsyncContextManager[int, str, float] + + def test_asynccontextmanager_type_params(self): + cm1 = typing_extensions.AsyncContextManager[int] + self.assertEqual(get_args(cm1), (int, typing.Optional[bool])) + cm2 = typing_extensions.AsyncContextManager[int, None] + self.assertEqual(get_args(cm2), (int, NoneType)) class TypeTests(BaseTestCase): @@ -5533,28 +5561,25 @@ def test_all_names_in___all__(self): self.assertLessEqual(exclude, actual_names) def test_typing_extensions_defers_when_possible(self): - exclude = { - 'dataclass_transform', - 'overload', - 'ParamSpec', - 'TypeVar', - 'TypeVarTuple', - 'get_type_hints', - } + exclude = set() if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'Any', 'NewType'} + exclude |= {'final', 'Any', 'NewType', 'overload'} if sys.version_info < (3, 12): exclude |= { 'SupportsAbs', 'SupportsBytes', 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', - 'SupportsRound', 'Unpack', + 'SupportsRound', 'Unpack', 'dataclass_transform', } if sys.version_info < (3, 13): - exclude |= {'NamedTuple', 'Protocol', 'runtime_checkable'} + exclude |= { + 'NamedTuple', 'Protocol', 'runtime_checkable', 'Generator', + 'AsyncGenerator', 'ContextManager', 'AsyncContextManager', + 'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints', + } if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ec145c0a..b4ca1bc2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,6 +1,7 @@ import abc import collections import collections.abc +import contextlib import functools import inspect import operator @@ -408,17 +409,96 @@ def clear_overloads(): AsyncIterable = typing.AsyncIterable AsyncIterator = typing.AsyncIterator Deque = typing.Deque -ContextManager = typing.ContextManager -AsyncContextManager = typing.AsyncContextManager DefaultDict = typing.DefaultDict OrderedDict = typing.OrderedDict Counter = typing.Counter ChainMap = typing.ChainMap -AsyncGenerator = typing.AsyncGenerator Text = typing.Text TYPE_CHECKING = typing.TYPE_CHECKING +if sys.version_info >= (3, 13, 0, "beta"): + from typing import ContextManager, AsyncContextManager, Generator, AsyncGenerator +else: + def _is_dunder(attr): + return attr.startswith('__') and attr.endswith('__') + + # Python <3.9 doesn't have typing._SpecialGenericAlias + _special_generic_alias_base = getattr( + typing, "_SpecialGenericAlias", typing._GenericAlias + ) + + class _SpecialGenericAlias(_special_generic_alias_base, _root=True): + def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()): + if _special_generic_alias_base is typing._GenericAlias: + # Python <3.9 + self.__origin__ = origin + self._nparams = nparams + super().__init__(origin, nparams, special=True, inst=inst, name=name) + else: + # Python >= 3.9 + super().__init__(origin, nparams, inst=inst, name=name) + self._defaults = defaults + + def __setattr__(self, attr, val): + allowed_attrs = {'_name', '_inst', '_nparams', '_defaults'} + if _special_generic_alias_base is typing._GenericAlias: + # Python <3.9 + allowed_attrs.add("__origin__") + if _is_dunder(attr) or attr in allowed_attrs: + object.__setattr__(self, attr, val) + else: + setattr(self.__origin__, attr, val) + + @typing._tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + msg = "Parameters to generic types must be types." + params = tuple(typing._type_check(p, msg) for p in params) + if ( + self._defaults + and len(params) < self._nparams + and len(params) + len(self._defaults) >= self._nparams + ): + params = (*params, *self._defaults[len(params) - self._nparams:]) + actual_len = len(params) + + if actual_len != self._nparams: + if self._defaults: + expected = f"at least {self._nparams - len(self._defaults)}" + else: + expected = str(self._nparams) + if not self._nparams: + raise TypeError(f"{self} is not a generic class") + raise TypeError( + f"Too {'many' if actual_len > self._nparams else 'few'}" + f" arguments for {self};" + f" actual {actual_len}, expected {expected}" + ) + return self.copy_with(params) + + _NoneType = type(None) + Generator = _SpecialGenericAlias( + collections.abc.Generator, 3, defaults=(_NoneType, _NoneType) + ) + AsyncGenerator = _SpecialGenericAlias( + collections.abc.AsyncGenerator, 2, defaults=(_NoneType,) + ) + ContextManager = _SpecialGenericAlias( + contextlib.AbstractContextManager, + 2, + name="ContextManager", + defaults=(typing.Optional[bool],) + ) + AsyncContextManager = _SpecialGenericAlias( + contextlib.AbstractAsyncContextManager, + 2, + name="AsyncContextManager", + defaults=(typing.Optional[bool],) + ) + + _PROTO_ALLOWLIST = { 'collections.abc': [ 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', @@ -3344,7 +3424,6 @@ def __eq__(self, other: object) -> bool: Dict = typing.Dict ForwardRef = typing.ForwardRef FrozenSet = typing.FrozenSet -Generator = typing.Generator Generic = typing.Generic Hashable = typing.Hashable IO = typing.IO From 76e84d4f36db641149e77febf175716110177b6b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 13 May 2024 11:58:15 -0400 Subject: [PATCH 51/75] Add more tests for `(Async)Generator` (#385) --- src/test_typing_extensions.py | 153 +++++++++++++++++++++++++++++++--- 1 file changed, 142 insertions(+), 11 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ad353735..43b8c095 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1590,12 +1590,92 @@ class MyCounter(typing_extensions.Counter[int]): self.assertIsInstance(d, collections.Counter) self.assertIsInstance(d, typing_extensions.Counter) - def test_async_generator(self): - async def f(): + +# These are a separate TestCase class, +# as (unlike most collections.abc aliases in typing_extensions), +# these are reimplemented on Python <=3.12 so that we can provide +# default values for the second and third parameters +class GeneratorTests(BaseTestCase): + + def test_generator_basics(self): + def foo(): yield 42 + g = foo() + + self.assertIsInstance(g, typing_extensions.Generator) + self.assertNotIsInstance(foo, typing_extensions.Generator) + self.assertIsSubclass(type(g), typing_extensions.Generator) + self.assertNotIsSubclass(type(foo), typing_extensions.Generator) + + parameterized = typing_extensions.Generator[int, str, None] + with self.assertRaises(TypeError): + isinstance(g, parameterized) + with self.assertRaises(TypeError): + issubclass(type(g), parameterized) + + def test_generator_default(self): + g1 = typing_extensions.Generator[int] + g2 = typing_extensions.Generator[int, None, None] + self.assertEqual(get_args(g1), (int, type(None), type(None))) + self.assertEqual(get_args(g1), get_args(g2)) + + g3 = typing_extensions.Generator[int, float] + g4 = typing_extensions.Generator[int, float, None] + self.assertEqual(get_args(g3), (int, float, type(None))) + self.assertEqual(get_args(g3), get_args(g4)) + + def test_no_generator_instantiation(self): + with self.assertRaises(TypeError): + typing_extensions.Generator() + with self.assertRaises(TypeError): + typing_extensions.Generator[T, T, T]() + with self.assertRaises(TypeError): + typing_extensions.Generator[int, int, int]() + + def test_subclassing_generator(self): + class G(typing_extensions.Generator[int, int, None]): + def send(self, value): + pass + def throw(self, typ, val=None, tb=None): + pass + def g(): yield 0 + + self.assertIsSubclass(G, typing_extensions.Generator) + self.assertIsSubclass(G, typing_extensions.Iterable) + self.assertIsSubclass(G, collections.abc.Generator) + self.assertIsSubclass(G, collections.abc.Iterable) + self.assertNotIsSubclass(type(g), G) + + instance = G() + self.assertIsInstance(instance, typing_extensions.Generator) + self.assertIsInstance(instance, typing_extensions.Iterable) + self.assertIsInstance(instance, collections.abc.Generator) + self.assertIsInstance(instance, collections.abc.Iterable) + self.assertNotIsInstance(type(g), G) + self.assertNotIsInstance(g, G) + + def test_async_generator_basics(self): + async def f(): + yield 42 g = f() + + self.assertIsInstance(g, typing_extensions.AsyncGenerator) self.assertIsSubclass(type(g), typing_extensions.AsyncGenerator) + self.assertNotIsInstance(f, typing_extensions.AsyncGenerator) + self.assertNotIsSubclass(type(f), typing_extensions.AsyncGenerator) + + parameterized = typing_extensions.AsyncGenerator[int, str] + with self.assertRaises(TypeError): + isinstance(g, parameterized) + with self.assertRaises(TypeError): + issubclass(type(g), parameterized) + + def test_async_generator_default(self): + ag1 = typing_extensions.AsyncGenerator[int] + ag2 = typing_extensions.AsyncGenerator[int, None] + self.assertEqual(get_args(ag1), (int, type(None))) + self.assertEqual(get_args(ag1), get_args(ag2)) def test_no_async_generator_instantiation(self): with self.assertRaises(TypeError): @@ -1628,16 +1708,67 @@ async def g(): yield 0 self.assertNotIsInstance(type(g), G) self.assertNotIsInstance(g, G) - def test_generator_default(self): - g1 = typing_extensions.Generator[int] - g2 = typing_extensions.Generator[int, None, None] - self.assertEqual(get_args(g1), (int, type(None), type(None))) - self.assertEqual(get_args(g1), get_args(g2)) + def test_subclassing_subclasshook(self): - g3 = typing_extensions.Generator[int, float] - g4 = typing_extensions.Generator[int, float, None] - self.assertEqual(get_args(g3), (int, float, type(None))) - self.assertEqual(get_args(g3), get_args(g4)) + class Base(typing_extensions.Generator): + @classmethod + def __subclasshook__(cls, other): + if other.__name__ == 'Foo': + return True + else: + return False + + class C(Base): ... + class Foo: ... + class Bar: ... + self.assertIsSubclass(Foo, Base) + self.assertIsSubclass(Foo, C) + self.assertNotIsSubclass(Bar, C) + + def test_subclassing_register(self): + + class A(typing_extensions.Generator): ... + class B(A): ... + + class C: ... + A.register(C) + self.assertIsSubclass(C, A) + self.assertNotIsSubclass(C, B) + + class D: ... + B.register(D) + self.assertIsSubclass(D, A) + self.assertIsSubclass(D, B) + + class M(): ... + collections.abc.Generator.register(M) + self.assertIsSubclass(M, typing_extensions.Generator) + + def test_collections_as_base(self): + + class M(collections.abc.Generator): ... + self.assertIsSubclass(M, typing_extensions.Generator) + self.assertIsSubclass(M, typing_extensions.Iterable) + + class S(collections.abc.AsyncGenerator): ... + self.assertIsSubclass(S, typing_extensions.AsyncGenerator) + self.assertIsSubclass(S, typing_extensions.AsyncIterator) + + class A(collections.abc.Generator, metaclass=abc.ABCMeta): ... + class B: ... + A.register(B) + self.assertIsSubclass(B, typing_extensions.Generator) + + @skipIf(sys.version_info < (3, 10), "PEP 604 has yet to be") + def test_or_and_ror(self): + self.assertEqual( + typing_extensions.Generator | typing_extensions.AsyncGenerator, + Union[typing_extensions.Generator, typing_extensions.AsyncGenerator] + ) + self.assertEqual( + typing_extensions.Generator | typing.Deque, + Union[typing_extensions.Generator, typing.Deque] + ) class OtherABCTests(BaseTestCase): From a370bf69c9691e159f02bec17cd40f77d3b29635 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 13 May 2024 12:05:38 -0400 Subject: [PATCH 52/75] Fix some docs typos (#386) --- CHANGELOG.md | 11 ++++++----- doc/index.rst | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1b8b43..e0a633bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,11 +26,12 @@ and the second parameter of `typing_extensions.AsyncGenerator`, now default to `None`. This matches the behaviour of `typing.Generator` and `typing.AsyncGenerator` on Python 3.13+. -- `typing.ContextManager` and `typing.AsyncContextManager` now have an - optional second parameter, which defaults to `Optional[bool]`. The new - parameter signifies the return type of the `__(a)exit__` method, - matching `typing.ContextManager` and `typing.AsyncContextManager` on - Python 3.13+. +- `typing_extensions.ContextManager` and + `typing_extensions.AsyncContextManager` now have an optional second + parameter, which defaults to `Optional[bool]`. The new parameter + signifies the return type of the `__(a)exit__` method, matching + `typing.ContextManager` and `typing.AsyncContextManager` on Python + 3.13+. # Release 4.11.0 (April 5, 2024) diff --git a/doc/index.rst b/doc/index.rst index 0b4c4bda..5863fe4c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -967,8 +967,8 @@ versions of Python, but all are listed here for completeness. .. versionchanged:: 4.12.0 - ``AsyncContextManager`` now has an optional second parameter, defaulting to - ``Optional[bool]``, signifying the return type of the ``__aexit__`` method. + ``ContextManager`` now has an optional second parameter, defaulting to + ``Optional[bool]``, signifying the return type of the ``__exit__`` method. .. class:: Coroutine From 08c066e3d99ce9b31380b6bdf3ea54172b6a8851 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 13 May 2024 12:26:54 -0400 Subject: [PATCH 53/75] Don't import anything from `typing.py` in our tests (#387) --- src/test_typing_extensions.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 43b8c095..f3bfae1c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -21,11 +21,6 @@ from unittest import TestCase, main, skipUnless, skipIf from unittest.mock import patch import typing -from typing import Optional, Union, AnyStr -from typing import T, KT, VT # Not in __all__. -from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable -from typing import Generic -from typing import no_type_check import warnings import typing_extensions @@ -36,12 +31,15 @@ from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload -from typing_extensions import NamedTuple, TypeIs +from typing_extensions import NamedTuple, TypeIs, no_type_check, Dict from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol -from typing_extensions import Doc, NoDefault +from typing_extensions import Doc, NoDefault, List, Union, AnyStr, Iterable, Generic, Optional, Set, Tuple, Callable from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated NoneType = type(None) +T = TypeVar("T") +KT = TypeVar("KT") +VT = TypeVar("VT") # Flags used to mark tests that only apply after a specific # version of the typing module. @@ -67,7 +65,7 @@ ) ANN_MODULE_SOURCE = '''\ -from typing import Optional +from typing import List, Optional from functools import wraps __annotations__[1] = 2 From 12a0f286e681ea7cbc40df1cc774e1dde42a5a07 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 14 May 2024 21:10:32 -0400 Subject: [PATCH 54/75] Backport `types.CapsuleType` (#390) --- CHANGELOG.md | 1 + doc/index.rst | 14 ++++++++++++++ src/test_typing_extensions.py | 10 ++++++++++ src/typing_extensions.py | 17 +++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0a633bd..783821fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ signifies the return type of the `__(a)exit__` method, matching `typing.ContextManager` and `typing.AsyncContextManager` on Python 3.13+. +- Backport `types.CapsuleType` from Python 3.13. # Release 4.11.0 (April 5, 2024) diff --git a/doc/index.rst b/doc/index.rst index 5863fe4c..be4b4ae3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -882,6 +882,20 @@ Annotation metadata The documentation string passed to :class:`Doc`. +Capsule objects +~~~~~~~~~~~~~~~ + +.. class:: CapsuleType + + The type of :py:ref:`capsule objects `. + See :py:class:`types.CapsuleType`, where it has existed since Python 3.13. + + Note that this may not exist on all implementations of Python; it is only + guaranteed to exist on CPython. + + .. versionadded:: 4.12.0 + + Pure aliases ~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f3bfae1c..c031cf32 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6765,5 +6765,15 @@ def test_pickle(self): self.assertEqual(doc_info, pickle.loads(pickled)) +@skipUnless( + hasattr(typing_extensions, "CapsuleType"), + "CapsuleType is not available on all Python implementations" +) +class CapsuleTypeTests(BaseTestCase): + def test_capsule_type(self): + import _datetime + self.assertIsInstance(_datetime.datetime_CAPI, typing_extensions.CapsuleType) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b4ca1bc2..c1c3c63a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3411,6 +3411,23 @@ def __eq__(self, other: object) -> bool: return self.documentation == other.documentation +_CapsuleType = getattr(_types, "CapsuleType", None) + +if _CapsuleType is None: + try: + import _socket + except ImportError: + pass + else: + _CAPI = getattr(_socket, "CAPI", None) + if _CAPI is not None: + _CapsuleType = type(_CAPI) + +if _CapsuleType is not None: + CapsuleType = _CapsuleType + __all__.append("CapsuleType") + + # Aliases for items that have always been in typing. # Explicitly assign these (rather than using `from typing import *` at the top), # so that we get a CI error if one of these is deleted from typing.py From 0bdf828d73e3bbe71ffc1d65c543acb3f62f78bd Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 15 May 2024 08:25:25 -0400 Subject: [PATCH 55/75] Improve the backport of `NoDefault` (#388) --- src/test_typing_extensions.py | 12 ++++++++++++ src/typing_extensions.py | 13 +++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c031cf32..0a6bba5a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6439,6 +6439,10 @@ def test_pickling(self): loaded = pickle.loads(s) self.assertIs(NoDefault, loaded) + @skip_if_py313_beta_1 + def test_doc(self): + self.assertIsInstance(NoDefault.__doc__, str) + def test_constructor(self): self.assertIs(NoDefault, type(NoDefault)()) with self.assertRaises(TypeError): @@ -6455,6 +6459,14 @@ def test_no_call(self): def test_immutable(self): with self.assertRaises(AttributeError): NoDefault.foo = 'bar' + with self.assertRaises(AttributeError): + NoDefault.foo + + # TypeError is consistent with the behavior of NoneType + with self.assertRaises(TypeError): + type(NoDefault).foo = 3 + with self.assertRaises(AttributeError): + type(NoDefault).foo class TypeVarInferVarianceTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c1c3c63a..ab552c71 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1440,7 +1440,16 @@ def TypeAlias(self, parameters): if hasattr(typing, "NoDefault"): NoDefault = typing.NoDefault else: - class NoDefaultType: + class NoDefaultTypeMeta(type): + def __setattr__(cls, attr, value): + # TypeError is consistent with the behavior of NoneType + raise TypeError( + f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" + ) + + class NoDefaultType(metaclass=NoDefaultTypeMeta): + """The type of the NoDefault singleton.""" + __slots__ = () def __new__(cls): @@ -1453,7 +1462,7 @@ def __reduce__(self): return "NoDefault" NoDefault = NoDefaultType() - del NoDefaultType + del NoDefaultType, NoDefaultTypeMeta def _set_default(type_param, default): From 028035e9efb9b1b647c1c777caf7a0276ad9a504 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 15 May 2024 08:27:32 -0400 Subject: [PATCH 56/75] Backport banning type parameters with defaults after TypeVarTuples (#389) --- CHANGELOG.md | 3 +++ doc/index.rst | 6 +++++ src/test_typing_extensions.py | 29 ++++++++++++++++++++++- src/typing_extensions.py | 43 +++++++++++++++++++++++++++++------ 4 files changed, 73 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 783821fd..5572a87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ at runtime rather than `types.NoneType`. - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python 3.13.0b1 and newer. +- It is now disallowed to use a `TypeVar` with a default value after a + `TypeVarTuple` in a type parameter list. This matches the CPython + implementation of PEP 696 on Python 3.13+. - Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of diff --git a/doc/index.rst b/doc/index.rst index be4b4ae3..1801fd45 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -549,6 +549,12 @@ Special typing primitives TypeVarTuples now have a ``has_default()`` method, for compatibility with :py:class:`typing.TypeVarTuple` on Python 3.13+. + .. versionchanged:: 4.12.0 + + It is now disallowed to use a `TypeVar` with a default value after a + `TypeVarTuple` in a type parameter list. This matches the CPython + implementation of PEP 696 on Python 3.13+. + .. data:: Unpack See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0a6bba5a..820833a9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6380,6 +6380,14 @@ class A(Generic[P]): ... self.assertIs(P_default.__default__, ...) self.assertTrue(P_default.has_default()) + def test_paramspec_none(self): + U = ParamSpec('U') + U_None = ParamSpec('U_None', default=None) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertIs(U_None.__default__, None) + self.assertTrue(U_None.has_default()) + def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) @@ -6394,7 +6402,26 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] - def test_erroneous_generic(self): + def test_no_default_after_typevar_tuple(self): + T = TypeVar("T", default=int) + Ts = TypeVarTuple("Ts") + Ts_default = TypeVarTuple("Ts_default", default=Unpack[Tuple[str, int]]) + + with self.assertRaises(TypeError): + class X(Generic[Unpack[Ts], T]): ... + + with self.assertRaises(TypeError): + class Y(Generic[Unpack[Ts_default], T]): ... + + def test_typevartuple_none(self): + U = TypeVarTuple('U') + U_None = TypeVarTuple('U_None', default=None) + self.assertIs(U.__default__, NoDefault) + self.assertFalse(U.has_default()) + self.assertIs(U_None.__default__, None) + self.assertTrue(U_None.has_default()) + + def test_no_default_after_non_default(self): DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) T = TypeVar('T') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ab552c71..cf8bde47 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2856,6 +2856,21 @@ def _check_generic(cls, parameters, elen): if not _PEP_696_IMPLEMENTED: typing._check_generic = _check_generic + +_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} + + +def _is_unpacked_typevartuple(x) -> bool: + if get_origin(x) is not Unpack: + return False + args = get_args(x) + return ( + bool(args) + and len(args) == 1 + and type(args[0]) in _TYPEVARTUPLE_TYPES + ) + + # Python 3.11+ _collect_type_vars was renamed to _collect_parameters if hasattr(typing, '_collect_type_vars'): def _collect_type_vars(types, typevar_types=None): @@ -2869,13 +2884,17 @@ def _collect_type_vars(types, typevar_types=None): tvars = [] # required TypeVarLike cannot appear after TypeVarLike with default default_encountered = False + # or after TypeVarTuple + type_var_tuple_encountered = False for t in types: - if ( - isinstance(t, typevar_types) and - t not in tvars and - not _is_unpack(t) - ): - if getattr(t, '__default__', NoDefault) is not NoDefault: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True + elif isinstance(t, typevar_types) and t not in tvars: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if has_default: + if type_var_tuple_encountered: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2899,6 +2918,8 @@ def _collect_parameters(args): parameters = [] # required TypeVarLike cannot appear after TypeVarLike with default default_encountered = False + # or after TypeVarTuple + type_var_tuple_encountered = False for t in args: if isinstance(t, type): # We don't want __parameters__ descriptor of a bare Python class. @@ -2912,7 +2933,13 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - if getattr(t, '__default__', NoDefault) is not NoDefault: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + + if type_var_tuple_encountered and has_default: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + + if has_default: default_encountered = True elif default_encountered: raise TypeError(f'Type parameter {t!r} without a default' @@ -2920,6 +2947,8 @@ def _collect_parameters(args): parameters.append(t) else: + if _is_unpacked_typevartuple(t): + type_var_tuple_encountered = True for x in getattr(t, '__parameters__', ()): if x not in parameters: parameters.append(x) From 479ac33a0c97ea91c8177f6b6887ff7a9fb9970d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 15 May 2024 20:59:25 -0400 Subject: [PATCH 57/75] Allow type parameters without default values to follow those with default values in some situations (#392) --- CHANGELOG.md | 4 ++ src/test_typing_extensions.py | 19 ++++++++++ src/typing_extensions.py | 70 ++++++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5572a87a..88445bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ at runtime rather than `types.NoneType`. - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python 3.13.0b1 and newer. +- Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774), + allowing type parameters without default values to follow those with + default values in some type parameter lists. Patch by Alex Waygood, + backporting a CPython PR by Jelle Zijlstra. - It is now disallowed to use a `TypeVar` with a default value after a `TypeVarTuple` in a type parameter list. This matches the CPython implementation of PEP 696 on Python 3.13+. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 820833a9..a0e55f0d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6457,6 +6457,25 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + @skip_if_py313_beta_1 + def test_allow_default_after_non_default_in_alias(self): + T_default = TypeVar('T_default', default=int) + T = TypeVar('T') + Ts = TypeVarTuple('Ts') + + a1 = Callable[[T_default], T] + self.assertEqual(a1.__args__, (T_default, T)) + + if sys.version_info >= (3, 9): + a2 = dict[T_default, T] + self.assertEqual(a2.__args__, (T_default, T)) + + a3 = typing.Dict[T_default, T] + self.assertEqual(a3.__args__, (T_default, T)) + + a4 = Callable[[Unpack[Ts]], T] + self.assertEqual(a4.__args__, (Unpack[Ts], T)) + class NoDefaultTests(BaseTestCase): @skip_if_py313_beta_1 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cf8bde47..f6039883 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2857,6 +2857,18 @@ def _check_generic(cls, parameters, elen): typing._check_generic = _check_generic +def _has_generic_or_protocol_as_origin() -> bool: + try: + frame = sys._getframe(2) + # not all platforms have sys._getframe() + except AttributeError: + return False # err on the side of leniency + else: + return frame.f_locals.get("origin") in { + typing.Generic, Protocol, typing.Protocol + } + + _TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} @@ -2882,23 +2894,29 @@ def _collect_type_vars(types, typevar_types=None): if typevar_types is None: typevar_types = typing.TypeVar tvars = [] - # required TypeVarLike cannot appear after TypeVarLike with default + + # A required TypeVarLike cannot appear after a TypeVarLike with a default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() default_encountered = False - # or after TypeVarTuple + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple type_var_tuple_encountered = False + for t in types: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True elif isinstance(t, typevar_types) and t not in tvars: - has_default = getattr(t, '__default__', NoDefault) is not NoDefault - if has_default: - if type_var_tuple_encountered: - raise TypeError('Type parameter with a default' - ' follows TypeVarTuple') - default_encountered = True - elif default_encountered: - raise TypeError(f'Type parameter {t!r} without a default' - ' follows type parameter with a default') + if enforce_default_ordering: + has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if has_default: + if type_var_tuple_encountered: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') tvars.append(t) if _should_collect_from_parameters(t): @@ -2916,10 +2934,15 @@ def _collect_parameters(args): assert _collect_parameters((T, Callable[P, T])) == (T, P) """ parameters = [] - # required TypeVarLike cannot appear after TypeVarLike with default + + # A required TypeVarLike cannot appear after a TypeVarLike with default + # if it was a direct call to `Generic[]` or `Protocol[]` + enforce_default_ordering = _has_generic_or_protocol_as_origin() default_encountered = False - # or after TypeVarTuple + + # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple type_var_tuple_encountered = False + for t in args: if isinstance(t, type): # We don't want __parameters__ descriptor of a bare Python class. @@ -2933,17 +2956,20 @@ def _collect_parameters(args): parameters.append(collected) elif hasattr(t, '__typing_subst__'): if t not in parameters: - has_default = getattr(t, '__default__', NoDefault) is not NoDefault + if enforce_default_ordering: + has_default = ( + getattr(t, '__default__', NoDefault) is not NoDefault + ) - if type_var_tuple_encountered and has_default: - raise TypeError('Type parameter with a default' - ' follows TypeVarTuple') + if type_var_tuple_encountered and has_default: + raise TypeError('Type parameter with a default' + ' follows TypeVarTuple') - if has_default: - default_encountered = True - elif default_encountered: - raise TypeError(f'Type parameter {t!r} without a default' - ' follows type parameter with a default') + if has_default: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') parameters.append(t) else: From 23378be090232065e8cf106cf8a513995448d93e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 07:42:04 -0400 Subject: [PATCH 58/75] Don't turn list defaults for ParamSpecs into tuples (#394) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 17 ++++++++++++++--- src/typing_extensions.py | 11 +---------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88445bc6..83f297f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ - It is now disallowed to use a `TypeVar` with a default value after a `TypeVarTuple` in a type parameter list. This matches the CPython implementation of PEP 696 on Python 3.13+. +- Fix bug in PEP-696 implementation where default values for `ParamSpec`s + would be cast to tuples if a list was provided as the default value. + Patch by Alex Waygood. - Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a0e55f0d..fa04e59e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4997,7 +4997,7 @@ def test_pickle(self): P = ParamSpec('P') P_co = ParamSpec('P_co', covariant=True) P_contra = ParamSpec('P_contra', contravariant=True) - P_default = ParamSpec('P_default', default=int) + P_default = ParamSpec('P_default', default=[int]) for proto in range(pickle.HIGHEST_PROTOCOL): with self.subTest(f'Pickle protocol {proto}'): for paramspec in (P, P_co, P_contra, P_default): @@ -6363,8 +6363,8 @@ def test_typevar_none(self): self.assertTrue(U_None.has_default()) def test_paramspec(self): - P = ParamSpec('P', default=(str, int)) - self.assertEqual(P.__default__, (str, int)) + P = ParamSpec('P', default=[str, int]) + self.assertEqual(P.__default__, [str, int]) self.assertTrue(P.has_default()) self.assertIsInstance(P, ParamSpec) if hasattr(typing, "ParamSpec"): @@ -6457,6 +6457,17 @@ def test_pickle(self): self.assertEqual(z.__bound__, typevar.__bound__) self.assertEqual(z.__default__, typevar.__default__) + def test_strange_defaults_are_allowed(self): + # Leave it to type checkers to check whether strange default values + # should be allowed or disallowed + def not_a_type(): ... + + for typevarlike_cls in TypeVar, ParamSpec, TypeVarTuple: + for default in not_a_type, 42, bytearray(), (int, not_a_type, 42): + with self.subTest(typevarlike_cls=typevarlike_cls, default=default): + T = typevarlike_cls("T", default=default) + self.assertEqual(T.__default__, default) + @skip_if_py313_beta_1 def test_allow_default_after_non_default_in_alias(self): T_default = TypeVar('T_default', default=int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f6039883..07def90e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1467,16 +1467,7 @@ def __reduce__(self): def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault - if isinstance(default, (tuple, list)): - type_param.__default__ = tuple((typing._type_check(d, "Default must be a type") - for d in default)) - elif default in (None, NoDefault): - type_param.__default__ = default - else: - if isinstance(type_param, ParamSpec) and default is ...: # ... not valid <3.11 - type_param.__default__ = default - else: - type_param.__default__ = typing._type_check(default, "Default must be a type") + type_param.__default__ = default def _set_module(typevarlike): From 074d053727f8c658e65dc9a3ccd9760421de52a9 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 16 May 2024 12:40:31 -0400 Subject: [PATCH 59/75] Backport PEP-696 specialisation on Python >=3.11.1 (#397) --- src/test_typing_extensions.py | 68 ++++++++++++++++++++++++ src/typing_extensions.py | 97 +++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fa04e59e..ae6ad076 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6402,6 +6402,34 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + @skipIf( + sys.version_info < (3, 11, 1), + "Not yet backported for older versions of Python" + ) + def test_typevartuple_specialization(self): + T = TypeVar("T") + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) + self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + class A(Generic[T, Unpack[Ts]]): ... + self.assertEqual(A[float].__args__, (float, str, int)) + self.assertEqual(A[float, range].__args__, (float, range)) + self.assertEqual(A[float, Unpack[tuple[int, ...]]].__args__, (float, Unpack[tuple[int, ...]])) + + @skipIf( + sys.version_info < (3, 11, 1), + "Not yet backported for older versions of Python" + ) + def test_typevar_and_typevartuple_specialization(self): + T = TypeVar("T") + U = TypeVar("U", default=float) + Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) + self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + class A(Generic[T, U, Unpack[Ts]]): ... + self.assertEqual(A[int].__args__, (int, float, str, int)) + self.assertEqual(A[int, str].__args__, (int, str, str, int)) + self.assertEqual(A[int, str, range].__args__, (int, str, range)) + self.assertEqual(A[int, str, Unpack[tuple[int, ...]]].__args__, (int, str, Unpack[tuple[int, ...]])) + def test_no_default_after_typevar_tuple(self): T = TypeVar("T", default=int) Ts = TypeVarTuple("Ts") @@ -6487,6 +6515,46 @@ def test_allow_default_after_non_default_in_alias(self): a4 = Callable[[Unpack[Ts]], T] self.assertEqual(a4.__args__, (Unpack[Ts], T)) + @skipIf( + sys.version_info < (3, 11, 1), + "Not yet backported for older versions of Python" + ) + def test_paramspec_specialization(self): + T = TypeVar("T") + P = ParamSpec('P', default=[str, int]) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, P]): ... + self.assertEqual(A[float].__args__, (float, (str, int))) + self.assertEqual(A[float, [range]].__args__, (float, (range,))) + + @skipIf( + sys.version_info < (3, 11, 1), + "Not yet backported for older versions of Python" + ) + def test_typevar_and_paramspec_specialization(self): + T = TypeVar("T") + U = TypeVar("U", default=float) + P = ParamSpec('P', default=[str, int]) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, U, P]): ... + self.assertEqual(A[float].__args__, (float, float, (str, int))) + self.assertEqual(A[float, int].__args__, (float, int, (str, int))) + self.assertEqual(A[float, int, [range]].__args__, (float, int, (range,))) + + @skipIf( + sys.version_info < (3, 11, 1), + "Not yet backported for older versions of Python" + ) + def test_paramspec_and_typevar_specialization(self): + T = TypeVar("T") + P = ParamSpec('P', default=[str, int]) + U = TypeVar("U", default=float) + self.assertEqual(P.__default__, [str, int]) + class A(Generic[T, P, U]): ... + self.assertEqual(A[float].__args__, (float, (str, int), float)) + self.assertEqual(A[float, [range]].__args__, (float, (range,), float)) + self.assertEqual(A[float, [range], int].__args__, (float, (range,), int)) + class NoDefaultTests(BaseTestCase): @skip_if_py313_beta_1 diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 07def90e..d549fb69 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1513,8 +1513,19 @@ def __new__(cls, name, *constraints, bound=None, if infer_variance and (covariant or contravariant): raise ValueError("Variance cannot be specified with infer_variance.") typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) _set_module(typevar) + + def _tvar_prepare_subst(alias, args): + if ( + typevar.has_default() + and alias.__parameters__.index(typevar) == len(args) + ): + args += (typevar.__default__,) + return args + + typevar.__typing_prepare_subst__ = _tvar_prepare_subst return typevar def __init_subclass__(cls) -> None: @@ -1613,6 +1624,24 @@ def __new__(cls, name, *, bound=None, _set_default(paramspec, default) _set_module(paramspec) + + def _paramspec_prepare_subst(alias, args): + params = alias.__parameters__ + i = params.index(paramspec) + if i == len(args) and paramspec.has_default(): + args = [*args, paramspec.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {alias}") + # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612. + if len(params) == 1 and not typing._is_param_expr(args[0]): + assert i == 0 + args = (args,) + # Convert lists to tuples to help other libraries cache the results. + elif isinstance(args[i], list): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + return args + + paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst return paramspec def __init_subclass__(cls) -> None: @@ -2311,6 +2340,17 @@ def __init__(self, getitem): class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + @_UnpackSpecialForm def Unpack(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2340,6 +2380,16 @@ def _is_unpack(obj): elif hasattr(typing, "TypeVarTuple"): # 3.11+ + def _unpack_args(*args): + newargs = [] + for arg in args: + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs is not None and not (subargs and subargs[-1] is ...): + newargs.extend(subargs) + else: + newargs.append(arg) + return newargs + # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): """Type variable tuple.""" @@ -2350,6 +2400,53 @@ def __new__(cls, name, *, default=NoDefault): tvt = typing.TypeVarTuple(name) _set_default(tvt, default) _set_module(tvt) + + def _typevartuple_prepare_subst(alias, args): + params = alias.__parameters__ + typevartuple_index = params.index(tvt) + for param in params[typevartuple_index + 1:]: + if isinstance(param, TypeVarTuple): + raise TypeError( + f"More than one TypeVarTuple parameter in {alias}" + ) + + alen = len(args) + plen = len(params) + left = typevartuple_index + right = plen - typevartuple_index - 1 + var_tuple_index = None + fillarg = None + for k, arg in enumerate(args): + if not isinstance(arg, type): + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs and len(subargs) == 2 and subargs[-1] is ...: + if var_tuple_index is not None: + raise TypeError( + "More than one unpacked " + "arbitrary-length tuple argument" + ) + var_tuple_index = k + fillarg = subargs[0] + if var_tuple_index is not None: + left = min(left, var_tuple_index) + right = min(right, alen - var_tuple_index - 1) + elif left + right > alen: + raise TypeError(f"Too few arguments for {alias};" + f" actual {alen}, expected at least {plen - 1}") + if left == alen - right and tvt.has_default(): + replacement = _unpack_args(tvt.__default__) + else: + replacement = args[left: alen - right] + + return ( + *args[:left], + *([fillarg] * (typevartuple_index - left)), + replacement, + *([fillarg] * (plen - right - left - typevartuple_index - 1)), + *args[alen - right:], + ) + + tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst return tvt def __init_subclass__(self, *args, **kwds): From 63d827754489f307630dea2e0f58dfab65837a66 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 14:58:06 -0400 Subject: [PATCH 60/75] Add workflow for Trusted Publishing (#395) Co-authored-by: Sebastian Rittau --- .github/workflows/package.yml | 76 ----------------- .github/workflows/publish.yml | 149 ++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + scripts/check_package.py | 60 ++++++++++++++ src/test_typing_extensions.py | 4 +- 5 files changed, 211 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/package.yml create mode 100644 .github/workflows/publish.yml create mode 100644 scripts/check_package.py diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml deleted file mode 100644 index 6b55f10e..00000000 --- a/.github/workflows/package.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Test packaging - -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - wheel: - name: Test wheel install - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3 - - - name: Install pypa/build - 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 - python -m pip install --upgrade build - python -m pip list - - - name: Build and install wheel - run: | - python -m build . - export path_to_file=$(find dist -type f -name "typing_extensions-*.whl") - echo "::notice::Installing wheel: $path_to_file" - pip install -vvv $path_to_file - python -m pip list - - - name: Attempt to import typing_extensions - run: python -c "import typing_extensions; print(typing_extensions.__all__)" - - sdist: - name: Test sdist install - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3 - - - name: Install pypa/build - 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 - python -m pip install --upgrade build - python -m pip list - - - name: Build and install sdist - run: | - python -m build . - export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz") - echo "::notice::Installing sdist: $path_to_file" - pip install -vvv $path_to_file - python -m pip list - - - name: Attempt to import typing_extensions - run: python -c "import typing_extensions; print(typing_extensions.__all__)" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..9b69e73e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,149 @@ +# Based on +# https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + +name: Test builds and publish Python distribution to PyPI + +on: + release: + types: [published] + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Check package metadata + run: python scripts/check_package.py ${{ github.ref }} + - name: Install pypa/build + 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 + python -m pip install --upgrade build + python -m pip list + - name: Build a binary wheel and a source tarball + run: python -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + test-wheel: + name: Test wheel + needs: + - build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Install wheel + run: | + export path_to_file=$(find dist -type f -name "typing_extensions-*.whl") + echo "::notice::Installing wheel: $path_to_file" + python -m pip install --user $path_to_file + python -m pip list + - name: Run typing_extensions tests against installed package + run: rm src/typing_extensions.py && python src/test_typing_extensions.py + + test-sdist: + name: Test source distribution + needs: + - build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Unpack and test source distribution + run: | + export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz") + echo "::notice::Unpacking source distribution: $path_to_file" + tar xzf $path_to_file -C dist/ + cd ${path_to_file%.tar.gz} + python src/test_typing_extensions.py + + test-sdist-installed: + name: Test installed source distribution + needs: + - build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Install source distribution + run: | + export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz") + echo "::notice::Installing source distribution: $path_to_file" + python -m pip install --user $path_to_file + python -m pip list + - name: Run typing_extensions tests against installed package + run: rm src/typing_extensions.py && python src/test_typing_extensions.py + + publish-to-pypi: + name: >- + Publish Python distribution to PyPI + if: github.event_name == 'release' # only publish to PyPI on releases + needs: + - test-sdist + - test-sdist-installed + - test-wheel + - build + runs-on: ubuntu-latest + environment: + name: publish + url: https://pypi.org/p/typing-extensions + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Ensure exactly one sdist and one wheel have been downloaded + run: test $(ls *.tar.gz | wc -l) = 1 && test $(ls *.whl | wc -l) = 1 + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/pyproject.toml b/pyproject.toml index 4b1a7601..a59c9a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development", ] diff --git a/scripts/check_package.py b/scripts/check_package.py new file mode 100644 index 00000000..f52df411 --- /dev/null +++ b/scripts/check_package.py @@ -0,0 +1,60 @@ +import argparse +import re +import sys +import tomllib +from pathlib import Path + + +class ValidationError(Exception): + pass + + +def check(github_ref: str | None) -> None: + pyproject = Path(__file__).parent.parent / "pyproject.toml" + if not pyproject.exists(): + raise ValidationError("pyproject.toml not found") + with pyproject.open("rb") as f: + data = tomllib.load(f) + pyproject_version = data["project"]["version"] + + if github_ref is not None and github_ref.startswith("refs/tags/"): + version = github_ref.removeprefix("refs/tags/") + if version != pyproject_version: + raise ValidationError( + f"Version mismatch: GitHub ref is {version}, " + f"but pyproject.toml is {pyproject_version}" + ) + + requires_python = data["project"]["requires-python"] + assert sys.version_info[0] == 3, "Rewrite this script when Python 4 comes out" + match = re.fullmatch(r">=3\.(\d+)", requires_python) + if not match: + raise ValidationError(f"Invalid requires-python: {requires_python!r}") + lowest_minor = int(match.group(1)) + + description = data["project"]["description"] + if not description.endswith(f"3.{lowest_minor}+"): + raise ValidationError(f"Description should mention Python 3.{lowest_minor}+") + + classifiers = set(data["project"]["classifiers"]) + for should_be_supported in range(lowest_minor, sys.version_info[1] + 1): + if ( + f"Programming Language :: Python :: 3.{should_be_supported}" + not in classifiers + ): + raise ValidationError( + f"Missing classifier for Python 3.{should_be_supported}" + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("Script to check the package metadata") + parser.add_argument( + "github_ref", type=str, help="The current GitHub ref", nargs="?" + ) + args = parser.parse_args() + try: + check(args.github_ref) + except ValidationError as e: + print(e) + sys.exit(1) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ae6ad076..5d37b5bf 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,5 +1,4 @@ import sys -import os import abc import gc import io @@ -5718,8 +5717,7 @@ def test_typing_extensions_defers_when_possible(self): getattr(typing, item)) def test_typing_extensions_compiles_with_opt(self): - file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'typing_extensions.py') + file_path = typing_extensions.__file__ try: subprocess.check_output(f'{sys.executable} -OO {file_path}', stderr=subprocess.STDOUT, From 21fde1f0d04f78e080ca4f6119293b607d9a0475 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 15:08:31 -0400 Subject: [PATCH 61/75] Prepare releaes 4.12.0a1 (#398) --- CHANGELOG.md | 5 ++++- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f297f2..d6535df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# Unreleased +# Release 4.12.0a1 + +This release primarily tests a revised release workflow. If all goes +well, release 4.12.0rc1 will follow soon. - Backport the `typing.NoDefault` sentinel object from Python 3.13. TypeVars, ParamSpecs and TypeVarTuples without default values now have diff --git a/pyproject.toml b/pyproject.toml index a59c9a0b..a557efd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.11.0" +version = "4.12.0a1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 465ba786ca98b99e75905fe2aaa98131ef27d54c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 15:16:41 -0400 Subject: [PATCH 62/75] Fix publish workflow (#399) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9b69e73e..fc787c9a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -144,6 +144,6 @@ jobs: name: python-package-distributions path: dist/ - name: Ensure exactly one sdist and one wheel have been downloaded - run: test $(ls *.tar.gz | wc -l) = 1 && test $(ls *.whl | wc -l) = 1 + run: test $(ls dist/*.tar.gz | wc -l) = 1 && test $(ls dist/*.whl | wc -l) = 1 - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 From 72298f05fc520f05ca9a280f5ed0fa3e5de27041 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 15:18:42 -0400 Subject: [PATCH 63/75] 4.12.0a2 (#400) --- CHANGELOG.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6535df7..c0e2530e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -# Release 4.12.0a1 +# Release 4.12.0a1 and 4.12.0a2 -This release primarily tests a revised release workflow. If all goes +These releases primarily test a revised release workflow. If all goes well, release 4.12.0rc1 will follow soon. - Backport the `typing.NoDefault` sentinel object from Python 3.13. diff --git a/pyproject.toml b/pyproject.toml index a557efd1..d829337d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.0a1" +version = "4.12.0a2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 1da5d3d24441cc059fe4d49131588b719ee41d59 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 16 May 2024 21:20:50 +0200 Subject: [PATCH 64/75] Update actions/setup-python (#401) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fc787c9a..d1da0258 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -104,7 +104,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Download all the dists From 0dbc7c971f7ec61f12a1466f8d8f222bdf9a4b31 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 15:55:11 -0400 Subject: [PATCH 65/75] Prepare release 4.12.0rc1 (#402) Co-authored-by: Alex Waygood --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0e2530e..d1ac543b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,29 +1,37 @@ -# Release 4.12.0a1 and 4.12.0a2 - -These releases primarily test a revised release workflow. If all goes -well, release 4.12.0rc1 will follow soon. - -- Backport the `typing.NoDefault` sentinel object from Python 3.13. - TypeVars, ParamSpecs and TypeVarTuples without default values now have - their `__default__` attribute set to this sentinel value. -- TypeVars, ParamSpecs and TypeVarTuples now have a `has_default()` - method, matching `typing.TypeVar`, `typing.ParamSpec` and - `typing.TypeVarTuple` on Python 3.13+. -- TypeVars, ParamSpecs and TypeVarTuples with `default=None` passed to - their constructors now have their `__default__` attribute set to `None` - at runtime rather than `types.NoneType`. -- Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python - 3.13.0b1 and newer. -- Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774), - allowing type parameters without default values to follow those with - default values in some type parameter lists. Patch by Alex Waygood, - backporting a CPython PR by Jelle Zijlstra. -- It is now disallowed to use a `TypeVar` with a default value after a - `TypeVarTuple` in a type parameter list. This matches the CPython - implementation of PEP 696 on Python 3.13+. -- Fix bug in PEP-696 implementation where default values for `ParamSpec`s - would be cast to tuples if a list was provided as the default value. - Patch by Alex Waygood. +# Release 4.12.0rc1 (May 16, 2024) + +This release focuses on compatibility with the upcoming release of +Python 3.13. Most changes are related to the implementation of type +parameter defaults (PEP 696). + +Thanks to all of the people who contributed patches, especially Alex +Waygood, who did most of the work adapting typing-extensions to the +CPython PEP 696 implementation. + +Full changelog: + +- Improve the implementation of type parameter defaults (PEP 696) + - Backport the `typing.NoDefault` sentinel object from Python 3.13. + TypeVars, ParamSpecs and TypeVarTuples without default values now have + their `__default__` attribute set to this sentinel value. + - TypeVars, ParamSpecs and TypeVarTuples now have a `has_default()` + method, matching `typing.TypeVar`, `typing.ParamSpec` and + `typing.TypeVarTuple` on Python 3.13+. + - TypeVars, ParamSpecs and TypeVarTuples with `default=None` passed to + their constructors now have their `__default__` attribute set to `None` + at runtime rather than `types.NoneType`. + - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python + 3.13.0b1 and newer. + - Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774), + allowing type parameters without default values to follow those with + default values in some type parameter lists. Patch by Alex Waygood, + backporting a CPython PR by Jelle Zijlstra. + - It is now disallowed to use a `TypeVar` with a default value after a + `TypeVarTuple` in a type parameter list. This matches the CPython + implementation of PEP 696 on Python 3.13+. + - Fix bug in PEP-696 implementation where a default value for a `ParamSpec` + would be cast to a tuple if a list was provided. + Patch by Alex Waygood. - Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new `__static_attributes__` attribute to all classes in Python, which broke some assumptions made by the implementation of @@ -46,6 +54,13 @@ well, release 4.12.0rc1 will follow soon. `typing.ContextManager` and `typing.AsyncContextManager` on Python 3.13+. - Backport `types.CapsuleType` from Python 3.13. +- Releases are now made using [Trusted Publishers](https://docs.pypi.org/trusted-publishers/) + improving the security of the release process. Patch by Jelle Zijlstra. + +# Release 4.12.0a1 and 4.12.0a2 (May 16, 2024) + +These releases primarily test a revised release workflow. If all goes +well, release 4.12.0rc1 will follow soon. # Release 4.11.0 (April 5, 2024) diff --git a/pyproject.toml b/pyproject.toml index d829337d..3b9ae5a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.0a2" +version = "4.12.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 910141ab8295b422851f83ffc46c9eb04bbca719 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 May 2024 16:33:34 -0400 Subject: [PATCH 66/75] Add security documentation (#403) --- SECURITY.md | 10 ++++++++++ doc/index.rst | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..efd1d6a3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,10 @@ +# Security Policy + +## Supported Versions + +Only the latest release is supported. + +## Reporting a Vulnerability + +To report an issue, go to https://github.com/python/typing_extensions/security. +We commit to respond to any issue within 14 days and promptly release any fixes. diff --git a/doc/index.rst b/doc/index.rst index 1801fd45..3f0d2d44 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1216,3 +1216,23 @@ versions of Python, but all are listed here for completeness. See :py:func:`typing.no_type_check_decorator`. .. versionadded:: 4.7.0 + +Security +-------- + +``typing_extensions`` is among the most widely used packages in the +Python ecosystem. Therefore, we take security seriously and strive +to use a transparent, secure release process. + +We commit to the following in order to keep the package secure in the +future: + +* ``typing_extensions`` will never include any native extensions, only + pure Python code. +* ``typing_extensions`` will not have any third-party dependencies. +* We will follow best practices for a secure release process. + +If you have any feedback on our security process, please `open an issue +`__. To report +an issue privately, use `GitHub's private reporting feature +`__. From 118e1a604a857d54cb70a2a1f930b425676d6cb4 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 23 May 2024 16:50:56 -0400 Subject: [PATCH 67/75] Make sure `isinstance(typing_extensions.ParamSpec("P"), typing.TypeVar)` is unaffected by `sys.setprofile()` (#407) --- .github/workflows/publish.yml | 4 ++-- CHANGELOG.md | 8 +++++++ src/test_typing_extensions.py | 42 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 2 +- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d1da0258..47704723 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -92,8 +92,8 @@ jobs: export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz") echo "::notice::Unpacking source distribution: $path_to_file" tar xzf $path_to_file -C dist/ - cd ${path_to_file%.tar.gz} - python src/test_typing_extensions.py + cd ${path_to_file%.tar.gz}/src + python test_typing_extensions.py test-sdist-installed: name: Test installed source distribution diff --git a/CHANGELOG.md b/CHANGELOG.md index d1ac543b..f315d1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Unreleased + +- Fix incorrect behaviour of `typing_extensions.ParamSpec` on Python 3.8 and + 3.9 that meant that + `isinstance(typing_extensions.ParamSpec("P"), typing.TypeVar)` would have a + different result in some situations depending on whether or not a profiling + function had been set using `sys.setprofile`. Patch by Alex Waygood. + # Release 4.12.0rc1 (May 16, 2024) This release focuses on compatibility with the upcoming release of diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 5d37b5bf..080c0f7c 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5020,6 +5020,48 @@ def test_eq(self): # won't be the same. self.assertNotEqual(hash(ParamSpec('P')), hash(P)) + def test_isinstance_results_unaffected_by_presence_of_tracing_function(self): + # See https://github.com/python/typing_extensions/issues/318 + + 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 ParamSpec + return isinstance(ParamSpec("P"), 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 ConcatenateTests(BaseTestCase): def test_basics(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d549fb69..57e59a8b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1711,7 +1711,7 @@ def kwargs(self): def __init__(self, name, *, bound=None, covariant=False, contravariant=False, infer_variance=False, default=NoDefault): - super().__init__([self]) + list.__init__(self, [self]) self.__name__ = name self.__covariant__ = bool(covariant) self.__contravariant__ = bool(contravariant) From f90a8dc40b60bf43510b8611a07d8cc570544ffe Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 23 May 2024 17:22:42 -0700 Subject: [PATCH 68/75] Prepare release 4.12.0 (#408) --- CHANGELOG.md | 5 ++++- CONTRIBUTING.md | 20 +++----------------- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f315d1ff..8c9cd298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -# Unreleased +# Release 4.12.0 (May 23, 2024) + +This release is mostly the same as 4.12.0rc1 but fixes one more +longstanding bug. - Fix incorrect behaviour of `typing_extensions.ParamSpec` on Python 3.8 and 3.9 that meant that diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a118a40e..1b030d56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,24 +61,10 @@ may have installed. - Update the version number in `typing_extensions/pyproject.toml` and in `typing_extensions/CHANGELOG.md`. -- Make sure your environment is up to date - - - `git checkout main` - - `git pull` - - `python -m pip install --upgrade build twine` - -- Build the source and wheel distributions: - - - `rm -rf dist/` - - `python -m build .` - -- Install the built distributions locally and test (if you were using `tox`, you already - tested the source distribution). - -- Run `twine upload dist/*`. Remember to use `__token__` as the username - and pass your API token as the password. - - Create a new GitHub release at https://github.com/python/typing_extensions/releases/new. Details: - The tag should be just the version number, e.g. `4.1.1`. - Copy the release notes from `CHANGELOG.md`. + +- Release automation will finish the release. You'll have to manually + approve the last step before upload. diff --git a/pyproject.toml b/pyproject.toml index 3b9ae5a8..c9762f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.0rc1" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From e792bce5508dad9f5f00066ad615d231cc1d64c1 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 May 2024 18:40:16 +0100 Subject: [PATCH 69/75] Ignore fewer flake8 rules when linting tests (#413) --- .flake8 | 8 ++- .flake8-tests | 31 ------------ .github/workflows/ci.yml | 3 -- src/test_typing_extensions.py | 94 +++++++++++++++-------------------- 4 files changed, 42 insertions(+), 94 deletions(-) delete mode 100644 .flake8-tests diff --git a/.flake8 b/.flake8 index 03237510..488a1a91 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,4 @@ [flake8] - max-line-length = 90 ignore = # irrelevant plugins @@ -11,8 +10,7 @@ ignore = W503, # consistency with mypy W504 -exclude = - # tests have more relaxed formatting rules - # and its own specific config in .flake8-tests - src/test_typing_extensions.py, +per-file-ignores = + # stylistic rules we don't care about in tests + src/test_typing_extensions.py:E302,E306,E501,E701,E704, noqa_require_code = true diff --git a/.flake8-tests b/.flake8-tests deleted file mode 100644 index 634160ab..00000000 --- a/.flake8-tests +++ /dev/null @@ -1,31 +0,0 @@ -# This configuration is specific to test_*.py; you need to invoke it -# by specifically naming this config, like this: -# -# $ flake8 --config=.flake8-tests [SOURCES] -# -# This will be possibly merged in the future. - -[flake8] -max-line-length = 100 -ignore = - # temporary ignores until we sort it out - B017, - E302, - E303, - E306, - E501, - E701, - E704, - F722, - F811, - F821, - F841, - W503, - # irrelevant plugins - B3, - DW12, - # Contradicts PEP8 nowadays - W503, - # consistency with mypy - W504 -noqa_require_code = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9d69774..c686b6e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,9 +110,6 @@ jobs: - name: Lint implementation run: flake8 --color always - - name: Lint tests - run: flake8 --config=.flake8-tests src/test_typing_extensions.py --color always - create-issue-on-failure: name: Create an issue if daily tests failed runs-on: ubuntu-latest diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 080c0f7c..962238e4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -29,7 +29,7 @@ from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases -from typing_extensions import clear_overloads, get_overloads, overload +from typing_extensions import clear_overloads, get_overloads, overload, Iterator from typing_extensions import NamedTuple, TypeIs, no_type_check, Dict from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol from typing_extensions import Doc, NoDefault, List, Union, AnyStr, Iterable, Generic, Optional, Set, Tuple, Callable @@ -220,7 +220,7 @@ def test_cannot_subclass(self): class A(self.bottom_type): pass with self.assertRaises(TypeError): - class A(type(self.bottom_type)): + class B(type(self.bottom_type)): pass def test_cannot_instantiate(self): @@ -322,7 +322,6 @@ def static_method_good_order(): def static_method_bad_order(): return 42 - self.assertIsSubclass(Derived, Base) instance = Derived() self.assertEqual(instance.normal_method(), 42) @@ -685,7 +684,7 @@ def test_cannot_subclass(self): class C(type(ClassVar)): pass with self.assertRaises(TypeError): - class C(type(ClassVar[int])): + class D(type(ClassVar[int])): pass def test_cannot_init(self): @@ -726,7 +725,7 @@ def test_cannot_subclass(self): class C(type(Final)): pass with self.assertRaises(TypeError): - class C(type(Final[int])): + class D(type(Final[int])): pass def test_cannot_init(self): @@ -771,7 +770,7 @@ def test_cannot_subclass(self): class C(type(Required)): pass with self.assertRaises(TypeError): - class C(type(Required[int])): + class D(type(Required[int])): pass def test_cannot_init(self): @@ -816,7 +815,7 @@ def test_cannot_subclass(self): class C(type(NotRequired)): pass with self.assertRaises(TypeError): - class C(type(NotRequired[int])): + class D(type(NotRequired[int])): pass def test_cannot_init(self): @@ -836,15 +835,15 @@ def test_no_isinstance(self): class IntVarTests(BaseTestCase): def test_valid(self): - T_ints = IntVar("T_ints") + IntVar("T_ints") def test_invalid(self): with self.assertRaises(TypeError): - T_ints = IntVar("T_ints", int) + IntVar("T_ints", int) with self.assertRaises(TypeError): - T_ints = IntVar("T_ints", bound=int) + IntVar("T_ints", bound=int) with self.assertRaises(TypeError): - T_ints = IntVar("T_ints", covariant=True) + IntVar("T_ints", covariant=True) class LiteralTests(BaseTestCase): @@ -1191,7 +1190,6 @@ async def __aexit__(self, etype, eval, tb): return None - class A: y: float class B(A): @@ -1336,7 +1334,7 @@ def test_respect_no_type_check(self): @no_type_check class NoTpCheck: class Inn: - def __init__(self, x: 'not a type'): ... + def __init__(self, x: 'not a type'): ... # noqa: F722 # (yes, there's a syntax error in this annotation, that's the point) self.assertTrue(NoTpCheck.__no_type_check__) self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__) self.assertEqual(gth(self.ann_module2.NTC.meth), {}) @@ -2034,10 +2032,10 @@ class BP(Protocol): pass class P(C, Protocol): pass with self.assertRaises(TypeError): - class P(Protocol, C): + class Q(Protocol, C): pass with self.assertRaises(TypeError): - class P(BP, C, Protocol): + class R(BP, C, Protocol): pass class D(BP, C): pass class E(C, BP): pass @@ -2350,7 +2348,7 @@ class NotAProtocolButAnImplicitSubclass3: meth: Callable[[], None] meth2: Callable[[int, str], bool] def meth(self): pass - def meth(self, x, y): return True + def meth2(self, x, y): return True self.assertNotIsSubclass(AnnotatedButNotAProtocol, CallableMembersProto) self.assertIsSubclass(NotAProtocolButAnImplicitSubclass, CallableMembersProto) @@ -3196,11 +3194,11 @@ def test_protocols_bad_subscripts(self): with self.assertRaises(TypeError): class P(Protocol[T, T]): pass with self.assertRaises(TypeError): - class P(Protocol[int]): pass + class P2(Protocol[int]): pass with self.assertRaises(TypeError): - class P(Protocol[T], Protocol[S]): pass + class P3(Protocol[T], Protocol[S]): pass with self.assertRaises(TypeError): - class P(typing.Mapping[T, S], Protocol[T]): pass + class P4(typing.Mapping[T, S], Protocol[T]): pass def test_generic_protocols_repr(self): T = TypeVar('T') @@ -3735,9 +3733,8 @@ def test_basics_functional_syntax(self): @skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13") def test_keywords_syntax_raises_on_3_13(self): - with self.assertRaises(TypeError): - with self.assertWarns(DeprecationWarning): - Emp = TypedDict('Emp', name=str, id=int) + with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning): + TypedDict('Emp', name=str, id=int) @skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs") def test_basics_keywords_syntax(self): @@ -4178,7 +4175,6 @@ class C(B[int]): with self.assertRaises(TypeError): C[str] - class Point3D(Point2DGeneric[T], Generic[T, KT]): c: KT @@ -4826,7 +4822,7 @@ def test_canonical_usage_with_variable_annotation(self): exec('Alias: TypeAlias = Employee', globals(), ns) def test_canonical_usage_with_type_comment(self): - Alias: TypeAlias = Employee + Alias: TypeAlias = Employee # noqa: F841 def test_cannot_instantiate(self): with self.assertRaises(TypeError): @@ -4849,7 +4845,7 @@ class C(TypeAlias): pass with self.assertRaises(TypeError): - class C(type(TypeAlias)): + class D(type(TypeAlias)): pass def test_repr(self): @@ -5078,11 +5074,15 @@ def test_valid_uses(self): C1 = Callable[Concatenate[int, P], int] C2 = Callable[Concatenate[int, T, P], T] + self.assertEqual(C1.__origin__, C2.__origin__) + self.assertNotEqual(C1, C2) # Test collections.abc.Callable too. if sys.version_info[:2] >= (3, 9): C3 = collections.abc.Callable[Concatenate[int, P], int] C4 = collections.abc.Callable[Concatenate[int, T, P], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) def test_invalid_uses(self): P = ParamSpec('P') @@ -5152,7 +5152,7 @@ def test_cannot_subclass(self): class C(type(TypeGuard)): pass with self.assertRaises(TypeError): - class C(type(TypeGuard[int])): + class D(type(TypeGuard[int])): pass def test_cannot_init(self): @@ -5196,7 +5196,7 @@ def test_cannot_subclass(self): class C(type(TypeIs)): pass with self.assertRaises(TypeError): - class C(type(TypeIs[int])): + class D(type(TypeIs[int])): pass def test_cannot_init(self): @@ -5242,7 +5242,7 @@ def test_cannot_subclass(self): class C(type(LiteralString)): pass with self.assertRaises(TypeError): - class C(LiteralString): + class D(LiteralString): pass def test_cannot_init(self): @@ -5785,17 +5785,6 @@ def double(self): return 2 * self.x -class XRepr(NamedTuple): - x: int - y: int = 1 - - def __str__(self): - return f'{self.x} -> {self.y}' - - def __add__(self, other): - return 0 - - class NamedTupleTests(BaseTestCase): class NestedEmployee(NamedTuple): name: str @@ -5887,11 +5876,11 @@ class X(NamedTuple, A): TypeError, 'can only inherit from a NamedTuple type and Generic' ): - class X(NamedTuple, tuple): + class Y(NamedTuple, tuple): x: int with self.assertRaisesRegex(TypeError, 'duplicate base class'): - class X(NamedTuple, NamedTuple): + class Z(NamedTuple, NamedTuple): x: int class A(NamedTuple): @@ -5900,7 +5889,7 @@ class A(NamedTuple): TypeError, 'can only inherit from a NamedTuple type and Generic' ): - class X(NamedTuple, A): + class XX(NamedTuple, A): y: str def test_generic(self): @@ -6156,11 +6145,6 @@ class NamedTupleClass(NamedTuple): attr = annoying namedtuple_exception = cm.exception - expected_note = ( - "Error calling __set_name__ on 'Annoying' instance " - "'attr' in 'NamedTupleClass'" - ) - self.assertIs(type(namedtuple_exception), RuntimeError) self.assertIs(type(namedtuple_exception), type(normal_exception)) self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args)) @@ -6316,8 +6300,8 @@ def test_or(self): X = TypeVar('X') # use a string because str doesn't implement # __or__/__ror__ itself - self.assertEqual(X | "x", Union[X, "x"]) - self.assertEqual("x" | X, Union["x", X]) + self.assertEqual(X | "x", Union[X, "x"]) # noqa: F821 + self.assertEqual("x" | X, Union["x", X]) # noqa: F821 # make sure the order is correct self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x"))) self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X)) @@ -6345,7 +6329,7 @@ def test_cannot_subclass(self): class V(TypeVar): pass T = TypeVar("T") with self.assertRaises(TypeError): - class V(T): pass + class W(T): pass def test_cannot_instantiate_vars(self): with self.assertRaises(TypeError): @@ -6392,7 +6376,7 @@ def test_typevar(self): self.assertIsInstance(typing_T, typing_extensions.TypeVar) class A(Generic[T]): ... - Alias = Optional[T] + self.assertEqual(Optional[T].__args__, (T, type(None))) def test_typevar_none(self): U = typing_extensions.TypeVar('U') @@ -6414,7 +6398,7 @@ def test_paramspec(self): self.assertIsInstance(typing_P, ParamSpec) class A(Generic[P]): ... - Alias = typing.Callable[P, None] + self.assertEqual(typing.Callable[P, None].__args__, (P, type(None))) P_default = ParamSpec('P_default', default=...) self.assertIs(P_default.__default__, ...) @@ -6440,7 +6424,7 @@ def test_typevartuple(self): self.assertIsInstance(typing_Ts, TypeVarTuple) class A(Generic[Unpack[Ts]]): ... - Alias = Optional[Unpack[Ts]] + self.assertEqual(Optional[Unpack[Ts]].__args__, (Unpack[Ts], type(None))) @skipIf( sys.version_info < (3, 11, 1), @@ -6494,7 +6478,7 @@ def test_no_default_after_non_default(self): T = TypeVar('T') with self.assertRaises(TypeError): - Test = Generic[DefaultStrT, T] + Generic[DefaultStrT, T] def test_need_more_params(self): DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) @@ -6508,7 +6492,7 @@ class A(Generic[T, U, DefaultStrT]): ... with self.assertRaises( TypeError, msg="Too few arguments for .+; actual 1, expected at least 2" ): - Test = A[int] + A[int] def test_pickle(self): global U, U_co, U_contra, U_default # pickle wants to reference the class by name From 920d60d09e929e23657a4459dd446fb428715981 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 27 May 2024 16:14:09 -0700 Subject: [PATCH 70/75] Support my PEP 649 branch (#412) --- CHANGELOG.md | 5 +++++ src/test_typing_extensions.py | 24 ++++++++++++++++++------ src/typing_extensions.py | 16 ++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9cd298..8060d6a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +- Preliminary changes for compatibility with the draft implementation + of PEP 649 in Python 3.14. + # Release 4.12.0 (May 23, 2024) This release is mostly the same as 4.12.0rc1 but fixes one more diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 962238e4..b8cf2b2b 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -64,10 +64,14 @@ ) ANN_MODULE_SOURCE = '''\ +import sys from typing import List, Optional from functools import wraps -__annotations__[1] = 2 +try: + __annotations__[1] = 2 +except NameError: + assert sys.version_info >= (3, 14) class C: @@ -77,8 +81,10 @@ class C: x: int = 5; y: str = x; f: Tuple[int, int] class M(type): - - __annotations__['123'] = 123 + try: + __annotations__['123'] = 123 + except NameError: + assert sys.version_info >= (3, 14) o: type = object (pars): bool = True @@ -1310,7 +1316,10 @@ def tearDownClass(cls): del sys.modules[modname] def test_get_type_hints_modules(self): - ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str} + if sys.version_info >= (3, 14): + ann_module_type_hints = {'f': Tuple[int, int], 'x': int, 'y': str} + else: + ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str} self.assertEqual(gth(self.ann_module), ann_module_type_hints) self.assertEqual(gth(self.ann_module2), {}) self.assertEqual(gth(self.ann_module3), {}) @@ -1319,7 +1328,10 @@ def test_get_type_hints_classes(self): self.assertEqual(gth(self.ann_module.C, self.ann_module.__dict__), {'y': Optional[self.ann_module.C]}) self.assertIsInstance(gth(self.ann_module.j_class), dict) - self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type}) + if sys.version_info >= (3, 14): + self.assertEqual(gth(self.ann_module.M), {'o': type}) + else: + self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type}) self.assertEqual(gth(self.ann_module.D), {'j': str, 'k': str, 'y': Optional[self.ann_module.C]}) self.assertEqual(gth(self.ann_module.Y), {'z': int}) @@ -2992,7 +3004,7 @@ def meth(self): pass # noqa: B027 acceptable_extra_attrs = { '_is_protocol', '_is_runtime_protocol', '__parameters__', - '__init__', '__annotations__', '__subclasshook__', + '__init__', '__annotations__', '__subclasshook__', '__annotate__' } self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) self.assertLessEqual( diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 57e59a8b..2afb49a7 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -942,7 +942,13 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): tp_dict.__orig_bases__ = bases annotations = {} - own_annotations = ns.get('__annotations__', {}) + if "__annotations__" in ns: + own_annotations = ns["__annotations__"] + elif "__annotate__" in ns: + # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated + own_annotations = ns["__annotate__"](1) + else: + own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" if _TAKES_MODULE: own_annotations = { @@ -3104,7 +3110,13 @@ def __new__(cls, typename, bases, ns): raise TypeError( 'can only inherit from a NamedTuple type and Generic') bases = tuple(tuple if base is _NamedTuple else base for base in bases) - types = ns.get('__annotations__', {}) + if "__annotations__" in ns: + types = ns["__annotations__"] + elif "__annotate__" in ns: + # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated + types = ns["__annotate__"](1) + else: + types = {} default_names = [] for field_name in types: if field_name in ns: From d76f5911b7d44aa1ff26de22e76047ca6c53f840 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 May 2024 15:31:11 +0100 Subject: [PATCH 71/75] Switch from flake8 to ruff (#414) --- .flake8 | 16 ---- .github/workflows/ci.yml | 11 +-- doc/conf.py | 3 +- pyproject.toml | 39 ++++++++++ src/_typed_dict_test_helper.py | 3 +- src/test_typing_extensions.py | 130 ++++++++++++++++++++++++--------- src/typing_extensions.py | 14 ++-- test-requirements.txt | 3 +- 8 files changed, 148 insertions(+), 71 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 488a1a91..00000000 --- a/.flake8 +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -max-line-length = 90 -ignore = - # irrelevant plugins - B3, - DW12, - # code is sometimes better without this - E129, - # Contradicts PEP8 nowadays - W503, - # consistency with mypy - W504 -per-file-ignores = - # stylistic rules we don't care about in tests - src/test_typing_extensions.py:E302,E306,E501,E701,E704, -noqa_require_code = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c686b6e1..9f062801 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ permissions: contents: read env: + FORCE_COLOR: 1 PIP_DISABLE_PIP_VERSION_CHECK: 1 concurrency: @@ -99,16 +100,10 @@ jobs: python-version: "3" cache: "pip" cache-dependency-path: "test-requirements.txt" - - name: Install dependencies - run: | - pip install -r test-requirements.txt - # not included in test-requirements.txt as it depends on typing-extensions, - # so it's a pain to have it installed locally - pip install flake8-noqa - + run: pip install -r test-requirements.txt - name: Lint implementation - run: flake8 --color always + run: ruff check create-issue-on-failure: name: Create an issue if daily tests failed diff --git a/doc/conf.py b/doc/conf.py index 40d3c6b7..42273604 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -5,8 +5,9 @@ import os.path import sys -from sphinx.writers.html5 import HTML5Translator + from docutils.nodes import Element +from sphinx.writers.html5 import HTML5Translator sys.path.insert(0, os.path.abspath('.')) diff --git a/pyproject.toml b/pyproject.toml index c9762f5f..5ee10946 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,42 @@ email = "levkivskyi@gmail.com" [tool.flit.sdist] include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"] exclude = [] + +[tool.ruff] +line-length = 90 +target-version = "py38" + +[tool.ruff.lint] +select = [ + "B", + "C4", + "E", + "F", + "I", + "ISC001", + "PGH004", + "RUF", + "SIM201", + "SIM202", + "UP", + "W", +] + +# Ignore various "modernization" rules that tell you off for importing/using +# deprecated things from the typing module, etc. +ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP038"] + +[tool.ruff.lint.per-file-ignores] +"!src/typing_extensions.py" = [ + "B018", + "B024", + "C4", + "E302", + "E306", + "E501", + "E701", +] + +[tool.ruff.lint.isort] +extra-standard-library = ["tomllib"] +known-first-party = ["typing_extensions", "_typed_dict_test_helper"] diff --git a/src/_typed_dict_test_helper.py b/src/_typed_dict_test_helper.py index c5582b15..73cf9199 100644 --- a/src/_typed_dict_test_helper.py +++ b/src/_typed_dict_test_helper.py @@ -1,7 +1,8 @@ from __future__ import annotations from typing import Generic, Optional, T -from typing_extensions import TypedDict, Annotated, Required + +from typing_extensions import Annotated, Required, TypedDict # this class must not be imported into test_typing_extensions.py at top level, otherwise diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b8cf2b2b..7214b709 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,39 +1,97 @@ -import sys import abc -import gc -import io -import contextlib import collections -from collections import defaultdict import collections.abc +import contextlib import copy -from functools import lru_cache +import gc import importlib import inspect +import io import pickle import re import subprocess +import sys import tempfile import textwrap import types -from pathlib import Path -from unittest import TestCase, main, skipUnless, skipIf -from unittest.mock import patch import typing import warnings +from collections import defaultdict +from functools import lru_cache +from pathlib import Path +from unittest import TestCase, main, skipIf, skipUnless +from unittest.mock import patch import typing_extensions -from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self -from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard -from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired, ReadOnly -from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict -from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString -from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases -from typing_extensions import clear_overloads, get_overloads, overload, Iterator -from typing_extensions import NamedTuple, TypeIs, no_type_check, Dict -from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol -from typing_extensions import Doc, NoDefault, List, Union, AnyStr, Iterable, Generic, Optional, Set, Tuple, Callable from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated +from typing_extensions import ( + Annotated, + Any, + AnyStr, + AsyncContextManager, + AsyncIterator, + Awaitable, + Buffer, + Callable, + ClassVar, + Concatenate, + Dict, + Doc, + Final, + Generic, + IntVar, + Iterable, + Iterator, + List, + Literal, + LiteralString, + NamedTuple, + Never, + NewType, + NoDefault, + NoReturn, + NotRequired, + Optional, + ParamSpec, + ParamSpecArgs, + ParamSpecKwargs, + Protocol, + ReadOnly, + Required, + Self, + Set, + Tuple, + Type, + TypeAlias, + TypeAliasType, + TypedDict, + TypeGuard, + TypeIs, + TypeVar, + TypeVarTuple, + Union, + Unpack, + assert_never, + assert_type, + clear_overloads, + dataclass_transform, + deprecated, + final, + get_args, + get_origin, + get_original_bases, + get_overloads, + get_protocol_members, + get_type_hints, + is_protocol, + is_typeddict, + no_type_check, + overload, + override, + reveal_type, + runtime, + runtime_checkable, +) NoneType = type(None) T = TypeVar("T") @@ -179,14 +237,14 @@ def g_bad_ann(): class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): if not issubclass(cls, class_or_tuple): - message = f'{cls!r} is not a subclass of {repr(class_or_tuple)}' + message = f'{cls!r} is not a subclass of {class_or_tuple!r}' if msg is not None: message += f' : {msg}' raise self.failureException(message) def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): if issubclass(cls, class_or_tuple): - message = f'{cls!r} is a subclass of {repr(class_or_tuple)}' + message = f'{cls!r} is a subclass of {class_or_tuple!r}' if msg is not None: message += f' : {msg}' raise self.failureException(message) @@ -765,11 +823,11 @@ def test_repr(self): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(Required), mod_name + '.Required') + self.assertEqual(repr(Required), f'{mod_name}.Required') cv = Required[int] - self.assertEqual(repr(cv), mod_name + '.Required[int]') + self.assertEqual(repr(cv), f'{mod_name}.Required[int]') cv = Required[Employee] - self.assertEqual(repr(cv), mod_name + '.Required[%s.Employee]' % __name__) + self.assertEqual(repr(cv), f'{mod_name}.Required[{__name__}.Employee]') def test_cannot_subclass(self): with self.assertRaises(TypeError): @@ -810,11 +868,11 @@ def test_repr(self): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(NotRequired), mod_name + '.NotRequired') + self.assertEqual(repr(NotRequired), f'{mod_name}.NotRequired') cv = NotRequired[int] - self.assertEqual(repr(cv), mod_name + '.NotRequired[int]') + self.assertEqual(repr(cv), f'{mod_name}.NotRequired[int]') cv = NotRequired[Employee] - self.assertEqual(repr(cv), mod_name + '.NotRequired[%s.Employee]' % __name__) + self.assertEqual(repr(cv), f'{mod_name}.NotRequired[{ __name__}.Employee]') def test_cannot_subclass(self): with self.assertRaises(TypeError): @@ -872,7 +930,7 @@ def test_illegal_parameters_do_not_raise_runtime_errors(self): Literal[int] Literal[Literal[1, 2], Literal[4, 5]] Literal[3j + 2, ..., ()] - Literal[b"foo", u"bar"] + Literal[b"foo", "bar"] Literal[{"foo": 3, "bar": 4}] Literal[T] @@ -1747,7 +1805,7 @@ class D: ... self.assertIsSubclass(D, A) self.assertIsSubclass(D, B) - class M(): ... + class M: ... collections.abc.Generator.register(M) self.assertIsSubclass(M, typing_extensions.Generator) @@ -2988,7 +3046,7 @@ class NonP(P): class NonPR(PR): pass class C(metaclass=abc.ABCMeta): x = 1 - class D(metaclass=abc.ABCMeta): # noqa: B024 + class D(metaclass=abc.ABCMeta): def meth(self): pass # noqa: B027 self.assertNotIsInstance(C(), NonP) self.assertNotIsInstance(D(), NonPR) @@ -3274,7 +3332,7 @@ def test_none_treated_correctly(self): @runtime_checkable class P(Protocol): x: int = None - class B(object): pass + class B: pass self.assertNotIsInstance(B(), P) class C: x = 1 @@ -5243,7 +5301,7 @@ def test_repr(self): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(LiteralString), '{}.LiteralString'.format(mod_name)) + self.assertEqual(repr(LiteralString), f'{mod_name}.LiteralString') def test_cannot_subscript(self): with self.assertRaises(TypeError): @@ -5297,7 +5355,7 @@ def test_repr(self): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(Self), '{}.Self'.format(mod_name)) + self.assertEqual(repr(Self), f'{mod_name}.Self') def test_cannot_subscript(self): with self.assertRaises(TypeError): @@ -5556,7 +5614,7 @@ def stmethod(): ... def prop(self): ... @final - @lru_cache() # noqa: B019 + @lru_cache # noqa: B019 def cached(self): ... # Use getattr_static because the descriptor returns the @@ -6312,8 +6370,8 @@ def test_or(self): X = TypeVar('X') # use a string because str doesn't implement # __or__/__ror__ itself - self.assertEqual(X | "x", Union[X, "x"]) # noqa: F821 - self.assertEqual("x" | X, Union["x", X]) # noqa: F821 + self.assertEqual(X | "x", Union[X, "x"]) + self.assertEqual("x" | X, Union["x", X]) # make sure the order is correct self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x"))) self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X)) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 2afb49a7..abf6f41e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -418,7 +418,7 @@ def clear_overloads(): if sys.version_info >= (3, 13, 0, "beta"): - from typing import ContextManager, AsyncContextManager, Generator, AsyncGenerator + from typing import AsyncContextManager, AsyncGenerator, ContextManager, Generator else: def _is_dunder(attr): return attr.startswith('__') and attr.endswith('__') @@ -739,8 +739,8 @@ def close(self): ... not their type signatures! """ if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False): - raise TypeError('@runtime_checkable can be only applied to protocol classes,' - ' got %r' % cls) + raise TypeError(f'@runtime_checkable can be only applied to protocol classes,' + f' got {cls!r}') cls._is_runtime_protocol = True # typing.Protocol classes on <=3.11 break if we execute this block, @@ -1271,7 +1271,7 @@ def __repr__(self): def __reduce__(self): return operator.getitem, ( - Annotated, (self.__origin__,) + self.__metadata__ + Annotated, (self.__origin__, *self.__metadata__) ) def __eq__(self, other): @@ -1397,7 +1397,7 @@ def get_args(tp): get_args(Callable[[], T][int]) == ([], int) """ if isinstance(tp, _AnnotatedAlias): - return (tp.__origin__,) + tp.__metadata__ + return (tp.__origin__, *tp.__metadata__) if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)): if getattr(tp, "_special", False): return () @@ -1811,7 +1811,7 @@ def _concatenate_getitem(self, parameters): # 3.10+ if hasattr(typing, 'Concatenate'): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811 + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # 3.9 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm @@ -3248,7 +3248,7 @@ class Employee(NamedTuple): if hasattr(collections.abc, "Buffer"): Buffer = collections.abc.Buffer else: - class Buffer(abc.ABC): + class Buffer(abc.ABC): # noqa: B024 """Base class for classes that implement the buffer protocol. The buffer protocol allows Python objects to expose a low-level diff --git a/test-requirements.txt b/test-requirements.txt index 675b2c5d..7242d3b5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1 @@ -flake8 -flake8-bugbear +ruff==0.4.5 From 8dfcf3c74a4f5d736a6d2ce8d82c3e85cd0c5b18 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 1 Jun 2024 17:31:23 +0100 Subject: [PATCH 72/75] Fix `TypeError` on nested `Annotated` types where the inner type has unhashable metadata (#417) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 8 ++++++++ src/typing_extensions.py | 4 ++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8060d6a4..c5f19626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ - Preliminary changes for compatibility with the draft implementation of PEP 649 in Python 3.14. +- Fix regression in v4.12.0 where nested `Annotated` types would cause + `TypeError` to be raised if the nested `Annotated` type had unhashable + metadata. # Release 4.12.0 (May 23, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7214b709..8ba0bf74 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4769,6 +4769,14 @@ def test_annotated_in_other_types(self): X = List[Annotated[T, 5]] self.assertEqual(X[int], List[Annotated[int, 5]]) + def test_nested_annotated_with_unhashable_metadata(self): + X = Annotated[ + List[Annotated[str, {"unhashable_metadata"}]], + "metadata" + ] + self.assertEqual(X.__origin__, List[Annotated[str, {"unhashable_metadata"}]]) + self.assertEqual(X.__metadata__, ("metadata",)) + class GetTypeHintsTests(BaseTestCase): def test_get_type_hints(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index abf6f41e..46084fa5 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2958,9 +2958,9 @@ def _has_generic_or_protocol_as_origin() -> bool: except AttributeError: return False # err on the side of leniency else: - return frame.f_locals.get("origin") in { + return frame.f_locals.get("origin") in ( typing.Generic, Protocol, typing.Protocol - } + ) _TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} From 726963800030ab35ba5b975fc3a60486c26c5050 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 1 Jun 2024 10:25:21 -0700 Subject: [PATCH 73/75] Prepare release 4.12.1 (#418) --- CHANGELOG.md | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f19626..3a5937a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ -# Unreleased +# Release 4.12.1 (June 1, 2024) - Preliminary changes for compatibility with the draft implementation - of PEP 649 in Python 3.14. + of PEP 649 in Python 3.14. Patch by Jelle Zijlstra. - Fix regression in v4.12.0 where nested `Annotated` types would cause `TypeError` to be raised if the nested `Annotated` type had unhashable - metadata. + metadata. Patch by Alex Waygood. # Release 4.12.0 (May 23, 2024) diff --git a/pyproject.toml b/pyproject.toml index 5ee10946..e15c923a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.0" +version = "4.12.1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 53bcdded534494674f893112f71d3be344d65363 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 3 Jun 2024 04:45:13 -0700 Subject: [PATCH 74/75] Avoid error if origin has a buggy __eq__ (#422) Fixes #419 Co-authored-by: Alex Waygood --- CHANGELOG.md | 6 ++++++ src/test_typing_extensions.py | 16 ++++++++++++++++ src/typing_extensions.py | 17 ++++++++++++----- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5937a6..776a101e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Fix regression in v4.12.0 where specialization of certain + generics with an overridden `__eq__` method would raise errors. + Patch by Jelle Zijlstra. + # Release 4.12.1 (June 1, 2024) - Preliminary changes for compatibility with the draft implementation diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8ba0bf74..bf7600a1 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6617,6 +6617,22 @@ def test_allow_default_after_non_default_in_alias(self): a4 = Callable[[Unpack[Ts]], T] self.assertEqual(a4.__args__, (Unpack[Ts], T)) + @skip_if_py313_beta_1 + def test_generic_with_broken_eq(self): + # See https://github.com/python/typing_extensions/pull/422 for context + class BrokenEq(type): + def __eq__(self, other): + if other is typing_extensions.Protocol: + raise TypeError("I'm broken") + return False + + class G(Generic[T], metaclass=BrokenEq): + pass + + alias = G[int] + self.assertIs(get_origin(alias), G) + self.assertEqual(get_args(alias), (int,)) + @skipIf( sys.version_info < (3, 11, 1), "Not yet backported for older versions of Python" diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 46084fa5..dec429ca 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2954,13 +2954,20 @@ def _check_generic(cls, parameters, elen): def _has_generic_or_protocol_as_origin() -> bool: try: frame = sys._getframe(2) - # not all platforms have sys._getframe() - except AttributeError: + # - Catch AttributeError: not all Python implementations have sys._getframe() + # - Catch ValueError: maybe we're called from an unexpected module + # and the call stack isn't deep enough + except (AttributeError, ValueError): return False # err on the side of leniency else: - return frame.f_locals.get("origin") in ( - typing.Generic, Protocol, typing.Protocol - ) + # If we somehow get invoked from outside typing.py, + # also err on the side of leniency + if frame.f_globals.get("__name__") != "typing": + return False + origin = frame.f_locals.get("origin") + # Cannot use "in" because origin may be an object with a buggy __eq__ that + # throws an error. + return origin is typing.Generic or origin is Protocol or origin is typing.Protocol _TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)} From e1250ff869e7ee5ad05170d8a4b65469f13801c3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 7 Jun 2024 19:48:59 +0100 Subject: [PATCH 75/75] Prepare release 4.12.2 (#426) --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- src/test_typing_extensions.py | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 776a101e..90f5b682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ -# Unreleased +# Release 4.12.2 (June 7, 2024) - Fix regression in v4.12.0 where specialization of certain generics with an overridden `__eq__` method would raise errors. Patch by Jelle Zijlstra. +- Fix tests so they pass on 3.13.0b2 # Release 4.12.1 (June 1, 2024) diff --git a/pyproject.toml b/pyproject.toml index e15c923a..3388d553 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.1" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index bf7600a1..2f98765b 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6617,7 +6617,10 @@ def test_allow_default_after_non_default_in_alias(self): a4 = Callable[[Unpack[Ts]], T] self.assertEqual(a4.__args__, (Unpack[Ts], T)) - @skip_if_py313_beta_1 + @skipIf( + typing_extensions.Protocol is typing.Protocol, + "Test currently fails with the CPython version of Protocol and that's not our fault" + ) def test_generic_with_broken_eq(self): # See https://github.com/python/typing_extensions/pull/422 for context class BrokenEq(type):