diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cbeaf5f..3df842da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,23 +38,21 @@ jobs: # For available versions, see: # 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" - "3.12.0" - "3.13" - "3.13.0" - - "pypy3.8" + - "3.14-dev" - "pypy3.9" - "pypy3.10" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -70,6 +68,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 diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index ec2d93f8..ce2337da 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 @@ -271,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: @@ -293,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 @@ -300,6 +300,76 @@ 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 + + + 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 @@ -307,11 +377,12 @@ jobs: needs: - pydantic - typing_inspect - - pyanalyze + - pycroscope - typeguard - typed-argument-parser - mypy - cattrs + - sqlalchemy if: >- ${{ @@ -321,11 +392,12 @@ 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' || needs.cattrs.result == 'failure' + || needs.sqlalchemy.result == 'failure' ) }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f7bcdf..ba1a6d78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# 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. + 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: + +- 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) + +- 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) + +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. + 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/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 bf8b431a..68402faf 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. @@ -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 @@ -656,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 ~~~~~~~~~~ @@ -754,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`. @@ -781,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 @@ -843,6 +858,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 +895,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`. @@ -933,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. @@ -945,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/pyproject.toml b/pyproject.toml index 76648a8b..1140ef78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,10 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.13.0" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.13.2" +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 = [ @@ -95,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 da4e3e44..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, @@ -110,7 +109,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. @@ -440,6 +438,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 @@ -707,6 +747,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: @@ -882,10 +941,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): @@ -1760,8 +1821,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) @@ -1798,20 +1858,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,)) @@ -3789,7 +3847,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)) @@ -4088,6 +4146,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 @@ -4534,7 +4618,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( @@ -4677,11 +4761,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] @@ -5053,6 +5135,121 @@ 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) + + 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): @@ -5144,7 +5341,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 @@ -5238,6 +5435,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): @@ -5456,21 +5658,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') @@ -5630,8 +5831,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") @@ -5761,9 +5960,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__) @@ -5811,7 +6007,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] @@ -5866,7 +6062,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): @@ -6630,6 +6826,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: @@ -6794,7 +6999,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. @@ -6809,19 +7013,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, @@ -6940,21 +7131,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') @@ -7175,8 +7358,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) @@ -7209,18 +7392,15 @@ 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, 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" ) @@ -7401,9 +7581,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)) @@ -7583,7 +7762,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 @@ -7819,6 +7997,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 @@ -8190,19 +8372,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: @@ -8241,10 +8430,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()), @@ -8336,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}, @@ -8384,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}, @@ -8731,7 +8927,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 @@ -8818,14 +9014,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) - - @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), @@ -8869,18 +9057,16 @@ 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) - 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 d2fb245b..1ab6220d 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 @@ -13,6 +14,9 @@ import typing import warnings +if sys.version_info >= (3, 14): + import annotationlib + __all__ = [ # Super-special typing primitives. 'Any', @@ -56,6 +60,8 @@ 'SupportsIndex', 'SupportsInt', 'SupportsRound', + 'Reader', + 'Writer', # One-off things. 'Annotated', @@ -166,12 +172,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 +437,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 +574,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 +775,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 @@ -863,19 +852,39 @@ def __round__(self, ndigits: int = 0) -> T_co: pass -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 - return inner +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 = ( @@ -940,8 +949,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 @@ -1014,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() } @@ -1041,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__', ())) @@ -1051,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( @@ -1066,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: @@ -1085,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) @@ -1105,17 +1154,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 @@ -1162,57 +1288,22 @@ 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, + ) - 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 +1316,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 +1345,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 +1399,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 +1412,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 +1456,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 +1472,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 +1494,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 +1514,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 +1528,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 +1542,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 +1615,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 +1726,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 +1831,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 +1856,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 +1879,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 +1906,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 +1922,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,16 +1995,16 @@ 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. 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) @@ -2091,7 +2023,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 +2042,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 +2057,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 +2110,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 +2157,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 +2191,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 +2314,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 +2352,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 +2375,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 +2424,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 +2465,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 = [] @@ -3123,7 +2812,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 +2893,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__: @@ -3544,10 +3234,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 @@ -3629,7 +3315,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. @@ -3695,6 +3380,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 @@ -3825,16 +3512,29 @@ def __ror__(self, other): if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType -# 3.8-3.13 +# <=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.13 + 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.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, @@ -3861,11 +3561,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. @@ -3908,7 +3603,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) ): @@ -4126,26 +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 - FORWARDREF = 2 - STRING = 3 - - -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. @@ -4184,6 +3868,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") @@ -4371,7 +4059,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, @@ -4418,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. @@ -4492,12 +4184,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_, @@ -4524,43 +4210,53 @@ def evaluate_forward_ref( ) -# 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 -# in a future version of Python -AbstractSet = typing.AbstractSet -AnyStr = typing.AnyStr -BinaryIO = typing.BinaryIO -Callable = typing.Callable -Collection = typing.Collection -Container = typing.Container -Dict = typing.Dict -ForwardRef = typing.ForwardRef -FrozenSet = typing.FrozenSet +# 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. +_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 +ForwardRef = typing.ForwardRef +Annotated = typing.Annotated