From e792bce5508dad9f5f00066ad615d231cc1d64c1 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 May 2024 18:40:16 +0100 Subject: [PATCH 01/81] 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 02/81] 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 03/81] 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 04/81] 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 05/81] 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 06/81] 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 07/81] 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): From 2d33f1f51df3f9f1601aae9a068e36937f78e966 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 9 Jun 2024 21:03:51 -0600 Subject: [PATCH 08/81] Add typing_extensions.get_annotations (#423) Co-authored-by: Alex Waygood --- CHANGELOG.md | 3 + doc/index.rst | 58 +++++ src/test_typing_extensions.py | 457 ++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 142 +++++++++++ 4 files changed, 660 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f5b682..89300be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Release 4.12.2 (June 7, 2024) +- Add `typing_extensions.get_annotations`, a backport of + `inspect.get_annotations` that adds features specified + by PEP 649. Patch by Jelle Zijlstra. - Fix regression in v4.12.0 where specialization of certain generics with an overridden `__eq__` method would raise errors. Patch by Jelle Zijlstra. diff --git a/doc/index.rst b/doc/index.rst index 3f0d2d44..15c9c8d5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -747,6 +747,25 @@ Functions .. versionadded:: 4.2.0 +.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) + + See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10. + + ``typing_extensions`` adds the keyword argument ``format``, as specified + by :pep:`649`. The supported formats are listed in the :class:`Format` enum. + The default format, :attr:`Format.VALUE`, behaves the same across all versions. + For the other two formats, ``typing_extensions`` provides a rough approximation + of the :pep:`649` behavior on versions of Python that do not support it. + + The purpose of this backport is to allow users who would like to use + :attr:`Format.FORWARDREF` or :attr:`Format.SOURCE` semantics once + :pep:`649` is implemented, but who also + want to support earlier Python versions, to simply write:: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + .. versionadded:: 4.13.0 + .. function:: get_args(tp) See :py:func:`typing.get_args`. In ``typing`` since 3.8. @@ -857,6 +876,45 @@ Functions .. versionadded:: 4.1.0 +Enums +~~~~~ + +.. class:: Format + + The formats for evaluating annotations introduced by :pep:`649`. + Members of this enum can be passed as the *format* argument + to :func:`get_annotations`. + + The final place of this enum in the standard library has not yet + been determined (see :pep:`649` and :pep:`749`), but the names + and integer values are stable and will continue to work. + + .. attribute:: VALUE + + Equal to 1. The default value. The function will return the conventional Python values + for the annotations. This format is identical to the return value for + the function under earlier versions of Python. + + .. attribute:: FORWARDREF + + Equal to 2. When :pep:`649` is implemented, this format will attempt to return the + conventional Python values for the annotations. However, if it encounters + an undefined name, it dynamically creates a proxy object (a ForwardRef) + that substitutes for that value in the expression. + + ``typing_extensions`` emulates this value on versions of Python which do + not support :pep:`649` by returning the same value as for ``VALUE`` semantics. + + .. attribute:: SOURCE + + Equal to 3. When :pep:`649` is implemented, this format will produce an annotation + dictionary where the values have been replaced by strings containing + an approximation of the original source code for the annotation expressions. + + ``typing_extensions`` emulates this by evaluating the annotations using + ``VALUE`` semantics and then stringifying the results. + + .. versionadded:: 4.13.0 Annotation metadata ~~~~~~~~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2f98765b..331a6438 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3,6 +3,7 @@ import collections.abc import contextlib import copy +import functools import gc import importlib import inspect @@ -25,6 +26,7 @@ import typing_extensions from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated from typing_extensions import ( + _PEP_649_OR_749_IMPLEMENTED, Annotated, Any, AnyStr, @@ -38,6 +40,7 @@ Dict, Doc, Final, + Format, Generic, IntVar, Iterable, @@ -77,6 +80,7 @@ dataclass_transform, deprecated, final, + get_annotations, get_args, get_origin, get_original_bases, @@ -234,6 +238,79 @@ def g_bad_ann(): ''' +STOCK_ANNOTATIONS = """ +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass +""" +STRINGIZED_ANNOTATIONS = """ +from __future__ import annotations + +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass + +class MyClassWithLocalAnnotations: + mytype = int + x: mytype +""" +STRINGIZED_ANNOTATIONS_2 = """ +from __future__ import annotations + + +def foo(a, b, c): pass +""" + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): if not issubclass(cls, class_or_tuple): @@ -7033,5 +7110,385 @@ def test_capsule_type(self): self.assertIsInstance(_datetime.datetime_CAPI, typing_extensions.CapsuleType) +def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a * 3, b * 3) + + return wrapper + + +class TestGetAnnotations(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "inspect_stock_annotations.py").write_text(STOCK_ANNOTATIONS) + Path(tempdir, "inspect_stringized_annotations.py").write_text(STRINGIZED_ANNOTATIONS) + Path(tempdir, "inspect_stringized_annotations_2.py").write_text(STRINGIZED_ANNOTATIONS_2) + cls.inspect_stock_annotations = importlib.import_module("inspect_stock_annotations") + cls.inspect_stringized_annotations = importlib.import_module("inspect_stringized_annotations") + cls.inspect_stringized_annotations_2 = importlib.import_module("inspect_stringized_annotations_2") + sys.path.pop() + + @classmethod + def tearDownClass(cls): + for modname in ( + "inspect_stock_annotations", + "inspect_stringized_annotations", + "inspect_stringized_annotations_2", + ): + delattr(cls, modname) + del sys.modules[modname] + + def test_builtin_type(self): + self.assertEqual(get_annotations(int), {}) + self.assertEqual(get_annotations(object), {}) + + def test_format(self): + def f1(a: int): + pass + + def f2(a: "undefined"): # noqa: F821 + pass + + self.assertEqual( + get_annotations(f1, format=Format.VALUE), {"a": int} + ) + self.assertEqual(get_annotations(f1, format=1), {"a": int}) + + self.assertEqual( + get_annotations(f2, format=Format.FORWARDREF), + {"a": "undefined"}, + ) + self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"}) + + self.assertEqual( + get_annotations(f1, format=Format.SOURCE), + {"a": "int"}, + ) + self.assertEqual(get_annotations(f1, format=3), {"a": "int"}) + + with self.assertRaises(ValueError): + get_annotations(f1, format=0) + + with self.assertRaises(ValueError): + get_annotations(f1, format=4) + + def test_custom_object_with_annotations(self): + class C: + def __init__(self, x: int = 0, y: str = ""): + self.__annotations__ = {"x": int, "y": str} + + self.assertEqual(get_annotations(C()), {"x": int, "y": str}) + + def test_custom_format_eval_str(self): + def foo(): + pass + + with self.assertRaises(ValueError): + get_annotations( + foo, format=Format.FORWARDREF, eval_str=True + ) + get_annotations( + foo, format=Format.SOURCE, eval_str=True + ) + + def test_stock_annotations(self): + def foo(a: int, b: str): + pass + + for format in (Format.VALUE, Format.FORWARDREF): + with self.subTest(format=format): + self.assertEqual( + get_annotations(foo, format=format), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(foo, format=Format.SOURCE), + {"a": "int", "b": "str"}, + ) + + foo.__annotations__ = {"a": "foo", "b": "str"} + for format in Format: + with self.subTest(format=format): + self.assertEqual( + get_annotations(foo, format=format), + {"a": "foo", "b": "str"}, + ) + + self.assertEqual( + get_annotations(foo, eval_str=True, locals=locals()), + {"a": foo, "b": str}, + ) + self.assertEqual( + get_annotations(foo, eval_str=True, globals=locals()), + {"a": foo, "b": str}, + ) + + def test_stock_annotations_in_module(self): + isa = self.inspect_stock_annotations + + for kwargs in [ + {}, + {"eval_str": False}, + {"format": Format.VALUE}, + {"format": Format.FORWARDREF}, + {"format": Format.VALUE, "eval_str": False}, + {"format": Format.FORWARDREF, "eval_str": False}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(inspect, **kwargs), {} + ) # inspect module has no annotations + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + for kwargs in [ + {"eval_str": True}, + {"format": Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": int, "b": str, "c": isa.MyClass}, + ) + self.assertEqual(get_annotations(inspect, **kwargs), {}) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + self.assertEqual( + get_annotations(isa, format=Format.SOURCE), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + get_annotations(isa.MyClass, format=Format.SOURCE), + {"a": "int", "b": "str"}, + ) + mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" + self.assertEqual( + get_annotations(isa.function, format=Format.SOURCE), + {"a": "int", "b": "str", "return": mycls}, + ) + self.assertEqual( + get_annotations( + isa.function2, format=Format.SOURCE + ), + {"a": "int", "b": "str", "c": mycls, "return": mycls}, + ) + self.assertEqual( + get_annotations( + isa.function3, format=Format.SOURCE + ), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(inspect, format=Format.SOURCE), + {}, + ) + self.assertEqual( + get_annotations( + isa.UnannotatedClass, format=Format.SOURCE + ), + {}, + ) + self.assertEqual( + get_annotations( + isa.unannotated_function, format=Format.SOURCE + ), + {}, + ) + + def test_stock_annotations_on_wrapper(self): + isa = self.inspect_stock_annotations + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual( + get_annotations(wrapped), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, format=Format.FORWARDREF), + {"a": int, "b": str, "return": isa.MyClass}, + ) + mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" + self.assertEqual( + get_annotations(wrapped, format=Format.SOURCE), + {"a": "int", "b": "str", "return": mycls}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=False), + {"a": int, "b": str, "return": isa.MyClass}, + ) + + def test_stringized_annotations_in_module(self): + isa = self.inspect_stringized_annotations + for kwargs in [ + {}, + {"eval_str": False}, + {"format": Format.VALUE}, + {"format": Format.FORWARDREF}, + {"format": Format.SOURCE}, + {"format": Format.VALUE, "eval_str": False}, + {"format": Format.FORWARDREF, "eval_str": False}, + {"format": Format.SOURCE, "eval_str": False}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": "int", "b": "str"} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, + ) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + for kwargs in [ + {"eval_str": True}, + {"format": Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + def test_stringized_annotations_in_empty_module(self): + isa2 = self.inspect_stringized_annotations_2 + self.assertEqual(get_annotations(isa2), {}) + self.assertEqual(get_annotations(isa2, eval_str=True), {}) + self.assertEqual(get_annotations(isa2, eval_str=False), {}) + + def test_stringized_annotations_on_wrapper(self): + isa = self.inspect_stringized_annotations + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual( + get_annotations(wrapped), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=False), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + + def test_stringized_annotations_on_class(self): + isa = self.inspect_stringized_annotations + # test that local namespace lookups work + self.assertEqual( + get_annotations(isa.MyClassWithLocalAnnotations), + {"x": "mytype"}, + ) + self.assertEqual( + get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), + {"x": int}, + ) + + def test_modify_annotations(self): + def f(x: int): + pass + + self.assertEqual(get_annotations(f), {"x": int}) + self.assertEqual( + get_annotations(f, format=Format.FORWARDREF), + {"x": int}, + ) + + f.__annotations__["x"] = str + self.assertEqual(get_annotations(f), {"x": str}) + + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dec429ca..342a7492 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2,6 +2,7 @@ import collections import collections.abc import contextlib +import enum import functools import inspect import operator @@ -64,6 +65,8 @@ 'Doc', 'get_overloads', 'final', + 'Format', + 'get_annotations', 'get_args', 'get_origin', 'get_original_bases', @@ -3599,6 +3602,145 @@ def __eq__(self, other: object) -> bool: __all__.append("CapsuleType") +# Using this convoluted approach so that this keeps working +# whether we end up using PEP 649 as written, PEP 749, or +# some other variation: in any case, inspect.get_annotations +# will continue to exist and will gain a `format` parameter. +_PEP_649_OR_749_IMPLEMENTED = ( + hasattr(inspect, 'get_annotations') + and inspect.get_annotations.__kwdefaults__ is not None + and "format" in inspect.get_annotations.__kwdefaults__ +) + + +class Format(enum.IntEnum): + VALUE = 1 + FORWARDREF = 2 + SOURCE = 3 + + +if _PEP_649_OR_749_IMPLEMENTED: + get_annotations = inspect.get_annotations +else: + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, + format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This is a backport of `inspect.get_annotations`, which has been + in the standard library since Python 3.10. See the standard library + documentation for more: + + https://docs.python.org/3/library/inspect.html#inspect.get_annotations + + This backport adds the *format* argument introduced by PEP 649. The + three formats supported are: + * VALUE: the annotations are returned as-is. This is the default and + it is compatible with the behavior on previous Python versions. + * FORWARDREF: return annotations as-is if possible, but replace any + undefined names with ForwardRef objects. The implementation proposed by + PEP 649 relies on language changes that cannot be backported; the + typing-extensions implementation simply returns the same result as VALUE. + * SOURCE: return annotations as strings, in a format close to the original + source. Again, this behavior cannot be replicated directly in a backport. + As an approximation, typing-extensions retrieves the annotations under + VALUE semantics and then stringifies them. + + The purpose of this backport is to allow users who would like to use + FORWARDREF or SOURCE semantics once PEP 649 is implemented, but who also + want to support earlier Python versions, to simply write: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + """ + format = Format(format) + + if eval_str and format is not Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, _types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, _types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = obj.__dict__ + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + elif hasattr(obj, '__annotations__'): + ann = obj.__annotations__ + obj_globals = obj_locals = unwrap = None + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + if format is Format.SOURCE: + return { + key: value if isinstance(value, str) else typing._type_repr(value) + for key, value in ann.items() + } + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + # 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 ece1201226c557f7328638e065b4a6c5ec4e13ce Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 15 Jun 2024 16:20:56 +0100 Subject: [PATCH 09/81] Backport bugfixes made to how `inspect.get_annotations()` deals with PEP-695 (#428) --- CHANGELOG.md | 7 +- src/test_typing_extensions.py | 227 ++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 8 +- 3 files changed, 239 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89300be5..c0226826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ -# Release 4.12.2 (June 7, 2024) +# Unreleased - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified - by PEP 649. Patch by Jelle Zijlstra. + by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. + +# 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. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 331a6438..362845fe 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -111,6 +111,7 @@ TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0) # 3.12 changes the representation of Unpack[] (PEP 692) +# and adds PEP 695 to CPython's grammar TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) # 3.13 drops support for the keyword argument syntax of TypedDict @@ -268,6 +269,7 @@ class UnannotatedClass: def unannotated_function(a, b, c): pass """ + STRINGIZED_ANNOTATIONS = """ from __future__ import annotations @@ -304,6 +306,7 @@ class MyClassWithLocalAnnotations: mytype = int x: mytype """ + STRINGIZED_ANNOTATIONS_2 = """ from __future__ import annotations @@ -311,6 +314,102 @@ class MyClassWithLocalAnnotations: def foo(a, b, c): pass """ +if TYPING_3_12_0: + STRINGIZED_ANNOTATIONS_PEP_695 = textwrap.dedent( + """ + from __future__ import annotations + from typing import Callable, Unpack + + + class A[T, *Ts, **P]: + x: T + y: tuple[*Ts] + z: Callable[P, str] + + + class B[T, *Ts, **P]: + T = int + Ts = str + P = bytes + x: T + y: Ts + z: P + + + Eggs = int + Spam = str + + + class C[Eggs, **Spam]: + x: Eggs + y: Spam + + + def generic_function[T, *Ts, **P]( + x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs + ) -> None: ... + + + def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass + + + class D: + Foo = int + Bar = str + + def generic_method[Foo, **Bar]( + self, x: Foo, y: Bar + ) -> None: ... + + def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + + # Eggs is `int` in globals, a TypeVar in type_params, and `str` in locals: + class E[Eggs]: + Eggs = str + x: Eggs + + + + def nested(): + from types import SimpleNamespace + from typing_extensions import get_annotations + + Eggs = bytes + Spam = memoryview + + + class F[Eggs, **Spam]: + x: Eggs + y: Spam + + def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + + def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass + + + # Eggs is `int` in globals, `bytes` in the function scope, + # a TypeVar in the type_params, and `str` in locals: + class G[Eggs]: + Eggs = str + x: Eggs + + + return SimpleNamespace( + F=F, + F_annotations=get_annotations(F, eval_str=True), + F_meth_annotations=get_annotations(F.generic_method, eval_str=True), + G_annotations=get_annotations(G, eval_str=True), + generic_func=generic_function, + generic_func_annotations=get_annotations(generic_function, eval_str=True) + ) + """ + ) +else: + STRINGIZED_ANNOTATIONS_PEP_695 = None + + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): if not issubclass(cls, class_or_tuple): @@ -7489,6 +7588,134 @@ def f(x: int): self.assertEqual(get_annotations(f), {"x": str}) +@skipIf(STRINGIZED_ANNOTATIONS_PEP_695 is None, "PEP 695 has yet to be") +class TestGetAnnotationsWithPEP695(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "inspect_stringized_annotations_pep_695.py").write_text(STRINGIZED_ANNOTATIONS_PEP_695) + cls.inspect_stringized_annotations_pep_695 = importlib.import_module( + "inspect_stringized_annotations_pep_695" + ) + sys.path.pop() + + @classmethod + def tearDownClass(cls): + del cls.inspect_stringized_annotations_pep_695 + del sys.modules["inspect_stringized_annotations_pep_695"] + + def test_pep695_generic_class_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + A_annotations = get_annotations(ann_module695.A, eval_str=True) + A_type_params = ann_module695.A.__type_params__ + self.assertIs(A_annotations["x"], A_type_params[0]) + self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]]) + self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) + + def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self): + B_annotations = get_annotations( + self.inspect_stringized_annotations_pep_695.B, eval_str=True + ) + self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes}) + + def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + C_annotations = get_annotations(ann_module695.C, eval_str=True) + self.assertEqual( + set(C_annotations.values()), + set(ann_module695.C.__type_params__) + ) + + def test_pep_695_generic_function_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + generic_func_annotations = get_annotations( + ann_module695.generic_function, eval_str=True + ) + func_t_params = ann_module695.generic_function.__type_params__ + self.assertEqual( + generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"} + ) + self.assertIs(generic_func_annotations["x"], func_t_params[0]) + self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]]) + self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2]) + self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2]) + + def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + get_annotations( + self.inspect_stringized_annotations_pep_695.generic_function_2, + eval_str=True + ).values() + ), + set( + self.inspect_stringized_annotations_pep_695.generic_function_2.__type_params__ + ) + ) + + def test_pep_695_generic_method_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + generic_method_annotations = get_annotations( + ann_module695.D.generic_method, eval_str=True + ) + params = { + param.__name__: param + for param in ann_module695.D.generic_method.__type_params__ + } + self.assertEqual( + generic_method_annotations, + {"x": params["Foo"], "y": params["Bar"], "return": None} + ) + + def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + get_annotations( + self.inspect_stringized_annotations_pep_695.D.generic_method_2, + eval_str=True + ).values() + ), + set( + self.inspect_stringized_annotations_pep_695.D.generic_method_2.__type_params__ + ) + ) + + def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self): + self.assertEqual( + get_annotations( + self.inspect_stringized_annotations_pep_695.E, eval_str=True + ), + {"x": str}, + ) + + def test_pep_695_generics_with_future_annotations_nested_in_function(self): + results = self.inspect_stringized_annotations_pep_695.nested() + + self.assertEqual( + set(results.F_annotations.values()), + set(results.F.__type_params__) + ) + self.assertEqual( + set(results.F_meth_annotations.values()), + set(results.F.generic_method.__type_params__) + ) + self.assertNotEqual( + set(results.F_meth_annotations.values()), + set(results.F.__type_params__) + ) + self.assertEqual( + set(results.F_meth_annotations.values()).intersection(results.F.__type_params__), + set() + ) + + self.assertEqual(results.G_annotations, {"x": str}) + + self.assertEqual( + set(results.generic_func_annotations.values()), + set(results.generic_func.__type_params__) + ) + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 342a7492..d5d0a115 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3734,7 +3734,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, if globals is None: globals = obj_globals if locals is None: - locals = obj_locals + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals return_value = {key: value if not isinstance(value, str) else eval(value, globals, locals) From e239100563edd4699d6cc74bdfe27b50aad88d61 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 22 Jun 2024 00:03:32 -0700 Subject: [PATCH 10/81] Add TypeExpr (#430) --- CHANGELOG.md | 2 ++ doc/index.rst | 6 ++++ src/test_typing_extensions.py | 59 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 50 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0226826..68c4cf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add `typing_extensions.TypeExpr` from PEP 747. Patch by + Jelle Zijlstra. - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. diff --git a/doc/index.rst b/doc/index.rst index 15c9c8d5..23a531c4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -367,6 +367,12 @@ Special typing primitives .. versionadded:: 4.6.0 +.. data:: TypeExpr + + See :pep:`747`. A type hint representing a type expression. + + .. versionadded:: 4.13.0 + .. data:: TypeGuard See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 362845fe..868e7938 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -68,6 +68,7 @@ TypeAlias, TypeAliasType, TypedDict, + TypeExpr, TypeGuard, TypeIs, TypeVar, @@ -5468,6 +5469,64 @@ def test_no_isinstance(self): issubclass(int, TypeIs) +class TypeExprTests(BaseTestCase): + def test_basics(self): + TypeExpr[int] # OK + self.assertEqual(TypeExpr[int], TypeExpr[int]) + + def foo(arg) -> TypeExpr[int]: ... + self.assertEqual(gth(foo), {'return': TypeExpr[int]}) + + def test_repr(self): + if hasattr(typing, 'TypeExpr'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(TypeExpr), f'{mod_name}.TypeExpr') + cv = TypeExpr[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[int]') + cv = TypeExpr[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[{__name__}.Employee]') + cv = TypeExpr[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[typing.Tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(TypeExpr)): + pass + with self.assertRaises(TypeError): + class D(type(TypeExpr[int])): + pass + + def test_call(self): + objs = [ + 1, + "int", + int, + Tuple[int, str], + ] + for obj in objs: + with self.subTest(obj=obj): + self.assertIs(TypeExpr(obj), obj) + + with self.assertRaises(TypeError): + TypeExpr() + with self.assertRaises(TypeError): + TypeExpr("too", "many") + + def test_cannot_init_type(self): + with self.assertRaises(TypeError): + type(TypeExpr)() + with self.assertRaises(TypeError): + type(TypeExpr[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeExpr[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeExpr) + + class LiteralStringTests(BaseTestCase): def test_basics(self): class Foo: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d5d0a115..8046dae1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -86,6 +86,7 @@ 'Text', 'TypeAlias', 'TypeAliasType', + 'TypeExpr', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -2045,6 +2046,55 @@ def f(val: Union[int, Awaitable[int]]) -> int: PEP 742 (Narrowing types with TypeIs). """) +# 3.14+? +if hasattr(typing, 'TypeExpr'): + TypeExpr = typing.TypeExpr +# 3.9 +elif sys.version_info[:2] >= (3, 9): + class _TypeExprForm(_ExtensionsSpecialForm, _root=True): + # TypeExpr(X) is equivalent to X but indicates to the type checker + # that the object is a TypeExpr. + def __call__(self, obj, /): + return obj + + @_TypeExprForm + def TypeExpr(self, parameters): + """Special typing form used to represent a type expression. + + Usage: + + def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeExprForm(_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,)) + + def __call__(self, obj, /): + return obj + + TypeExpr = _TypeExprForm( + 'TypeExpr', + doc="""Special typing form used to represent a type expression. + + Usage: + + def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True): From 70cec91bec65155dc339d631ede2a933582558df Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:03:16 -0400 Subject: [PATCH 11/81] Add logging for third party installs (#436) --- .github/workflows/third_party.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 8424d8fe..720ee7a8 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -110,6 +110,7 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typing_inspect test dependencies run: | + set -x 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 @@ -158,6 +159,7 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install pyanalyze test requirements run: | + set -x 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 @@ -206,6 +208,7 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typeguard test requirements run: | + set -x 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 @@ -259,6 +262,7 @@ jobs: git config --global user.name "Your Name" - name: Install typed-argument-parser test requirements run: | + set -x 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) @@ -308,6 +312,7 @@ jobs: run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install mypy test requirements run: | + set -x cd mypy 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 . From d9509f902c20e3d51c62b8abe522809b4760c0ff Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 29 Aug 2024 17:51:20 +0200 Subject: [PATCH 12/81] Copy the coroutine status in deprecated (#438) Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 38 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 8 ++++++++ 3 files changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c4cf34..0eafc6b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. +- Copy the coroutine status of functions and methods wrapped + with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 868e7938..474c02cc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,4 +1,5 @@ import abc +import asyncio import collections import collections.abc import contextlib @@ -115,9 +116,15 @@ # and adds PEP 695 to CPython's grammar TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) +# @deprecated works differently in Python 3.12 +TYPING_3_12_ONLY = (3, 12) <= sys.version_info < (3, 13) + # 3.13 drops support for the keyword argument syntax of TypedDict TYPING_3_13_0 = sys.version_info[:3] >= (3, 13, 0) +# 3.13.0.rc1 fixes a problem with @deprecated +TYPING_3_13_0_RC = sys.version_info[:4] >= (3, 13, 0, "candidate") + # 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 @@ -850,6 +857,37 @@ def d(): pass isinstance(cell.cell_contents, deprecated) for cell in d.__closure__ )) +@deprecated("depr") +def func(): + pass + +@deprecated("depr") +async def coro(): + pass + +class Cls: + @deprecated("depr") + def func(self): + pass + + @deprecated("depr") + async def coro(self): + pass + +class DeprecatedCoroTests(BaseTestCase): + def test_asyncio_iscoroutinefunction(self): + self.assertFalse(asyncio.coroutines.iscoroutinefunction(func)) + self.assertFalse(asyncio.coroutines.iscoroutinefunction(Cls.func)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(coro)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(Cls.coro)) + + @skipUnless(TYPING_3_12_ONLY or TYPING_3_13_0_RC, "inspect.iscoroutinefunction works differently on Python < 3.12") + def test_inspect_iscoroutinefunction(self): + self.assertFalse(inspect.iscoroutinefunction(func)) + self.assertFalse(inspect.iscoroutinefunction(Cls.func)) + self.assertTrue(inspect.iscoroutinefunction(coro)) + self.assertTrue(inspect.iscoroutinefunction(Cls.coro)) + class AnyTests(BaseTestCase): def test_can_subclass(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 8046dae1..1adc5823 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2898,13 +2898,21 @@ def __init_subclass__(*args, **kwargs): __init_subclass__.__deprecated__ = msg return arg elif callable(arg): + import asyncio.coroutines import functools + import inspect @functools.wraps(arg) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) return arg(*args, **kwargs) + if asyncio.coroutines.iscoroutinefunction(arg): + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + wrapper._is_coroutine = asyncio.coroutines._is_coroutine + arg.__deprecated__ = wrapper.__deprecated__ = msg return wrapper else: From 28493326b1a0f10f46e9a1ed85ff8da6341942ca Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:39:05 -0700 Subject: [PATCH 13/81] Remove typeguard PyPy tests (#463) This one has been flaky for a while (since July I think), but has recently started failing pretty consistently --- .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 720ee7a8..b5d65903 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -186,7 +186,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From b6c0558a167c0daffaf22c13c190fb9658c6b072 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 23 Sep 2024 11:35:54 -0700 Subject: [PATCH 14/81] Add tests for metaclasses and typing_extensions.get_annotations (#440) Tests from python/cpython#122074. We don't have to use the base descriptor approach here because we find the annotations directly in the `__dict__` for the class, which avoids metaclass problems. --- pyproject.toml | 16 +++++++-- src/test_typing_extensions.py | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3388d553..51276151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,9 +81,19 @@ select = [ "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"] +ignore = [ + # Ignore various "modernization" rules that tell you off for importing/using + # deprecated things from the typing module, etc. + "UP006", + "UP007", + "UP013", + "UP014", + "UP019", + "UP035", + "UP038", + # Not relevant here + "RUF012", +] [tool.ruff.lint.per-file-ignores] "!src/typing_extensions.py" = [ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 474c02cc..acd762ee 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9,6 +9,7 @@ import importlib import inspect import io +import itertools import pickle import re import subprocess @@ -7685,6 +7686,71 @@ def f(x: int): self.assertEqual(get_annotations(f), {"x": str}) +class TestGetAnnotationsMetaclasses(BaseTestCase): + def test_annotated_meta(self): + class Meta(type): + a: int + + class X(metaclass=Meta): + pass + + class Y(metaclass=Meta): + b: float + + self.assertEqual(get_annotations(Meta), {"a": int}) + self.assertEqual(get_annotations(X), {}) + self.assertEqual(get_annotations(Y), {"b": float}) + + def test_unannotated_meta(self): + class Meta(type): pass + + class X(metaclass=Meta): + a: str + + class Y(X): pass + + self.assertEqual(get_annotations(Meta), {}) + self.assertEqual(get_annotations(Y), {}) + self.assertEqual(get_annotations(X), {"a": str}) + + def test_ordering(self): + # Based on a sample by David Ellis + # https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38 + + def make_classes(): + class Meta(type): + a: int + expected_annotations = {"a": int} + + class A(type, metaclass=Meta): + b: float + expected_annotations = {"b": float} + + class B(metaclass=A): + c: str + expected_annotations = {"c": str} + + class C(B): + expected_annotations = {} + + class D(metaclass=Meta): + expected_annotations = {} + + return Meta, A, B, C, D + + classes = make_classes() + class_count = len(classes) + for order in itertools.permutations(range(class_count), class_count): + names = ", ".join(classes[i].__name__ for i in order) + with self.subTest(names=names): + classes = make_classes() # Regenerate classes + for i in order: + get_annotations(classes[i]) + for c in classes: + with self.subTest(c=c): + self.assertEqual(get_annotations(c), c.expected_annotations) + + @skipIf(STRINGIZED_ANNOTATIONS_PEP_695 is None, "PEP 695 has yet to be") class TestGetAnnotationsWithPEP695(BaseTestCase): @classmethod From 2c84de153037bfb72bca4ebfc974b0c6aeeda68f Mon Sep 17 00:00:00 2001 From: Daraan Date: Thu, 26 Sep 2024 18:14:28 +0200 Subject: [PATCH 15/81] Raise TypeError when TypeAliasType is subscripted without having type_params (#473) Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 14 ++++++++++++++ src/typing_extensions.py | 2 ++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eafc6b6..9c17c1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. - Copy the coroutine status of functions and methods wrapped with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. +- Fix bug where `TypeAliasType` instances could be subscripted even + where they were not generic. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index acd762ee..79f01901 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7247,6 +7247,20 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + def test_subscription_without_type_params(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): + Simple[int] + + # A TypeVar in the value does not allow subscription + T = TypeVar('T') + MissingTypeParamsErr = TypeAliasType("MissingTypeParamsErr", List[T]) + self.assertEqual(MissingTypeParamsErr.__type_params__, ()) + self.assertEqual(MissingTypeParamsErr.__parameters__, ()) + with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): + MissingTypeParamsErr[int] + + def test_pickle(self): global Alias Alias = TypeAliasType("Alias", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1adc5823..3b9239d1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3525,6 +3525,8 @@ def __repr__(self) -> str: return self.__name__ def __getitem__(self, parameters): + if not self.__type_params__: + raise TypeError("Only generic type aliases are subscriptable") if not isinstance(parameters, tuple): parameters = (parameters,) parameters = [ From 832253d743e60cd75c54c84af08d4ac17b985bdd Mon Sep 17 00:00:00 2001 From: Daraan Date: Thu, 26 Sep 2024 19:03:18 +0200 Subject: [PATCH 16/81] Add missing dunder attributes for TypeAliasType instances (#470) Co-authored-by: Alex Waygood --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 24 ++++++++++++++++++++- src/typing_extensions.py | 40 ++++++++++++++++++++++++++++++++--- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c17c1ba..0d3cafa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. - Fix bug where `TypeAliasType` instances could be subscripted even where they were not generic. Patch by [Daraan](https://github.com/Daraan). +- Fix bug where a subscripted `TypeAliasType` instance did not have all + attributes of the original `TypeAliasType` instance on older Python versions. + Patch by [Daraan](https://github.com/Daraan) and Alex Waygood. # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 79f01901..05b3083f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7247,6 +7247,29 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + def test_alias_attributes(self): + T = TypeVar('T') + T2 = TypeVar('T2') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + + subscripted = ListOrSetT[int] + self.assertEqual(subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(subscripted.__name__, "ListOrSetT") + self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(subscripted.__type_params__, (T,)) + + still_generic = ListOrSetT[Iterable[T2]] + self.assertEqual(still_generic.__module__, ListOrSetT.__module__) + self.assertEqual(still_generic.__name__, "ListOrSetT") + self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) + self.assertEqual(still_generic.__type_params__, (T,)) + + fully_subscripted = still_generic[float] + self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(fully_subscripted.__name__, "ListOrSetT") + self.assertEqual(fully_subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(fully_subscripted.__type_params__, (T,)) + def test_subscription_without_type_params(self): Simple = TypeAliasType("Simple", int) with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): @@ -7260,7 +7283,6 @@ def test_subscription_without_type_params(self): with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): MissingTypeParamsErr[int] - def test_pickle(self): global Alias Alias = TypeAliasType("Alias", int) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3b9239d1..13bd5442 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3452,6 +3452,37 @@ def _is_unionable(obj): TypeAliasType, )) + if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): + def __getattr__(self, attr): + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) + + if sys.version_info < (3, 9): + def __getitem__(self, item): + result = super().__getitem__(item) + result.__class__ = type(self) + return result + class TypeAliasType: """Create named, parameterized type aliases. @@ -3529,13 +3560,16 @@ def __getitem__(self, parameters): raise TypeError("Only generic type aliases are subscriptable") if not isinstance(parameters, tuple): parameters = (parameters,) - parameters = [ + # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, parameters) + parameters = tuple( typing._type_check( item, f'Subscripting {self.__name__} requires a type.' ) for item in parameters - ] - return typing._GenericAlias(self, tuple(parameters)) + ) + return _TypeAliasGenericAlias(self, parameters) def __reduce__(self): return self.__name__ From 7632716f82422d0540d0f0da54cca6006fef2798 Mon Sep 17 00:00:00 2001 From: Daraan Date: Thu, 26 Sep 2024 20:12:38 +0200 Subject: [PATCH 17/81] _collect_type_vars should not collect Unpack objects itself (#472) Co-authored-by: Alex Waygood --- CHANGELOG.md | 4 ++++ src/test_typing_extensions.py | 15 +++++++++++++++ src/typing_extensions.py | 5 ++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3cafa1..01eee613 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Fix bug where a subscripted `TypeAliasType` instance did not have all attributes of the original `TypeAliasType` instance on older Python versions. Patch by [Daraan](https://github.com/Daraan) and Alex Waygood. +- Fix bug where subscripted `TypeAliasType` instances (and some other + subscripted objects) had wrong parameters if they were directly + subscripted with an `Unpack` object. + Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 05b3083f..81a4c7ca 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7247,6 +7247,21 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + def test_unpack_parameter_collection(self): + Ts = TypeVarTuple("Ts") + + class Foo(Generic[Unpack[Ts]]): + bar: Tuple[Unpack[Ts]] + + FooAlias = TypeAliasType("FooAlias", Foo[Unpack[Ts]], type_params=(Ts,)) + self.assertEqual(FooAlias[Unpack[Tuple[str]]].__parameters__, ()) + self.assertEqual(FooAlias[Unpack[Tuple[T]]].__parameters__, (T,)) + + P = ParamSpec("P") + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + call_int_T = CallableP[Unpack[Tuple[int, T]]] + self.assertEqual(call_int_T.__parameters__, (T,)) + def test_alias_attributes(self): T = TypeVar('T') T2 = TypeVar('T2') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 13bd5442..3fd797a2 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3068,7 +3068,10 @@ def _collect_type_vars(types, typevar_types=None): for t in types: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True - elif isinstance(t, typevar_types) and t not in tvars: + elif ( + isinstance(t, typevar_types) and not isinstance(t, _UnpackAlias) + and t not in tvars + ): if enforce_default_ordering: has_default = getattr(t, '__default__', NoDefault) is not NoDefault if has_default: From 08d866b39203ba639e8d0b4f07194e5547f973f5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Sep 2024 14:27:09 -0700 Subject: [PATCH 18/81] Rename TypeExpr to TypeForm (#475) No backwards compatibility required because we never released TypeExpr. Also took the opportunity to expand the docstring. --- CHANGELOG.md | 2 +- doc/index.rst | 4 +-- src/test_typing_extensions.py | 46 +++++++++++++++++------------------ src/typing_extensions.py | 44 +++++++++++++++++++++------------ 4 files changed, 55 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01eee613..db6719c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Add `typing_extensions.TypeExpr` from PEP 747. Patch by +- Add `typing_extensions.TypeForm` from PEP 747. Patch by Jelle Zijlstra. - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified diff --git a/doc/index.rst b/doc/index.rst index 23a531c4..91740aa7 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -367,9 +367,9 @@ Special typing primitives .. versionadded:: 4.6.0 -.. data:: TypeExpr +.. data:: TypeForm - See :pep:`747`. A type hint representing a type expression. + See :pep:`747`. A special form representing the value of a type expression. .. versionadded:: 4.13.0 diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 81a4c7ca..8c2726f8 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -70,7 +70,7 @@ TypeAlias, TypeAliasType, TypedDict, - TypeExpr, + TypeForm, TypeGuard, TypeIs, TypeVar, @@ -5508,33 +5508,33 @@ def test_no_isinstance(self): issubclass(int, TypeIs) -class TypeExprTests(BaseTestCase): +class TypeFormTests(BaseTestCase): def test_basics(self): - TypeExpr[int] # OK - self.assertEqual(TypeExpr[int], TypeExpr[int]) + TypeForm[int] # OK + self.assertEqual(TypeForm[int], TypeForm[int]) - def foo(arg) -> TypeExpr[int]: ... - self.assertEqual(gth(foo), {'return': TypeExpr[int]}) + def foo(arg) -> TypeForm[int]: ... + self.assertEqual(gth(foo), {'return': TypeForm[int]}) def test_repr(self): - if hasattr(typing, 'TypeExpr'): + if hasattr(typing, 'TypeForm'): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(TypeExpr), f'{mod_name}.TypeExpr') - cv = TypeExpr[int] - self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[int]') - cv = TypeExpr[Employee] - self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[{__name__}.Employee]') - cv = TypeExpr[Tuple[int]] - self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[typing.Tuple[int]]') + self.assertEqual(repr(TypeForm), f'{mod_name}.TypeForm') + cv = TypeForm[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[int]') + cv = TypeForm[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[{__name__}.Employee]') + cv = TypeForm[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[typing.Tuple[int]]') def test_cannot_subclass(self): with self.assertRaises(TypeError): - class C(type(TypeExpr)): + class C(type(TypeForm)): pass with self.assertRaises(TypeError): - class D(type(TypeExpr[int])): + class D(type(TypeForm[int])): pass def test_call(self): @@ -5546,24 +5546,24 @@ def test_call(self): ] for obj in objs: with self.subTest(obj=obj): - self.assertIs(TypeExpr(obj), obj) + self.assertIs(TypeForm(obj), obj) with self.assertRaises(TypeError): - TypeExpr() + TypeForm() with self.assertRaises(TypeError): - TypeExpr("too", "many") + TypeForm("too", "many") def test_cannot_init_type(self): with self.assertRaises(TypeError): - type(TypeExpr)() + type(TypeForm)() with self.assertRaises(TypeError): - type(TypeExpr[Optional[int]])() + type(TypeForm[Optional[int]])() def test_no_isinstance(self): with self.assertRaises(TypeError): - isinstance(1, TypeExpr[int]) + isinstance(1, TypeForm[int]) with self.assertRaises(TypeError): - issubclass(int, TypeExpr) + issubclass(int, TypeForm) class LiteralStringTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 3fd797a2..5bf4f2dc 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -86,7 +86,7 @@ 'Text', 'TypeAlias', 'TypeAliasType', - 'TypeExpr', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -2047,23 +2047,30 @@ def f(val: Union[int, Awaitable[int]]) -> int: """) # 3.14+? -if hasattr(typing, 'TypeExpr'): - TypeExpr = typing.TypeExpr +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm # 3.9 elif sys.version_info[:2] >= (3, 9): - class _TypeExprForm(_ExtensionsSpecialForm, _root=True): - # TypeExpr(X) is equivalent to X but indicates to the type checker - # that the object is a TypeExpr. + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. def __call__(self, obj, /): return obj - @_TypeExprForm - def TypeExpr(self, parameters): - """Special typing form used to represent a type expression. + @_TypeFormForm + def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. Usage: - def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + def cast[T](typ: TypeForm[T], value: Any) -> T: ... reveal_type(cast(int, "x")) # int @@ -2073,7 +2080,7 @@ def cast[T](typ: TypeExpr[T], value: Any) -> T: ... return typing._GenericAlias(self, (item,)) # 3.8 else: - class _TypeExprForm(_ExtensionsSpecialForm, _root=True): + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type') @@ -2082,13 +2089,20 @@ def __getitem__(self, parameters): def __call__(self, obj, /): return obj - TypeExpr = _TypeExprForm( - 'TypeExpr', - doc="""Special typing form used to represent a type expression. + TypeForm = _TypeFormForm( + 'TypeForm', + doc="""A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. Usage: - def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + def cast[T](typ: TypeForm[T], value: Any) -> T: ... reveal_type(cast(int, "x")) # int From 17d3a37635bad3902c4e913a48d969cbebfb08c3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 10 Oct 2024 20:44:22 -0700 Subject: [PATCH 19/81] third-party test: Update Python versions (#484) Drop 3.8. One of them (pyanalyze) already dropped support for 3.8, and likely others will follow soon. I'd like to wait a bit to drop support in typing-extensions itself, but I'm no longer interested in how 3.8 works in third-party packages. Also add 3.12 and 3.13 for all of them. If any don't work, I'll drop them again. --- .github/workflows/third_party.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index b5d65903..7987f74e 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -44,7 +44,7 @@ jobs: # 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"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -89,7 +89,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -137,7 +137,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + # 3.13 support is pending + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -186,7 +187,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -218,6 +219,7 @@ jobs: - name: Run typeguard tests run: | cd typeguard + export PYTHON_COLORS=0 # A test fails if tracebacks are colorized pytest typed-argument-parser: @@ -235,7 +237,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + # 3.13 support: https://github.com/swansonk14/typed-argument-parser/issues/150 + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -290,7 +293,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -340,7 +343,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + # skip 3.13 because msgspec doesn't support 3.13 yet + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 7f4aef716f092ccdfda02daf3dfd421eb68d99f1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 15 Oct 2024 10:16:57 -0700 Subject: [PATCH 20/81] third_party: enable 3.13 for pyanalyze (#486) --- .github/workflows/third_party.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 7987f74e..c47c8bdc 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -137,8 +137,7 @@ jobs: strategy: fail-fast: false matrix: - # 3.13 support is pending - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From a2abfe62b7ceba6968bf10f0380c9205f6a80243 Mon Sep 17 00:00:00 2001 From: Daraan Date: Mon, 21 Oct 2024 19:04:35 +0200 Subject: [PATCH 21/81] Fix subscription of Unpack causing nested Unpacks to not be resolved correctly (#480) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 41 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 22 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6719c6..f127ada0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ subscripted objects) had wrong parameters if they were directly subscripted with an `Unpack` object. Patch by [Daraan](https://github.com/Daraan). +- Fix error in subscription of `Unpack` aliases causing nested Unpacks + to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8c2726f8..528763d6 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5779,6 +5779,47 @@ class D(Protocol[T1, T2, Unpack[Ts]]): pass with self.assertRaises(TypeError): klass[int] + def test_substitution(self): + Ts = TypeVarTuple("Ts") + unpacked_str = Unpack[Ts][str] # This should not raise an error + self.assertIs(unpacked_str, str) + + @skipUnless(TYPING_3_11_0, "Needs Issue #103 for <3.11") + def test_nested_unpack(self): + Ts = TypeVarTuple("Ts") + Variadic = Tuple[int, Unpack[Ts]] + # Tuple[int, int, Tuple[str, int]] + direct_subscription = Variadic[int, Tuple[str, int]] + # Tuple[int, int, Tuple[*Ts, int]] + TupleAliasTs = Variadic[int, Tuple[Unpack[Ts], int]] + + # Tuple[int, int, Tuple[str, int]] + recursive_unpack = TupleAliasTs[str] + self.assertEqual(direct_subscription, recursive_unpack) + self.assertEqual(get_args(recursive_unpack), (int, int, Tuple[str, int])) + + # Test with Callable + T = TypeVar("T") + # Tuple[int, (*Ts) -> T] + CallableAliasTsT = Variadic[Callable[[Unpack[Ts]], T]] + # Tuple[int, (str, int) -> object] + callable_fully_subscripted = CallableAliasTsT[Unpack[Tuple[str, int]], object] + self.assertEqual(get_args(callable_fully_subscripted), (int, Callable[[str, int], object])) + + @skipUnless(TYPING_3_11_0, "Needs Issue #103 for <3.11") + def test_equivalent_nested_variadics(self): + T = TypeVar("T") + Ts = TypeVarTuple("Ts") + Variadic = Tuple[int, Unpack[Ts]] + TupleAliasTsT = Variadic[Tuple[Unpack[Ts], T]] + nested_tuple_bare = TupleAliasTsT[str, int, object] + + self.assertEqual(get_args(nested_tuple_bare), (int, Tuple[str, int, object])) + # Variants + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int, object]]]) + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int]], object]) + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str]], Unpack[Tuple[int]], object]) + class TypeVarTupleTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5bf4f2dc..d194d623 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2424,6 +2424,17 @@ def __typing_unpacked_tuple_args__(self): return arg.__args__ return None + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + @_UnpackSpecialForm def Unpack(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2436,6 +2447,17 @@ def _is_unpack(obj): class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + class _UnpackForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, From 80958f37f65c33d6a8b73a53693b1bf6ee882a80 Mon Sep 17 00:00:00 2001 From: Daraan Date: Mon, 21 Oct 2024 22:26:51 +0200 Subject: [PATCH 22/81] Support subscription of `Callable[Concatenate[P], Any]` with `...` in Python 3.10 (#479) Co-authored-by: Alex Waygood Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 14 ++++++++++++- src/typing_extensions.py | 38 ++++++++++++++++++++++++++++------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f127ada0..2ddac4b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ subscripted objects) had wrong parameters if they were directly subscripted with an `Unpack` object. Patch by [Daraan](https://github.com/Daraan). +- Backport to Python 3.10 the ability to substitute `...` in generic `Callable` +aliases that have a `Concatenate` special form as their argument. + Patch by [Daraan](https://github.com/Daraan). - Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 528763d6..dfea3e3a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5401,6 +5401,18 @@ def test_invalid_uses(self): ): Concatenate[1, P] + @skipUnless(TYPING_3_10_0, "Missing backport to <=3.9. See issue #48") + def test_alias_subscription_with_ellipsis(self): + P = ParamSpec('P') + X = Callable[Concatenate[int, P], Any] + + C1 = X[...] + self.assertEqual(C1.__parameters__, ()) + with self.subTest("Compare Concatenate[int, ...]"): + if sys.version_info[:2] == (3, 10): + self.skipTest("Needs Issue #110 | PR #481: construct Concatenate with ...") + self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) + def test_basic_introspection(self): P = ParamSpec('P') C1 = Concatenate[int, P] @@ -6130,7 +6142,7 @@ def test_typing_extensions_defers_when_possible(self): if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'Any', 'NewType', 'overload'} + exclude |= {'final', 'Any', 'NewType', 'overload', 'Concatenate'} if sys.version_info < (3, 12): exclude |= { 'SupportsAbs', 'SupportsBytes', diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d194d623..b02510e9 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1795,28 +1795,52 @@ def __parameters__(self): return tuple( tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias + # 3.10 + if sys.version_info < (3, 11): + _typing_ConcatenateGenericAlias = _ConcatenateGenericAlias -# 3.8-3.9 + class _ConcatenateGenericAlias(_typing_ConcatenateGenericAlias, _root=True): + # needed for checks in collections.abc.Callable to accept this class + __module__ = "typing" + + def copy_with(self, params): + if isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return super(_typing_ConcatenateGenericAlias, self).copy_with(params) + + +# 3.8-3.10 @typing._tp_cache def _concatenate_getitem(self, parameters): if parameters == (): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - if not isinstance(parameters[-1], ParamSpec): + elif not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): raise TypeError("The last parameter to Concatenate should be a " - "ParamSpec variable.") + "ParamSpec variable or ellipsis.") msg = "Concatenate[arg, ...]: each arg must be a type." parameters = tuple(typing._type_check(p, msg) for p in parameters) + if (3, 10, 2) < sys.version_info < (3, 11): + return _ConcatenateGenericAlias(self, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) return _ConcatenateGenericAlias(self, parameters) -# 3.10+ -if hasattr(typing, 'Concatenate'): +# 3.11+ +if sys.version_info >= (3, 11): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias -# 3.9 +# 3.9-3.10 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm def Concatenate(self, parameters): From 3ebe884321d7c842bac523a215ba7f14591b467b Mon Sep 17 00:00:00 2001 From: Daraan Date: Tue, 22 Oct 2024 14:57:14 +0200 Subject: [PATCH 23/81] Support Ellipsis argument to Concatenate (#481) --- CHANGELOG.md | 2 + doc/index.rst | 2 +- src/test_typing_extensions.py | 83 +++++++++++++++++++++++++---------- src/typing_extensions.py | 41 +++++++++++++---- 4 files changed, 96 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ddac4b4..f62d31d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ - Backport to Python 3.10 the ability to substitute `...` in generic `Callable` aliases that have a `Concatenate` special form as their argument. Patch by [Daraan](https://github.com/Daraan). +- Extended the `Concatenate` backport for Python 3.8-3.10 to now accept + `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). - Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). diff --git a/doc/index.rst b/doc/index.rst index 91740aa7..d321ce04 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -178,7 +178,7 @@ Special typing primitives See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. The backport does not support certain operations involving ``...`` as - a parameter; see :issue:`48` and :issue:`110` for details. + a parameter; see :issue:`48` and :pr:`481` for details. .. data:: Final diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index dfea3e3a..1b43f90f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1720,12 +1720,14 @@ class C(Generic[T]): pass # In 3.9 and lower we use typing_extensions's hacky implementation # of ParamSpec, which gets incorrectly wrapped in a list self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)]) - self.assertEqual(get_args(Callable[Concatenate[int, P], int]), - (Concatenate[int, P], int)) self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(Unpack[Ts]), (Ts,)) self.assertEqual(get_args(Unpack), ()) + self.assertEqual(get_args(Callable[Concatenate[int, P], int]), + (Concatenate[int, P], int)) + self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), + (Concatenate[int, ...], int)) class CollectionsAbcTests(BaseTestCase): @@ -5267,6 +5269,10 @@ class Y(Protocol[T, P]): self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) self.assertEqual(G2.__parameters__, (P_2,)) + G3 = klass[int, Concatenate[int, ...]] + self.assertEqual(G3.__args__, (int, Concatenate[int, ...])) + self.assertEqual(G3.__parameters__, ()) + # The following are some valid uses cases in PEP 612 that don't work: # These do not work in 3.9, _type_check blocks the list and ellipsis. # G3 = X[int, [int, bool]] @@ -5362,21 +5368,28 @@ class MyClass: ... c = Concatenate[MyClass, P] self.assertNotEqual(c, Concatenate) + # Test Ellipsis Concatenation + d = Concatenate[MyClass, ...] + self.assertNotEqual(d, c) + self.assertNotEqual(d, Concatenate) + def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') + for callable_variant in (Callable, collections.abc.Callable): + with self.subTest(callable_variant=callable_variant): + if not TYPING_3_9_0 and callable_variant is collections.abc.Callable: + self.skipTest("Needs PEP 585") - C1 = Callable[Concatenate[int, P], int] - C2 = Callable[Concatenate[int, T, P], T] - self.assertEqual(C1.__origin__, C2.__origin__) - self.assertNotEqual(C1, C2) + C1 = callable_variant[Concatenate[int, P], int] + C2 = callable_variant[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) + C3 = callable_variant[Concatenate[int, ...], int] + C4 = callable_variant[Concatenate[int, T, ...], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) def test_invalid_uses(self): P = ParamSpec('P') @@ -5390,16 +5403,30 @@ def test_invalid_uses(self): with self.assertRaisesRegex( TypeError, - 'The last parameter to Concatenate should be a ParamSpec variable', + 'The last parameter to Concatenate should be a ParamSpec variable or ellipsis', ): Concatenate[P, T] - if not TYPING_3_11_0: - with self.assertRaisesRegex( - TypeError, - 'each arg must be a type', - ): - Concatenate[1, P] + # Test with tuple argument + with self.assertRaisesRegex( + TypeError, + "The last parameter to Concatenate should be a ParamSpec variable or ellipsis.", + ): + Concatenate[(P, T)] + + with self.assertRaisesRegex( + TypeError, + 'is not a generic class', + ): + Callable[Concatenate[int, ...], Any][Any] + + # Assure that `_type_check` is called. + P = ParamSpec('P') + with self.assertRaisesRegex( + TypeError, + "each arg must be a type", + ): + Concatenate[(str,), P] @skipUnless(TYPING_3_10_0, "Missing backport to <=3.9. See issue #48") def test_alias_subscription_with_ellipsis(self): @@ -5408,19 +5435,22 @@ def test_alias_subscription_with_ellipsis(self): C1 = X[...] self.assertEqual(C1.__parameters__, ()) - with self.subTest("Compare Concatenate[int, ...]"): - if sys.version_info[:2] == (3, 10): - self.skipTest("Needs Issue #110 | PR #481: construct Concatenate with ...") - self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) + self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) def test_basic_introspection(self): P = ParamSpec('P') C1 = Concatenate[int, P] C2 = Concatenate[int, T, P] + C3 = Concatenate[int, ...] + C4 = Concatenate[int, T, ...] self.assertEqual(C1.__origin__, Concatenate) self.assertEqual(C1.__args__, (int, P)) self.assertEqual(C2.__origin__, Concatenate) self.assertEqual(C2.__args__, (int, T, P)) + self.assertEqual(C3.__origin__, Concatenate) + self.assertEqual(C3.__args__, (int, Ellipsis)) + self.assertEqual(C4.__origin__, Concatenate) + self.assertEqual(C4.__args__, (int, T, Ellipsis)) def test_eq(self): P = ParamSpec('P') @@ -5431,6 +5461,13 @@ def test_eq(self): self.assertEqual(hash(C1), hash(C2)) self.assertNotEqual(C1, C3) + C4 = Concatenate[int, ...] + C5 = Concatenate[int, ...] + C6 = Concatenate[int, T, ...] + self.assertEqual(C4, C5) + self.assertEqual(hash(C4), hash(C5)) + self.assertNotEqual(C4, C6) + class TypeGuardTests(BaseTestCase): def test_basics(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b02510e9..c5e84b31 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1818,6 +1818,34 @@ def copy_with(self, params): return super(_typing_ConcatenateGenericAlias, self).copy_with(params) +# 3.8-3.9.2 +class _EllipsisDummy: ... + + +# 3.8-3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Hack: Arguments must be types, replace it with one. + parameters = (*parameters[:-1], _EllipsisDummy) + if sys.version_info >= (3, 10, 2): + concatenate = _ConcatenateGenericAlias(origin, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummy: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummy) + return concatenate + + # 3.8-3.10 @typing._tp_cache def _concatenate_getitem(self, parameters): @@ -1825,19 +1853,16 @@ def _concatenate_getitem(self, parameters): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - elif not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): + if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): raise TypeError("The last parameter to Concatenate should be a " "ParamSpec variable or ellipsis.") msg = "Concatenate[arg, ...]: each arg must be a type." - parameters = tuple(typing._type_check(p, msg) for p in parameters) - if (3, 10, 2) < sys.version_info < (3, 11): - return _ConcatenateGenericAlias(self, parameters, - _typevar_types=(TypeVar, ParamSpec), - _paramspec_tvars=True) - return _ConcatenateGenericAlias(self, parameters) + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _create_concatenate_alias(self, parameters) -# 3.11+ +# 3.11+; Concatenate does not accept ellipsis in 3.10 if sys.version_info >= (3, 11): Concatenate = typing.Concatenate # 3.9-3.10 From 82d512ae1082b9c7c82678771746753a583cc64b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 22 Oct 2024 07:03:53 -0700 Subject: [PATCH 24/81] Run 3.13 tests for typed_argument_parser (#487) --- .github/workflows/third_party.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index c47c8bdc..effe98a7 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -236,8 +236,7 @@ jobs: strategy: fail-fast: false matrix: - # 3.13 support: https://github.com/swansonk14/typed-argument-parser/issues/150 - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 139ac686aab0fc4ead81d3d1d199f386977a1532 Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 25 Oct 2024 04:42:59 +0200 Subject: [PATCH 25/81] [3.14] Address invalid inputs of TypeAliasType (#477) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 3 ++ src/test_typing_extensions.py | 78 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 21 +++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f62d31d0..043b5feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ aliases that have a `Concatenate` special form as their argument. `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). - Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). +- Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795) + and fix that `TypeAliasType` not raising an error on non-tupple inputs for `type_params`. + Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 1b43f90f..ec629b40 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6192,6 +6192,10 @@ def test_typing_extensions_defers_when_possible(self): 'AsyncGenerator', 'ContextManager', 'AsyncContextManager', 'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints', } + if sys.version_info < (3, 14): + exclude |= { + 'TypeAliasType' + } if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: @@ -7402,6 +7406,80 @@ def test_no_instance_subclassing(self): class MyAlias(TypeAliasType): pass + def test_type_var_compatibility(self): + # Regression test to assure compatibility with typing variants + typingT = typing.TypeVar('typingT') + T1 = TypeAliasType("TypingTypeVar", ..., type_params=(typingT,)) + self.assertEqual(T1.__type_params__, (typingT,)) + + # Test typing_extensions backports + textT = TypeVar('textT') + T2 = TypeAliasType("TypingExtTypeVar", ..., type_params=(textT,)) + self.assertEqual(T2.__type_params__, (textT,)) + + textP = ParamSpec("textP") + T3 = TypeAliasType("TypingExtParamSpec", ..., type_params=(textP,)) + self.assertEqual(T3.__type_params__, (textP,)) + + textTs = TypeVarTuple("textTs") + T4 = TypeAliasType("TypingExtTypeVarTuple", ..., type_params=(textTs,)) + self.assertEqual(T4.__type_params__, (textTs,)) + + @skipUnless(TYPING_3_10_0, "typing.ParamSpec is not available before 3.10") + def test_param_spec_compatibility(self): + # Regression test to assure compatibility with typing variant + typingP = typing.ParamSpec("typingP") + T5 = TypeAliasType("TypingParamSpec", ..., type_params=(typingP,)) + self.assertEqual(T5.__type_params__, (typingP,)) + + @skipUnless(TYPING_3_12_0, "typing.TypeVarTuple is not available before 3.12") + def test_type_var_tuple_compatibility(self): + # Regression test to assure compatibility with typing variant + typingTs = typing.TypeVarTuple("typingTs") + T6 = TypeAliasType("TypingTypeVarTuple", ..., type_params=(typingTs,)) + self.assertEqual(T6.__type_params__, (typingTs,)) + + def test_type_params_possibilities(self): + T = TypeVar('T') + # Test not a tuple + with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): + TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) + + # Test default order and other invalid inputs + T_default = TypeVar('T_default', default=int) + Ts = TypeVarTuple('Ts') + Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]]) + P = ParamSpec('P') + P_default = ParamSpec('P_default', default=[str, int]) + + # NOTE: PEP 696 states: "TypeVars with defaults cannot immediately follow TypeVarTuples" + # this is currently not enforced for the type statement and is not tested. + # PEP 695: Double usage of the same name is also not enforced and not tested. + valid_cases = [ + (T, P, Ts), + (T, Ts_default), + (P_default, T_default), + (P, T_default, Ts_default), + (T_default, P_default, Ts_default), + ] + invalid_cases = [ + ((T_default, T), f"non-default type parameter {T!r} follows default"), + ((P_default, P), f"non-default type parameter {P!r} follows default"), + ((Ts_default, T), f"non-default type parameter {T!r} follows default"), + # Only type params are accepted + ((1,), "Expected a type param, got 1"), + ((str,), f"Expected a type param, got {str!r}"), + # Unpack is not a TypeVar but isinstance(Unpack[Ts], TypeVar) is True in Python < 3.12 + ((Unpack[Ts],), f"Expected a type param, got {re.escape(repr(Unpack[Ts]))}"), + ] + + for case in valid_cases: + with self.subTest(type_params=case): + TypeAliasType("OkCase", List[T], type_params=case) + for case, msg in invalid_cases: + with self.subTest(type_params=case): + with self.assertRaisesRegex(TypeError, msg): + TypeAliasType("InvalidCase", List[T], type_params=case) class DocTests(BaseTestCase): def test_annotation(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c5e84b31..f9f93d7c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3528,8 +3528,9 @@ def __ror__(self, other): return typing.Union[other, self] -if hasattr(typing, "TypeAliasType"): +if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType +# 3.8-3.13 else: def _is_unionable(obj): """Corresponds to is_unionable() in unionobject.c in CPython.""" @@ -3602,11 +3603,29 @@ class TypeAliasType: def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") self.__value__ = value self.__type_params__ = type_params + default_value_encountered = False parameters = [] for type_param in type_params: + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # 3.8-3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) + or _is_unpack(type_param) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f'non-default type parameter {type_param!r}' + ' follows default type parameter') + if has_default: + default_value_encountered = True if isinstance(type_param, TypeVarTuple): parameters.extend(type_param) else: From a50639823cd50de40b9ee04d31ce7f4ee685a716 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 25 Oct 2024 15:48:44 +0100 Subject: [PATCH 26/81] Fix changelog typos (#494) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043b5feb..069381db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,8 @@ aliases that have a `Concatenate` special form as their argument. `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). - Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). -- Backport of CPython PR [#124795](https://github.com/python/cpython/pull/124795) - and fix that `TypeAliasType` not raising an error on non-tupple inputs for `type_params`. +- Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): + fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) From 9340ce7f4e5e89617e63ac30a00118934200a471 Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 25 Oct 2024 17:50:48 +0200 Subject: [PATCH 27/81] TypeAliasType: Add apostrophe to error message for compatibility with CPython (#495) --- src/test_typing_extensions.py | 6 +++--- src/typing_extensions.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ec629b40..3471f0a3 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7463,9 +7463,9 @@ def test_type_params_possibilities(self): (T_default, P_default, Ts_default), ] invalid_cases = [ - ((T_default, T), f"non-default type parameter {T!r} follows default"), - ((P_default, P), f"non-default type parameter {P!r} follows default"), - ((Ts_default, T), f"non-default type parameter {T!r} follows default"), + ((T_default, T), f"non-default type parameter '{T!r}' follows default"), + ((P_default, P), f"non-default type parameter '{P!r}' follows default"), + ((Ts_default, T), f"non-default type parameter '{T!r}' follows default"), # Only type params are accepted ((1,), "Expected a type param, got 1"), ((str,), f"Expected a type param, got {str!r}"), diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f9f93d7c..5cdafb70 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3622,8 +3622,8 @@ def __init__(self, name: str, value, *, type_params=()): getattr(type_param, '__default__', NoDefault) is not NoDefault ) if default_value_encountered and not has_default: - raise TypeError(f'non-default type parameter {type_param!r}' - ' follows default type parameter') + raise TypeError(f"non-default type parameter '{type_param!r}'" + " follows default type parameter") if has_default: default_value_encountered = True if isinstance(type_param, TypeVarTuple): From 67c16e190e7ebe6a9130b63839520ce01bdd6422 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Fri, 25 Oct 2024 15:11:05 -0700 Subject: [PATCH 28/81] Change issue text on third party failure (#496) It can be confusing to click on an old issue, click the first red thing you see and you're seeing some new failure, not the old one This has tripped me up before and it looks like it briefly tripped up Daraan yesterday --- .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 effe98a7..359a6e4a 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -412,5 +412,5 @@ jobs: owner: "python", repo: "typing_extensions", title: `Third-party tests failed on ${new Date().toDateString()}`, - body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", + body: "Full history of runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", }) From e391124974c0b616c6d8728c47888d3c6c30f2ae Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 10 Nov 2024 19:37:04 -0500 Subject: [PATCH 29/81] Migrate pydantic tests to uv (#501) Migrate third_party workflow pydantic tests to uv --- .github/workflows/third_party.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 359a6e4a..cf58d2a0 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -60,19 +60,18 @@ jobs: uses: actions/checkout@v4 with: path: typing-extensions-latest - - name: Setup pdm for pydantic tests - uses: pdm-project/setup-pdm@v4 - with: - python-version: ${{ matrix.python-version }} - allow-python-prereleases: true + - name: Install uv + uses: astral-sh/setup-uv@v3 + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} - name: Add local version of typing_extensions as a dependency - run: pdm add ./typing-extensions-latest + run: uv add --editable ./typing-extensions-latest - name: Install pydantic test dependencies - run: pdm install -G testing -G email + run: uv sync --group testing --group dev - name: List installed dependencies - run: pdm list -vv # pdm equivalent to `pip list` + run: uv pip list - name: Run pydantic tests - run: pdm run pytest + run: uv run pytest typing_inspect: name: typing_inspect tests From e888dfdabcfcd03330477cb5de3c25c21029f425 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:09:13 +0100 Subject: [PATCH 30/81] Fix Pydantic third party integration tests (#504) --- .github/workflows/third_party.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index cf58d2a0..9ecad7b1 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -52,10 +52,6 @@ jobs: uses: actions/checkout@v4 with: repository: pydantic/pydantic - - name: Edit pydantic pyproject.toml - # pydantic's python-requires means pdm won't let us add typing-extensions-latest - # 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@v4 with: @@ -67,7 +63,7 @@ jobs: - name: Add local version of typing_extensions as a dependency run: uv add --editable ./typing-extensions-latest - name: Install pydantic test dependencies - run: uv sync --group testing --group dev + run: uv sync --group dev - name: List installed dependencies run: uv pip list - name: Run pydantic tests From bf9a252f9a5314f504bde0da14c2832c4a7015bc Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 17 Nov 2024 10:49:47 -0800 Subject: [PATCH 31/81] Just use git for checkout (#506) actions/checkout is repeatedly flaky See also https://github.com/actions/checkout/issues/1951 On the other hand, plain git clone seems to work just fine and never has connection issues checking out the default branch Funnily enough, the YAML is shorter this way too --- .github/workflows/third_party.yml | 119 +++++++++++++----------------- 1 file changed, 52 insertions(+), 67 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 9ecad7b1..4742d39b 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -48,26 +48,27 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout pydantic - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 with: - repository: pydantic/pydantic + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Checkout pydantic + run: git clone https://github.com/pydantic/pydantic.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: path: typing-extensions-latest - - name: Install uv - uses: astral-sh/setup-uv@v3 - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} - name: Add local version of typing_extensions as a dependency - run: uv add --editable ./typing-extensions-latest + run: cd pydantic; uv add --editable ../typing-extensions-latest - name: Install pydantic test dependencies - run: uv sync --group dev + run: cd pydantic; uv sync --group dev - name: List installed dependencies - run: uv pip list + run: cd pydantic; uv pip list - name: Run pydantic tests - run: uv run pytest + run: cd pydantic; uv run pytest typing_inspect: name: typing_inspect tests @@ -88,21 +89,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout typing_inspect - uses: actions/checkout@v4 - with: - repository: ilevkivskyi/typing_inspect - path: typing_inspect - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python 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: Checkout typing_inspect + run: git clone https://github.com/ilevkivskyi/typing_inspect.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Install typing_inspect test dependencies run: | set -x @@ -136,15 +134,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out pyanalyze - uses: actions/checkout@v4 - with: - repository: quora/pyanalyze - path: pyanalyze - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -152,6 +141,12 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Check out pyanalyze + run: git clone https://github.com/quora/pyanalyze.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Install pyanalyze test requirements run: | set -x @@ -185,15 +180,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out typeguard - uses: actions/checkout@v4 - with: - repository: agronholm/typeguard - path: typeguard - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -201,6 +187,12 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Check out typeguard + run: git clone https://github.com/agronholm/typeguard.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Install typeguard test requirements run: | set -x @@ -235,21 +227,18 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out typed-argument-parser - uses: actions/checkout@v4 - with: - repository: swansonk14/typed-argument-parser - path: typed-argument-parser - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python 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: Check out typed-argument-parser + run: git clone https://github.com/swansonk14/typed-argument-parser.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Configure git for typed-argument-parser tests # typed-argument parser does this in their CI, # and the tests fail unless we do this @@ -290,15 +279,6 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout mypy for stubtest and mypyc tests - uses: actions/checkout@v4 - with: - repository: python/mypy - path: mypy - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -306,6 +286,12 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Checkout mypy for stubtest and mypyc tests + run: git clone https://github.com/python/mypy.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Install mypy test requirements run: | set -x @@ -341,30 +327,29 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout cattrs - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 with: - repository: python-attrs/cattrs + python-version: ${{ matrix.python-version }} + - name: Checkout cattrs + run: git clone https://github.com/python-attrs/cattrs.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: path: typing-extensions-latest - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - name: Install pdm for cattrs run: pip install pdm - name: Add latest typing-extensions as a dependency run: | + cd cattrs pdm remove typing-extensions - pdm add --dev ./typing-extensions-latest + pdm add --dev ../typing-extensions-latest - name: Install cattrs test dependencies - run: pdm install --dev -G :all + run: cd cattrs; pdm install --dev -G :all - name: List all installed dependencies - run: pdm list -vv + run: cd cattrs; pdm list -vv - name: Run cattrs tests - run: pdm run pytest tests + run: cd cattrs; pdm run pytest tests create-issue-on-failure: name: Create an issue if daily tests failed From bf141ec923f7dde90e468ce43f254a22a0fee8e6 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Sun, 17 Nov 2024 23:42:39 -0800 Subject: [PATCH 32/81] Add a retry for clones (#509) --- .github/workflows/third_party.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 4742d39b..0bf1c820 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -56,7 +56,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Checkout pydantic - run: git clone https://github.com/pydantic/pydantic.git + run: git clone --depth=1 https://github.com/pydantic/pydantic.git || git clone --depth=1 https://github.com/pydantic/pydantic.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -96,7 +96,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Checkout typing_inspect - run: git clone https://github.com/ilevkivskyi/typing_inspect.git + run: git clone --depth=1 https://github.com/ilevkivskyi/typing_inspect.git || git clone --depth=1 https://github.com/ilevkivskyi/typing_inspect.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -142,7 +142,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Check out pyanalyze - run: git clone https://github.com/quora/pyanalyze.git + run: git clone --depth=1 https://github.com/quora/pyanalyze.git || git clone --depth=1 https://github.com/quora/pyanalyze.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -188,7 +188,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Check out typeguard - run: git clone https://github.com/agronholm/typeguard.git + run: git clone --depth=1 https://github.com/agronholm/typeguard.git || git clone --depth=1 https://github.com/agronholm/typeguard.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -234,7 +234,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Check out typed-argument-parser - run: git clone https://github.com/swansonk14/typed-argument-parser.git + run: git clone --depth=1 https://github.com/swansonk14/typed-argument-parser.git || git clone --depth=1 https://github.com/swansonk14/typed-argument-parser.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -287,7 +287,7 @@ jobs: - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Checkout mypy for stubtest and mypyc tests - run: git clone https://github.com/python/mypy.git + run: git clone --depth=1 https://github.com/python/mypy.git || git clone --depth=1 https://github.com/python/mypy.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: @@ -332,7 +332,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Checkout cattrs - run: git clone https://github.com/python-attrs/cattrs.git + run: git clone --depth=1 https://github.com/python-attrs/cattrs.git || git clone --depth=1 https://github.com/python-attrs/cattrs.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: From b7d63534658b01c3b24e45a34dbf9c38a3ce8ae9 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:06:10 +0100 Subject: [PATCH 33/81] Use SPDX license identifier (#507) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 51276151..f66cf6bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" -license = { file = "LICENSE" } +license = { text = "PSF-2.0" } keywords = [ "annotations", "backport", From f2d0667fe891b6dd4c5fdeb9a819bf3f69a475b0 Mon Sep 17 00:00:00 2001 From: Daraan Date: Tue, 26 Nov 2024 05:21:13 +0100 Subject: [PATCH 34/81] Support ParamSpec for TypeAliasType (#449) Co-authored-by: Alex Waygood --- CHANGELOG.md | 3 + src/test_typing_extensions.py | 193 +++++++++++++++++++++++++++++++++- src/typing_extensions.py | 42 ++++++-- 3 files changed, 229 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069381db..6333d7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ aliases that have a `Concatenate` special form as their argument. - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). +- Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` + instances before Python 3.11. + Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 3471f0a3..a7e6885e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7267,6 +7267,80 @@ def test_attributes(self): self.assertEqual(Variadic.__type_params__, (Ts,)) self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )) + self.assertEqual(CallableP.__name__, "CallableP") + self.assertEqual(CallableP.__value__, Callable[P, Any]) + self.assertEqual(CallableP.__type_params__, (P,)) + self.assertEqual(CallableP.__parameters__, (P,)) + + def test_alias_types_and_substitutions(self): + T = TypeVar('T') + T2 = TypeVar('T2') + T_default = TypeVar("T_default", default=int) + Ts = TypeVarTuple("Ts") + P = ParamSpec('P') + + test_argument_cases = { + # arguments : expected parameters + int : (), + ... : (), + None : (), + T2 : (T2,), + Union[int, List[T2]] : (T2,), + Tuple[int, str] : (), + Tuple[T, T_default, T2] : (T, T_default, T2), + Tuple[Unpack[Ts]] : (Ts,), + Callable[[Unpack[Ts]], T2] : (Ts, T2), + Callable[P, T2] : (P, T2), + Callable[Concatenate[T2, P], T_default] : (T2, P, T_default), + TypeAliasType("NestedAlias", List[T], type_params=(T,))[T2] : (T2,), + Unpack[Ts] : (Ts,), + Unpack[Tuple[int, T2]] : (T2,), + Concatenate[int, P] : (P,), + # Not tested usage of bare TypeVarTuple, would need 3.11+ + # Ts : (Ts,), # invalid case + } + + test_alias_cases = [ + # Simple cases + TypeAliasType("ListT", List[T], type_params=(T,)), + TypeAliasType("UnionT", Union[int, List[T]], type_params=(T,)), + # Value has no parameter but in type_param + TypeAliasType("ValueWithoutT", int, type_params=(T,)), + # Callable + TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )), + TypeAliasType("CallableT", Callable[..., T], type_params=(T, )), + TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )), + # TypeVarTuple + TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)), + # TypeVar with default + TypeAliasType("TupleT_default", Tuple[T_default, T], type_params=(T, T_default)), + TypeAliasType("CallableT_default", Callable[[T], T_default], type_params=(T, T_default)), + ] + + for alias in test_alias_cases: + with self.subTest(alias=alias, args=[]): + subscripted = alias[[]] + self.assertEqual(get_args(subscripted), ([],)) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=()): + subscripted = alias[()] + self.assertEqual(get_args(subscripted), ()) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=(int, float)): + subscripted = alias[int, float] + self.assertEqual(get_args(subscripted), (int, float)) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=[int, float]): + subscripted = alias[[int, float]] + self.assertEqual(get_args(subscripted), ([int, float],)) + self.assertEqual(subscripted.__parameters__, ()) + for expected_args, expected_parameters in test_argument_cases.items(): + with self.subTest(alias=alias, args=expected_args): + self.assertEqual(get_args(alias[expected_args]), (expected_args,)) + self.assertEqual(alias[expected_args].__parameters__, expected_parameters) + def test_cannot_set_attributes(self): Simple = TypeAliasType("Simple", int) with self.assertRaisesRegex(AttributeError, "readonly attribute"): @@ -7327,12 +7401,19 @@ def test_or(self): Alias | "Ref" def test_getitem(self): + T = TypeVar('T') ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) subscripted = ListOrSetT[int] self.assertEqual(get_args(subscripted), (int,)) self.assertIs(get_origin(subscripted), ListOrSetT) - with self.assertRaises(TypeError): - subscripted[str] + with self.assertRaisesRegex(TypeError, + "not a generic class" + # types.GenericAlias raises a different error in 3.10 + if sys.version_info[:2] != (3, 10) + else "There are no type variables left in ListOrSetT" + ): + subscripted[int] + still_generic = ListOrSetT[Iterable[T]] self.assertEqual(get_args(still_generic), (Iterable[T],)) @@ -7341,6 +7422,114 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + ValueWithoutTypeVar = TypeAliasType("ValueWithoutTypeVar", int, type_params=(T,)) + still_subscripted = ValueWithoutTypeVar[str] + self.assertEqual(get_args(still_subscripted), (str,)) + + def test_callable_without_concatenate(self): + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + get_args_test_cases = [ + # List of (alias, expected_args) + # () -> Any + (CallableP[()], ()), + (CallableP[[]], ([],)), + # (int) -> Any + (CallableP[int], (int,)), + (CallableP[[int]], ([int],)), + # (int, int) -> Any + (CallableP[int, int], (int, int)), + (CallableP[[int, int]], ([int, int],)), + # (...) -> Any + (CallableP[...], (...,)), + # (int, ...) -> Any + (CallableP[[int, ...]], ([int, ...],)), + ] + + for index, (expression, expected_args) in enumerate(get_args_test_cases): + with self.subTest(index=index, expression=expression): + self.assertEqual(get_args(expression), expected_args) + + self.assertEqual(CallableP[...], CallableP[(...,)]) + # (T) -> Any + CallableT = CallableP[T] + self.assertEqual(get_args(CallableT), (T,)) + self.assertEqual(CallableT.__parameters__, (T,)) + + def test_callable_with_concatenate(self): + P = ParamSpec('P') + P2 = ParamSpec('P2') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + + callable_concat = CallableP[Concatenate[int, P2]] + self.assertEqual(callable_concat.__parameters__, (P2,)) + concat_usage = callable_concat[str] + with self.subTest("get_args of Concatenate in TypeAliasType"): + if not TYPING_3_9_0: + # args are: ([, ~P2],) + self.skipTest("Nested ParamSpec is not substituted") + if sys.version_info < (3, 10, 2): + self.skipTest("GenericAlias keeps Concatenate in __args__ prior to 3.10.2") + self.assertEqual(get_args(concat_usage), ((int, str),)) + with self.subTest("Equality of parameter_expression without []"): + if not TYPING_3_10_0: + self.skipTest("Nested list is invalid type form") + self.assertEqual(concat_usage, callable_concat[[str]]) + + def test_substitution(self): + T = TypeVar('T') + Ts = TypeVarTuple("Ts") + + CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )) + unpack_callable = CallableTs[Unpack[Tuple[int, T]]] + self.assertEqual(get_args(unpack_callable), (Unpack[Tuple[int, T]],)) + + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P, T)) + callable_concat = CallableP[Concatenate[int, P], Any] + self.assertEqual(get_args(callable_concat), (Concatenate[int, P], Any)) + + def test_wrong_amount_of_parameters(self): + T = TypeVar('T') + T2 = TypeVar("T2") + P = ParamSpec('P') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + TwoT = TypeAliasType("TwoT", Union[List[T], Set[T2]], type_params=(T, T2)) + CallablePT = TypeAliasType("CallablePT", Callable[P, T], type_params=(P, T)) + + # Not enough parameters + test_cases = [ + # not_enough + (TwoT[int], [(int,), ()]), + (TwoT[T], [(T,), (T,)]), + # callable and not enough + (CallablePT[int], [(int,), ()]), + # too many + (ListOrSetT[int, bool], [(int, bool), ()]), + # callable and too many + (CallablePT[str, float, int], [(str, float, int), ()]), + # Check if TypeVar is still present even if over substituted + (ListOrSetT[int, T], [(int, T), (T,)]), + # With and without list for ParamSpec + (CallablePT[str, float, T], [(str, float, T), (T,)]), + (CallablePT[[str], float, int, T2], [([str], float, int, T2), (T2,)]), + ] + + for index, (alias, [expected_args, expected_params]) in enumerate(test_cases): + with self.subTest(index=index, alias=alias): + self.assertEqual(get_args(alias), expected_args) + self.assertEqual(alias.__parameters__, expected_params) + + # The condition should align with the version of GeneriAlias usage in __getitem__ or be 3.11+ + @skipIf(TYPING_3_10_0, "Most arguments are allowed in 3.11+ or with GenericAlias") + def test_invalid_cases_before_3_10(self): + T = TypeVar('T') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + with self.assertRaises(TypeError): + ListOrSetT[Generic[T]] + with self.assertRaises(TypeError): + ListOrSetT[(Generic[T], )] + def test_unpack_parameter_collection(self): Ts = TypeVarTuple("Ts") diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5cdafb70..dc35b3d4 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3662,6 +3662,33 @@ def _raise_attribute_error(self, name: str) -> Never: def __repr__(self) -> str: return self.__name__ + if sys.version_info < (3, 11): + def _check_single_param(self, param, recursion=0): + # Allow [], [int], [int, str], [int, ...], [int, T] + if param is ...: + return ... + if param is None: + return None + # Note in <= 3.9 _ConcatenateGenericAlias inherits from list + if isinstance(param, list) and recursion == 0: + return [self._check_single_param(arg, recursion+1) + for arg in param] + return typing._type_check( + param, f'Subscripting {self.__name__} requires a type.' + ) + + def _check_parameters(self, parameters): + if sys.version_info < (3, 11): + return tuple( + self._check_single_param(item) + for item in parameters + ) + return tuple(typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ) + def __getitem__(self, parameters): if not self.__type_params__: raise TypeError("Only generic type aliases are subscriptable") @@ -3670,13 +3697,14 @@ def __getitem__(self, parameters): # Using 3.9 here will create problems with Concatenate if sys.version_info >= (3, 10): return _types.GenericAlias(self, parameters) - parameters = tuple( - typing._type_check( - item, f'Subscripting {self.__name__} requires a type.' - ) - for item in parameters - ) - return _TypeAliasGenericAlias(self, parameters) + type_vars = _collect_type_vars(parameters) + parameters = self._check_parameters(parameters) + alias = _TypeAliasGenericAlias(self, parameters) + # alias.__parameters__ is not complete if Concatenate is present + # as it is converted to a list from which no parameters are extracted. + if alias.__parameters__ != type_vars: + alias.__parameters__ = type_vars + return alias def __reduce__(self): return self.__name__ From cb719753b9d6a3692f3db8045d2662686bafbcf9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 6 Dec 2024 00:05:42 -0800 Subject: [PATCH 35/81] Disable broken typed-argument-parser tests (#515) Fixes #157 --- .github/workflows/third_party.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 0bf1c820..622d13d4 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -223,7 +223,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # TODO: reenable 3.12 and 3.13 after they are fixed: + # https://github.com/swansonk14/typed-argument-parser/issues/156 + python-version: ["3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 25076900a2a1d3414cc7c7e64fd819e64c5b243c Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Fri, 6 Dec 2024 15:12:19 -0500 Subject: [PATCH 36/81] Change issue text on third_party failure to include exact run URL (#516) Use current run URL in issue body --- .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 622d13d4..9de7745d 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -394,5 +394,5 @@ jobs: owner: "python", repo: "typing_extensions", title: `Third-party tests failed on ${new Date().toDateString()}`, - body: "Full history of runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", + body: "Run listed here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", }) From 700eadd2bad0bb118a884dd85adbcd073a2c0046 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 8 Dec 2024 13:59:37 -0500 Subject: [PATCH 37/81] Revert "Disable broken typed-argument-parser tests" (#517) --- .github/workflows/third_party.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 9de7745d..d5a85399 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -223,9 +223,7 @@ jobs: strategy: fail-fast: false matrix: - # TODO: reenable 3.12 and 3.13 after they are fixed: - # https://github.com/swansonk14/typed-argument-parser/issues/156 - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From ca41832bbce2f2d800edbca0a2a48fb66ae406be Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 13 Dec 2024 05:36:54 +0100 Subject: [PATCH 38/81] Fix Concatenate and Generic with ParamSpec substitution (#489) --- src/test_typing_extensions.py | 176 ++++++++++++++++++++++++++++++++- src/typing_extensions.py | 181 ++++++++++++++++++++++++++++++++-- 2 files changed, 342 insertions(+), 15 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a7e6885e..ec90590d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3705,6 +3705,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ... self.assertEqual(Y.__parameters__, ()) self.assertEqual(Y.__args__, ((int, str, str), bytes, memoryview)) + # Regression test; fixing #126 might cause an error here + with self.assertRaisesRegex(TypeError, "not a generic class"): + Y[int] + def test_protocol_generic_over_typevartuple(self): Ts = TypeVarTuple("Ts") T = TypeVar("T") @@ -5259,6 +5263,7 @@ class X(Generic[T, P]): class Y(Protocol[T, P]): pass + things = "arguments" if sys.version_info >= (3, 10) else "parameters" for klass in X, Y: with self.subTest(klass=klass.__name__): G1 = klass[int, P_2] @@ -5273,13 +5278,69 @@ class Y(Protocol[T, P]): self.assertEqual(G3.__args__, (int, Concatenate[int, ...])) self.assertEqual(G3.__parameters__, ()) + with self.assertRaisesRegex( + TypeError, + f"Too few {things} for {klass}" + ): + klass[int] + # The following are some valid uses cases in PEP 612 that don't work: # These do not work in 3.9, _type_check blocks the list and ellipsis. # G3 = X[int, [int, bool]] # G4 = X[int, ...] # G5 = Z[[int, str, bool]] - # Not working because this is special-cased in 3.10. - # G6 = Z[int, str, bool] + + def test_single_argument_generic(self): + P = ParamSpec("P") + T = TypeVar("T") + P_2 = ParamSpec("P_2") + + class Z(Generic[P]): + pass + + class ProtoZ(Protocol[P]): + pass + + for klass in Z, ProtoZ: + with self.subTest(klass=klass.__name__): + # Note: For 3.10+ __args__ are nested tuples here ((int, ),) instead of (int, ) + G6 = klass[int, str, T] + G6args = G6.__args__[0] if sys.version_info >= (3, 10) else G6.__args__ + self.assertEqual(G6args, (int, str, T)) + self.assertEqual(G6.__parameters__, (T,)) + + # P = [int] + G7 = klass[int] + G7args = G7.__args__[0] if sys.version_info >= (3, 10) else G7.__args__ + self.assertEqual(G7args, (int,)) + self.assertEqual(G7.__parameters__, ()) + + G8 = klass[Concatenate[T, ...]] + self.assertEqual(G8.__args__, (Concatenate[T, ...], )) + self.assertEqual(G8.__parameters__, (T,)) + + G9 = klass[Concatenate[T, P_2]] + self.assertEqual(G9.__args__, (Concatenate[T, P_2], )) + + # This is an invalid form but useful for testing correct subsitution + G10 = klass[int, Concatenate[str, P]] + G10args = G10.__args__[0] if sys.version_info >= (3, 10) else G10.__args__ + self.assertEqual(G10args, (int, Concatenate[str, P], )) + + @skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10") + def test_is_param_expr(self): + P = ParamSpec("P") + P_typing = typing.ParamSpec("P_typing") + self.assertTrue(typing_extensions._is_param_expr(P)) + self.assertTrue(typing_extensions._is_param_expr(P_typing)) + if hasattr(typing, "_is_param_expr"): + self.assertTrue(typing._is_param_expr(P)) + self.assertTrue(typing._is_param_expr(P_typing)) + + def test_single_argument_generic_with_parameter_expressions(self): + P = ParamSpec("P") + T = TypeVar("T") + P_2 = ParamSpec("P_2") class Z(Generic[P]): pass @@ -5287,6 +5348,76 @@ class Z(Generic[P]): class ProtoZ(Protocol[P]): pass + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + for klass in Z, ProtoZ: + with self.subTest(klass=klass.__name__): + G8 = klass[Concatenate[T, ...]] + + H8_1 = G8[int] + self.assertEqual(H8_1.__parameters__, ()) + with self.assertRaisesRegex(TypeError, "not a generic class"): + H8_1[str] + + H8_2 = G8[T][int] + self.assertEqual(H8_2.__parameters__, ()) + with self.assertRaisesRegex(TypeError, "not a generic class"): + H8_2[str] + + G9 = klass[Concatenate[T, P_2]] + self.assertEqual(G9.__parameters__, (T, P_2)) + + with self.assertRaisesRegex(TypeError, + "The last parameter to Concatenate should be a ParamSpec variable or ellipsis." + if sys.version_info < (3, 10) else + # from __typing_subst__ + "Expected a list of types, an ellipsis, ParamSpec, or Concatenate" + ): + G9[int, int] + + with self.assertRaisesRegex(TypeError, f"Too few {things}"): + G9[int] + + with self.subTest("Check list as parameter expression", klass=klass.__name__): + if sys.version_info < (3, 10): + self.skipTest("Cannot pass non-types") + G5 = klass[[int, str, T]] + self.assertEqual(G5.__parameters__, (T,)) + self.assertEqual(G5.__args__, ((int, str, T),)) + + H9 = G9[int, [T]] + self.assertEqual(H9.__parameters__, (T,)) + + # This is an invalid parameter expression but useful for testing correct subsitution + G10 = klass[int, Concatenate[str, P]] + with self.subTest("Check invalid form substitution"): + self.assertEqual(G10.__parameters__, (P, )) + if sys.version_info < (3, 9): + self.skipTest("3.8 typing._type_subst does not support this substitution process") + H10 = G10[int] + if (3, 10) <= sys.version_info < (3, 11, 3): + self.skipTest("3.10-3.11.2 does not substitute Concatenate here") + self.assertEqual(H10.__parameters__, ()) + H10args = H10.__args__[0] if sys.version_info >= (3, 10) else H10.__args__ + self.assertEqual(H10args, (int, (str, int))) + + @skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10") + def test_substitution_with_typing_variants(self): + # verifies substitution and typing._check_generic working with typing variants + P = ParamSpec("P") + typing_P = typing.ParamSpec("typing_P") + typing_Concatenate = typing.Concatenate[int, P] + + class Z(Generic[typing_P]): + pass + + P1 = Z[typing_P] + self.assertEqual(P1.__parameters__, (typing_P,)) + self.assertEqual(P1.__args__, (typing_P,)) + + C1 = Z[typing_Concatenate] + self.assertEqual(C1.__parameters__, (P,)) + self.assertEqual(C1.__args__, (typing_Concatenate,)) + def test_pickle(self): global P, P_co, P_contra, P_default P = ParamSpec('P') @@ -5468,6 +5599,43 @@ def test_eq(self): self.assertEqual(hash(C4), hash(C5)) self.assertNotEqual(C4, C6) + def test_substitution(self): + T = TypeVar('T') + P = ParamSpec('P') + Ts = TypeVarTuple("Ts") + + C1 = Concatenate[str, T, ...] + self.assertEqual(C1[int], Concatenate[str, int, ...]) + + C2 = Concatenate[str, P] + self.assertEqual(C2[...], Concatenate[str, ...]) + self.assertEqual(C2[int], (str, int)) + U1 = Unpack[Tuple[int, str]] + U2 = Unpack[Ts] + self.assertEqual(C2[U1], (str, int, str)) + self.assertEqual(C2[U2], (str, Unpack[Ts])) + self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2"))) + + if (3, 12, 0) <= sys.version_info < (3, 12, 4): + with self.assertRaises(AssertionError): + C2[Unpack[U2]] + else: + with self.assertRaisesRegex(TypeError, "must be used with a tuple type"): + C2[Unpack[U2]] + + C3 = Concatenate[str, T, P] + self.assertEqual(C3[int, [bool]], (str, int, bool)) + + @skipUnless(TYPING_3_10_0, "Concatenate not present before 3.10") + def test_is_param_expr(self): + P = ParamSpec('P') + concat = Concatenate[str, P] + typing_concat = typing.Concatenate[str, P] + self.assertTrue(typing_extensions._is_param_expr(concat)) + self.assertTrue(typing_extensions._is_param_expr(typing_concat)) + if hasattr(typing, "_is_param_expr"): + self.assertTrue(typing._is_param_expr(concat)) + self.assertTrue(typing._is_param_expr(typing_concat)) class TypeGuardTests(BaseTestCase): def test_basics(self): @@ -7465,11 +7633,9 @@ def test_callable_with_concatenate(self): self.assertEqual(callable_concat.__parameters__, (P2,)) concat_usage = callable_concat[str] with self.subTest("get_args of Concatenate in TypeAliasType"): - if not TYPING_3_9_0: + if not TYPING_3_10_0: # args are: ([, ~P2],) self.skipTest("Nested ParamSpec is not substituted") - if sys.version_info < (3, 10, 2): - self.skipTest("GenericAlias keeps Concatenate in __args__ prior to 3.10.2") self.assertEqual(get_args(concat_usage), ((int, str),)) with self.subTest("Equality of parameter_expression without []"): if not TYPING_3_10_0: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dc35b3d4..5c0d5f49 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1765,6 +1765,23 @@ def __call__(self, *args, **kwargs): # 3.8-3.9 if not hasattr(typing, 'Concatenate'): # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + + # 3.9.0-1 + if not hasattr(typing, '_type_convert'): + def _type_convert(arg, module=None, *, allow_special_forms=False): + """For converting None to type(None), and strings to ForwardRef.""" + if arg is None: + return type(None) + if isinstance(arg, str): + if sys.version_info <= (3, 9, 6): + return ForwardRef(arg) + if sys.version_info <= (3, 9, 7): + return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, is_class=allow_special_forms) + return arg + else: + _type_convert = typing._type_convert + class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. @@ -1795,6 +1812,96 @@ def __parameters__(self): return tuple( tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) + + # 3.8; needed for typing._subst_tvars + # 3.9 used by __getitem__ below + def copy_with(self, params): + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return self.__class__(self.__origin__, params) + + # 3.9; accessed during GenericAlias.__getitem__ when substituting + def __getitem__(self, args): + if self.__origin__ in (Generic, Protocol): + # Can't subscript Generic[...] or Protocol[...]. + raise TypeError(f"Cannot subscript already-subscripted {self}") + if not self.__parameters__: + raise TypeError(f"{self} is not a generic class") + + if not isinstance(args, tuple): + args = (args,) + args = _unpack_args(*(_type_convert(p) for p in args)) + params = self.__parameters__ + for param in params: + prepare = getattr(param, "__typing_prepare_subst__", None) + if prepare is not None: + args = prepare(self, args) + # 3.8 - 3.9 & typing.ParamSpec + elif isinstance(param, ParamSpec): + i = params.index(param) + if ( + i == len(args) + and getattr(param, '__default__', NoDefault) is not NoDefault + ): + args = [*args, param.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {self}") + # Special case for Z[[int, str, bool]] == Z[int, str, bool] + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + elif ( + isinstance(args[i], list) + # 3.8 - 3.9 + # This class inherits from list do not convert + and not isinstance(args[i], _ConcatenateGenericAlias) + ): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + + alen = len(args) + plen = len(params) + if alen != plen: + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}" + ) + + subst = dict(zip(self.__parameters__, args)) + # determine new args + new_args = [] + for arg in self.__args__: + if isinstance(arg, type): + new_args.append(arg) + continue + if isinstance(arg, TypeVar): + arg = subst[arg] + if ( + (isinstance(arg, typing._GenericAlias) and _is_unpack(arg)) + or ( + hasattr(_types, "GenericAlias") + and isinstance(arg, _types.GenericAlias) + and getattr(arg, "__unpacked__", False) + ) + ): + raise TypeError(f"{arg} is not valid as type argument") + + elif isinstance(arg, + typing._GenericAlias + if not hasattr(_types, "GenericAlias") else + (typing._GenericAlias, _types.GenericAlias) + ): + subparams = arg.__parameters__ + if subparams: + subargs = tuple(subst[x] for x in subparams) + arg = arg[subargs] + new_args.append(arg) + return self.copy_with(tuple(new_args)) + # 3.10+ else: _ConcatenateGenericAlias = typing._ConcatenateGenericAlias @@ -1817,6 +1924,12 @@ def copy_with(self, params): "ParamSpec variable or ellipsis.") return super(_typing_ConcatenateGenericAlias, self).copy_with(params) + def __getitem__(self, args): + value = super().__getitem__(args) + if isinstance(value, tuple) and any(_is_unpack(t) for t in value): + return tuple(_unpack_args(*(n for n in value))) + return value + # 3.8-3.9.2 class _EllipsisDummy: ... @@ -2496,6 +2609,17 @@ def _is_unpack(obj): 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): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + @property def __typing_is_unpacked_typevartuple__(self): assert self.__origin__ is Unpack @@ -2519,21 +2643,22 @@ def _is_unpack(obj): return isinstance(obj, _UnpackAlias) +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 + + if _PEP_696_IMPLEMENTED: from typing import TypeVarTuple 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.""" @@ -3006,6 +3131,24 @@ def wrapper(*args, **kwargs): f"a class or callable, not {arg!r}" ) +if sys.version_info < (3, 10): + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, (tuple, list, ParamSpec, _ConcatenateGenericAlias) + ) +else: + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, + ( + tuple, + list, + ParamSpec, + _ConcatenateGenericAlias, + typing._ConcatenateGenericAlias, + ), + ) + # We have to do some monkey patching to deal with the dual nature of # Unpack/TypeVarTuple: @@ -3020,6 +3163,17 @@ def _check_generic(cls, parameters, elen=_marker): This gives a nice error message in case of count mismatch. """ + # If substituting a single ParamSpec with multiple arguments + # we do not check the count + if (inspect.isclass(cls) and issubclass(cls, typing.Generic) + and len(cls.__parameters__) == 1 + and isinstance(cls.__parameters__[0], ParamSpec) + and parameters + and not _is_param_expr(parameters[0]) + ): + # Generic modifies parameters variable, but here we cannot do this + return + if not elen: raise TypeError(f"{cls} is not a generic class") if elen is _marker: @@ -3171,6 +3325,13 @@ def _collect_type_vars(types, typevar_types=None): tvars.append(t) if _should_collect_from_parameters(t): tvars.extend([t for t in t.__parameters__ if t not in tvars]) + elif isinstance(t, tuple): + # Collect nested type_vars + # tuple wrapped by _prepare_paramspec_params(cls, params) + for x in t: + for collected in _collect_type_vars([x]): + if collected not in tvars: + tvars.append(collected) return tuple(tvars) typing._collect_type_vars = _collect_type_vars From f9a2055472b83c775a43b632792e7391941fb597 Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 13 Dec 2024 05:37:37 +0100 Subject: [PATCH 39/81] Fix recursive use of Concatenate when mixing modules (#512) --- CHANGELOG.md | 2 ++ src/test_typing_extensions.py | 13 +++++++++++++ src/typing_extensions.py | 7 +++---- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6333d7b1..4c11f96b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ aliases that have a `Concatenate` special form as their argument. - Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` instances before Python 3.11. Patch by [Daraan](https://github.com/Daraan). +- Fix error on Python 3.10 when using `typing.Concatenate` and + `typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ec90590d..14994cc7 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5504,6 +5504,19 @@ class MyClass: ... self.assertNotEqual(d, c) self.assertNotEqual(d, Concatenate) + @skipUnless(TYPING_3_10_0, "Concatenate not available in <3.10") + def test_typing_compatibility(self): + P = ParamSpec('P') + C1 = Concatenate[int, P][typing.Concatenate[int, P]] + self.assertEqual(C1, Concatenate[int, int, P]) + self.assertEqual(get_args(C1), (int, int, P)) + + C2 = typing.Concatenate[int, P][Concatenate[int, P]] + with self.subTest("typing compatibility with typing_extensions"): + if sys.version_info < (3, 10, 3): + self.skipTest("Unpacking not introduced until 3.10.3") + self.assertEqual(get_args(C2), (int, int, P)) + def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5c0d5f49..e242d427 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1908,21 +1908,20 @@ def __getitem__(self, args): # 3.10 if sys.version_info < (3, 11): - _typing_ConcatenateGenericAlias = _ConcatenateGenericAlias - class _ConcatenateGenericAlias(_typing_ConcatenateGenericAlias, _root=True): + class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True): # needed for checks in collections.abc.Callable to accept this class __module__ = "typing" def copy_with(self, params): if isinstance(params[-1], (list, tuple)): return (*params[:-1], *params[-1]) - if isinstance(params[-1], _ConcatenateGenericAlias): + if isinstance(params[-1], typing._ConcatenateGenericAlias): params = (*params[:-1], *params[-1].__args__) elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): raise TypeError("The last parameter to Concatenate should be a " "ParamSpec variable or ellipsis.") - return super(_typing_ConcatenateGenericAlias, self).copy_with(params) + return super(typing._ConcatenateGenericAlias, self).copy_with(params) def __getitem__(self, args): value = super().__getitem__(args) From 6f84687ce030c72c633f51dfdfa839c84f373519 Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 13 Dec 2024 15:26:44 +0100 Subject: [PATCH 40/81] Add 3.12.0 and 3.13.0 tests to CI (#521) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f062801..9db5bc7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,9 @@ jobs: - "3.11" - "3.11.0" - "3.12" + - "3.12.0" - "3.13" + - "3.13.0" - "pypy3.8" - "pypy3.9" - "pypy3.10" From 1ee20f7c4a4031a690123f013bf048fe7d79cbed Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sat, 21 Dec 2024 15:36:22 +0100 Subject: [PATCH 41/81] Fix typos in `TypeIs` docstring (#524) --- src/typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e242d427..993a284d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2141,7 +2141,7 @@ def TypeIs(self, parameters): 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 + is the intersection of the type inside ``TypeIs`` and the argument's previously known type. For example:: @@ -2189,7 +2189,7 @@ def __getitem__(self, parameters): 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 + is the intersection of the type inside ``TypeIs`` and the argument's previously known type. For example:: From 15d48b2122dbb7124a3c3142222bc4647bb8ef4b Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Sun, 22 Dec 2024 18:56:27 -0500 Subject: [PATCH 42/81] Fix third party scheduled tests running on forks (#525) * Fix third party scheduled tests running on forks * Refactor repeated condition to predicate job --- .github/workflows/third_party.yml | 88 +++++++------------------------ 1 file changed, 18 insertions(+), 70 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index d5a85399..d0cbb0bc 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -26,18 +26,20 @@ concurrency: cancel-in-progress: true jobs: + skip-schedule-on-fork: + name: Check for schedule trigger on fork + runs-on: ubuntu-latest + # if 'schedule' was the trigger, + # don't run it on contributors' forks + if: >- + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + steps: + - run: true + pydantic: name: pydantic tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -72,16 +74,7 @@ jobs: typing_inspect: name: typing_inspect tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -117,16 +110,7 @@ jobs: pyanalyze: name: pyanalyze tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -163,16 +147,7 @@ jobs: typeguard: name: typeguard tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -210,16 +185,7 @@ jobs: typed-argument-parser: name: typed-argument-parser tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -262,16 +228,7 @@ jobs: mypy: name: stubtest & mypyc tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: @@ -309,16 +266,7 @@ jobs: cattrs: name: cattrs tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: From dbf852b68291746b7cbc6026f2b32310e775900a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 8 Jan 2025 14:27:06 +0300 Subject: [PATCH 43/81] Fix `test_typing.test_readonly_inheritance` (#526) --- src/test_typing_extensions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 14994cc7..2a3a800e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4624,13 +4624,13 @@ class Child1(Base1): self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) class Base2(TypedDict): - a: ReadOnly[int] + a: int class Child2(Base2): - b: str + b: ReadOnly[str] - self.assertEqual(Child1.__readonly_keys__, frozenset({'a'})) - self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) + self.assertEqual(Child2.__readonly_keys__, frozenset({'b'})) + self.assertEqual(Child2.__mutable_keys__, frozenset({'a'})) def test_make_mutable_key_readonly(self): class Base(TypedDict): From 8184ac61398c187203dad819eb5b9d34005a96ae Mon Sep 17 00:00:00 2001 From: Daraan Date: Fri, 17 Jan 2025 05:58:06 +0100 Subject: [PATCH 44/81] Add backport of `evaluate_forward_ref` (#497) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 3 + doc/index.rst | 35 ++++- src/test_typing_extensions.py | 228 +++++++++++++++++++++++++++-- src/typing_extensions.py | 267 +++++++++++++++++++++++++++++++++- 4 files changed, 513 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c11f96b..139d92c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ aliases that have a `Concatenate` special form as their argument. - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). +- Backport `evaluate_forward_ref` from CPython PR + [#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. - Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` instances before Python 3.11. Patch by [Daraan](https://github.com/Daraan). diff --git a/doc/index.rst b/doc/index.rst index d321ce04..ea5d776d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -753,6 +753,37 @@ Functions .. versionadded:: 4.2.0 +.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE) + + Evaluate an :py:class:`typing.ForwardRef` as a :py:term:`type hint`. + + This is similar to calling :py:meth:`annotationlib.ForwardRef.evaluate`, + but unlike that method, :func:`!evaluate_forward_ref` also: + + * Recursively evaluates forward references nested within the type hint. + However, the amount of recursion is limited in Python 3.8 and 3.10. + * Raises :exc:`TypeError` when it encounters certain objects that are + not valid type hints. + * Replaces type hints that evaluate to :const:`!None` with + :class:`types.NoneType`. + * Supports the :attr:`Format.FORWARDREF` and + :attr:`Format.STRING` formats. + + *forward_ref* must be an instance of :py:class:`typing.ForwardRef`. + *owner*, if given, should be the object that holds the annotations that + the forward reference derived from, such as a module, class object, or function. + It is used to infer the namespaces to use for looking up names. + *globals* and *locals* can also be explicitly given to provide + the global and local namespaces. + *type_params* is a tuple of :py:ref:`type parameters ` that + are in scope when evaluating the forward reference. + This parameter must be provided (though it may be an empty tuple) if *owner* + is not given and the forward reference does not already have an owner set. + *format* specifies the format of the annotation and is a member of + the :class:`Format` enum. + + .. versionadded:: 4.13.0 + .. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10. @@ -764,7 +795,7 @@ Functions of the :pep:`649` behavior on versions of Python that do not support it. The purpose of this backport is to allow users who would like to use - :attr:`Format.FORWARDREF` or :attr:`Format.SOURCE` semantics once + :attr:`Format.FORWARDREF` or :attr:`Format.STRING` semantics once :pep:`649` is implemented, but who also want to support earlier Python versions, to simply write:: @@ -911,7 +942,7 @@ Enums ``typing_extensions`` emulates this value on versions of Python which do not support :pep:`649` by returning the same value as for ``VALUE`` semantics. - .. attribute:: SOURCE + .. attribute:: STRING Equal to 3. When :pep:`649` is implemented, this format will produce an annotation dictionary where the values have been replaced by strings containing diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2a3a800e..10efcd24 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -28,6 +28,7 @@ import typing_extensions from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated from typing_extensions import ( + _FORWARD_REF_HAS_CLASS, _PEP_649_OR_749_IMPLEMENTED, Annotated, Any, @@ -82,6 +83,7 @@ clear_overloads, dataclass_transform, deprecated, + evaluate_forward_ref, final, get_annotations, get_args, @@ -7948,7 +7950,7 @@ def f2(a: "undefined"): # noqa: F821 self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"}) self.assertEqual( - get_annotations(f1, format=Format.SOURCE), + get_annotations(f1, format=Format.STRING), {"a": "int"}, ) self.assertEqual(get_annotations(f1, format=3), {"a": "int"}) @@ -7975,7 +7977,7 @@ def foo(): foo, format=Format.FORWARDREF, eval_str=True ) get_annotations( - foo, format=Format.SOURCE, eval_str=True + foo, format=Format.STRING, eval_str=True ) def test_stock_annotations(self): @@ -7989,7 +7991,7 @@ def foo(a: int, b: str): {"a": int, "b": str}, ) self.assertEqual( - get_annotations(foo, format=Format.SOURCE), + get_annotations(foo, format=Format.STRING), {"a": "int", "b": "str"}, ) @@ -8084,43 +8086,43 @@ def test_stock_annotations_in_module(self): ) self.assertEqual( - get_annotations(isa, format=Format.SOURCE), + get_annotations(isa, format=Format.STRING), {"a": "int", "b": "str"}, ) self.assertEqual( - get_annotations(isa.MyClass, format=Format.SOURCE), + get_annotations(isa.MyClass, format=Format.STRING), {"a": "int", "b": "str"}, ) mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" self.assertEqual( - get_annotations(isa.function, format=Format.SOURCE), + get_annotations(isa.function, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, ) self.assertEqual( get_annotations( - isa.function2, format=Format.SOURCE + isa.function2, format=Format.STRING ), {"a": "int", "b": "str", "c": mycls, "return": mycls}, ) self.assertEqual( get_annotations( - isa.function3, format=Format.SOURCE + isa.function3, format=Format.STRING ), {"a": "int", "b": "str", "c": "MyClass"}, ) self.assertEqual( - get_annotations(inspect, format=Format.SOURCE), + get_annotations(inspect, format=Format.STRING), {}, ) self.assertEqual( get_annotations( - isa.UnannotatedClass, format=Format.SOURCE + isa.UnannotatedClass, format=Format.STRING ), {}, ) self.assertEqual( get_annotations( - isa.unannotated_function, format=Format.SOURCE + isa.unannotated_function, format=Format.STRING ), {}, ) @@ -8141,7 +8143,7 @@ def test_stock_annotations_on_wrapper(self): ) mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" self.assertEqual( - get_annotations(wrapped, format=Format.SOURCE), + get_annotations(wrapped, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, ) self.assertEqual( @@ -8160,10 +8162,10 @@ def test_stringized_annotations_in_module(self): {"eval_str": False}, {"format": Format.VALUE}, {"format": Format.FORWARDREF}, - {"format": Format.SOURCE}, + {"format": Format.STRING}, {"format": Format.VALUE, "eval_str": False}, {"format": Format.FORWARDREF, "eval_str": False}, - {"format": Format.SOURCE, "eval_str": False}, + {"format": Format.STRING, "eval_str": False}, ]: with self.subTest(**kwargs): self.assertEqual( @@ -8466,6 +8468,204 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self): set(results.generic_func.__type_params__) ) +class TestEvaluateForwardRefs(BaseTestCase): + def test_global_constant(self): + if sys.version_info[:3] > (3, 10, 0): + self.assertTrue(_FORWARD_REF_HAS_CLASS) + + def test_forward_ref_fallback(self): + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("doesntexist")) + ref = typing.ForwardRef("doesntexist") + self.assertIs(evaluate_forward_ref(ref, format=Format.FORWARDREF), ref) + + class X: + unresolvable = "doesnotexist2" + + evaluated_ref = evaluate_forward_ref( + typing.ForwardRef("X.unresolvable"), + locals={"X": X}, + type_params=None, + format=Format.FORWARDREF, + ) + self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2")) + + def test_evaluate_with_type_params(self): + # Use a T name that is not in globals + self.assertNotIn("Tx", globals()) + if not TYPING_3_12_0: + Tx = TypeVar("Tx") + class Gen(Generic[Tx]): + alias = int + if not hasattr(Gen, "__type_params__"): + Gen.__type_params__ = (Tx,) + self.assertEqual(Gen.__type_params__, (Tx,)) + del Tx + else: + ns = {} + exec(textwrap.dedent(""" + class Gen[Tx]: + alias = int + """), None, ns) + Gen = ns["Gen"] + + # owner=None, type_params=None + # NOTE: The behavior of owner=None might change in the future when ForwardRef.__owner__ is available + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx")) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=()) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), owner=int) + + (Tx,) = Gen.__type_params__ + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=Gen.__type_params__), Tx) + + # For this test its important that Tx is not a global variable, i.e. do not use "T" here + self.assertNotIn("Tx", globals()) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), owner=Gen), Tx) + + # Different type_params take precedence + not_Tx = TypeVar("Tx") # different TypeVar with same name + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=(not_Tx,), owner=Gen), not_Tx) + + # globals can take higher precedence + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, globals={"Tx": str}), str) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, type_params=(not_Tx,), globals={"Tx": str}), str) + + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), type_params=Gen.__type_params__) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen), int) + # If you pass custom locals, we don't look at the owner's locals + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={}) + # But if the name exists in the locals, it works + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={"alias": str}), str + ) + + @skipUnless( + HAS_FORWARD_MODULE, "Needs module 'forward' to test forward references" + ) + def test_fwdref_with_module(self): + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("Counter", module="collections")), collections.Counter + ) + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]", module="collections")), + collections.Counter[int], + ) + + with self.assertRaises(NameError): + # If globals are passed explicitly, we don't look at the module dict + evaluate_forward_ref(typing.ForwardRef("Format", module="annotationlib"), globals={}) + + def test_fwdref_to_builtin(self): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + if HAS_FORWARD_MODULE: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int", module="collections")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), owner=str), int) + + # builtins are still searched with explicit globals + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={}), int) + + def test_fwdref_with_globals(self): + # explicit values in globals have precedence + obj = object() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj) + + def test_fwdref_value_is_cached(self): + fr = typing.ForwardRef("hello") + with self.assertRaises(NameError): + evaluate_forward_ref(fr) + self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str) + self.assertIs(evaluate_forward_ref(fr), str) + + @skipUnless(TYPING_3_9_0, "Needs PEP 585 support") + def test_fwdref_with_owner(self): + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections), + collections.Counter[int], + ) + + def test_name_lookup_without_eval(self): + # test the codepath where we look up simple names directly in the + # namespaces without going through eval() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": str}), str) + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": float}, globals={"int": str}), + float, + ) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": str}), str) + import builtins + + from test import support + with support.swap_attr(builtins, "int", dict): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), dict) + + def test_nested_strings(self): + # This variable must have a different name TypeVar + Tx = TypeVar("Tx") + + class Y(Generic[Tx]): + a = "X" + bT = "Y[T_nonlocal]" + + Z = TypeAliasType("Z", Y[Tx], type_params=(Tx,)) + + evaluated_ref1a = evaluate_forward_ref(typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y, "Tx": Tx}) + self.assertEqual(get_origin(evaluated_ref1a), Y) + self.assertEqual(get_args(evaluated_ref1a), (Y[Tx],)) + + evaluated_ref1b = evaluate_forward_ref( + typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y}, type_params=(Tx,) + ) + self.assertEqual(get_origin(evaluated_ref1b), Y) + self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],)) + + with self.subTest("nested string of TypeVar"): + evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y}) + self.assertEqual(get_origin(evaluated_ref2), Y) + if not TYPING_3_9_0: + self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8") + self.assertEqual(get_args(evaluated_ref2), (Y[Tx],)) + + with self.subTest("nested string of TypeAliasType and alias"): + # NOTE: Using Y here works for 3.10 + evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str}) + self.assertEqual(get_origin(evaluated_ref3), Y) + if sys.version_info[:2] in ((3,8), (3, 10)): + self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10") + self.assertEqual(get_args(evaluated_ref3), (Z[str],)) + + def test_invalid_special_forms(self): + # tests _lax_type_check to raise errors the same way as the typing module. + # Regex capture "< class 'module.name'> and "module.name" + with self.assertRaisesRegex( + TypeError, r"Plain .*Protocol('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing)) + with self.assertRaisesRegex( + TypeError, r"Plain .*Generic('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing)) + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)) + else: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar) + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 993a284d..ded403fe 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,10 +1,12 @@ import abc +import builtins import collections import collections.abc import contextlib import enum import functools import inspect +import keyword import operator import sys import types as _types @@ -63,6 +65,7 @@ 'dataclass_transform', 'deprecated', 'Doc', + 'evaluate_forward_ref', 'get_overloads', 'final', 'Format', @@ -142,6 +145,9 @@ GenericMeta = type _PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") +# Added with bpo-45166 to 3.10.1+ and some 3.9 versions +_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. @@ -4005,7 +4011,7 @@ def __eq__(self, other: object) -> bool: class Format(enum.IntEnum): VALUE = 1 FORWARDREF = 2 - SOURCE = 3 + STRING = 3 if _PEP_649_OR_749_IMPLEMENTED: @@ -4036,13 +4042,13 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, undefined names with ForwardRef objects. The implementation proposed by PEP 649 relies on language changes that cannot be backported; the typing-extensions implementation simply returns the same result as VALUE. - * SOURCE: return annotations as strings, in a format close to the original + * STRING: return annotations as strings, in a format close to the original source. Again, this behavior cannot be replicated directly in a backport. As an approximation, typing-extensions retrieves the annotations under VALUE semantics and then stringifies them. The purpose of this backport is to allow users who would like to use - FORWARDREF or SOURCE semantics once PEP 649 is implemented, but who also + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also want to support earlier Python versions, to simply write: typing_extensions.get_annotations(obj, format=Format.FORWARDREF) @@ -4101,7 +4107,7 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, return {} if not eval_str: - if format is Format.SOURCE: + if format is Format.STRING: return { key: value if isinstance(value, str) else typing._type_repr(value) for key, value in ann.items() @@ -4136,6 +4142,259 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, for key, value in ann.items() } return return_value + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) + if type_params is not None: + globals = dict(globals) + locals = dict(locals) + for param in type_params: + param_name = param.__name__ + if ( + _FORWARD_REF_HAS_CLASS and not forward_ref.__forward_is_class__ + ) or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def _lax_type_check( + value, msg, is_argument=True, *, module=None, allow_special_forms=False + ): + """ + A lax Python 3.11+ like version of typing._type_check + """ + if hasattr(typing, "_type_convert"): + if _FORWARD_REF_HAS_CLASS: + type_ = typing._type_convert( + value, + module=module, + allow_special_forms=allow_special_forms, + ) + # module was added with bpo-41249 before is_class (bpo-46539) + elif "__forward_module__" in typing.ForwardRef.__slots__: + type_ = typing._type_convert(value, module=module) + else: + type_ = typing._type_convert(value) + else: + if value is None: + return type(None) + if isinstance(value, str): + return ForwardRef(value) + type_ = value + invalid_generic_forms = (Generic, Protocol) + if not allow_special_forms: + invalid_generic_forms += (ClassVar,) + if is_argument: + invalid_generic_forms += (Final,) + if ( + isinstance(type_, typing._GenericAlias) + and get_origin(type_) in invalid_generic_forms + ): + raise TypeError(f"{type_} is not valid as type argument") from None + if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias): + return type_ + if allow_special_forms and type_ in (ClassVar, Final): + return type_ + if ( + isinstance(type_, (_SpecialForm, typing._SpecialForm)) + or type_ in (Generic, Protocol) + ): + raise TypeError(f"Plain {type_} is not valid as type argument") from None + if type(type_) is tuple: # lax version with tuple instead of callable + raise TypeError(f"{msg} Got {type_!r:.100}.") + return type_ + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=Format.VALUE, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + msg = "Forward references must evaluate to types." + if not _FORWARD_REF_HAS_CLASS: + allow_special_forms = not forward_ref.__forward_is_argument__ + else: + allow_special_forms = forward_ref.__forward_is_class__ + type_ = _lax_type_check( + value, + msg, + is_argument=forward_ref.__forward_is_argument__, + allow_special_forms=allow_special_forms, + ) + + # Recursively evaluate the type + if isinstance(type_, ForwardRef): + if getattr(type_, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + type_, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 9): + return typing._eval_type( + type_, + globals, + locals, + ) + if sys.version_info < (3, 12, 5): + return typing._eval_type( + type_, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + if sys.version_info < (3, 14): + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + format=format, + owner=owner, + ) + + # 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 86cf372510ee23aee6e7318293568c79e263b26c Mon Sep 17 00:00:00 2001 From: Daraan Date: Tue, 11 Feb 2025 04:45:39 +0100 Subject: [PATCH 45/81] Fix `Union[..., NoneType]` injection by `get_type_hints` if a `None` default value is used. (#482) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 4 ++ src/test_typing_extensions.py | 89 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 80 +++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 139d92c1..2aa42922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ aliases that have a `Concatenate` special form as their argument. Patch by [Daraan](https://github.com/Daraan). - Extended the `Concatenate` backport for Python 3.8-3.10 to now accept `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). +- Fix backport of `get_type_hints` to reflect Python 3.11+ behavior which does not add + `Union[..., NoneType]` to annotations that have a `None` default value anymore. + This fixes wrapping of `Annotated` in an unwanted `Optional` in such cases. + Patch by [Daraan](https://github.com/Daraan). - Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 10efcd24..ac8bb0f3 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1647,6 +1647,95 @@ def test_final_forward_ref(self): self.assertNotEqual(gth(Loop, globals())['attr'], Final[int]) self.assertNotEqual(gth(Loop, globals())['attr'], Final) + def test_annotation_and_optional_default(self): + annotation = Annotated[Union[int, None], "data"] + NoneAlias = None + StrAlias = str + T_default = TypeVar("T_default", default=None) + Ts = TypeVarTuple("Ts") + + cases = { + # annotation: expected_type_hints + Annotated[None, "none"] : Annotated[None, "none"], + annotation : annotation, + Optional[int] : Optional[int], + Optional[List[str]] : Optional[List[str]], + Optional[annotation] : Optional[annotation], + Union[str, None, str] : Optional[str], + Unpack[Tuple[int, None]]: Unpack[Tuple[int, None]], + # Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485 + Unpack[Ts] : Unpack[Ts], + } + # contains a ForwardRef, TypeVar(~prefix) or no expression + do_not_stringify_cases = { + () : {}, # Special-cased below to create an unannotated parameter + int : int, + "int" : int, + None : type(None), + "NoneAlias" : type(None), + List["str"] : List[str], + Union[str, "str"] : str, + Union[str, None, "str"] : Optional[str], + Union[str, "NoneAlias", "StrAlias"]: Optional[str], + Union[str, "Union[None, StrAlias]"]: Optional[str], + Union["annotation", T_default] : Union[annotation, T_default], + Annotated["annotation", "nested"] : Annotated[Union[int, None], "data", "nested"], + } + if TYPING_3_10_0: # cannot construct UnionTypes before 3.10 + do_not_stringify_cases["str | NoneAlias | StrAlias"] = str | None + cases[str | None] = Optional[str] + cases.update(do_not_stringify_cases) + for (annot, expected), none_default, as_str, wrap_optional in itertools.product( + cases.items(), (False, True), (False, True), (False, True) + ): + # Special case: + skip_reason = None + annot_unchanged = annot + if sys.version_info[:2] == (3, 10) and annot == "str | NoneAlias | StrAlias" and none_default: + # In 3.10 converts Optional[str | None] to Optional[str] which has a different repr + skip_reason = "UnionType not preserved in 3.10" + if wrap_optional: + if annot_unchanged == (): + continue + annot = Optional[annot] + expected = {"x": Optional[expected]} + else: + expected = {"x": expected} if annot_unchanged != () else {} + if as_str: + if annot_unchanged in do_not_stringify_cases or annot_unchanged == (): + continue + annot = str(annot) + with self.subTest( + annotation=annot, + as_str=as_str, + wrap_optional=wrap_optional, + none_default=none_default, + expected_type_hints=expected, + ): + # Create function to check + if annot_unchanged == (): + if none_default: + def func(x=None): pass + else: + def func(x): pass + elif none_default: + def func(x: annot = None): pass + else: + def func(x: annot): pass + type_hints = get_type_hints(func, globals(), locals(), include_extras=True) + # Equality + self.assertEqual(type_hints, expected) + # Hash + for k in type_hints.keys(): + self.assertEqual(hash(type_hints[k]), hash(expected[k])) + # Test if UnionTypes are preserved + self.assertIs(type(type_hints[k]), type(expected[k])) + # Repr + with self.subTest("Check str and repr"): + if skip_reason == "UnionType not preserved in 3.10": + self.skipTest(skip_reason) + self.assertEqual(repr(type_hints), repr(expected)) + class GetUtilitiesTestCase(TestCase): def test_get_origin(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ded403fe..e7d20815 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1242,10 +1242,90 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): ) else: # 3.8 hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) + if sys.version_info < (3, 9): + # In 3.8 eval_type does not flatten Optional[ForwardRef] correctly + # This will recreate and and cache Unions. + hint = { + k: (t + if get_origin(t) != Union + else Union[t.__args__]) + for k, t in hint.items() + } if include_extras: return hint return {k: _strip_extras(t) for k, t in hint.items()} + _NoneType = type(None) + + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + # 3.8+ compatible checking before _UnionGenericAlias + if get_origin(t) is not Union: + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True + + # < 3.11 + def _clean_optional(obj, hints, globalns=None, localns=None): + # reverts injected Union[..., None] cases from typing.get_type_hints + # when a None default value is used. + # see https://github.com/python/typing_extensions/issues/310 + if not hints or isinstance(obj, type): + return + defaults = typing._get_defaults(obj) # avoid accessing __annotations___ + if not defaults: + return + original_hints = obj.__annotations__ + for name, value in hints.items(): + # Not a Union[..., None] or replacement conditions not fullfilled + if (not _could_be_inserted_optional(value) + or name not in defaults + or defaults[name] is not None + ): + continue + original_value = original_hints[name] + # value=NoneType should have caused a skip above but check for safety + if original_value is None: + original_value = _NoneType + # Forward reference + if isinstance(original_value, str): + if globalns is None: + if isinstance(obj, _types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + if sys.version_info < (3, 9): + original_value = ForwardRef(original_value) + else: + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: + # Union[str, None, "str"] is not reduced to Union[str, None] + original_evaluated = Union[original_evaluated.__args__] + # Compare if values differ. Note that even if equal + # value might be cached by typing._tp_cache contrary to original_evaluated + if original_evaluated != value or ( + # 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias + hasattr(_types, "UnionType") + and isinstance(original_evaluated, _types.UnionType) + and not isinstance(value, _types.UnionType) + ): + hints[name] = original_evaluated # Python 3.9+ has PEP 593 (Annotated) if hasattr(typing, 'Annotated'): From 2a4dead5a6241c8cc15c876622f0f435d68506e9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 11 Feb 2025 08:17:13 -0800 Subject: [PATCH 46/81] Upgrade ruff (#529) --- pyproject.toml | 2 ++ test-requirements.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f66cf6bc..dfef244c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,8 @@ ignore = [ "UP038", # Not relevant here "RUF012", + "RUF022", + "RUF023", ] [tool.ruff.lint.per-file-ignores] diff --git a/test-requirements.txt b/test-requirements.txt index 7242d3b5..4b0fc81e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1 @@ -ruff==0.4.5 +ruff==0.9.6 From b931f1b03298101bf785cfd9f6907bd65c2789d4 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:59:54 +0100 Subject: [PATCH 47/81] Update flit to use PEP 639 (#530) --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dfef244c..b0046a7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ # Build system requirements. [build-system] -requires = ["flit_core >=3.4,<4"] +requires = ["flit_core >=3.11,<4"] build-backend = "flit_core.buildapi" # Project metadata @@ -10,7 +10,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" -license = { text = "PSF-2.0" } +license = "PSF-2.0" +license-files = ["LICENSE"] keywords = [ "annotations", "backport", @@ -30,7 +31,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", From caf24b48eb4f601b81c8aab5a50c82a4694aa005 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 20 Feb 2025 20:42:50 -0800 Subject: [PATCH 48/81] docs: Fix "Final[42]" (#532) This fails at runtime in older versions, and in any case it is an invalid type. Fixes #531 --- doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index ea5d776d..d150c7db 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -133,7 +133,7 @@ Example usage:: False >>> is_literal(get_origin(typing.Literal[42])) True - >>> is_literal(get_origin(typing_extensions.Final[42])) + >>> is_literal(get_origin(typing_extensions.Final[int])) False Python version support From 3f47bf98d0b3b0149e55ba4e4eb8bdb855739a36 Mon Sep 17 00:00:00 2001 From: Daraan Date: Sat, 22 Feb 2025 19:33:49 +0100 Subject: [PATCH 49/81] Remove unnecessary hasattr check from TypedDict (#533) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 4 ++++ src/test_typing_extensions.py | 31 +++++++++++++++++++++++++++++++ src/typing_extensions.py | 3 +-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aa42922..690bf5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,10 @@ aliases that have a `Concatenate` special form as their argument. Patch by [Daraan](https://github.com/Daraan). - Fix error on Python 3.10 when using `typing.Concatenate` and `typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan). +- Backport of CPython PR [#109544](https://github.com/python/cpython/pull/109544) + to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a + `TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ac8bb0f3..17ce28f2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4237,6 +4237,37 @@ def test_total(self): self.assertEqual(Options.__required_keys__, frozenset()) self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'}) + def test_total_inherits_non_total(self): + class TD1(TypedDict, total=False): + a: int + + self.assertIs(TD1.__total__, False) + + class TD2(TD1): + b: str + + self.assertIs(TD2.__total__, True) + + def test_total_with_assigned_value(self): + class TD(TypedDict): + __total__ = "some_value" + + self.assertIs(TD.__total__, True) + + class TD2(TypedDict, total=True): + __total__ = "some_value" + + self.assertIs(TD2.__total__, True) + + class TD3(TypedDict, total=False): + __total__ = "some value" + + self.assertIs(TD3.__total__, False) + + TD4 = TypedDict('TD4', {'__total__': "some_value"}) # noqa: F821 + self.assertIs(TD4.__total__, True) + + def test_optional_keys(self): class Point2Dor3D(Point2D, total=False): z: int diff --git a/src/typing_extensions.py b/src/typing_extensions.py index e7d20815..fe492a3f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1029,8 +1029,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): 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 + tp_dict.__total__ = total tp_dict.__closed__ = closed tp_dict.__extra_items__ = extra_items_type return tp_dict From 7def253cd65b3a916fcb76a6eb51428b60e1bcc8 Mon Sep 17 00:00:00 2001 From: Daraan Date: Wed, 12 Mar 2025 23:24:49 +0100 Subject: [PATCH 50/81] Fix `isinstance(Unpack[Ts], TypeVar)` to be `False` in 3.11 (#539) --- CHANGELOG.md | 3 +++ src/test_typing_extensions.py | 6 ++++++ src/typing_extensions.py | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 690bf5d5..a02f48c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ aliases that have a `Concatenate` special form as their argument. to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a `TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor. Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. +- Fix for Python 3.11 that now `isinstance(typing_extensions.Unpack[...], TypeVar)` + evaluates to `False`, however still `True` for <3.11. + Patch by [Daraan](https://github.com/Daraan) # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 17ce28f2..beab0057 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6172,6 +6172,12 @@ def test_equivalent_nested_variadics(self): self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int]], object]) self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str]], Unpack[Tuple[int]], object]) + @skipUnless(TYPING_3_11_0, "Needed for backport") + def test_type_var_inheritance(self): + Ts = TypeVarTuple("Ts") + self.assertFalse(isinstance(Unpack[Ts], TypeVar)) + self.assertFalse(isinstance(Unpack[Ts], typing.TypeVar)) + class TypeVarTupleTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index fe492a3f..9589f035 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2657,7 +2657,9 @@ def __init__(self, getitem): self.__doc__ = _UNPACK_DOC class _UnpackAlias(typing._GenericAlias, _root=True): - __class__ = typing.TypeVar + if sys.version_info < (3, 11): + # needed for compatibility with Generic[Unpack[Ts]] + __class__ = typing.TypeVar @property def __typing_unpacked_tuple_args__(self): From 75c95c493e1ac487bdca629036c35a38f92234f0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 18 Mar 2025 10:35:44 -0700 Subject: [PATCH 51/81] Start PEP 728 implementation (#519) --- src/test_typing_extensions.py | 211 ++++++++++++++++++++++++++-------- src/typing_extensions.py | 130 ++++++++++++++------- 2 files changed, 249 insertions(+), 92 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index beab0057..4e3520fc 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -55,6 +55,7 @@ Never, NewType, NoDefault, + NoExtraItems, NoReturn, NotRequired, Optional, @@ -128,6 +129,8 @@ # 3.13.0.rc1 fixes a problem with @deprecated TYPING_3_13_0_RC = sys.version_info[:4] >= (3, 13, 0, "candidate") +TYPING_3_14_0 = sys.version_info[:3] >= (3, 14, 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 @@ -4140,18 +4143,25 @@ def test_basics_keywords_syntax(self): def test_typeddict_special_keyword_names(self): with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, - fields=list, _fields=dict) + fields=list, _fields=dict, + closed=bool, extra_items=bool) self.assertEqual(TD.__name__, 'TD') self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, - '_typename': int, 'fields': list, '_fields': dict}) + '_typename': int, 'fields': list, '_fields': dict, + 'closed': bool, 'extra_items': bool}) + self.assertIsNone(TD.__closed__) + self.assertIs(TD.__extra_items__, NoExtraItems) a = TD(cls=str, self=42, typename='foo', _typename=53, - fields=[('bar', tuple)], _fields={'baz', set}) + fields=[('bar', tuple)], _fields={'baz', set}, + closed=None, extra_items="tea pot") self.assertEqual(a['cls'], str) self.assertEqual(a['self'], 42) self.assertEqual(a['typename'], 'foo') self.assertEqual(a['_typename'], 53) self.assertEqual(a['fields'], [('bar', tuple)]) self.assertEqual(a['_fields'], {'baz', set}) + self.assertIsNone(a['closed']) + self.assertEqual(a['extra_items'], "tea pot") def test_typeddict_create_errors(self): with self.assertRaises(TypeError): @@ -4414,24 +4424,6 @@ 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), @@ -4448,6 +4440,53 @@ class ChildClosed(Unclosed, Closed): class Wrong(*bases): pass + def test_closed_values(self): + class Implicit(TypedDict): ... + class ExplicitTrue(TypedDict, closed=True): ... + class ExplicitFalse(TypedDict, closed=False): ... + + self.assertIsNone(Implicit.__closed__) + self.assertIs(ExplicitTrue.__closed__, True) + self.assertIs(ExplicitFalse.__closed__, False) + + + @skipIf(TYPING_3_14_0, "only supported on older versions") + def test_closed_typeddict_compat(self): + class Closed(TypedDict, closed=True): + __extra_items__: None + + class Unclosed(TypedDict, closed=False): + ... + + class ChildUnclosed(Closed, Unclosed): + ... + + self.assertIsNone(ChildUnclosed.__closed__) + self.assertEqual(ChildUnclosed.__extra_items__, NoExtraItems) + + class ChildClosed(Unclosed, Closed): + ... + + self.assertIsNone(ChildClosed.__closed__) + self.assertEqual(ChildClosed.__extra_items__, NoExtraItems) + + def test_extra_items_class_arg(self): + class TD(TypedDict, extra_items=int): + a: str + + self.assertIs(TD.__extra_items__, int) + self.assertEqual(TD.__annotations__, {'a': str}) + self.assertEqual(TD.__required_keys__, frozenset({'a'})) + self.assertEqual(TD.__optional_keys__, frozenset()) + + class NoExtra(TypedDict): + a: str + + self.assertIs(NoExtra.__extra_items__, NoExtraItems) + self.assertEqual(NoExtra.__annotations__, {'a': str}) + self.assertEqual(NoExtra.__required_keys__, frozenset({'a'})) + self.assertEqual(NoExtra.__optional_keys__, frozenset()) + def test_is_typeddict(self): self.assertIs(is_typeddict(Point2D), True) self.assertIs(is_typeddict(Point2Dor3D), True) @@ -4803,7 +4842,8 @@ class AllTheThings(TypedDict): }, ) - def test_extra_keys_non_readonly(self): + @skipIf(TYPING_3_14_0, "Old syntax only supported on <3.14") + def test_extra_keys_non_readonly_legacy(self): class Base(TypedDict, closed=True): __extra_items__: str @@ -4815,7 +4855,8 @@ class Child(Base): self.assertEqual(Child.__readonly_keys__, frozenset({})) self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) - def test_extra_keys_readonly(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_keys_readonly_legacy(self): class Base(TypedDict, closed=True): __extra_items__: ReadOnly[str] @@ -4827,7 +4868,21 @@ class Child(Base): self.assertEqual(Child.__readonly_keys__, frozenset({})) self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) - def test_extra_key_required(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_keys_readonly_explicit_closed_legacy(self): + class Base(TypedDict, closed=True): + __extra_items__: ReadOnly[str] + + class Child(Base, closed=True): + 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'})) + + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_key_required_legacy(self): with self.assertRaisesRegex( TypeError, "Special key __extra_items__ does not support Required" @@ -4840,7 +4895,7 @@ def test_extra_key_required(self): ): TypedDict("A", {"__extra_items__": NotRequired[int]}, closed=True) - def test_regular_extra_items(self): + def test_regular_extra_items_legacy(self): class ExtraReadOnly(TypedDict): __extra_items__: ReadOnly[str] @@ -4848,8 +4903,8 @@ class ExtraReadOnly(TypedDict): 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__) + self.assertIs(ExtraReadOnly.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraReadOnly.__closed__) class ExtraRequired(TypedDict): __extra_items__: Required[str] @@ -4858,8 +4913,8 @@ class ExtraRequired(TypedDict): 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__) + self.assertIs(ExtraRequired.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraRequired.__closed__) class ExtraNotRequired(TypedDict): __extra_items__: NotRequired[str] @@ -4868,10 +4923,11 @@ class ExtraNotRequired(TypedDict): 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__) + self.assertIs(ExtraNotRequired.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraNotRequired.__closed__) - def test_closed_inheritance(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_closed_inheritance_legacy(self): class Base(TypedDict, closed=True): __extra_items__: ReadOnly[Union[str, None]] @@ -4881,49 +4937,97 @@ class Base(TypedDict, closed=True): self.assertEqual(Base.__mutable_keys__, frozenset({})) self.assertEqual(Base.__annotations__, {}) self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) - self.assertTrue(Base.__closed__) + self.assertIs(Base.__closed__, True) - class Child(Base): + class Child(Base, closed=True): a: int __extra_items__: int - self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(Child.__required_keys__, frozenset({'a'})) 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__) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": int}) + self.assertIs(Child.__extra_items__, int) + self.assertIs(Child.__closed__, True) class GrandChild(Child, closed=True): __extra_items__: str - self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) 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__) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": int}) + self.assertIs(GrandChild.__extra_items__, str) + self.assertIs(GrandChild.__closed__, True) + + def test_closed_inheritance(self): + class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]): + a: int + + self.assertEqual(Base.__required_keys__, frozenset({"a"})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({"a"})) + self.assertEqual(Base.__annotations__, {"a": int}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertIsNone(Base.__closed__) + + class Child(Base, extra_items=int): + a: str + + self.assertEqual(Child.__required_keys__, frozenset({'a'})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": str}) + self.assertIs(Child.__extra_items__, int) + self.assertIsNone(Child.__closed__) + + class GrandChild(Child, closed=True): + a: float + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": float}) + self.assertIs(GrandChild.__extra_items__, NoExtraItems) + self.assertIs(GrandChild.__closed__, True) + + class GrandGrandChild(GrandChild): + ... + self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__annotations__, {"a": float}) + self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems) + self.assertIsNone(GrandGrandChild.__closed__) def test_implicit_extra_items(self): class Base(TypedDict): a: int - self.assertEqual(Base.__extra_items__, None) - self.assertFalse(Base.__closed__) + self.assertIs(Base.__extra_items__, NoExtraItems) + self.assertIsNone(Base.__closed__) class ChildA(Base, closed=True): ... - self.assertEqual(ChildA.__extra_items__, Never) - self.assertTrue(ChildA.__closed__) + self.assertEqual(ChildA.__extra_items__, NoExtraItems) + self.assertIs(ChildA.__closed__, True) + @skipIf(TYPING_3_14_0, "Backwards compatibility only for Python 3.13") + def test_implicit_extra_items_before_3_14(self): + class Base(TypedDict): + a: int class ChildB(Base, closed=True): __extra_items__: None - self.assertEqual(ChildB.__extra_items__, type(None)) - self.assertTrue(ChildB.__closed__) + self.assertIs(ChildB.__extra_items__, type(None)) + self.assertIs(ChildB.__closed__, True) @skipIf( TYPING_3_13_0, @@ -4933,9 +5037,14 @@ class ChildB(Base, closed=True): def test_backwards_compatibility(self): with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", closed=int) - self.assertFalse(TD.__closed__) + self.assertIs(TD.__closed__, None) self.assertEqual(TD.__annotations__, {"closed": int}) + with self.assertWarns(DeprecationWarning): + TD = TypedDict("TD", extra_items=int) + self.assertIs(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__annotations__, {"extra_items": int}) + class AnnotatedTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9589f035..d2fb245b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -98,6 +98,8 @@ 'ReadOnly', 'Required', 'NotRequired', + 'NoDefault', + 'NoExtraItems', # Pure aliases, have always been in typing 'AbstractSet', @@ -124,7 +126,6 @@ 'MutableMapping', 'MutableSequence', 'MutableSet', - 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -877,6 +878,63 @@ def inner(func): return inner +_NEEDS_SINGLETONMETA = ( + not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") +) + +if _NEEDS_SINGLETONMETA: + class SingletonMeta(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}" + ) + + +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType(metaclass=SingletonMeta): + """The type of the NoDefault singleton.""" + + __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 + +if hasattr(typing, "NoExtraItems"): + NoExtraItems = typing.NoExtraItems +else: + class NoExtraItemsType(metaclass=SingletonMeta): + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoExtraItems" + + def __reduce__(self): + return "NoExtraItems" + + NoExtraItems = NoExtraItemsType() + del NoExtraItemsType + +if _NEEDS_SINGLETONMETA: + del SingletonMeta + + # Update this to something like >=3.13.0b1 if and when # PEP 728 is implemented in CPython _PEP_728_IMPLEMENTED = False @@ -923,7 +981,9 @@ def _get_typeddict_qualifiers(annotation_type): break class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, *, total=True, closed=False): + + def __new__(cls, name, bases, ns, *, total=True, closed=None, + extra_items=NoExtraItems): """Create new typed dict class object. This method is called when TypedDict is subclassed, @@ -935,6 +995,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): if type(base) is not _TypedDictMeta and base is not typing.Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, typing.Generic) for b in bases): generic_base = (typing.Generic,) @@ -974,7 +1036,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): optional_keys = set() readonly_keys = set() mutable_keys = set() - extra_items_type = None + extra_items_type = extra_items for base in bases: base_dict = base.__dict__ @@ -984,13 +1046,12 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): 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: + # This was specified in an earlier version of PEP 728. Support + # is retained for backwards compatibility, but only for Python + # 3.13 and lower. + if (closed and sys.version_info < (3, 14) + and "__extra_items__" in own_annotations): annotation_type = own_annotations.pop("__extra_items__") qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -1045,7 +1106,16 @@ def __subclasscheck__(cls, other): _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) @_ensure_subclassable(lambda bases: (_TypedDict,)) - def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs): + def TypedDict( + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **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 @@ -1105,9 +1175,14 @@ 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: + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: kwargs["closed"] = closed - closed = False + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems fields = kwargs elif kwargs: raise TypeError("TypedDict takes either a dict or keyword arguments," @@ -1129,7 +1204,8 @@ class Point2D(TypedDict): # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total, closed=closed) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td @@ -1532,34 +1608,6 @@ def TypeAlias(self, parameters): ) -if hasattr(typing, "NoDefault"): - NoDefault = typing.NoDefault -else: - 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): - return globals().get("NoDefault") or object.__new__(cls) - - def __repr__(self): - return "typing_extensions.NoDefault" - - def __reduce__(self): - return "NoDefault" - - NoDefault = NoDefaultType() - del NoDefaultType, NoDefaultTypeMeta - - def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault type_param.__default__ = default From 3c66d2692214bdc29550bcb8e537b104ea23cb0b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 18 Mar 2025 11:00:18 -0700 Subject: [PATCH 52/81] Prepare release 4.13.0rc1 (#540) --- CHANGELOG.md | 22 ++++++++++++++-------- pyproject.toml | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a02f48c3..035af817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,19 @@ -# Unreleased +# Release 4.13.0rc1 (March 18, 2025) + +New features: - Add `typing_extensions.TypeForm` from PEP 747. Patch by Jelle Zijlstra. - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. +- Backport `evaluate_forward_ref` from CPython PR + [#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. + +Bugfixes and changed features: + +- Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra. - Copy the coroutine status of functions and methods wrapped with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. - Fix bug where `TypeAliasType` instances could be subscripted even @@ -17,7 +26,7 @@ subscripted with an `Unpack` object. Patch by [Daraan](https://github.com/Daraan). - Backport to Python 3.10 the ability to substitute `...` in generic `Callable` -aliases that have a `Concatenate` special form as their argument. + aliases that have a `Concatenate` special form as their argument. Patch by [Daraan](https://github.com/Daraan). - Extended the `Concatenate` backport for Python 3.8-3.10 to now accept `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). @@ -25,24 +34,21 @@ aliases that have a `Concatenate` special form as their argument. `Union[..., NoneType]` to annotations that have a `None` default value anymore. This fixes wrapping of `Annotated` in an unwanted `Optional` in such cases. Patch by [Daraan](https://github.com/Daraan). -- Fix error in subscription of `Unpack` aliases causing nested Unpacks +- Fix error in subscription of `Unpack` aliases causing nested Unpacks to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). -- Backport `evaluate_forward_ref` from CPython PR - [#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s. - Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. - Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` instances before Python 3.11. Patch by [Daraan](https://github.com/Daraan). -- Fix error on Python 3.10 when using `typing.Concatenate` and +- Fix error on Python 3.10 when using `typing.Concatenate` and `typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan). - Backport of CPython PR [#109544](https://github.com/python/cpython/pull/109544) to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a `TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor. Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. -- Fix for Python 3.11 that now `isinstance(typing_extensions.Unpack[...], TypeVar)` +- Fix for Python 3.11 that now `isinstance(typing_extensions.Unpack[...], TypeVar)` evaluates to `False`, however still `True` for <3.11. Patch by [Daraan](https://github.com/Daraan) diff --git a/pyproject.toml b/pyproject.toml index b0046a7b..f9f10d50 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.2" +version = "4.13.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 478b2b366beb30d74d5dd0029848141bf911db7f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 18 Mar 2025 11:25:06 -0700 Subject: [PATCH 53/81] Copy-edit and add test (#541) Co-authored-by: Alex Waygood --- CHANGELOG.md | 8 ++++---- src/test_typing_extensions.py | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 035af817..440422c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ Bugfixes and changed features: - Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. Patch by [Daraan](https://github.com/Daraan). -- Fix that lists and ... could not be used for parameter expressions for `TypeAliasType` +- Fix that lists and `...` could not be used for parameter expressions for `TypeAliasType` instances before Python 3.11. Patch by [Daraan](https://github.com/Daraan). - Fix error on Python 3.10 when using `typing.Concatenate` and @@ -48,9 +48,9 @@ Bugfixes and changed features: to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a `TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor. Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. -- Fix for Python 3.11 that now `isinstance(typing_extensions.Unpack[...], TypeVar)` - evaluates to `False`, however still `True` for <3.11. - Patch by [Daraan](https://github.com/Daraan) +- `isinstance(typing_extensions.Unpack[...], TypeVar)` now evaluates to `False` on Python 3.11 + and newer, but remains `True` on versions before 3.11. + Patch by [Daraan](https://github.com/Daraan). # Release 4.12.2 (June 7, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 4e3520fc..da4e3e44 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5045,6 +5045,14 @@ def test_backwards_compatibility(self): self.assertIs(TD.__extra_items__, NoExtraItems) self.assertEqual(TD.__annotations__, {"extra_items": int}) + def test_cannot_combine_closed_and_extra_items(self): + with self.assertRaisesRegex( + TypeError, + "Cannot combine closed=True and extra_items" + ): + class TD(TypedDict, closed=True, extra_items=range): + x: str + class AnnotatedTests(BaseTestCase): From 3218d774e438e19d72f7259593ce03b451edfe24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Sun, 23 Mar 2025 19:56:57 +0100 Subject: [PATCH 54/81] DOC: add missing 'In typing since ...' mention for `TypeIs` (#548) --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/index.rst b/doc/index.rst index d150c7db..bf8b431a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -380,6 +380,7 @@ Special typing primitives .. data:: TypeIs See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing. + In ``typing`` since 3.13. .. versionadded:: 4.10.0 From e77e8e2dbdab9d7edf3d88c9493c26f759a25978 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Tue, 25 Mar 2025 04:02:22 -0700 Subject: [PATCH 55/81] Disable pyanalyze tests for now (#554) --- .github/workflows/third_party.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index d0cbb0bc..ec2d93f8 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -140,10 +140,11 @@ jobs: run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies run: uv pip freeze - - name: Run pyanalyze tests - run: | - cd pyanalyze - pytest pyanalyze/ + # TODO: re-enable + # - name: Run pyanalyze tests + # run: | + # cd pyanalyze + # pytest pyanalyze/ typeguard: name: typeguard tests From 671a337a3231b90f7cd979300f9af9fa25cdd35f Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 25 Mar 2025 08:37:08 -0400 Subject: [PATCH 56/81] Fix 'Test and lint' workflow running on forks (#551) --- .github/workflows/ci.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9db5bc7e..8cbeaf5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,16 +24,11 @@ jobs: tests: name: Run tests + # if 'schedule' was the trigger, + # don't run it on contributors' forks if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' strategy: fail-fast: false From 6239d868113cbf60c3db359775bb5f5c948c6978 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Tue, 25 Mar 2025 09:26:51 -0400 Subject: [PATCH 57/81] Use latest Python docs as intersphinx base rather than 3.12 docs (#549) --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 42273604..cbb15a70 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,7 +27,7 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} +intersphinx_mapping = {'py': ('https://docs.python.org/3', None)} add_module_names = False From c8934015b7e2feb65dc461fef202ef69611d7d0e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 25 Mar 2025 20:44:47 -0700 Subject: [PATCH 58/81] Prepare release 4.13.0 (#555) --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 440422c8..98f7bcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Release 4.13.0 (March 25, 2025) + +No user-facing changes since 4.13.0rc1. + # Release 4.13.0rc1 (March 18, 2025) New features: diff --git a/pyproject.toml b/pyproject.toml index f9f10d50..76648a8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.13.0rc1" +version = "4.13.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 9f93d6fb752698504d80b1ed0c73b0a2a9d0cff6 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Thu, 27 Mar 2025 02:35:55 -0400 Subject: [PATCH 59/81] Add intersphinx links for 3.13 typing features (#550) --- doc/index.rst | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index bf8b431a..2c1a149c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -255,7 +255,7 @@ Special typing primitives .. data:: NoDefault - See :py:class:`typing.NoDefault`. In ``typing`` since 3.13.0. + See :py:data:`typing.NoDefault`. In ``typing`` since 3.13. .. versionadded:: 4.12.0 @@ -341,7 +341,9 @@ Special typing primitives .. data:: ReadOnly - See :pep:`705`. Indicates that a :class:`TypedDict` item may not be modified. + See :py:data:`typing.ReadOnly` and :pep:`705`. In ``typing`` since 3.13. + + Indicates that a :class:`TypedDict` item may not be modified. .. versionadded:: 4.9.0 @@ -379,8 +381,9 @@ Special typing primitives .. data:: TypeIs - See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing. - In ``typing`` since 3.13. + See :py:data:`typing.TypeIs` and :pep:`742`. In ``typing`` since 3.13. + + Similar to :data:`TypeGuard`, but allows more type narrowing. .. versionadded:: 4.10.0 @@ -843,6 +846,8 @@ Functions .. function:: get_protocol_members(tp) + See :py:func:`typing.get_protocol_members`. In ``typing`` since 3.13. + Return the set of members defined in a :class:`Protocol`. This works with protocols defined using either :class:`typing.Protocol` or :class:`typing_extensions.Protocol`. @@ -878,6 +883,8 @@ Functions .. function:: is_protocol(tp) + See :py:func:`typing.is_protocol`. In ``typing`` since 3.13. + Determine if a type is a :class:`Protocol`. This works with protocols defined using either :py:class:`typing.Protocol` or :class:`typing_extensions.Protocol`. From ebe2b9405c493749429de6c82c8daddd1107c9e2 Mon Sep 17 00:00:00 2001 From: Daraan Date: Thu, 27 Mar 2025 16:28:10 +0100 Subject: [PATCH 60/81] Fix duplicated keywords for typing._ConcatenateGenericAlias in 3.10.2 (#557) --- CHANGELOG.md | 6 ++++++ src/typing_extensions.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f7bcdf..df2f24cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +Bugfixes and changed features: +- Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. + Patch by [Daraan](https://github.com/Daraan). + # Release 4.13.0 (March 25, 2025) No user-facing changes since 4.13.0rc1. diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d2fb245b..8333d890 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2072,7 +2072,7 @@ def _create_concatenate_alias(origin, parameters): if parameters[-1] is ... and sys.version_info < (3, 9, 2): # Hack: Arguments must be types, replace it with one. parameters = (*parameters[:-1], _EllipsisDummy) - if sys.version_info >= (3, 10, 2): + if sys.version_info >= (3, 10, 3): concatenate = _ConcatenateGenericAlias(origin, parameters, _typevar_types=(TypeVar, ParamSpec), _paramspec_tvars=True) From 304f5cb17d709950ece3e9c84a76174bf7405b90 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Thu, 27 Mar 2025 15:51:19 -0400 Subject: [PATCH 61/81] Add SQLAlchemy to third-party daily tests (#561) --- .github/workflows/third_party.yml | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index ec2d93f8..5f444c9f 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -300,6 +300,49 @@ jobs: - name: Run cattrs tests run: cd cattrs; pdm run pytest tests + sqlalchemy: + name: sqlalchemy tests + needs: skip-schedule-on-fork + strategy: + fail-fast: false + matrix: + # PyPy is deliberately omitted here, since SQLAlchemy's tests + # fail on PyPy for reasons unrelated to typing_extensions. + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + checkout-ref: [ "main", "rel_2_0" ] + # sqlalchemy tests fail when using the Ubuntu 24.04 runner + # https://github.com/sqlalchemy/sqlalchemy/commit/8d73205f352e68c6603e90494494ef21027ec68f + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Checkout sqlalchemy + run: git clone -b ${{ matrix.checkout-ref }} --depth=1 https://github.com/sqlalchemy/sqlalchemy.git || git clone -b ${{ matrix.checkout-ref }} --depth=1 https://github.com/sqlalchemy/sqlalchemy.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest + - name: Install sqlalchemy test dependencies + run: uv pip install --system tox setuptools + - name: List installed dependencies + # Note: tox installs SQLAlchemy and its dependencies in a different isolated + # environment before running the tests. To see the dependencies installed + # in the test environment, look for the line 'freeze> python -m pip freeze --all' + # in the output of the test step below. + run: uv pip list + - name: Run sqlalchemy tests + run: | + cd sqlalchemy + tox -e github-nocext \ + --force-dep "typing-extensions @ file://$(pwd)/../typing-extensions-latest" \ + -- -q --nomemory --notimingintensive + create-issue-on-failure: name: Create an issue if daily tests failed runs-on: ubuntu-latest @@ -312,6 +355,7 @@ jobs: - typed-argument-parser - mypy - cattrs + - sqlalchemy if: >- ${{ @@ -326,6 +370,7 @@ jobs: || needs.typed-argument-parser.result == 'failure' || needs.mypy.result == 'failure' || needs.cattrs.result == 'failure' + || needs.sqlalchemy.result == 'failure' ) }} From 5ce0e69b20992f8bf410849a31381cd656e3eb6b Mon Sep 17 00:00:00 2001 From: Daraan Date: Tue, 1 Apr 2025 17:38:35 +0200 Subject: [PATCH 62/81] Fix TypeError with evaluate_forward_ref on some 3.10 and 3.9 versions (#558) https://github.com/python/cpython/pull/30926 --- CHANGELOG.md | 2 ++ src/typing_extensions.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2f24cf..e7043945 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Bugfixes and changed features: - Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. Patch by [Daraan](https://github.com/Daraan). +- Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10. + Patch by [Daraan](https://github.com/Daraan). # Release 4.13.0 (March 25, 2025) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 8333d890..4b95dee7 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4371,7 +4371,11 @@ def _lax_type_check( A lax Python 3.11+ like version of typing._type_check """ if hasattr(typing, "_type_convert"): - if _FORWARD_REF_HAS_CLASS: + if ( + sys.version_info >= (3, 10, 3) + or (3, 9, 10) < sys.version_info[:3] < (3, 10) + ): + # allow_special_forms introduced later cpython/#30926 (bpo-46539) type_ = typing._type_convert( value, module=module, From f264e58146479d2d8456dd6e660d785dc07d6f26 Mon Sep 17 00:00:00 2001 From: Brian Schubert Date: Wed, 2 Apr 2025 13:58:43 -0400 Subject: [PATCH 63/81] Move CI to "ubuntu-latest" (round 2) (#570) GitHub is decommissioning Ubuntu 20.04. I wouldn't expect our tests to have a lot of OS version dependencies, so let's try just running ubuntu-latest everywhere. Co-authored-by: Jelle Zijlstra --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cbeaf5f..d0ced0b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,11 +39,10 @@ jobs: # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json python-version: - "3.8" - - "3.8.0" - "3.9" - - "3.9.0" + - "3.9.12" - "3.10" - - "3.10.0" + - "3.10.4" - "3.11" - "3.11.0" - "3.12" @@ -54,7 +53,7 @@ jobs: - "pypy3.9" - "pypy3.10" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -70,6 +69,7 @@ jobs: # Be wary of running `pip install` here, since it becomes easy for us to # accidentally pick up typing_extensions as installed by a dependency cd src + python --version # just to make sure we're running the right one python -m unittest test_typing_extensions.py - name: Test CPython typing test suite From 45a8847aad979d2f1f7dff075ac52df5df7b7adb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 3 Apr 2025 09:06:38 -0700 Subject: [PATCH 64/81] Prepare release 4.13.1 (#573) --- CHANGELOG.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7043945..2e0122cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -# Unreleased +# Release 4.13.1 (April 3, 2025) -Bugfixes and changed features: +Bugfixes: - Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. Patch by [Daraan](https://github.com/Daraan). - Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10. diff --git a/pyproject.toml b/pyproject.toml index 76648a8b..fd85b2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.13.0" +version = "4.13.1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 8092c3996f4902ad9c74ac2d1d8dd19371ecbaa3 Mon Sep 17 00:00:00 2001 From: Joren Hammudoglu Date: Fri, 4 Apr 2025 15:56:47 +0200 Subject: [PATCH 65/81] fix `TypeAliasType` union with `typing.TypeAliasType` (#575) --- CHANGELOG.md | 6 ++++++ src/test_typing_extensions.py | 4 ++++ src/typing_extensions.py | 29 +++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0122cc..0d7f109c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a + `typing.TypeAliasType` on Python 3.12 and 3.13. + Patch by [Joren Hammudoglu](https://github.com/jorenham). + # Release 4.13.1 (April 3, 2025) Bugfixes: diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index da4e3e44..b8f5d4b7 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -7819,6 +7819,10 @@ def test_or(self): self.assertEqual(Alias | None, Union[Alias, None]) self.assertEqual(Alias | (int | str), Union[Alias, int | str]) self.assertEqual(Alias | list[float], Union[Alias, list[float]]) + + if sys.version_info >= (3, 12): + Alias2 = typing.TypeAliasType("Alias2", str) + self.assertEqual(Alias | Alias2, Union[Alias, Alias2]) else: with self.assertRaises(TypeError): Alias | int diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4b95dee7..c6c3b88e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3827,14 +3827,27 @@ def __ror__(self, other): TypeAliasType = typing.TypeAliasType # 3.8-3.13 else: - def _is_unionable(obj): - """Corresponds to is_unionable() in unionobject.c in CPython.""" - return obj is None or isinstance(obj, ( - type, - _types.GenericAlias, - _types.UnionType, - TypeAliasType, - )) + if sys.version_info >= (3, 12): + # 3.12-3.14 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + typing.TypeAliasType, + TypeAliasType, + )) + else: + # 3.8-3.11 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) if sys.version_info < (3, 10): # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, From 281d7b0ca6edad384e641d1066b759c280602919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Sun, 6 Apr 2025 16:42:01 +0200 Subject: [PATCH 66/81] Add 3rd party tests for litestar (#578) --- .github/workflows/third_party.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 5f444c9f..b477b930 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -343,6 +343,33 @@ jobs: --force-dep "typing-extensions @ file://$(pwd)/../typing-extensions-latest" \ -- -q --nomemory --notimingintensive + + litestar: + name: litestar tests + needs: skip-schedule-on-fork + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Checkout litestar + run: git clone --depth=1 https://github.com/litestar-org/litestar.git || git clone --depth=1 https://github.com/litestar-org/litestar.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Run litestar tests + run: uv run --with=../typing-extensions-latest -- python -m pytest tests/unit/test_typing.py tests/unit/test_dto + working-directory: litestar + create-issue-on-failure: name: Create an issue if daily tests failed runs-on: ubuntu-latest From 88a0c200ceb0ccfe4329d3db8a1a863a2381e44c Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:51:18 +0200 Subject: [PATCH 67/81] Do not shadow user arguments in generated `__new__` by `@deprecated` (#581) Backport of: https://github.com/python/cpython/pull/132160 --- CHANGELOG.md | 4 ++++ src/test_typing_extensions.py | 19 +++++++++++++++++++ src/typing_extensions.py | 5 +++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d7f109c..ab520c0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ - Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a `typing.TypeAliasType` on Python 3.12 and 3.13. Patch by [Joren Hammudoglu](https://github.com/jorenham). +- Backport from CPython PR [#132160](https://github.com/python/cpython/pull/132160) + to avoid having user arguments shadowed in generated `__new__` by + `@typing_extensions.deprecated`. + Patch by [Victorien Plot](https://github.com/Viicos). # Release 4.13.1 (April 3, 2025) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b8f5d4b7..584b0fa4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -707,6 +707,25 @@ class Child(Base, Mixin): instance = Child(42) self.assertEqual(instance.a, 42) + def test_do_not_shadow_user_arguments(self): + new_called = False + new_called_cls = None + + @deprecated("MyMeta will go away soon") + class MyMeta(type): + def __new__(mcs, name, bases, attrs, cls=None): + nonlocal new_called, new_called_cls + new_called = True + new_called_cls = cls + return super().__new__(mcs, name, bases, attrs) + + with self.assertWarnsRegex(DeprecationWarning, "MyMeta will go away soon"): + class Foo(metaclass=MyMeta, cls='haha'): + pass + + self.assertTrue(new_called) + self.assertEqual(new_called_cls, 'haha') + def test_existing_init_subclass(self): @deprecated("C will go away soon") class C: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index c6c3b88e..fa89c83e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3123,7 +3123,8 @@ def method(self) -> None: return arg -if hasattr(warnings, "deprecated"): +# Python 3.13.3+ contains a fix for the wrapped __new__ +if sys.version_info >= (3, 13, 3): deprecated = warnings.deprecated else: _T = typing.TypeVar("_T") @@ -3203,7 +3204,7 @@ def __call__(self, arg: _T, /) -> _T: original_new = arg.__new__ @functools.wraps(original_new) - def __new__(cls, *args, **kwargs): + def __new__(cls, /, *args, **kwargs): if cls is arg: warnings.warn(msg, category=category, stacklevel=stacklevel + 1) if original_new is not object.__new__: From 4525e9dbbd177b4ef8a84f55ff5fe127582a071d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 10 Apr 2025 07:16:36 -0700 Subject: [PATCH 68/81] Prepare release 4.13.2 (#583) --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab520c0f..c2105ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Release 4.13.2 (April 10, 2025) - Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a `typing.TypeAliasType` on Python 3.12 and 3.13. diff --git a/pyproject.toml b/pyproject.toml index fd85b2d4..b2f62fe6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.13.1" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From 7cfb2c060563a24f3c1f444d125bd04f1b0976ad Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Wed, 16 Apr 2025 22:25:21 +0200 Subject: [PATCH 69/81] Drop support for Python 3.8 (#585) --- .github/workflows/ci.yml | 2 - CHANGELOG.md | 4 + doc/index.rst | 2 +- pyproject.toml | 7 +- src/test_typing_extensions.py | 100 ++----- src/typing_extensions.py | 544 +++------------------------------- 6 files changed, 86 insertions(+), 573 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0ced0b5..1f9d0650 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,6 @@ jobs: # For available versions, see: # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json python-version: - - "3.8" - "3.9" - "3.9.12" - "3.10" @@ -49,7 +48,6 @@ jobs: - "3.12.0" - "3.13" - "3.13.0" - - "pypy3.8" - "pypy3.9" - "pypy3.10" diff --git a/CHANGELOG.md b/CHANGELOG.md index c2105ca9..560971ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased + +- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). + # Release 4.13.2 (April 10, 2025) - Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a diff --git a/doc/index.rst b/doc/index.rst index 2c1a149c..e652c9e4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -139,7 +139,7 @@ Example usage:: Python version support ---------------------- -``typing_extensions`` currently supports Python versions 3.8 and higher. In the future, +``typing_extensions`` currently supports Python versions 3.9 and higher. In the future, support for older Python versions will be dropped some time after that version reaches end of life. diff --git a/pyproject.toml b/pyproject.toml index b2f62fe6..48e2f914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,9 +7,9 @@ build-backend = "flit_core.buildapi" [project] name = "typing_extensions" version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +description = "Backported and Experimental Type Hints for Python 3.9+" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = "PSF-2.0" license-files = ["LICENSE"] keywords = [ @@ -34,7 +34,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -63,7 +62,7 @@ exclude = [] [tool.ruff] line-length = 90 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] select = [ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 584b0fa4..a6948951 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -110,7 +110,6 @@ # 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) TYPING_3_10_0 = sys.version_info[:3] >= (3, 10, 0) # 3.11 makes runtime type checks (_type_check) more lenient. @@ -1779,8 +1778,7 @@ class C(Generic[T]): pass self.assertIs(get_origin(List), list) self.assertIs(get_origin(Tuple), tuple) self.assertIs(get_origin(Callable), collections.abc.Callable) - if sys.version_info >= (3, 9): - self.assertIs(get_origin(list[int]), list) + self.assertIs(get_origin(list[int]), list) self.assertIs(get_origin(list), None) self.assertIs(get_origin(P.args), P) self.assertIs(get_origin(P.kwargs), P) @@ -1817,20 +1815,18 @@ class C(Generic[T]): pass self.assertEqual(get_args(List), ()) self.assertEqual(get_args(Tuple), ()) self.assertEqual(get_args(Callable), ()) - if sys.version_info >= (3, 9): - self.assertEqual(get_args(list[int]), (int,)) + self.assertEqual(get_args(list[int]), (int,)) self.assertEqual(get_args(list), ()) - if sys.version_info >= (3, 9): - # Support Python versions with and without the fix for - # https://bugs.python.org/issue42195 - # The first variant is for 3.9.2+, the second for 3.9.0 and 1 - self.assertIn(get_args(collections.abc.Callable[[int], str]), - (([int], str), ([[int]], str))) - self.assertIn(get_args(collections.abc.Callable[[], str]), - (([], str), ([[]], str))) - self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str)) + # Support Python versions with and without the fix for + # https://bugs.python.org/issue42195 + # The first variant is for 3.9.2+, the second for 3.9.0 and 1 + self.assertIn(get_args(collections.abc.Callable[[int], str]), + (([int], str), ([[int]], str))) + self.assertIn(get_args(collections.abc.Callable[[], str]), + (([], str), ([[]], str))) + self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str)) P = ParamSpec('P') - # In 3.9 and lower we use typing_extensions's hacky implementation + # In 3.9 we use typing_extensions's hacky implementation # of ParamSpec, which gets incorrectly wrapped in a list self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)]) self.assertEqual(get_args(Required[int]), (int,)) @@ -3808,7 +3804,7 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ... MemoizedFunc[[int, str, str]] if sys.version_info >= (3, 10): - # These unfortunately don't pass on <=3.9, + # These unfortunately don't pass on 3.9, # due to typing._type_check on older Python versions X = MemoizedFunc[[int, str, str], T, T2] self.assertEqual(X.__parameters__, (T, T2)) @@ -4553,7 +4549,7 @@ class PointDict3D(PointDict2D, total=False): assert is_typeddict(PointDict2D) is True assert is_typeddict(PointDict3D) is True - @skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9") + @skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9.7") def test_get_type_hints_cross_module_subclass(self): self.assertNotIn("_DoNotImport", globals()) self.assertEqual( @@ -4696,11 +4692,9 @@ class WithImplicitAny(B): with self.assertRaises(TypeError): WithImplicitAny[str] - @skipUnless(TYPING_3_9_0, "Was changed in 3.9") def test_non_generic_subscript(self): # For backward compatibility, subscription works # on arbitrary TypedDict types. - # (But we don't attempt to backport this misfeature onto 3.8.) class TD(TypedDict): a: T A = TD[int] @@ -5163,7 +5157,7 @@ class C: A.x = 5 self.assertEqual(C.x, 5) - @skipIf(sys.version_info[:2] in ((3, 9), (3, 10)), "Waiting for bpo-46491 bugfix.") + @skipIf(sys.version_info[:2] == (3, 10), "Waiting for https://github.com/python/cpython/issues/90649 bugfix.") def test_special_form_containment(self): class C: classvar: Annotated[ClassVar[int], "a decoration"] = 4 @@ -5475,21 +5469,20 @@ def test_valid_uses(self): self.assertEqual(C2.__parameters__, (P, T)) # Test collections.abc.Callable too. - if sys.version_info[:2] >= (3, 9): - # Note: no tests for Callable.__parameters__ here - # because types.GenericAlias Callable is hardcoded to search - # for tp_name "TypeVar" in C. This was changed in 3.10. - C3 = collections.abc.Callable[P, int] - self.assertEqual(C3.__args__, (P, int)) - C4 = collections.abc.Callable[P, T] - self.assertEqual(C4.__args__, (P, T)) + # Note: no tests for Callable.__parameters__ here + # because types.GenericAlias Callable is hardcoded to search + # for tp_name "TypeVar" in C. This was changed in 3.10. + C3 = collections.abc.Callable[P, int] + self.assertEqual(C3.__args__, (P, int)) + C4 = collections.abc.Callable[P, T] + self.assertEqual(C4.__args__, (P, T)) # ParamSpec instances should also have args and kwargs attributes. # Note: not in dir(P) because of __class__ hacks self.assertTrue(hasattr(P, 'args')) self.assertTrue(hasattr(P, 'kwargs')) - @skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs bpo-46676.") + @skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs https://github.com/python/cpython/issues/90834.") def test_args_kwargs(self): P = ParamSpec('P') P_2 = ParamSpec('P_2') @@ -5649,8 +5642,6 @@ class ProtoZ(Protocol[P]): G10 = klass[int, Concatenate[str, P]] with self.subTest("Check invalid form substitution"): self.assertEqual(G10.__parameters__, (P, )) - if sys.version_info < (3, 9): - self.skipTest("3.8 typing._type_subst does not support this substitution process") H10 = G10[int] if (3, 10) <= sys.version_info < (3, 11, 3): self.skipTest("3.10-3.11.2 does not substitute Concatenate here") @@ -5780,9 +5771,6 @@ def test_valid_uses(self): T = TypeVar('T') for callable_variant in (Callable, collections.abc.Callable): with self.subTest(callable_variant=callable_variant): - if not TYPING_3_9_0 and callable_variant is collections.abc.Callable: - self.skipTest("Needs PEP 585") - C1 = callable_variant[Concatenate[int, P], int] C2 = callable_variant[Concatenate[int, T, P], T] self.assertEqual(C1.__origin__, C2.__origin__) @@ -5830,7 +5818,7 @@ def test_invalid_uses(self): ): Concatenate[(str,), P] - @skipUnless(TYPING_3_10_0, "Missing backport to <=3.9. See issue #48") + @skipUnless(TYPING_3_10_0, "Missing backport to 3.9. See issue #48") def test_alias_subscription_with_ellipsis(self): P = ParamSpec('P') X = Callable[Concatenate[int, P], Any] @@ -6813,7 +6801,6 @@ class Y(Generic[T], NamedTuple): with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] - @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") def test_non_generic_subscript_py39_plus(self): # For backward compatibility, subscription works # on arbitrary NamedTuple types. @@ -6828,19 +6815,6 @@ class Group(NamedTuple): self.assertIs(type(a), Group) self.assertEqual(a, (1, [2])) - @skipIf(TYPING_3_9_0, "Test isn't relevant to 3.9+") - def test_non_generic_subscript_error_message_py38(self): - class Group(NamedTuple): - key: T - group: List[T] - - with self.assertRaisesRegex(TypeError, 'not subscriptable'): - Group[int] - - for attr in ('__args__', '__origin__', '__parameters__'): - with self.subTest(attr=attr): - self.assertFalse(hasattr(Group, attr)) - def test_namedtuple_keyword_usage(self): with self.assertWarnsRegex( DeprecationWarning, @@ -6959,21 +6933,13 @@ def test_copy_and_pickle(self): def test_docstring(self): self.assertIsInstance(NamedTuple.__doc__, str) - @skipUnless(TYPING_3_9_0, "NamedTuple was a class on 3.8 and lower") - def test_same_as_typing_NamedTuple_39_plus(self): + def test_same_as_typing_NamedTuple(self): self.assertEqual( set(dir(NamedTuple)) - {"__text_signature__"}, set(dir(typing.NamedTuple)) ) self.assertIs(type(NamedTuple), type(typing.NamedTuple)) - @skipIf(TYPING_3_9_0, "tests are only relevant to <=3.8") - def test_same_as_typing_NamedTuple_38_minus(self): - self.assertEqual( - self.NestedEmployee.__annotations__, - self.NestedEmployee._field_types - ) - def test_orig_bases(self): T = TypeVar('T') @@ -7235,11 +7201,8 @@ def test_bound_errors(self): r"Bound must be a type\. Got \(1, 2\)\."): TypeVar('X', bound=(1, 2)) - # Technically we could run it on later versions of 3.8, - # but that's not worth the effort. - @skipUnless(TYPING_3_9_0, "Fix was not backported") def test_missing__name__(self): - # See bpo-39942 + # See https://github.com/python/cpython/issues/84123 code = ("import typing\n" "T = typing.TypeVar('T')\n" ) @@ -7420,9 +7383,8 @@ def test_allow_default_after_non_default_in_alias(self): 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)) + 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)) @@ -7602,7 +7564,6 @@ class D(B[str], float): pass with self.assertRaisesRegex(TypeError, "Expected an instance of type"): get_original_bases(object()) - @skipUnless(TYPING_3_9_0, "PEP 585 is yet to be") def test_builtin_generics(self): class E(list[T]): pass class F(list[int]): pass @@ -8848,7 +8809,6 @@ def test_fwdref_value_is_cached(self): self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str) self.assertIs(evaluate_forward_ref(fr), str) - @skipUnless(TYPING_3_9_0, "Needs PEP 585 support") def test_fwdref_with_owner(self): self.assertEqual( evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections), @@ -8894,16 +8854,14 @@ class Y(Generic[Tx]): with self.subTest("nested string of TypeVar"): evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y}) self.assertEqual(get_origin(evaluated_ref2), Y) - if not TYPING_3_9_0: - self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8") self.assertEqual(get_args(evaluated_ref2), (Y[Tx],)) with self.subTest("nested string of TypeAliasType and alias"): # NOTE: Using Y here works for 3.10 evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str}) self.assertEqual(get_origin(evaluated_ref3), Y) - if sys.version_info[:2] in ((3,8), (3, 10)): - self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 3.10") + if sys.version_info[:2] == (3, 10): + self.skipTest("Nested string 'StrAlias' is not resolved in 3.10") self.assertEqual(get_args(evaluated_ref3), (Z[str],)) def test_invalid_special_forms(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index fa89c83e..f8b2f76e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -166,12 +166,9 @@ def _should_collect_from_parameters(t): return isinstance( t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType) ) -elif sys.version_info >= (3, 9): - def _should_collect_from_parameters(t): - return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) else: def _should_collect_from_parameters(t): - return isinstance(t, typing._GenericAlias) and not t._special + return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) NoReturn = typing.NoReturn @@ -434,28 +431,14 @@ def clear_overloads(): 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): + class _SpecialGenericAlias(typing._SpecialGenericAlias, _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) + 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: @@ -585,7 +568,7 @@ class _ProtocolMeta(type(typing.Protocol)): # but is necessary for several reasons... # # NOTE: DO NOT call super() in any methods in this class - # That would call the methods on typing._ProtocolMeta on Python 3.8-3.11 + # That would call the methods on typing._ProtocolMeta on Python <=3.11 # and those are slow def __new__(mcls, name, bases, namespace, **kwargs): if name == "Protocol" and len(bases) < 2: @@ -786,7 +769,7 @@ def close(self): ... runtime = runtime_checkable -# Our version of runtime-checkable protocols is faster on Python 3.8-3.11 +# Our version of runtime-checkable protocols is faster on Python <=3.11 if sys.version_info >= (3, 12): SupportsInt = typing.SupportsInt SupportsFloat = typing.SupportsFloat @@ -864,17 +847,9 @@ def __round__(self, ndigits: int = 0) -> T_co: def _ensure_subclassable(mro_entries): - def inner(func): - if sys.implementation.name == "pypy" and sys.version_info < (3, 9): - cls_dict = { - "__call__": staticmethod(func), - "__mro_entries__": staticmethod(mro_entries) - } - t = type(func.__name__, (), cls_dict) - return functools.update_wrapper(t(), func) - else: - func.__mro_entries__ = mro_entries - return func + def inner(obj): + obj.__mro_entries__ = mro_entries + return obj return inner @@ -940,8 +915,6 @@ def __reduce__(self): _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" # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 # The standard library TypedDict below Python 3.11 does not store runtime @@ -1209,10 +1182,7 @@ class Point2D(TypedDict): td.__orig_bases__ = (TypedDict,) return td - if hasattr(typing, "_TypedDictMeta"): - _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) - else: - _TYPEDDICT_TYPES = (_TypedDictMeta,) + _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) def is_typeddict(tp): """Check if an annotation is a TypedDict class @@ -1225,9 +1195,6 @@ class Film(TypedDict): is_typeddict(Film) # => True is_typeddict(Union[list, str]) # => False """ - # On 3.8, this would otherwise return True - if hasattr(typing, "TypedDict") and tp is typing.TypedDict: - return False return isinstance(tp, _TYPEDDICT_TYPES) @@ -1257,7 +1224,7 @@ def greet(name: str) -> None: # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" - if isinstance(t, _AnnotatedAlias): + if isinstance(t, typing._AnnotatedAlias): return _strip_extras(t.__origin__) if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): return _strip_extras(t.__args__[0]) @@ -1311,23 +1278,11 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): - If two dict arguments are passed, they specify globals and locals, respectively. """ - if hasattr(typing, "Annotated"): # 3.9+ - hint = typing.get_type_hints( - obj, globalns=globalns, localns=localns, include_extras=True - ) - else: # 3.8 - hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + hint = typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ) if sys.version_info < (3, 11): _clean_optional(obj, hint, globalns, localns) - if sys.version_info < (3, 9): - # In 3.8 eval_type does not flatten Optional[ForwardRef] correctly - # This will recreate and and cache Unions. - hint = { - k: (t - if get_origin(t) != Union - else Union[t.__args__]) - for k, t in hint.items() - } if include_extras: return hint return {k: _strip_extras(t) for k, t in hint.items()} @@ -1336,8 +1291,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): def _could_be_inserted_optional(t): """detects Union[..., None] pattern""" - # 3.8+ compatible checking before _UnionGenericAlias - if get_origin(t) is not Union: + if not isinstance(t, typing._UnionGenericAlias): return False # Assume if last argument is not None they are user defined if t.__args__[-1] is not _NoneType: @@ -1381,17 +1335,12 @@ def _clean_optional(obj, hints, globalns=None, localns=None): localns = globalns elif localns is None: localns = globalns - if sys.version_info < (3, 9): - original_value = ForwardRef(original_value) - else: - original_value = ForwardRef( - original_value, - is_argument=not isinstance(obj, _types.ModuleType) - ) + + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) original_evaluated = typing._eval_type(original_value, globalns, localns) - if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: - # Union[str, None, "str"] is not reduced to Union[str, None] - original_evaluated = Union[original_evaluated.__args__] # Compare if values differ. Note that even if equal # value might be cached by typing._tp_cache contrary to original_evaluated if original_evaluated != value or ( @@ -1402,130 +1351,13 @@ def _clean_optional(obj, hints, globalns=None, localns=None): ): hints[name] = original_evaluated -# Python 3.9+ has PEP 593 (Annotated) -if hasattr(typing, 'Annotated'): - Annotated = typing.Annotated - # Not exported and not a public API, but needed for get_origin() and get_args() - # to work. - _AnnotatedAlias = typing._AnnotatedAlias -# 3.8 -else: - class _AnnotatedAlias(typing._GenericAlias, _root=True): - """Runtime representation of an annotated type. - - At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' - with extra annotations. The alias behaves like a normal typing alias, - instantiating is the same as instantiating the underlying type, binding - it to types is also the same. - """ - def __init__(self, origin, metadata): - if isinstance(origin, _AnnotatedAlias): - metadata = origin.__metadata__ + metadata - origin = origin.__origin__ - super().__init__(origin, origin) - self.__metadata__ = metadata - - def copy_with(self, params): - assert len(params) == 1 - new_type = params[0] - return _AnnotatedAlias(new_type, self.__metadata__) - - def __repr__(self): - return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, " - f"{', '.join(repr(a) for a in self.__metadata__)}]") - - def __reduce__(self): - return operator.getitem, ( - Annotated, (self.__origin__, *self.__metadata__) - ) - - def __eq__(self, other): - if not isinstance(other, _AnnotatedAlias): - return NotImplemented - if self.__origin__ != other.__origin__: - return False - return self.__metadata__ == other.__metadata__ - - def __hash__(self): - return hash((self.__origin__, self.__metadata__)) - - class Annotated: - """Add context specific metadata to a type. - - Example: Annotated[int, runtime_check.Unsigned] indicates to the - hypothetical runtime_check module that this type is an unsigned int. - Every other consumer of this type can ignore this metadata and treat - this type as int. - - The first argument to Annotated must be a valid type (and will be in - the __origin__ field), the remaining arguments are kept as a tuple in - the __extra__ field. - - Details: - - - It's an error to call `Annotated` with less than two arguments. - - Nested Annotated are flattened:: - - Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] - - - Instantiating an annotated type is equivalent to instantiating the - underlying type:: - - Annotated[C, Ann1](5) == C(5) - - - Annotated can be used as a generic type alias:: - - Optimized = Annotated[T, runtime.Optimize()] - Optimized[int] == Annotated[int, runtime.Optimize()] - - OptimizedList = Annotated[List[T], runtime.Optimize()] - OptimizedList[int] == Annotated[List[int], runtime.Optimize()] - """ - - __slots__ = () - - def __new__(cls, *args, **kwargs): - raise TypeError("Type Annotated cannot be instantiated.") - - @typing._tp_cache - def __class_getitem__(cls, params): - if not isinstance(params, tuple) or len(params) < 2: - raise TypeError("Annotated[...] should be used " - "with at least two arguments (a type and an " - "annotation).") - allowed_special_forms = (ClassVar, Final) - if get_origin(params[0]) in allowed_special_forms: - origin = params[0] - else: - msg = "Annotated[t, ...]: t must be a type." - origin = typing._type_check(params[0], msg) - metadata = tuple(params[1:]) - return _AnnotatedAlias(origin, metadata) - - def __init_subclass__(cls, *args, **kwargs): - raise TypeError( - f"Cannot subclass {cls.__module__}.Annotated" - ) - -# Python 3.8 has get_origin() and get_args() but those implementations aren't -# Annotated-aware, so we can't use those. Python 3.9's versions don't support +# Python 3.9 has get_origin() and get_args() but those implementations don't support # ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do. if sys.version_info[:2] >= (3, 10): get_origin = typing.get_origin get_args = typing.get_args -# 3.8-3.9 +# 3.9 else: - try: - # 3.9+ - from typing import _BaseGenericAlias - except ImportError: - _BaseGenericAlias = typing._GenericAlias - try: - # 3.9+ - from typing import GenericAlias as _typing_GenericAlias - except ImportError: - _typing_GenericAlias = typing._GenericAlias - def get_origin(tp): """Get the unsubscripted version of a type. @@ -1541,9 +1373,9 @@ def get_origin(tp): get_origin(List[Tuple[T, T]][int]) == list get_origin(P.args) is P """ - if isinstance(tp, _AnnotatedAlias): + if isinstance(tp, typing._AnnotatedAlias): return Annotated - if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias, _BaseGenericAlias, + if isinstance(tp, (typing._BaseGenericAlias, _types.GenericAlias, ParamSpecArgs, ParamSpecKwargs)): return tp.__origin__ if tp is typing.Generic: @@ -1561,11 +1393,9 @@ def get_args(tp): get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) get_args(Callable[[], T][int]) == ([], int) """ - if isinstance(tp, _AnnotatedAlias): + if isinstance(tp, typing._AnnotatedAlias): return (tp.__origin__, *tp.__metadata__) - if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)): - if getattr(tp, "_special", False): - return () + if isinstance(tp, (typing._GenericAlias, _types.GenericAlias)): res = tp.__args__ if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: res = (list(res[:-1]), res[-1]) @@ -1577,7 +1407,7 @@ def get_args(tp): if hasattr(typing, 'TypeAlias'): TypeAlias = typing.TypeAlias # 3.9 -elif sys.version_info[:2] >= (3, 9): +else: @_ExtensionsSpecialForm def TypeAlias(self, parameters): """Special marker indicating that an assignment should @@ -1591,21 +1421,6 @@ def TypeAlias(self, parameters): It's invalid when used anywhere except as in the example above. """ raise TypeError(f"{self} is not subscriptable") -# 3.8 -else: - TypeAlias = _ExtensionsSpecialForm( - 'TypeAlias', - doc="""Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example - above.""" - ) def _set_default(type_param, default): @@ -1679,7 +1494,7 @@ def __init_subclass__(cls) -> None: if hasattr(typing, 'ParamSpecArgs'): ParamSpecArgs = typing.ParamSpecArgs ParamSpecKwargs = typing.ParamSpecKwargs -# 3.8-3.9 +# 3.9 else: class _Immutable: """Mixin to indicate that object should not be copied.""" @@ -1790,7 +1605,7 @@ def _paramspec_prepare_subst(alias, args): def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") -# 3.8-3.9 +# 3.9 else: # Inherits from list as a workaround for Callable checks in Python < 3.9.2. @@ -1895,7 +1710,7 @@ def __call__(self, *args, **kwargs): pass -# 3.8-3.9 +# 3.9 if not hasattr(typing, 'Concatenate'): # Inherits from list as a workaround for Callable checks in Python < 3.9.2. @@ -1920,9 +1735,6 @@ class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. __class__ = typing._GenericAlias - # Flag in 3.8. - _special = False - def __init__(self, origin, args): super().__init__(args) self.__origin__ = origin @@ -1946,7 +1758,6 @@ def __parameters__(self): tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) - # 3.8; needed for typing._subst_tvars # 3.9 used by __getitem__ below def copy_with(self, params): if isinstance(params[-1], _ConcatenateGenericAlias): @@ -1974,7 +1785,7 @@ def __getitem__(self, args): prepare = getattr(param, "__typing_prepare_subst__", None) if prepare is not None: args = prepare(self, args) - # 3.8 - 3.9 & typing.ParamSpec + # 3.9 & typing.ParamSpec elif isinstance(param, ParamSpec): i = params.index(param) if ( @@ -1990,7 +1801,7 @@ def __getitem__(self, args): args = (args,) elif ( isinstance(args[i], list) - # 3.8 - 3.9 + # 3.9 # This class inherits from list do not convert and not isinstance(args[i], _ConcatenateGenericAlias) ): @@ -2063,11 +1874,11 @@ def __getitem__(self, args): return value -# 3.8-3.9.2 +# 3.9.2 class _EllipsisDummy: ... -# 3.8-3.10 +# <=3.10 def _create_concatenate_alias(origin, parameters): if parameters[-1] is ... and sys.version_info < (3, 9, 2): # Hack: Arguments must be types, replace it with one. @@ -2091,7 +1902,7 @@ def _create_concatenate_alias(origin, parameters): return concatenate -# 3.8-3.10 +# <=3.10 @typing._tp_cache def _concatenate_getitem(self, parameters): if parameters == (): @@ -2110,8 +1921,8 @@ def _concatenate_getitem(self, parameters): # 3.11+; Concatenate does not accept ellipsis in 3.10 if sys.version_info >= (3, 11): Concatenate = typing.Concatenate -# 3.9-3.10 -elif sys.version_info[:2] >= (3, 9): +# <=3.10 +else: @_ExtensionsSpecialForm def Concatenate(self, parameters): """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a @@ -2125,30 +1936,13 @@ def Concatenate(self, parameters): See PEP 612 for detailed information. """ return _concatenate_getitem(self, parameters) -# 3.8 -else: - class _ConcatenateForm(_ExtensionsSpecialForm, _root=True): - def __getitem__(self, parameters): - return _concatenate_getitem(self, parameters) - - Concatenate = _ConcatenateForm( - 'Concatenate', - doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - - Callable[Concatenate[int, P], int] - See PEP 612 for detailed information. - """) # 3.10+ if hasattr(typing, 'TypeGuard'): TypeGuard = typing.TypeGuard # 3.9 -elif sys.version_info[:2] >= (3, 9): +else: @_ExtensionsSpecialForm def TypeGuard(self, parameters): """Special typing form used to annotate the return type of a user-defined @@ -2195,64 +1989,13 @@ def is_str(val: Union[str, float]): """ item = typing._type_check(parameters, f'{self} accepts only a single type.') return typing._GenericAlias(self, (item,)) -# 3.8 -else: - class _TypeGuardForm(_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,)) - TypeGuard = _TypeGuardForm( - 'TypeGuard', - doc="""Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` 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 ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` 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 type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """) # 3.13+ if hasattr(typing, 'TypeIs'): TypeIs = typing.TypeIs -# 3.9 -elif sys.version_info[:2] >= (3, 9): +# <=3.12 +else: @_ExtensionsSpecialForm def TypeIs(self, parameters): """Special typing form used to annotate the return type of a user-defined @@ -2293,58 +2036,13 @@ def f(val: Union[int, Awaitable[int]]) -> int: """ 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 ``TypeIs`` 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). - """) # 3.14+? if hasattr(typing, 'TypeForm'): TypeForm = typing.TypeForm -# 3.9 -elif sys.version_info[:2] >= (3, 9): +# <=3.13 +else: class _TypeFormForm(_ExtensionsSpecialForm, _root=True): # TypeForm(X) is equivalent to X but indicates to the type checker # that the object is a TypeForm. @@ -2372,36 +2070,6 @@ def cast[T](typ: TypeForm[T], value: Any) -> T: ... """ item = typing._type_check(parameters, f'{self} accepts only a single type.') return typing._GenericAlias(self, (item,)) -# 3.8 -else: - class _TypeFormForm(_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,)) - - def __call__(self, obj, /): - return obj - - TypeForm = _TypeFormForm( - 'TypeForm', - doc="""A special form representing the value that results from the evaluation - of a type expression. This value encodes the information supplied in the - type expression, and it represents the type described by that type expression. - - When used in a type expression, TypeForm describes a set of type form objects. - It accepts a single type argument, which must be a valid type expression. - ``TypeForm[T]`` describes the set of all type form objects that represent - the type T or types that are assignable to T. - - Usage: - - def cast[T](typ: TypeForm[T], value: Any) -> T: ... - - reveal_type(cast(int, "x")) # int - - See PEP 747 for more information. - """) # Vendored from cpython typing._SpecialFrom @@ -2525,7 +2193,7 @@ def int_or_str(arg: int | str) -> None: if hasattr(typing, 'Required'): # 3.11+ Required = typing.Required NotRequired = typing.NotRequired -elif sys.version_info[:2] >= (3, 9): # 3.9-3.10 +else: # <=3.10 @_ExtensionsSpecialForm def Required(self, parameters): """A special typing construct to mark a key of a total=False TypedDict @@ -2563,49 +2231,10 @@ class Movie(TypedDict): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') return typing._GenericAlias(self, (item,)) -else: # 3.8 - class _RequiredForm(_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,)) - - Required = _RequiredForm( - 'Required', - doc="""A special typing construct to mark a key of a total=False TypedDict - as required. For example: - - class Movie(TypedDict, total=False): - title: Required[str] - year: int - - m = Movie( - title='The Matrix', # typechecker error if key is omitted - year=1999, - ) - - There is no runtime checking that a required key is actually provided - when instantiating a related TypedDict. - """) - NotRequired = _RequiredForm( - 'NotRequired', - doc="""A special typing construct to mark a key of a TypedDict as - potentially missing. For example: - - class Movie(TypedDict): - title: str - year: NotRequired[int] - - m = Movie( - title='The Matrix', # typechecker error if key is omitted - year=1999, - ) - """) - if hasattr(typing, 'ReadOnly'): ReadOnly = typing.ReadOnly -elif sys.version_info[:2] >= (3, 9): # 3.9-3.12 +else: # <=3.12 @_ExtensionsSpecialForm def ReadOnly(self, parameters): """A special typing construct to mark an item of a TypedDict as read-only. @@ -2625,30 +2254,6 @@ def mutate_movie(m: Movie) -> None: 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. @@ -2698,7 +2303,7 @@ def foo(**kwargs: Unpack[Movie]): ... def _is_unpack(obj): return get_origin(obj) is Unpack -elif sys.version_info[:2] >= (3, 9): # 3.9+ +else: # <=3.11 class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): def __init__(self, getitem): super().__init__(getitem) @@ -2739,43 +2344,6 @@ def Unpack(self, parameters): def _is_unpack(obj): return isinstance(obj, _UnpackAlias) -else: # 3.8 - 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): - if arg.__origin__ is not tuple: - raise TypeError("Unpack[...] must be used with a tuple type") - return arg.__args__ - return None - - @property - def __typing_is_unpacked_typevartuple__(self): - assert self.__origin__ is Unpack - assert len(self.__args__) == 1 - return isinstance(self.__args__[0], TypeVarTuple) - - def __getitem__(self, args): - if self.__typing_is_unpacked_typevartuple__: - return args - return super().__getitem__(args) - - class _UnpackForm(_ExtensionsSpecialForm, _root=True): - def __getitem__(self, parameters): - item = typing._type_check(parameters, - f'{self._name} accepts only a single type.') - return _UnpackAlias(self, (item,)) - - Unpack = _UnpackForm('Unpack', doc=_UNPACK_DOC) - - def _is_unpack(obj): - return isinstance(obj, _UnpackAlias) - def _unpack_args(*args): newargs = [] @@ -3545,10 +3113,6 @@ def _make_nmtuple(name, types, module, defaults=()): nm_tpl = collections.namedtuple(name, fields, defaults=defaults, module=module) nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations - # The `_field_types` attribute was removed in 3.9; - # in earlier versions, it is the same as the `__annotations__` attribute - if sys.version_info < (3, 9): - nm_tpl._field_types = annotations return nm_tpl _prohibited_namedtuple_fields = typing._prohibited @@ -3826,10 +3390,10 @@ def __ror__(self, other): if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType -# 3.8-3.13 +# <=3.13 else: if sys.version_info >= (3, 12): - # 3.12-3.14 + # 3.12-3.13 def _is_unionable(obj): """Corresponds to is_unionable() in unionobject.c in CPython.""" return obj is None or isinstance(obj, ( @@ -3840,7 +3404,7 @@ def _is_unionable(obj): TypeAliasType, )) else: - # 3.8-3.11 + # <=3.11 def _is_unionable(obj): """Corresponds to is_unionable() in unionobject.c in CPython.""" return obj is None or isinstance(obj, ( @@ -3875,11 +3439,6 @@ def __getattr__(self, attr): return object.__getattr__(self, attr) return getattr(self.__origin__, attr) - if sys.version_info < (3, 9): - def __getitem__(self, item): - result = super().__getitem__(item) - result.__class__ = type(self) - return result class TypeAliasType: """Create named, parameterized type aliases. @@ -3922,7 +3481,7 @@ def __init__(self, name: str, value, *, type_params=()): for type_param in type_params: if ( not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) - # 3.8-3.11 + # <=3.11 # Unpack Backport passes isinstance(type_param, TypeVar) or _is_unpack(type_param) ): @@ -4510,12 +4069,6 @@ def evaluate_forward_ref( for tvar in type_params: if tvar.__name__ not in locals: # lets not overwrite something present locals[tvar.__name__] = tvar - if sys.version_info < (3, 9): - return typing._eval_type( - type_, - globals, - locals, - ) if sys.version_info < (3, 12, 5): return typing._eval_type( type_, @@ -4547,6 +4100,7 @@ def evaluate_forward_ref( # so that we get a CI error if one of these is deleted from typing.py # in a future version of Python AbstractSet = typing.AbstractSet +Annotated = typing.Annotated AnyStr = typing.AnyStr BinaryIO = typing.BinaryIO Callable = typing.Callable From 7ab72d7a9dfd7f0f247672ab561d09e262d99aa0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 17 Apr 2025 09:30:04 -0700 Subject: [PATCH 70/81] Add back _AnnotatedAlias (#587) Fixes #586 --- src/test_typing_extensions.py | 5 +++++ src/typing_extensions.py | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a6948951..095505aa 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5251,6 +5251,11 @@ def test_nested_annotated_with_unhashable_metadata(self): self.assertEqual(X.__origin__, List[Annotated[str, {"unhashable_metadata"}]]) self.assertEqual(X.__metadata__, ("metadata",)) + def test_compatibility(self): + # Test that the _AnnotatedAlias compatibility alias works + self.assertTrue(hasattr(typing_extensions, "_AnnotatedAlias")) + self.assertIs(typing_extensions._AnnotatedAlias, typing._AnnotatedAlias) + class GetTypeHintsTests(BaseTestCase): def test_get_type_hints(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f8b2f76e..1c968f72 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4095,7 +4095,7 @@ def evaluate_forward_ref( ) -# Aliases for items that have always been in typing. +# Aliases for items that are in typing in all supported versions. # 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 # in a future version of Python @@ -4136,3 +4136,6 @@ def evaluate_forward_ref( cast = typing.cast no_type_check = typing.no_type_check no_type_check_decorator = typing.no_type_check_decorator +# This is private, but it was defined by typing_extensions for a long time +# and some users rely on it. +_AnnotatedAlias = typing._AnnotatedAlias From 28f08acd0c44a8d533c6d5cebc59cfc82ad18047 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:53:45 +0200 Subject: [PATCH 71/81] Implement support for PEP 764 (inline typed dictionaries) (#580) --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 57 ++++++++++++ src/typing_extensions.py | 157 +++++++++++++++++++++------------- 3 files changed, 157 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 560971ad..2ea7c833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased - Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). +- Add support for inline typed dictionaries ([PEP 764](https://peps.python.org/pep-0764/)). + Patch by [Victorien Plot](https://github.com/Viicos). # Release 4.13.2 (April 10, 2025) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 095505aa..a542aa75 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5066,6 +5066,63 @@ def test_cannot_combine_closed_and_extra_items(self): class TD(TypedDict, closed=True, extra_items=range): x: str + def test_typed_dict_signature(self): + self.assertListEqual( + list(inspect.signature(TypedDict).parameters), + ['typename', 'fields', 'total', 'closed', 'extra_items', 'kwargs'] + ) + + def test_inline_too_many_arguments(self): + with self.assertRaises(TypeError): + TypedDict[{"a": int}, "extra"] + + def test_inline_not_a_dict(self): + with self.assertRaises(TypeError): + TypedDict["not_a_dict"] + + # a tuple of elements isn't allowed, even if the first element is a dict: + with self.assertRaises(TypeError): + TypedDict[({"key": int},)] + + def test_inline_empty(self): + TD = TypedDict[{}] + self.assertIs(TD.__total__, True) + self.assertIs(TD.__closed__, True) + self.assertEqual(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__required_keys__, set()) + self.assertEqual(TD.__optional_keys__, set()) + self.assertEqual(TD.__readonly_keys__, set()) + self.assertEqual(TD.__mutable_keys__, set()) + + def test_inline(self): + TD = TypedDict[{ + "a": int, + "b": Required[int], + "c": NotRequired[int], + "d": ReadOnly[int], + }] + self.assertIsSubclass(TD, dict) + self.assertIsSubclass(TD, typing.MutableMapping) + self.assertNotIsSubclass(TD, collections.abc.Sequence) + self.assertTrue(is_typeddict(TD)) + self.assertEqual(TD.__name__, "") + self.assertEqual( + TD.__annotations__, + {"a": int, "b": Required[int], "c": NotRequired[int], "d": ReadOnly[int]}, + ) + self.assertEqual(TD.__module__, __name__) + self.assertEqual(TD.__bases__, (dict,)) + self.assertIs(TD.__total__, True) + self.assertIs(TD.__closed__, True) + self.assertEqual(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__required_keys__, {"a", "b", "d"}) + self.assertEqual(TD.__optional_keys__, {"c"}) + self.assertEqual(TD.__readonly_keys__, {"d"}) + self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"}) + + inst = TD(a=1, b=2, d=3) + self.assertIs(type(inst), dict) + self.assertEqual(inst["a"], 1) class AnnotatedTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1c968f72..b541bac5 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -846,13 +846,6 @@ def __round__(self, ndigits: int = 0) -> T_co: pass -def _ensure_subclassable(mro_entries): - def inner(obj): - obj.__mro_entries__ = mro_entries - return obj - return inner - - _NEEDS_SINGLETONMETA = ( not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") ) @@ -1078,17 +1071,94 @@ def __subclasscheck__(cls, other): _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) - @_ensure_subclassable(lambda bases: (_TypedDict,)) - def TypedDict( + def _create_typeddict( typename, - fields=_marker, + fields, /, *, - total=True, - closed=None, - extra_items=NoExtraItems, - **kwargs + typing_is_inline, + total, + closed, + extra_items, + **kwargs, ): + if fields is _marker or fields is None: + if fields is _marker: + deprecated_thing = ( + "Failing to pass a value for the 'fields' parameter" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + + example = f"`{typename} = TypedDict({typename!r}, {{}})`" + deprecation_msg = ( + f"{deprecated_thing} is deprecated and will be disallowed in " + "Python 3.15. To create a TypedDict class with 0 fields " + "using the functional syntax, pass an empty dictionary, e.g. " + ) + example + "." + warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: + kwargs["closed"] = closed + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems + fields = kwargs + elif kwargs: + 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 " + "understood by third-party type checkers.", + DeprecationWarning, + stacklevel=2, + ) + + ns = {'__annotations__': dict(fields)} + module = _caller(depth=5 if typing_is_inline else 3) + if module is not None: + # Setting correct module is necessary to make typed dict classes + # pickleable. + ns['__module__'] = module + + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) + td.__orig_bases__ = (TypedDict,) + return td + + class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True): + def __call__( + self, + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): + return _create_typeddict( + typename, + fields, + typing_is_inline=False, + total=total, + closed=closed, + extra_items=extra_items, + **kwargs, + ) + + def __mro_entries__(self, bases): + return (_TypedDict,) + + @_TypedDictSpecialForm + def TypedDict(self, args): """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 @@ -1135,52 +1205,20 @@ class Point2D(TypedDict): See PEP 655 for more details on Required and NotRequired. """ - if fields is _marker or fields is None: - if fields is _marker: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - - example = f"`{typename} = TypedDict({typename!r}, {{}})`" - deprecation_msg = ( - f"{deprecated_thing} is deprecated and will be disallowed in " - "Python 3.15. To create a TypedDict class with 0 fields " - "using the functional syntax, pass an empty dictionary, e.g. " - ) + example + "." - warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) - # Support a field called "closed" - if closed is not False and closed is not True and closed is not None: - kwargs["closed"] = closed - closed = None - # Or "extra_items" - if extra_items is not NoExtraItems: - kwargs["extra_items"] = extra_items - extra_items = NoExtraItems - fields = kwargs - elif kwargs: - 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 " - "understood by third-party type checkers.", - DeprecationWarning, - stacklevel=2, + # This runs when creating inline TypedDicts: + if not isinstance(args, dict): + raise TypeError( + "TypedDict[...] should be used with a single dict argument" ) - ns = {'__annotations__': dict(fields)} - module = _caller() - if module is not None: - # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = module - - td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, - extra_items=extra_items) - td.__orig_bases__ = (TypedDict,) - return td + return _create_typeddict( + "", + args, + typing_is_inline=True, + total=True, + closed=True, + extra_items=NoExtraItems, + ) _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) @@ -3194,7 +3232,6 @@ def _namedtuple_mro_entries(bases): assert NamedTuple in bases return (_NamedTuple,) - @_ensure_subclassable(_namedtuple_mro_entries) def NamedTuple(typename, fields=_marker, /, **kwargs): """Typed version of namedtuple. @@ -3260,6 +3297,8 @@ class Employee(NamedTuple): nt.__orig_bases__ = (NamedTuple,) return nt + NamedTuple.__mro_entries__ = _namedtuple_mro_entries + if hasattr(collections.abc, "Buffer"): Buffer = collections.abc.Buffer From f02b99d3be02ef8b308503641d537ff16884b360 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Wed, 30 Apr 2025 15:08:13 +0200 Subject: [PATCH 72/81] Add Reader and Writer protocols (#582) --- CHANGELOG.md | 6 ++++++ doc/conf.py | 4 +++- doc/index.rst | 12 +++++++++++ src/test_typing_extensions.py | 26 ++++++++++++++++++++++++ src/typing_extensions.py | 38 +++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea7c833..8f9523f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Unreleased - Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). + +New features: + - Add support for inline typed dictionaries ([PEP 764](https://peps.python.org/pep-0764/)). Patch by [Victorien Plot](https://github.com/Viicos). +- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by + Sebastian Rittau. # Release 4.13.2 (April 10, 2025) @@ -17,6 +22,7 @@ # Release 4.13.1 (April 3, 2025) Bugfixes: + - Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. Patch by [Daraan](https://github.com/Daraan). - Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10. diff --git a/doc/conf.py b/doc/conf.py index cbb15a70..db9b5185 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,7 +27,9 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -intersphinx_mapping = {'py': ('https://docs.python.org/3', None)} +# This should usually point to /3, unless there is a necessity to link to +# features in future versions of Python. +intersphinx_mapping = {'py': ('https://docs.python.org/3.14', None)} add_module_names = False diff --git a/doc/index.rst b/doc/index.rst index e652c9e4..325182eb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -659,6 +659,18 @@ Protocols .. versionadded:: 4.6.0 +.. class:: Reader + + See :py:class:`io.Reader`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + +.. class:: Writer + + See :py:class:`io.Writer`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + Decorators ~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a542aa75..01e2b270 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4103,6 +4103,32 @@ def foo(self): pass self.assertIsSubclass(Bar, Functor) +class SpecificProtocolTests(BaseTestCase): + def test_reader_runtime_checkable(self): + class MyReader: + def read(self, n: int) -> bytes: + return b"" + + class WrongReader: + def readx(self, n: int) -> bytes: + return b"" + + self.assertIsInstance(MyReader(), typing_extensions.Reader) + self.assertNotIsInstance(WrongReader(), typing_extensions.Reader) + + def test_writer_runtime_checkable(self): + class MyWriter: + def write(self, b: bytes) -> int: + return 0 + + class WrongWriter: + def writex(self, b: bytes) -> int: + return 0 + + self.assertIsInstance(MyWriter(), typing_extensions.Writer) + self.assertNotIsInstance(WrongWriter(), typing_extensions.Writer) + + class Point2DGeneric(Generic[T], TypedDict): a: T b: T diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b541bac5..f2bee507 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -6,6 +6,7 @@ import enum import functools import inspect +import io import keyword import operator import sys @@ -56,6 +57,8 @@ 'SupportsIndex', 'SupportsInt', 'SupportsRound', + 'Reader', + 'Writer', # One-off things. 'Annotated', @@ -846,6 +849,41 @@ def __round__(self, ndigits: int = 0) -> T_co: pass +if hasattr(io, "Reader") and hasattr(io, "Writer"): + Reader = io.Reader + Writer = io.Writer +else: + @runtime_checkable + class Reader(Protocol[T_co]): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size: int = ..., /) -> T_co: + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @runtime_checkable + class Writer(Protocol[T_contra]): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data: T_contra, /) -> int: + """Write *data* to the output stream and return the number of items written.""" # noqa: E501 + + _NEEDS_SINGLETONMETA = ( not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") ) From fe121919f9305c0775bcc719dd2c08cbfcb5ff21 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 2 May 2025 19:45:27 -0700 Subject: [PATCH 73/81] Fix test failures on Python 3.14 (#566) --- src/test_typing_extensions.py | 40 ++++++++++++++++++++++++----------- src/typing_extensions.py | 9 ++++++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 01e2b270..92e1e4cd 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -900,10 +900,12 @@ async def coro(self): class DeprecatedCoroTests(BaseTestCase): def test_asyncio_iscoroutinefunction(self): - self.assertFalse(asyncio.coroutines.iscoroutinefunction(func)) - self.assertFalse(asyncio.coroutines.iscoroutinefunction(Cls.func)) - self.assertTrue(asyncio.coroutines.iscoroutinefunction(coro)) - self.assertTrue(asyncio.coroutines.iscoroutinefunction(Cls.coro)) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + self.assertFalse(asyncio.coroutines.iscoroutinefunction(func)) + self.assertFalse(asyncio.coroutines.iscoroutinefunction(Cls.func)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(coro)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(Cls.coro)) @skipUnless(TYPING_3_12_ONLY or TYPING_3_13_0_RC, "inspect.iscoroutinefunction works differently on Python < 3.12") def test_inspect_iscoroutinefunction(self): @@ -7282,7 +7284,7 @@ def test_cannot_instantiate_vars(self): def test_bound_errors(self): with self.assertRaises(TypeError): - TypeVar('X', bound=Union) + TypeVar('X', bound=Optional) with self.assertRaises(TypeError): TypeVar('X', str, float, bound=Employee) with self.assertRaisesRegex(TypeError, @@ -8262,19 +8264,26 @@ def f2(a: "undefined"): # noqa: F821 get_annotations(f2, format=Format.FORWARDREF), {"a": "undefined"}, ) - self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"}) + # Test that the raw int also works + self.assertEqual( + get_annotations(f2, format=Format.FORWARDREF.value), + {"a": "undefined"}, + ) self.assertEqual( get_annotations(f1, format=Format.STRING), {"a": "int"}, ) - self.assertEqual(get_annotations(f1, format=3), {"a": "int"}) + self.assertEqual( + get_annotations(f1, format=Format.STRING.value), + {"a": "int"}, + ) with self.assertRaises(ValueError): get_annotations(f1, format=0) with self.assertRaises(ValueError): - get_annotations(f1, format=4) + get_annotations(f1, format=42) def test_custom_object_with_annotations(self): class C: @@ -8313,10 +8322,17 @@ def foo(a: int, b: str): foo.__annotations__ = {"a": "foo", "b": "str"} for format in Format: with self.subTest(format=format): - self.assertEqual( - get_annotations(foo, format=format), - {"a": "foo", "b": "str"}, - ) + if format is Format.VALUE_WITH_FAKE_GLOBALS: + with self.assertRaisesRegex( + ValueError, + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ): + get_annotations(foo, format=format) + else: + self.assertEqual( + get_annotations(foo, format=format), + {"a": "foo", "b": "str"}, + ) self.assertEqual( get_annotations(foo, eval_str=True, locals=locals()), diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f2bee507..04fa2cb8 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3789,8 +3789,9 @@ def __eq__(self, other: object) -> bool: class Format(enum.IntEnum): VALUE = 1 - FORWARDREF = 2 - STRING = 3 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 if _PEP_649_OR_749_IMPLEMENTED: @@ -3834,6 +3835,10 @@ def get_annotations(obj, *, globals=None, locals=None, eval_str=False, """ format = Format(format) + if format is Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError( + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ) if eval_str and format is not Format.VALUE: raise ValueError("eval_str=True is only supported with format=Format.VALUE") From 21be122b9e1bc60a860066f3f50913a0e3d690b7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 4 May 2025 14:27:21 -0700 Subject: [PATCH 74/81] pyanalyze -> pycroscope (#590) --- .github/workflows/third_party.yml | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index b477b930..8bf6acca 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -108,8 +108,8 @@ jobs: cd typing_inspect pytest - pyanalyze: - name: pyanalyze tests + pycroscope: + name: pycroscope tests needs: skip-schedule-on-fork strategy: fail-fast: false @@ -125,26 +125,25 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Check out pyanalyze - run: git clone --depth=1 https://github.com/quora/pyanalyze.git || git clone --depth=1 https://github.com/quora/pyanalyze.git + - name: Check out pycroscope + run: git clone --depth=1 https://github.com/JelleZijlstra/pycroscope.git || git clone --depth=1 https://github.com/JelleZijlstra/pycroscope.git - name: Checkout typing_extensions uses: actions/checkout@v4 with: path: typing-extensions-latest - - name: Install pyanalyze test requirements + - name: Install pycroscope test requirements run: | set -x - cd pyanalyze - uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + cd pycroscope + uv pip install --system 'pycroscope[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies run: uv pip freeze - # TODO: re-enable - # - name: Run pyanalyze tests - # run: | - # cd pyanalyze - # pytest pyanalyze/ + - name: Run pycroscope tests + run: | + cd pycroscope + pytest pycroscope/ typeguard: name: typeguard tests @@ -377,7 +376,7 @@ jobs: needs: - pydantic - typing_inspect - - pyanalyze + - pycroscope - typeguard - typed-argument-parser - mypy @@ -392,7 +391,7 @@ jobs: && ( needs.pydantic.result == 'failure' || needs.typing_inspect.result == 'failure' - || needs.pyanalyze.result == 'failure' + || needs.pycroscope.result == 'failure' || needs.typeguard.result == 'failure' || needs.typed-argument-parser.result == 'failure' || needs.mypy.result == 'failure' From d44e9cf73eb4d917b9114d9a23ecc73b03ce6e5f Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Mon, 5 May 2025 16:32:32 +0200 Subject: [PATCH 75/81] Test Python 3.14 in CI (#565) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f9d0650..451fc313 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,7 @@ jobs: - "3.12.0" - "3.13" - "3.13.0" + - "3.14-dev" - "pypy3.9" - "pypy3.10" @@ -69,6 +70,7 @@ jobs: cd src python --version # just to make sure we're running the right one python -m unittest test_typing_extensions.py + continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }} - name: Test CPython typing test suite # Test suite fails on PyPy even without typing_extensions @@ -78,6 +80,7 @@ jobs: # 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 + continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }} linting: name: Lint From 11cc786b464985d5efbd5fb5bc4ba9b1eb518988 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 5 May 2025 09:50:14 -0700 Subject: [PATCH 76/81] Fix tests on Python 3.14 (#592) --- .github/workflows/ci.yml | 2 - CHANGELOG.md | 1 + src/test_typing_extensions.py | 117 ++++++++++++++++++++++++++++++---- src/typing_extensions.py | 67 +++++++++++++++---- 4 files changed, 162 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 451fc313..3df842da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,6 @@ jobs: cd src python --version # just to make sure we're running the right one python -m unittest test_typing_extensions.py - continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }} - name: Test CPython typing test suite # Test suite fails on PyPy even without typing_extensions @@ -80,7 +79,6 @@ jobs: # 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 - continue-on-error: ${{ endsWith(matrix.python-version, '-dev') }} linting: name: Lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9523f1..5ba8e152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ New features: Patch by [Victorien Plot](https://github.com/Viicos). - Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by Sebastian Rittau. +- Fix tests for Python 3.14. Patch by Jelle Zijlstra. # Release 4.13.2 (April 10, 2025) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 92e1e4cd..dc882f9f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -439,6 +439,48 @@ def assertNotIsSubclass(self, cls, class_or_tuple, msg=None): raise self.failureException(message) +class EqualToForwardRef: + """Helper to ease use of annotationlib.ForwardRef in tests. + + This checks only attributes that can be set using the constructor. + + """ + + def __init__( + self, + arg, + *, + module=None, + owner=None, + is_class=False, + ): + self.__forward_arg__ = arg + self.__forward_is_class__ = is_class + self.__forward_module__ = module + self.__owner__ = owner + + def __eq__(self, other): + if not isinstance(other, (EqualToForwardRef, typing.ForwardRef)): + return NotImplemented + if sys.version_info >= (3, 14) and self.__owner__ != other.__owner__: + return False + return ( + self.__forward_arg__ == other.__forward_arg__ + and self.__forward_module__ == other.__forward_module__ + and self.__forward_is_class__ == other.__forward_is_class__ + ) + + def __repr__(self): + extra = [] + if self.__forward_module__ is not None: + extra.append(f", module={self.__forward_module__!r}") + if self.__forward_is_class__: + extra.append(", is_class=True") + if sys.version_info >= (3, 14) and self.__owner__ is not None: + extra.append(f", owner={self.__owner__!r}") + return f"EqualToForwardRef({self.__forward_arg__!r}{''.join(extra)})" + + class Employee: pass @@ -5152,6 +5194,64 @@ def test_inline(self): self.assertIs(type(inst), dict) self.assertEqual(inst["a"], 1) + def test_annotations(self): + # _type_check is applied + with self.assertRaisesRegex(TypeError, "Plain typing.Optional is not valid as type argument"): + class X(TypedDict): + a: Optional + + # _type_convert is applied + class Y(TypedDict): + a: None + b: "int" + if sys.version_info >= (3, 14): + import annotationlib + + fwdref = EqualToForwardRef('int', module=__name__) + self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref}) + self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) + else: + self.assertEqual(Y.__annotations__, {'a': type(None), 'b': typing.ForwardRef('int', module=__name__)}) + + @skipUnless(TYPING_3_14_0, "Only supported on 3.14") + def test_delayed_type_check(self): + # _type_check is also applied later + class Z(TypedDict): + a: undefined # noqa: F821 + + with self.assertRaises(NameError): + Z.__annotations__ + + undefined = Final + with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): + Z.__annotations__ + + undefined = None # noqa: F841 + self.assertEqual(Z.__annotations__, {'a': type(None)}) + + @skipUnless(TYPING_3_14_0, "Only supported on 3.14") + def test_deferred_evaluation(self): + class A(TypedDict): + x: NotRequired[undefined] # noqa: F821 + y: ReadOnly[undefined] # noqa: F821 + z: Required[undefined] # noqa: F821 + + self.assertEqual(A.__required_keys__, frozenset({'y', 'z'})) + self.assertEqual(A.__optional_keys__, frozenset({'x'})) + self.assertEqual(A.__readonly_keys__, frozenset({'y'})) + self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'})) + + with self.assertRaises(NameError): + A.__annotations__ + + import annotationlib + self.assertEqual( + A.__annotate__(annotationlib.Format.STRING), + {'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]', + 'z': 'Required[undefined]'}, + ) + + class AnnotatedTests(BaseTestCase): def test_repr(self): @@ -5963,7 +6063,7 @@ def test_substitution(self): U2 = Unpack[Ts] self.assertEqual(C2[U1], (str, int, str)) self.assertEqual(C2[U2], (str, Unpack[Ts])) - self.assertEqual(C2["U2"], (str, typing.ForwardRef("U2"))) + self.assertEqual(C2["U2"], (str, EqualToForwardRef("U2"))) if (3, 12, 0) <= sys.version_info < (3, 12, 4): with self.assertRaises(AssertionError): @@ -7250,8 +7350,8 @@ def test_or(self): 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)) + self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x"))) + self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X)) def test_union_constrained(self): A = TypeVar('A', str, bytes) @@ -8819,7 +8919,7 @@ class X: type_params=None, format=Format.FORWARDREF, ) - self.assertEqual(evaluated_ref, typing.ForwardRef("doesnotexist2")) + self.assertEqual(evaluated_ref, EqualToForwardRef("doesnotexist2")) def test_evaluate_with_type_params(self): # Use a T name that is not in globals @@ -8906,13 +9006,6 @@ def test_fwdref_with_globals(self): obj = object() self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj) - def test_fwdref_value_is_cached(self): - fr = typing.ForwardRef("hello") - with self.assertRaises(NameError): - evaluate_forward_ref(fr) - self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str) - self.assertIs(evaluate_forward_ref(fr), str) - def test_fwdref_with_owner(self): self.assertEqual( evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections), @@ -8956,7 +9049,7 @@ class Y(Generic[Tx]): self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],)) with self.subTest("nested string of TypeVar"): - evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y}) + evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y, "Tx": Tx}) self.assertEqual(get_origin(evaluated_ref2), Y) self.assertEqual(get_args(evaluated_ref2), (Y[Tx],)) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 04fa2cb8..269ca650 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -14,6 +14,9 @@ import typing import warnings +if sys.version_info >= (3, 14): + import annotationlib + __all__ = [ # Super-special typing primitives. 'Any', @@ -1018,21 +1021,31 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, tp_dict.__orig_bases__ = bases annotations = {} + own_annotate = None 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) + elif sys.version_info >= (3, 14): + if hasattr(annotationlib, "get_annotate_from_class_namespace"): + own_annotate = annotationlib.get_annotate_from_class_namespace(ns) + else: + # 3.14.0a7 and earlier + own_annotate = ns.get("__annotate__") + if own_annotate is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, Format.FORWARDREF, owner=tp_dict + ) + else: + own_annotations = {} else: own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" if _TAKES_MODULE: - own_annotations = { + own_checked_annotations = { n: typing._type_check(tp, msg, module=tp_dict.__module__) for n, tp in own_annotations.items() } else: - own_annotations = { + own_checked_annotations = { n: typing._type_check(tp, msg) for n, tp in own_annotations.items() } @@ -1045,7 +1058,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, for base in bases: base_dict = base.__dict__ - annotations.update(base_dict.get('__annotations__', {})) + if sys.version_info <= (3, 14): + 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__', ())) @@ -1055,8 +1069,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, # is retained for backwards compatibility, but only for Python # 3.13 and lower. if (closed and sys.version_info < (3, 14) - and "__extra_items__" in own_annotations): - annotation_type = own_annotations.pop("__extra_items__") + and "__extra_items__" in own_checked_annotations): + annotation_type = own_checked_annotations.pop("__extra_items__") qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: raise TypeError( @@ -1070,8 +1084,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, ) extra_items_type = annotation_type - annotations.update(own_annotations) - for annotation_key, annotation_type in own_annotations.items(): + annotations.update(own_checked_annotations) + for annotation_key, annotation_type in own_checked_annotations.items(): qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -1089,7 +1103,38 @@ def __new__(cls, name, bases, ns, *, total=True, closed=None, mutable_keys.add(annotation_key) readonly_keys.discard(annotation_key) - tp_dict.__annotations__ = annotations + if sys.version_info >= (3, 14): + def __annotate__(format): + annos = {} + for base in bases: + if base is Generic: + continue + base_annotate = base.__annotate__ + if base_annotate is None: + continue + base_annos = annotationlib.call_annotate_function( + base.__annotate__, format, owner=base) + annos.update(base_annos) + if own_annotate is not None: + own = annotationlib.call_annotate_function( + own_annotate, format, owner=tp_dict) + if format != Format.STRING: + own = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own.items() + } + elif format == Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (Format.FORWARDREF, Format.VALUE): + own = own_checked_annotations + else: + raise NotImplementedError(format) + annos.update(own) + return annos + + tp_dict.__annotate__ = __annotate__ + else: + 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) From 25235237a5b02fb687213813334bc9e2abd35f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janek=20Nouvertn=C3=A9?= Date: Mon, 5 May 2025 18:50:25 +0200 Subject: [PATCH 77/81] Enable Python 3.13 in cattrs 3rd party tests (#577) --- .github/workflows/third_party.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 8bf6acca..ce2337da 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -270,8 +270,7 @@ jobs: strategy: fail-fast: false matrix: - # skip 3.13 because msgspec doesn't support 3.13 yet - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -292,6 +291,8 @@ jobs: cd cattrs pdm remove typing-extensions pdm add --dev ../typing-extensions-latest + pdm update --group=docs pendulum # pinned version in lockfile is incompatible with py313 as of 2025/05/05 + pdm sync --clean - name: Install cattrs test dependencies run: cd cattrs; pdm install --dev -G :all - name: List all installed dependencies From d90a2f402e52b0e2e195c1c075aeb5a0bb8943b4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 10 May 2025 07:33:04 -0700 Subject: [PATCH 78/81] Become robust to things being removed from typing (#595) --- CHANGELOG.md | 2 + pyproject.toml | 3 ++ src/test_typing_extensions.py | 9 ++++ src/typing_extensions.py | 90 +++++++++++++++++++---------------- 4 files changed, 62 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba8e152..cfb45718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased - Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). +- Do not attempt to re-export names that have been removed from `typing`, + anticipating the removal of `typing.no_type_check_decorator` in Python 3.15. New features: diff --git a/pyproject.toml b/pyproject.toml index 48e2f914..1140ef78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,9 @@ ignore = [ "RUF012", "RUF022", "RUF023", + # Ruff doesn't understand the globals() assignment; we test __all__ + # directly in test_all_names_in___all__. + "F822", ] [tool.ruff.lint.per-file-ignores] diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index dc882f9f..333b4867 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -6827,6 +6827,15 @@ def test_typing_extensions_defers_when_possible(self): getattr(typing_extensions, item), getattr(typing, item)) + def test_alias_names_still_exist(self): + for name in typing_extensions._typing_names: + # If this fails, change _typing_names to conditionally add the name + # depending on the Python version. + self.assertTrue( + hasattr(typing_extensions, name), + f"{name} no longer exists in typing", + ) + def test_typing_extensions_compiles_with_opt(self): file_path = typing_extensions.__file__ try: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 269ca650..cf0427f3 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -4223,46 +4223,52 @@ def evaluate_forward_ref( # Aliases for items that are in typing in all supported versions. -# 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 -# in a future version of Python -AbstractSet = typing.AbstractSet -Annotated = typing.Annotated -AnyStr = typing.AnyStr -BinaryIO = typing.BinaryIO -Callable = typing.Callable -Collection = typing.Collection -Container = typing.Container -Dict = typing.Dict -ForwardRef = typing.ForwardRef -FrozenSet = typing.FrozenSet +# We use hasattr() checks so this library will continue to import on +# future versions of Python that may remove these names. +_typing_names = [ + "AbstractSet", + "AnyStr", + "BinaryIO", + "Callable", + "Collection", + "Container", + "Dict", + "FrozenSet", + "Hashable", + "IO", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "List", + "Mapping", + "MappingView", + "Match", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Optional", + "Pattern", + "Reversible", + "Sequence", + "Set", + "Sized", + "TextIO", + "Tuple", + "Union", + "ValuesView", + "cast", + "no_type_check", + "no_type_check_decorator", + # This is private, but it was defined by typing_extensions for a long time + # and some users rely on it. + "_AnnotatedAlias", +] +globals().update( + {name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)} +) +# These are defined unconditionally because they are used in +# typing-extensions itself. Generic = typing.Generic -Hashable = typing.Hashable -IO = typing.IO -ItemsView = typing.ItemsView -Iterable = typing.Iterable -Iterator = typing.Iterator -KeysView = typing.KeysView -List = typing.List -Mapping = typing.Mapping -MappingView = typing.MappingView -Match = typing.Match -MutableMapping = typing.MutableMapping -MutableSequence = typing.MutableSequence -MutableSet = typing.MutableSet -Optional = typing.Optional -Pattern = typing.Pattern -Reversible = typing.Reversible -Sequence = typing.Sequence -Set = typing.Set -Sized = typing.Sized -TextIO = typing.TextIO -Tuple = typing.Tuple -Union = typing.Union -ValuesView = typing.ValuesView -cast = typing.cast -no_type_check = typing.no_type_check -no_type_check_decorator = typing.no_type_check_decorator -# This is private, but it was defined by typing_extensions for a long time -# and some users rely on it. -_AnnotatedAlias = typing._AnnotatedAlias +ForwardRef = typing.ForwardRef +Annotated = typing.Annotated From f74a56a725e8d60727fccbeebe0dd71037bdf4bb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 10 May 2025 12:23:12 -0700 Subject: [PATCH 79/81] Update PEP 649/749 implementation (#596) --- CHANGELOG.md | 5 ++++- doc/index.rst | 18 ++++++++++++++---- src/test_typing_extensions.py | 5 ++--- src/typing_extensions.py | 30 +++++++++--------------------- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfb45718..ba1a6d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ - Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). - Do not attempt to re-export names that have been removed from `typing`, anticipating the removal of `typing.no_type_check_decorator` in Python 3.15. + Patch by Jelle Zijlstra. +- Update `typing_extensions.Format` and `typing_extensions.evaluate_forward_ref` to align + with changes in Python 3.14. Patch by Jelle Zijlstra. +- Fix tests for Python 3.14. Patch by Jelle Zijlstra. New features: @@ -10,7 +14,6 @@ New features: Patch by [Victorien Plot](https://github.com/Viicos). - Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by Sebastian Rittau. -- Fix tests for Python 3.14. Patch by Jelle Zijlstra. # Release 4.13.2 (April 10, 2025) diff --git a/doc/index.rst b/doc/index.rst index 325182eb..68402faf 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -769,7 +769,7 @@ Functions .. versionadded:: 4.2.0 -.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE) +.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=None) Evaluate an :py:class:`typing.ForwardRef` as a :py:term:`type hint`. @@ -796,7 +796,7 @@ Functions This parameter must be provided (though it may be an empty tuple) if *owner* is not given and the forward reference does not already have an owner set. *format* specifies the format of the annotation and is a member of - the :class:`Format` enum. + the :class:`Format` enum, defaulting to :attr:`Format.VALUE`. .. versionadded:: 4.13.0 @@ -952,9 +952,19 @@ Enums for the annotations. This format is identical to the return value for the function under earlier versions of Python. + .. attribute:: VALUE_WITH_FAKE_GLOBALS + + Equal to 2. Special value used to signal that an annotate function is being + evaluated in a special environment with fake globals. When passed this + value, annotate functions should either return the same value as for + the :attr:`Format.VALUE` format, or raise :exc:`NotImplementedError` + to signal that they do not support execution in this environment. + This format is only used internally and should not be passed to + the functions in this module. + .. attribute:: FORWARDREF - Equal to 2. When :pep:`649` is implemented, this format will attempt to return the + Equal to 3. When :pep:`649` is implemented, this format will attempt to return the conventional Python values for the annotations. However, if it encounters an undefined name, it dynamically creates a proxy object (a ForwardRef) that substitutes for that value in the expression. @@ -964,7 +974,7 @@ Enums .. attribute:: STRING - Equal to 3. When :pep:`649` is implemented, this format will produce an annotation + Equal to 4. When :pep:`649` is implemented, this format will produce an annotation dictionary where the values have been replaced by strings containing an approximation of the original source code for the annotation expressions. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 333b4867..a7953dc5 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -29,7 +29,6 @@ from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated from typing_extensions import ( _FORWARD_REF_HAS_CLASS, - _PEP_649_OR_749_IMPLEMENTED, Annotated, Any, AnyStr, @@ -8533,7 +8532,7 @@ def test_stock_annotations_in_module(self): get_annotations(isa.MyClass, format=Format.STRING), {"a": "int", "b": "str"}, ) - mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" + mycls = "MyClass" if sys.version_info >= (3, 14) else "inspect_stock_annotations.MyClass" self.assertEqual( get_annotations(isa.function, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, @@ -8581,7 +8580,7 @@ def test_stock_annotations_on_wrapper(self): get_annotations(wrapped, format=Format.FORWARDREF), {"a": int, "b": str, "return": isa.MyClass}, ) - mycls = "MyClass" if _PEP_649_OR_749_IMPLEMENTED else "inspect_stock_annotations.MyClass" + mycls = "MyClass" if sys.version_info >= (3, 14) else "inspect_stock_annotations.MyClass" self.assertEqual( get_annotations(wrapped, format=Format.STRING), {"a": "int", "b": "str", "return": mycls}, diff --git a/src/typing_extensions.py b/src/typing_extensions.py index cf0427f3..1ab6220d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -3821,27 +3821,15 @@ def __eq__(self, other: object) -> bool: __all__.append("CapsuleType") -# Using this convoluted approach so that this keeps working -# whether we end up using PEP 649 as written, PEP 749, or -# some other variation: in any case, inspect.get_annotations -# will continue to exist and will gain a `format` parameter. -_PEP_649_OR_749_IMPLEMENTED = ( - hasattr(inspect, 'get_annotations') - and inspect.get_annotations.__kwdefaults__ is not None - and "format" in inspect.get_annotations.__kwdefaults__ -) - - -class Format(enum.IntEnum): - VALUE = 1 - VALUE_WITH_FAKE_GLOBALS = 2 - FORWARDREF = 3 - STRING = 4 - - -if _PEP_649_OR_749_IMPLEMENTED: - get_annotations = inspect.get_annotations +if sys.version_info >= (3,14): + from annotationlib import Format, get_annotations else: + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE): """Compute the annotations dict for an object. @@ -4122,7 +4110,7 @@ def evaluate_forward_ref( globals=None, locals=None, type_params=None, - format=Format.VALUE, + format=None, _recursive_guard=frozenset(), ): """Evaluate a forward reference as a type hint. From 479dae13d084c070301aa91265d1af278b181457 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Tue, 13 May 2025 17:18:45 +0200 Subject: [PATCH 80/81] Add support for sentinels (PEP 661) (#594) Co-authored-by: Jelle Zijlstra --- CHANGELOG.md | 2 ++ doc/index.rst | 28 ++++++++++++++++++++++++ src/test_typing_extensions.py | 40 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba1a6d78..92a19a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ New features: Patch by [Victorien Plot](https://github.com/Viicos). - Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by Sebastian Rittau. +- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)). Patch by + [Victorien Plot](https://github.com/Viicos). # Release 4.13.2 (April 10, 2025) diff --git a/doc/index.rst b/doc/index.rst index 68402faf..21d6fa60 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1027,6 +1027,34 @@ Capsule objects .. versionadded:: 4.12.0 +Sentinel objects +~~~~~~~~~~~~~~~~ + +.. class:: Sentinel(name, repr=None) + + A type used to define sentinel values. The *name* argument should be the + name of the variable to which the return value shall be assigned. + + If *repr* is provided, it will be used for the :meth:`~object.__repr__` + of the sentinel object. If not provided, ``""`` will be used. + + Example:: + + >>> from typing_extensions import Sentinel, assert_type + >>> MISSING = Sentinel('MISSING') + >>> def func(arg: int | MISSING = MISSING) -> None: + ... if arg is MISSING: + ... assert_type(arg, MISSING) + ... else: + ... assert_type(arg, int) + ... + >>> func(MISSING) + + .. versionadded:: 4.14.0 + + See :pep:`661` + + Pure aliases ~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index a7953dc5..c23e94b7 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -65,6 +65,7 @@ ReadOnly, Required, Self, + Sentinel, Set, Tuple, Type, @@ -9096,5 +9097,44 @@ def test_invalid_special_forms(self): self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar) +class TestSentinels(BaseTestCase): + def test_sentinel_no_repr(self): + sentinel_no_repr = Sentinel('sentinel_no_repr') + + self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') + self.assertEqual(repr(sentinel_no_repr), '') + + def test_sentinel_explicit_repr(self): + sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') + + self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') + + @skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9') + def test_sentinel_type_expression_union(self): + sentinel = Sentinel('sentinel') + + def func1(a: int | sentinel = sentinel): pass + def func2(a: sentinel | int = sentinel): pass + + self.assertEqual(func1.__annotations__['a'], Union[int, sentinel]) + self.assertEqual(func2.__annotations__['a'], Union[sentinel, int]) + + def test_sentinel_not_callable(self): + sentinel = Sentinel('sentinel') + with self.assertRaisesRegex( + TypeError, + "'Sentinel' object is not callable" + ): + sentinel() + + def test_sentinel_not_picklable(self): + sentinel = Sentinel('sentinel') + with self.assertRaisesRegex( + TypeError, + "Cannot pickle 'Sentinel' object" + ): + pickle.dumps(sentinel) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1ab6220d..d4e92a4c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -89,6 +89,7 @@ 'overload', 'override', 'Protocol', + 'Sentinel', 'reveal_type', 'runtime', 'runtime_checkable', @@ -4210,6 +4211,42 @@ def evaluate_forward_ref( ) +class Sentinel: + """Create a unique sentinel object. + + *name* should be the name of the variable to which the return value shall be assigned. + + *repr*, if supplied, will be used for the repr of the sentinel object. + If not provided, "" will be used. + """ + + def __init__( + self, + name: str, + repr: typing.Optional[str] = None, + ): + self._name = name + self._repr = repr if repr is not None else f'<{name}>' + + def __repr__(self): + return self._repr + + if sys.version_info < (3, 11): + # The presence of this method convinces typing._type_check + # that Sentinels are types. + def __call__(self, *args, **kwargs): + raise TypeError(f"{type(self).__name__!r} object is not callable") + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __getstate__(self): + raise TypeError(f"Cannot pickle {type(self).__name__!r} object") + + # Aliases for items that are in typing in all supported versions. # We use hasattr() checks so this library will continue to import on # future versions of Python that may remove these names. From 34bfd8423a22797619b14aa622ac0be82f6bf50d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 19 May 2025 20:18:51 -0700 Subject: [PATCH 81/81] third party: fix typeguard (#600) --- .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 ce2337da..a15735b0 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -172,7 +172,7 @@ jobs: run: | set -x cd typeguard - uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system "typeguard @ ." --group test --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies