diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e221022c..e9d69774 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,6 +51,7 @@ jobs:
- "3.11"
- "3.11.0"
- "3.12"
+ - "3.13"
- "pypy3.8"
- "pypy3.9"
- "pypy3.10"
@@ -58,10 +59,10 @@ jobs:
runs-on: ubuntu-20.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@@ -73,6 +74,15 @@ jobs:
cd src
python -m unittest test_typing_extensions.py
+ - name: Test CPython typing test suite
+ # Test suite fails on PyPy even without typing_extensions
+ if: ${{ !startsWith(matrix.python-version, 'pypy') }}
+ run: |
+ cd src
+ # Run the typing test suite from CPython with typing_extensions installed,
+ # because we monkeypatch typing under some circumstances.
+ python -c 'import typing_extensions; import test.__main__' test_typing -v
+
linting:
name: Lint
@@ -82,9 +92,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3"
cache: "pip"
@@ -121,7 +131,7 @@ jobs:
issues: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index ad2deee1..6b55f10e 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -20,10 +20,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: 3
@@ -50,10 +50,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: 3
diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml
index b318e333..a0feeefc 100644
--- a/.github/workflows/third_party.yml
+++ b/.github/workflows/third_party.yml
@@ -41,12 +41,15 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"]
+ # PyPy is deliberately omitted here,
+ # since pydantic's tests intermittently segfault on PyPy,
+ # and it's nothing to do with typing_extensions
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout pydantic
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: pydantic/pydantic
- name: Edit pydantic pyproject.toml
@@ -54,13 +57,14 @@ jobs:
# as a requirement unless we do this
run: sed -i 's/^requires-python = .*/requires-python = ">=3.8"/' pyproject.toml
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup pdm for pydantic tests
uses: pdm-project/setup-pdm@v3
with:
python-version: ${{ matrix.python-version }}
+ allow-python-prereleases: true
- name: Add local version of typing_extensions as a dependency
run: pdm add ./typing-extensions-latest
- name: Install pydantic test dependencies
@@ -90,24 +94,28 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout typing_inspect
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: ilevkivskyi/typing_inspect
path: typing_inspect
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install typing_inspect test dependencies
- run: pip install -r typing_inspect/test-requirements.txt
+ run: |
+ cd typing_inspect
+ uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typing_inspect tests
run: |
cd typing_inspect
@@ -133,25 +141,29 @@ jobs:
timeout-minutes: 60
steps:
- name: Check out pyanalyze
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: quora/pyanalyze
path: pyanalyze
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ 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: Install pyanalyze test requirements
- run: pip install ./pyanalyze[tests]
+ run: |
+ cd pyanalyze
+ uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run pyanalyze tests
run: |
cd pyanalyze
@@ -172,30 +184,34 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out typeguard
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: agronholm/typeguard
path: typeguard
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ 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: Install typeguard test requirements
- run: pip install -e ./typeguard[test]
+ run: |
+ cd typeguard
+ uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typeguard tests
run: |
cd typeguard
@@ -216,23 +232,25 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out typed-argument-parser
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: swansonk14/typed-argument-parser
path: typed-argument-parser
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Configure git for typed-argument-parser tests
# typed-argument parser does this in their CI,
# and the tests fail unless we do this
@@ -241,12 +259,13 @@ jobs:
git config --global user.name "Your Name"
- name: Install typed-argument-parser test requirements
run: |
- pip install -e ./typed-argument-parser
- pip install pytest
+ cd typed-argument-parser
+ uv pip install --system "typed-argument-parser @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
+ uv pip install --system pytest --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typed-argument-parser tests
run: |
cd typed-argument-parser
@@ -272,28 +291,30 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout mypy for stubtest and mypyc tests
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python/mypy
path: mypy
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ 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: Install mypy test requirements
run: |
cd mypy
- pip install -r test-requirements.txt
- pip install -e .
+ uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
+ uv pip install --system -e .
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run stubtest & mypyc tests
run: |
cd mypy
@@ -314,20 +335,20 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"]
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout cattrs
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python-attrs/cattrs
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install pdm for cattrs
@@ -376,7 +397,7 @@ jobs:
issues: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..60419be8
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,13 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.12"
+
+sphinx:
+ configuration: doc/conf.py
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58f0487d..4cf71773 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,75 @@
+# Release 4.11.0 (April 5, 2024)
+
+This feature release provides improvements to various recently
+added features, most importantly type parameter defaults (PEP 696).
+
+There are no changes since 4.11.0rc1.
+
+# Release 4.11.0rc1 (March 24, 2024)
+
+- Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra.
+- Fix the runtime behavior of type parameters with defaults (PEP 696).
+ Patch by Nadir Chowdhury.
+- Fix minor discrepancy between error messages produced by `typing`
+ and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra.
+- When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation.
+
+# Release 4.10.0 (February 24, 2024)
+
+This feature release adds support for PEP 728 (TypedDict with extra
+items) and PEP 742 (``TypeIs``).
+
+There are no changes since 4.10.0rc1.
+
+# Release 4.10.0rc1 (February 17, 2024)
+
+- Add support for PEP 728, supporting the `closed` keyword argument and the
+ special `__extra_items__` key for TypedDict. Patch by Zixuan James Li.
+- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch
+ by Jelle Zijlstra.
+- Drop runtime error when a read-only `TypedDict` item overrides a mutable
+ one. Type checkers should still flag this as an error. Patch by Jelle
+ Zijlstra.
+- Speedup `issubclass()` checks against simple runtime-checkable protocols by
+ around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex
+ Waygood).
+- Fix a regression in the implementation of protocols where `typing.Protocol`
+ classes that were not marked as `@runtime_checkable` would be unnecessarily
+ introspected, potentially causing exceptions to be raised if the protocol had
+ problematic members. Patch by Alex Waygood, backporting
+ https://github.com/python/cpython/pull/113401.
+
+# Release 4.9.0 (December 9, 2023)
+
+This feature release adds `typing_extensions.ReadOnly`, as specified
+by PEP 705, and makes various other improvements, especially to
+`@typing_extensions.deprecated()`.
+
+There are no changes since 4.9.0rc1.
+
+# Release 4.9.0rc1 (November 29, 2023)
+
+- Add support for PEP 705, adding `typing_extensions.ReadOnly`. Patch
+ by Jelle Zijlstra.
+- All parameters on `NewType.__call__` are now positional-only. This means that
+ the signature of `typing_extensions.NewType.__call__` now exactly matches the
+ signature of `typing.NewType.__call__`. Patch by Alex Waygood.
+- Fix bug with using `@deprecated` on a mixin class. Inheriting from a
+ deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.
+- `@deprecated` now gives a better error message if you pass a non-`str`
+ argument to the `msg` parameter. Patch by Alex Waygood.
+- `@deprecated` is now implemented as a class for better introspectability.
+ Patch by Jelle Zijlstra.
+- Exclude `__match_args__` from `Protocol` members.
+ Backport of https://github.com/python/cpython/pull/110683 by Nikita Sobolev.
+- When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__`
+ is called on all objects that define `__set_name__` and exist in the values
+ of the `NamedTuple` class's class dictionary. Patch by Alex Waygood,
+ backporting https://github.com/python/cpython/pull/111876.
+- Improve the error message when trying to call `issubclass()` against a
+ `Protocol` that has non-method members. Patch by Alex Waygood (backporting
+ https://github.com/python/cpython/pull/112344, by Randolph Scholz).
+
# Release 4.8.0 (September 17, 2023)
No changes since 4.8.0rc1.
diff --git a/doc/conf.py b/doc/conf.py
index 7984bc22..40d3c6b7 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -5,6 +5,8 @@
import os.path
import sys
+from sphinx.writers.html5 import HTML5Translator
+from docutils.nodes import Element
sys.path.insert(0, os.path.abspath('.'))
@@ -26,9 +28,22 @@
intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)}
+add_module_names = False
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster'
-html_static_path = ['_static']
+
+
+class MyTranslator(HTML5Translator):
+ """Adds a link target to name without `typing_extensions.` prefix."""
+ def visit_desc_signature(self, node: Element) -> None:
+ desc_name = node.get("fullname")
+ if desc_name:
+ self.body.append(f'')
+ super().visit_desc_signature(node)
+
+
+def setup(app):
+ app.set_translator('html', MyTranslator)
diff --git a/doc/index.rst b/doc/index.rst
index 28b795a3..f9097a41 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,3 +1,4 @@
+.. module:: typing_extensions
Welcome to typing_extensions's documentation!
=============================================
@@ -318,6 +319,12 @@ Special typing primitives
present in a protocol class's :py:term:`method resolution order`. See
:issue:`245` for some examples.
+.. data:: ReadOnly
+
+ See :pep:`705`. Indicates that a :class:`TypedDict` item may not be modified.
+
+ .. versionadded:: 4.9.0
+
.. data:: Required
See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11.
@@ -344,7 +351,13 @@ Special typing primitives
See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10.
-.. class:: TypedDict
+.. data:: TypeIs
+
+ See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing.
+
+ .. versionadded:: 4.10.0
+
+.. class:: TypedDict(dict, total=True)
See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8.
@@ -366,6 +379,55 @@ Special typing primitives
raises a :py:exc:`DeprecationWarning` when this syntax is used in Python 3.12
or lower and fails with a :py:exc:`TypeError` in Python 3.13 and higher.
+ ``typing_extensions`` supports the experimental :data:`ReadOnly` qualifier
+ proposed by :pep:`705`. It is reflected in the following attributes:
+
+ .. attribute:: __readonly_keys__
+
+ A :py:class:`frozenset` containing the names of all read-only keys. Keys
+ are read-only if they carry the :data:`ReadOnly` qualifier.
+
+ .. versionadded:: 4.9.0
+
+ .. attribute:: __mutable_keys__
+
+ A :py:class:`frozenset` containing the names of all mutable keys. Keys
+ are mutable if they do not carry the :data:`ReadOnly` qualifier.
+
+ .. versionadded:: 4.9.0
+
+ The experimental ``closed`` keyword argument and the special key
+ ``__extra_items__`` proposed in :pep:`728` are supported.
+
+ When ``closed`` is unspecified or ``closed=False`` is given,
+ ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a
+ special key that does not show up in ``__readonly_keys__``,
+ ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or
+ ``__annotations__``.
+
+ For runtime introspection, two attributes can be looked at:
+
+ .. attribute:: __closed__
+
+ A boolean flag indicating whether the current ``TypedDict`` is
+ considered closed. This is not inherited by the ``TypedDict``'s
+ subclasses.
+
+ .. versionadded:: 4.10.0
+
+ .. attribute:: __extra_items__
+
+ The type annotation of the extra items allowed on the ``TypedDict``.
+ This attribute defaults to ``None`` on a TypedDict that has itself and
+ all its bases non-closed. This default is different from ``type(None)``
+ that represents ``__extra_items__: None`` defined on a closed
+ ``TypedDict``.
+
+ If ``__extra_items__`` is not defined or inherited on a closed
+ ``TypedDict``, this defaults to ``Never``.
+
+ .. versionadded:: 4.10.0
+
.. versionchanged:: 4.3.0
Added support for generic ``TypedDict``\ s.
@@ -394,6 +456,15 @@ Special typing primitives
disallowed in Python 3.15. To create a TypedDict class with 0 fields,
use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.
+ .. versionchanged:: 4.9.0
+
+ Support for the :data:`ReadOnly` qualifier was added.
+
+ .. versionchanged:: 4.10.0
+
+ The keyword argument ``closed`` and the special key ``__extra_items__``
+ when ``closed=True`` is given were supported.
+
.. class:: TypeVar(name, *constraints, bound=None, covariant=False,
contravariant=False, infer_variance=False, default=...)
@@ -549,10 +620,15 @@ Decorators
.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1)
- See :pep:`702`. Experimental; not yet part of the standard library.
+ See :pep:`702`. In the :mod:`warnings` module since Python 3.13.
.. versionadded:: 4.5.0
+ .. versionchanged:: 4.9.0
+
+ Inheriting from a deprecated class now also raises a runtime
+ :py:exc:`DeprecationWarning`.
+
.. decorator:: final
See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8.
@@ -684,6 +760,11 @@ Functions
Interaction with :data:`Required` and :data:`NotRequired`.
+ .. versionchanged:: 4.11.0
+
+ When ``include_extra=False``, ``get_type_hints()`` now strips
+ :data:`ReadOnly` from the annotation.
+
.. function:: is_protocol(tp)
Determine if a type is a :class:`Protocol`. This works with protocols
diff --git a/pyproject.toml b/pyproject.toml
index b71e6d01..4b1a7601 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi"
# Project metadata
[project]
name = "typing_extensions"
-version = "4.8.0"
+version = "4.11.0"
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 97717bce..27488550 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -31,12 +31,12 @@
import typing_extensions
from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
-from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
+from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired, ReadOnly
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict
from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
from typing_extensions import clear_overloads, get_overloads, overload
-from typing_extensions import NamedTuple
+from typing_extensions import NamedTuple, TypeIs
from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol
from typing_extensions import Doc
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
@@ -52,10 +52,18 @@
# 3.12 changes the representation of Unpack[] (PEP 692)
TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0)
+# 3.13 drops support for the keyword argument syntax of TypedDict
+TYPING_3_13_0 = sys.version_info[:3] >= (3, 13, 0)
+
# https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10
# versions, but not all
HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters
+skip_if_early_py313_alpha = skipIf(
+ sys.version_info[:4] == (3, 13, 0, 'alpha') and sys.version_info.serial < 3,
+ "Bugfixes will be released in 3.13.0a3"
+)
+
ANN_MODULE_SOURCE = '''\
from typing import Optional
from functools import wraps
@@ -418,6 +426,93 @@ def __new__(cls, x):
self.assertEqual(instance.x, 42)
self.assertTrue(new_called)
+ def test_mixin_class(self):
+ @deprecated("Mixin will go away soon")
+ class Mixin:
+ pass
+
+ class Base:
+ def __init__(self, a) -> None:
+ self.a = a
+
+ with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
+ class Child(Base, Mixin):
+ pass
+
+ instance = Child(42)
+ self.assertEqual(instance.a, 42)
+
+ def test_existing_init_subclass(self):
+ @deprecated("C will go away soon")
+ class C:
+ def __init_subclass__(cls) -> None:
+ cls.inited = True
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ C()
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ class D(C):
+ pass
+
+ self.assertTrue(D.inited)
+ self.assertIsInstance(D(), D) # no deprecation
+
+ def test_existing_init_subclass_in_base(self):
+ class Base:
+ def __init_subclass__(cls, x) -> None:
+ cls.inited = x
+
+ @deprecated("C will go away soon")
+ class C(Base, x=42):
+ pass
+
+ self.assertEqual(C.inited, 42)
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ C()
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ class D(C, x=3):
+ pass
+
+ self.assertEqual(D.inited, 3)
+
+ def test_init_subclass_has_correct_cls(self):
+ init_subclass_saw = None
+
+ @deprecated("Base will go away soon")
+ class Base:
+ def __init_subclass__(cls) -> None:
+ nonlocal init_subclass_saw
+ init_subclass_saw = cls
+
+ self.assertIsNone(init_subclass_saw)
+
+ with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
+ class C(Base):
+ pass
+
+ self.assertIs(init_subclass_saw, C)
+
+ def test_init_subclass_with_explicit_classmethod(self):
+ init_subclass_saw = None
+
+ @deprecated("Base will go away soon")
+ class Base:
+ @classmethod
+ def __init_subclass__(cls) -> None:
+ nonlocal init_subclass_saw
+ init_subclass_saw = cls
+
+ self.assertIsNone(init_subclass_saw)
+
+ with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
+ class C(Base):
+ pass
+
+ self.assertIs(init_subclass_saw, C)
+
def test_function(self):
@deprecated("b will go away soon")
def b():
@@ -480,6 +575,29 @@ def d():
warnings.simplefilter("error")
d()
+ def test_only_strings_allowed(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ "Expected an object of type str for 'message', not 'type'"
+ ):
+ @deprecated
+ class Foo: ...
+
+ with self.assertRaisesRegex(
+ TypeError,
+ "Expected an object of type str for 'message', not 'function'"
+ ):
+ @deprecated
+ def foo(): ...
+
+ def test_no_retained_references_to_wrapper_instance(self):
+ @deprecated('depr')
+ def d(): pass
+
+ self.assertFalse(any(
+ isinstance(cell.cell_contents, deprecated) for cell in d.__closure__
+ ))
+
class AnyTests(BaseTestCase):
def test_can_subclass(self):
@@ -2506,6 +2624,39 @@ class Bad: pass
self.assertNotIsInstance(Other(), Concrete)
self.assertIsInstance(NT(1, 2), Position)
+ def test_runtime_checkable_with_match_args(self):
+ @runtime_checkable
+ class P_regular(Protocol):
+ x: int
+ y: int
+
+ @runtime_checkable
+ class P_match(Protocol):
+ __match_args__ = ("x", "y")
+ x: int
+ y: int
+
+ class Regular:
+ def __init__(self, x: int, y: int):
+ self.x = x
+ self.y = y
+
+ class WithMatch:
+ __match_args__ = ("x", "y", "z")
+ def __init__(self, x: int, y: int, z: int):
+ self.x = x
+ self.y = y
+ self.z = z
+
+ class Nope: ...
+
+ self.assertIsInstance(Regular(1, 2), P_regular)
+ self.assertIsInstance(Regular(1, 2), P_match)
+ self.assertIsInstance(WithMatch(1, 2, 3), P_regular)
+ self.assertIsInstance(WithMatch(1, 2, 3), P_match)
+ self.assertNotIsInstance(Nope(), P_regular)
+ self.assertNotIsInstance(Nope(), P_match)
+
def test_protocols_isinstance_init(self):
T = TypeVar('T')
@runtime_checkable
@@ -2669,8 +2820,8 @@ def meth(self): pass # noqa: B027
self.assertNotIn("__protocol_attrs__", vars(NonP))
self.assertNotIn("__protocol_attrs__", vars(NonPR))
- self.assertNotIn("__callable_proto_members_only__", vars(NonP))
- self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonP))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonPR))
acceptable_extra_attrs = {
'_is_protocol', '_is_runtime_protocol', '__parameters__',
@@ -2743,11 +2894,26 @@ def __subclasshook__(cls, other):
@skip_if_py312b1
def test_issubclass_fails_correctly(self):
@runtime_checkable
- class P(Protocol):
+ class NonCallableMembers(Protocol):
x = 1
+
+ class NotRuntimeCheckable(Protocol):
+ def callable_member(self) -> int: ...
+
+ @runtime_checkable
+ class RuntimeCheckable(Protocol):
+ def callable_member(self) -> int: ...
+
class C: pass
- with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
- issubclass(C(), P)
+
+ # These three all exercise different code paths,
+ # but should result in the same error message:
+ for protocol in NonCallableMembers, NotRuntimeCheckable, RuntimeCheckable:
+ with self.subTest(proto_name=protocol.__name__):
+ with self.assertRaisesRegex(
+ TypeError, r"issubclass\(\) arg 1 must be a class"
+ ):
+ issubclass(C(), protocol)
def test_defining_generic_protocols(self):
T = TypeVar('T')
@@ -3096,7 +3262,7 @@ def __call__(self, *args: Unpack[Ts]) -> T: ...
self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2))
self.assertTrue(MemoizedFunc._is_protocol)
- things = "arguments" if sys.version_info >= (3, 11) else "parameters"
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
# A bug was fixed in 3.11.1
# (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3)
@@ -3306,6 +3472,57 @@ def method(self) -> None: ...
self.assertIsInstance(Foo(), ProtocolWithMixedMembers)
self.assertNotIsInstance(42, ProtocolWithMixedMembers)
+ @skip_if_early_py313_alpha
+ def test_protocol_issubclass_error_message(self):
+ @runtime_checkable
+ class Vec2D(Protocol):
+ x: float
+ y: float
+
+ def square_norm(self) -> float:
+ return self.x ** 2 + self.y ** 2
+
+ self.assertEqual(Vec2D.__protocol_attrs__, {'x', 'y', 'square_norm'})
+ expected_error_message = (
+ "Protocols with non-method members don't support issubclass()."
+ " Non-method members: 'x', 'y'."
+ )
+ with self.assertRaisesRegex(TypeError, re.escape(expected_error_message)):
+ issubclass(int, Vec2D)
+
+ def test_nonruntime_protocol_interaction_with_evil_classproperty(self):
+ class classproperty:
+ def __get__(self, instance, type):
+ raise RuntimeError("NO")
+
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ # recognised as a protocol attr,
+ # but not actually accessed by the protocol metaclass
+ # (which would raise RuntimeError) for non-runtime protocols.
+ # See gh-113320
+ self.assertEqual(get_protocol_members(Commentable), {"evil"})
+
+ def test_runtime_protocol_interaction_with_evil_classproperty(self):
+ class CustomError(Exception): pass
+
+ class classproperty:
+ def __get__(self, instance, type):
+ raise CustomError
+
+ with self.assertRaises(TypeError) as cm:
+ @runtime_checkable
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ exc = cm.exception
+ self.assertEqual(
+ exc.args[0],
+ "Failed to determine whether protocol member 'evil' is a method member"
+ )
+ self.assertIs(type(exc.__cause__), CustomError)
+
class Point2DGeneric(Generic[T], TypedDict):
a: T
@@ -3339,7 +3556,8 @@ def test_basics_functional_syntax(self):
@skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13")
def test_keywords_syntax_raises_on_3_13(self):
with self.assertRaises(TypeError):
- Emp = TypedDict('Emp', name=str, id=int)
+ with self.assertWarns(DeprecationWarning):
+ Emp = TypedDict('Emp', name=str, id=int)
@skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs")
def test_basics_keywords_syntax(self):
@@ -3383,17 +3601,9 @@ def test_typeddict_create_errors(self):
with self.assertRaises(TypeError):
TypedDict('Emp', [('name', str)], None)
- with self.assertWarns(DeprecationWarning):
- Emp = TypedDict('Emp', name=str, id=int)
- self.assertEqual(Emp.__name__, 'Emp')
- self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
-
def test_typeddict_errors(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
- if sys.version_info >= (3, 13):
- self.assertEqual(TypedDict.__module__, 'typing')
- else:
- self.assertEqual(TypedDict.__module__, 'typing_extensions')
+ self.assertEqual(TypedDict.__module__, 'typing_extensions')
jim = Emp(name='Jim', id=1)
with self.assertRaises(TypeError):
isinstance({}, Emp)
@@ -3613,6 +3823,24 @@ class ChildWithInlineAndOptional(Untotal, Inline):
{'inline': bool, 'untotal': str, 'child': bool},
)
+ class Closed(TypedDict, closed=True):
+ __extra_items__: None
+
+ class Unclosed(TypedDict, closed=False):
+ ...
+
+ class ChildUnclosed(Closed, Unclosed):
+ ...
+
+ self.assertFalse(ChildUnclosed.__closed__)
+ self.assertEqual(ChildUnclosed.__extra_items__, type(None))
+
+ class ChildClosed(Unclosed, Closed):
+ ...
+
+ self.assertFalse(ChildClosed.__closed__)
+ self.assertEqual(ChildClosed.__extra_items__, type(None))
+
wrong_bases = [
(One, Regular),
(Regular, One),
@@ -3917,6 +4145,207 @@ class T4(TypedDict, Generic[S]): pass
self.assertEqual(klass.__optional_keys__, set())
self.assertIsInstance(klass(), dict)
+ def test_readonly_inheritance(self):
+ class Base1(TypedDict):
+ a: ReadOnly[int]
+
+ class Child1(Base1):
+ b: str
+
+ self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
+
+ class Base2(TypedDict):
+ a: ReadOnly[int]
+
+ class Child2(Base2):
+ b: str
+
+ self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
+
+ def test_make_mutable_key_readonly(self):
+ class Base(TypedDict):
+ a: int
+
+ self.assertEqual(Base.__readonly_keys__, frozenset())
+ self.assertEqual(Base.__mutable_keys__, frozenset({'a'}))
+
+ class Child(Base):
+ a: ReadOnly[int] # type checker error, but allowed at runtime
+
+ self.assertEqual(Child.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__mutable_keys__, frozenset())
+
+ def test_can_make_readonly_key_mutable(self):
+ class Base(TypedDict):
+ a: ReadOnly[int]
+
+ class Child(Base):
+ a: int
+
+ self.assertEqual(Child.__readonly_keys__, frozenset())
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_combine_qualifiers(self):
+ class AllTheThings(TypedDict):
+ a: Annotated[Required[ReadOnly[int]], "why not"]
+ b: Required[Annotated[ReadOnly[int], "why not"]]
+ c: ReadOnly[NotRequired[Annotated[int, "why not"]]]
+ d: NotRequired[Annotated[int, "why not"]]
+
+ self.assertEqual(AllTheThings.__required_keys__, frozenset({'a', 'b'}))
+ self.assertEqual(AllTheThings.__optional_keys__, frozenset({'c', 'd'}))
+ self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'}))
+ self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'}))
+
+ self.assertEqual(
+ get_type_hints(AllTheThings, include_extras=False),
+ {'a': int, 'b': int, 'c': int, 'd': int},
+ )
+ self.assertEqual(
+ get_type_hints(AllTheThings, include_extras=True),
+ {
+ 'a': Annotated[Required[ReadOnly[int]], 'why not'],
+ 'b': Required[Annotated[ReadOnly[int], 'why not']],
+ 'c': ReadOnly[NotRequired[Annotated[int, 'why not']]],
+ 'd': NotRequired[Annotated[int, 'why not']],
+ },
+ )
+
+ def test_extra_keys_non_readonly(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: str
+
+ class Child(Base):
+ a: NotRequired[int]
+
+ self.assertEqual(Child.__required_keys__, frozenset({}))
+ self.assertEqual(Child.__optional_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_extra_keys_readonly(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: ReadOnly[str]
+
+ class Child(Base):
+ a: NotRequired[str]
+
+ self.assertEqual(Child.__required_keys__, frozenset({}))
+ self.assertEqual(Child.__optional_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_extra_key_required(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ "Special key __extra_items__ does not support Required"
+ ):
+ TypedDict("A", {"__extra_items__": Required[int]}, closed=True)
+
+ with self.assertRaisesRegex(
+ TypeError,
+ "Special key __extra_items__ does not support NotRequired"
+ ):
+ TypedDict("A", {"__extra_items__": NotRequired[int]}, closed=True)
+
+ def test_regular_extra_items(self):
+ class ExtraReadOnly(TypedDict):
+ __extra_items__: ReadOnly[str]
+
+ self.assertEqual(ExtraReadOnly.__required_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraReadOnly.__optional_keys__, frozenset({}))
+ self.assertEqual(ExtraReadOnly.__readonly_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraReadOnly.__mutable_keys__, frozenset({}))
+ self.assertEqual(ExtraReadOnly.__extra_items__, None)
+ self.assertFalse(ExtraReadOnly.__closed__)
+
+ class ExtraRequired(TypedDict):
+ __extra_items__: Required[str]
+
+ self.assertEqual(ExtraRequired.__required_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraRequired.__optional_keys__, frozenset({}))
+ self.assertEqual(ExtraRequired.__readonly_keys__, frozenset({}))
+ self.assertEqual(ExtraRequired.__mutable_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraRequired.__extra_items__, None)
+ self.assertFalse(ExtraRequired.__closed__)
+
+ class ExtraNotRequired(TypedDict):
+ __extra_items__: NotRequired[str]
+
+ self.assertEqual(ExtraNotRequired.__required_keys__, frozenset({}))
+ self.assertEqual(ExtraNotRequired.__optional_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraNotRequired.__readonly_keys__, frozenset({}))
+ self.assertEqual(ExtraNotRequired.__mutable_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraNotRequired.__extra_items__, None)
+ self.assertFalse(ExtraNotRequired.__closed__)
+
+ def test_closed_inheritance(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: ReadOnly[Union[str, None]]
+
+ self.assertEqual(Base.__required_keys__, frozenset({}))
+ self.assertEqual(Base.__optional_keys__, frozenset({}))
+ self.assertEqual(Base.__readonly_keys__, frozenset({}))
+ self.assertEqual(Base.__mutable_keys__, frozenset({}))
+ self.assertEqual(Base.__annotations__, {})
+ self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]])
+ self.assertTrue(Base.__closed__)
+
+ class Child(Base):
+ a: int
+ __extra_items__: int
+
+ self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(Child.__optional_keys__, frozenset({}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(Child.__annotations__, {"__extra_items__": int, "a": int})
+ self.assertEqual(Child.__extra_items__, ReadOnly[Union[str, None]])
+ self.assertFalse(Child.__closed__)
+
+ class GrandChild(Child, closed=True):
+ __extra_items__: str
+
+ self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(GrandChild.__optional_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__readonly_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(GrandChild.__annotations__, {"__extra_items__": int, "a": int})
+ self.assertEqual(GrandChild.__extra_items__, str)
+ self.assertTrue(GrandChild.__closed__)
+
+ def test_implicit_extra_items(self):
+ class Base(TypedDict):
+ a: int
+
+ self.assertEqual(Base.__extra_items__, None)
+ self.assertFalse(Base.__closed__)
+
+ class ChildA(Base, closed=True):
+ ...
+
+ self.assertEqual(ChildA.__extra_items__, Never)
+ self.assertTrue(ChildA.__closed__)
+
+ class ChildB(Base, closed=True):
+ __extra_items__: None
+
+ self.assertEqual(ChildB.__extra_items__, type(None))
+ self.assertTrue(ChildB.__closed__)
+
+ @skipIf(
+ TYPING_3_13_0,
+ "The keyword argument alternative to define a "
+ "TypedDict type using the functional syntax is no longer supported"
+ )
+ def test_backwards_compatibility(self):
+ with self.assertWarns(DeprecationWarning):
+ TD = TypedDict("TD", closed=int)
+ self.assertFalse(TD.__closed__)
+ self.assertEqual(TD.__annotations__, {"closed": int})
+
class AnnotatedTests(BaseTestCase):
@@ -4519,6 +4948,50 @@ def test_no_isinstance(self):
issubclass(int, TypeGuard)
+class TypeIsTests(BaseTestCase):
+ def test_basics(self):
+ TypeIs[int] # OK
+ self.assertEqual(TypeIs[int], TypeIs[int])
+
+ def foo(arg) -> TypeIs[int]: ...
+ self.assertEqual(gth(foo), {'return': TypeIs[int]})
+
+ def test_repr(self):
+ if hasattr(typing, 'TypeIs'):
+ mod_name = 'typing'
+ else:
+ mod_name = 'typing_extensions'
+ self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs')
+ cv = TypeIs[int]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]')
+ cv = TypeIs[Employee]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]')
+ cv = TypeIs[Tuple[int]]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]')
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(TypeIs)):
+ pass
+ with self.assertRaises(TypeError):
+ class C(type(TypeIs[int])):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ TypeIs()
+ with self.assertRaises(TypeError):
+ type(TypeIs)()
+ with self.assertRaises(TypeError):
+ type(TypeIs[Optional[int]])()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, TypeIs[int])
+ with self.assertRaises(TypeError):
+ issubclass(int, TypeIs)
+
+
class LiteralStringTests(BaseTestCase):
def test_basics(self):
class Foo:
@@ -5052,12 +5525,14 @@ def test_typing_extensions_defers_when_possible(self):
exclude |= {'final', 'Any', 'NewType'}
if sys.version_info < (3, 12):
exclude |= {
- 'Protocol', 'SupportsAbs', 'SupportsBytes',
+ 'SupportsAbs', 'SupportsBytes',
'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt',
'SupportsRound', 'Unpack',
}
if sys.version_info < (3, 13):
- exclude |= {'NamedTuple', 'TypedDict', 'is_typeddict'}
+ exclude |= {'NamedTuple', 'Protocol', 'runtime_checkable'}
+ if not typing_extensions._PEP_728_IMPLEMENTED:
+ exclude |= {'TypedDict', 'is_typeddict'}
for item in typing_extensions.__all__:
if item not in exclude and hasattr(typing, item):
self.assertIs(
@@ -5236,8 +5711,7 @@ class Y(Generic[T], NamedTuple):
self.assertIsInstance(a, G)
self.assertEqual(a.x, 3)
- things = "arguments" if sys.version_info >= (3, 11) else "parameters"
-
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
with self.assertRaisesRegex(TypeError, f'Too many {things}'):
G[int, str]
@@ -5418,6 +5892,136 @@ class GenericNamedTuple(NamedTuple, Generic[T]):
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
+ @skip_if_early_py313_alpha
+ def test_setname_called_on_values_in_class_dictionary(self):
+ class Vanilla:
+ def __set_name__(self, owner, name):
+ self.name = name
+
+ class Foo(NamedTuple):
+ attr = Vanilla()
+
+ foo = Foo()
+ self.assertEqual(len(foo), 0)
+ self.assertNotIn('attr', Foo._fields)
+ self.assertIsInstance(foo.attr, Vanilla)
+ self.assertEqual(foo.attr.name, "attr")
+
+ class Bar(NamedTuple):
+ attr: Vanilla = Vanilla()
+
+ bar = Bar()
+ self.assertEqual(len(bar), 1)
+ self.assertIn('attr', Bar._fields)
+ self.assertIsInstance(bar.attr, Vanilla)
+ self.assertEqual(bar.attr.name, "attr")
+
+ @skipIf(
+ TYPING_3_12_0,
+ "__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
+ )
+ def test_setname_raises_the_same_as_on_other_classes_py311_minus(self):
+ class CustomException(BaseException): pass
+
+ class Annoying:
+ def __set_name__(self, owner, name):
+ raise CustomException
+
+ annoying = Annoying()
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NormalClass:
+ attr = annoying
+ normal_exception = cm.exception
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NamedTupleClass(NamedTuple):
+ attr = annoying
+ namedtuple_exception = cm.exception
+
+ expected_note = (
+ "Error calling __set_name__ on 'Annoying' instance "
+ "'attr' in 'NamedTupleClass'"
+ )
+
+ self.assertIs(type(namedtuple_exception), RuntimeError)
+ self.assertIs(type(namedtuple_exception), type(normal_exception))
+ self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args))
+ self.assertEqual(
+ namedtuple_exception.args[0],
+ normal_exception.args[0].replace("NormalClass", "NamedTupleClass")
+ )
+
+ self.assertIs(type(namedtuple_exception.__cause__), CustomException)
+ self.assertIs(
+ type(namedtuple_exception.__cause__), type(normal_exception.__cause__)
+ )
+ self.assertEqual(
+ namedtuple_exception.__cause__.args, normal_exception.__cause__.args
+ )
+
+ @skipUnless(
+ TYPING_3_12_0,
+ "__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
+ )
+ @skip_if_early_py313_alpha
+ def test_setname_raises_the_same_as_on_other_classes_py312_plus(self):
+ class CustomException(BaseException): pass
+
+ class Annoying:
+ def __set_name__(self, owner, name):
+ raise CustomException
+
+ annoying = Annoying()
+
+ with self.assertRaises(CustomException) as cm:
+ class NormalClass:
+ attr = annoying
+ normal_exception = cm.exception
+
+ with self.assertRaises(CustomException) as cm:
+ class NamedTupleClass(NamedTuple):
+ attr = annoying
+ namedtuple_exception = cm.exception
+
+ expected_note = (
+ "Error calling __set_name__ on 'Annoying' instance "
+ "'attr' in 'NamedTupleClass'"
+ )
+
+ self.assertIs(type(namedtuple_exception), CustomException)
+ self.assertIs(type(namedtuple_exception), type(normal_exception))
+ self.assertEqual(namedtuple_exception.args, normal_exception.args)
+
+ self.assertEqual(len(namedtuple_exception.__notes__), 1)
+ self.assertEqual(
+ len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
+ )
+
+ self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
+ self.assertEqual(
+ namedtuple_exception.__notes__[0],
+ normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
+ )
+
+ @skip_if_early_py313_alpha
+ def test_strange_errors_when_accessing_set_name_itself(self):
+ class CustomException(Exception): pass
+
+ class Meta(type):
+ def __getattribute__(self, attr):
+ if attr == "__set_name__":
+ raise CustomException
+ return object.__getattribute__(self, attr)
+
+ class VeryAnnoying(metaclass=Meta): pass
+
+ very_annoying = VeryAnnoying()
+
+ with self.assertRaises(CustomException):
+ class Foo(NamedTuple):
+ attr = very_annoying
+
class TypeVarTests(BaseTestCase):
def test_basic_plain(self):
@@ -5610,6 +6214,27 @@ def test_typevartuple(self):
class A(Generic[Unpack[Ts]]): ...
Alias = Optional[Unpack[Ts]]
+ def test_erroneous_generic(self):
+ DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
+ T = TypeVar('T')
+
+ with self.assertRaises(TypeError):
+ Test = Generic[DefaultStrT, T]
+
+ def test_need_more_params(self):
+ DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
+ T = typing_extensions.TypeVar('T')
+ U = typing_extensions.TypeVar('U')
+
+ class A(Generic[T, U, DefaultStrT]): ...
+ A[int, bool]
+ A[int, bool, str]
+
+ with self.assertRaises(
+ TypeError, msg="Too few arguments for .+; actual 1, expected at least 2"
+ ):
+ Test = A[int]
+
def test_pickle(self):
global U, U_co, U_contra, U_default # pickle wants to reference the class by name
U = typing_extensions.TypeVar('U')
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index c96bf90f..9ccd519c 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -83,9 +83,11 @@
'TypeAlias',
'TypeAliasType',
'TypeGuard',
+ 'TypeIs',
'TYPE_CHECKING',
'Never',
'NoReturn',
+ 'ReadOnly',
'Required',
'NotRequired',
@@ -145,27 +147,6 @@ def __repr__(self):
_marker = _Sentinel()
-def _check_generic(cls, parameters, elen=_marker):
- """Check correct count for parameters of a generic cls (internal helper).
- This gives a nice error message in case of count mismatch.
- """
- if not elen:
- raise TypeError(f"{cls} is not a generic class")
- if elen is _marker:
- if not hasattr(cls, "__parameters__") or not cls.__parameters__:
- raise TypeError(f"{cls} is not a generic class")
- elen = len(cls.__parameters__)
- alen = len(parameters)
- if alen != elen:
- if hasattr(cls, "__parameters__"):
- parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
- num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
- if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
- return
- raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
- f" actual {alen}, expected {elen}")
-
-
if sys.version_info >= (3, 10):
def _should_collect_from_parameters(t):
return isinstance(
@@ -179,27 +160,6 @@ def _should_collect_from_parameters(t):
return isinstance(t, typing._GenericAlias) and not t._special
-def _collect_type_vars(types, typevar_types=None):
- """Collect all type variable contained in types in order of
- first appearance (lexicographic order). For example::
-
- _collect_type_vars((T, List[S, T])) == (T, S)
- """
- if typevar_types is None:
- typevar_types = typing.TypeVar
- tvars = []
- for t in types:
- if (
- isinstance(t, typevar_types) and
- t not in tvars and
- not _is_unpack(t)
- ):
- tvars.append(t)
- if _should_collect_from_parameters(t):
- tvars.extend([t for t in t.__parameters__ if t not in tvars])
- return tuple(tvars)
-
-
NoReturn = typing.NoReturn
# Some unconstrained type variables. These are used by the container types.
@@ -472,7 +432,8 @@ def clear_overloads():
"_is_runtime_protocol", "__dict__", "__slots__", "__parameters__",
"__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__",
"__subclasshook__", "__orig_class__", "__init__", "__new__",
- "__protocol_attrs__", "__callable_proto_members_only__",
+ "__protocol_attrs__", "__non_callable_proto_members__",
+ "__match_args__",
}
if sys.version_info >= (3, 9):
@@ -503,9 +464,9 @@ def _caller(depth=2):
return None
-# The performance of runtime-checkable protocols is significantly improved on Python 3.12,
-# so we backport the 3.12 version of Protocol to Python <=3.11
-if sys.version_info >= (3, 12):
+# `__match_args__` attribute was removed from protocol members in 3.13,
+# we want to backport this change to older Python versions.
+if sys.version_info >= (3, 13):
Protocol = typing.Protocol
else:
def _allow_reckless_class_checks(depth=3):
@@ -519,6 +480,22 @@ def _no_init(self, *args, **kwargs):
if type(self)._is_protocol:
raise TypeError('Protocols cannot be instantiated')
+ def _type_check_issubclass_arg_1(arg):
+ """Raise TypeError if `arg` is not an instance of `type`
+ in `issubclass(arg, )`.
+
+ In most cases, this is verified by type.__subclasscheck__.
+ Checking it again unnecessarily would slow down issubclass() checks,
+ so, we don't perform this check unless we absolutely have to.
+
+ For various error paths, however,
+ we want to ensure that *this* error message is shown to the user
+ where relevant, rather than a typing.py-specific error message.
+ """
+ if not isinstance(arg, type):
+ # Same error message as for issubclass(1, int).
+ raise TypeError('issubclass() arg 1 must be a class')
+
# Inheriting from typing._ProtocolMeta isn't actually desirable,
# but is necessary to allow typing.Protocol and typing_extensions.Protocol
# to mix without getting TypeErrors about "metaclass conflict"
@@ -549,11 +526,6 @@ def __init__(cls, *args, **kwargs):
abc.ABCMeta.__init__(cls, *args, **kwargs)
if getattr(cls, "_is_protocol", False):
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
- # PEP 544 prohibits using issubclass()
- # with protocols that have non-method members.
- cls.__callable_proto_members_only__ = all(
- callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
- )
def __subclasscheck__(cls, other):
if cls is Protocol:
@@ -562,21 +534,23 @@ def __subclasscheck__(cls, other):
getattr(cls, '_is_protocol', False)
and not _allow_reckless_class_checks()
):
- if not isinstance(other, type):
- # Same error message as for issubclass(1, int).
- raise TypeError('issubclass() arg 1 must be a class')
- if (
- not cls.__callable_proto_members_only__
- and cls.__dict__.get("__subclasshook__") is _proto_hook
- ):
- raise TypeError(
- "Protocols with non-method members don't support issubclass()"
- )
if not getattr(cls, '_is_runtime_protocol', False):
+ _type_check_issubclass_arg_1(other)
raise TypeError(
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
+ if (
+ # this attribute is set by @runtime_checkable:
+ cls.__non_callable_proto_members__
+ and cls.__dict__.get("__subclasshook__") is _proto_hook
+ ):
+ _type_check_issubclass_arg_1(other)
+ non_method_attrs = sorted(cls.__non_callable_proto_members__)
+ raise TypeError(
+ "Protocols with non-method members don't support issubclass()."
+ f" Non-method members: {str(non_method_attrs)[1:-1]}."
+ )
return abc.ABCMeta.__subclasscheck__(cls, other)
def __instancecheck__(cls, instance):
@@ -603,7 +577,8 @@ def __instancecheck__(cls, instance):
val = inspect.getattr_static(instance, attr)
except AttributeError:
break
- if val is None and callable(getattr(cls, attr, None)):
+ # this attribute is set by @runtime_checkable:
+ if val is None and attr not in cls.__non_callable_proto_members__:
break
else:
return True
@@ -671,8 +646,58 @@ def __init_subclass__(cls, *args, **kwargs):
cls.__init__ = _no_init
+if sys.version_info >= (3, 13):
+ runtime_checkable = typing.runtime_checkable
+else:
+ def runtime_checkable(cls):
+ """Mark a protocol class as a runtime protocol.
+
+ Such protocol can be used with isinstance() and issubclass().
+ Raise TypeError if applied to a non-protocol class.
+ This allows a simple-minded structural check very similar to
+ one trick ponies in collections.abc such as Iterable.
+
+ For example::
+
+ @runtime_checkable
+ class Closable(Protocol):
+ def close(self): ...
+
+ assert isinstance(open('/some/file'), Closable)
+
+ Warning: this will check only the presence of the required methods,
+ not their type signatures!
+ """
+ if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False):
+ raise TypeError('@runtime_checkable can be only applied to protocol classes,'
+ ' got %r' % cls)
+ cls._is_runtime_protocol = True
+
+ # Only execute the following block if it's a typing_extensions.Protocol class.
+ # typing.Protocol classes don't need it.
+ if isinstance(cls, _ProtocolMeta):
+ # PEP 544 prohibits using issubclass()
+ # with protocols that have non-method members.
+ # See gh-113320 for why we compute this attribute here,
+ # rather than in `_ProtocolMeta.__init__`
+ cls.__non_callable_proto_members__ = set()
+ for attr in cls.__protocol_attrs__:
+ try:
+ is_callable = callable(getattr(cls, attr, None))
+ except Exception as e:
+ raise TypeError(
+ f"Failed to determine whether protocol member {attr!r} "
+ "is a method member"
+ ) from e
+ else:
+ if not is_callable:
+ cls.__non_callable_proto_members__.add(attr)
+
+ return cls
+
+
# The "runtime" alias exists for backwards compatibility.
-runtime = runtime_checkable = typing.runtime_checkable
+runtime = runtime_checkable
# Our version of runtime-checkable protocols is faster on Python 3.8-3.11
@@ -767,7 +792,11 @@ def inner(func):
return inner
-if sys.version_info >= (3, 13):
+# Update this to something like >=3.13.0b1 if and when
+# PEP 728 is implemented in CPython
+_PEP_728_IMPLEMENTED = False
+
+if _PEP_728_IMPLEMENTED:
# The standard library TypedDict in Python 3.8 does not store runtime information
# about which (if any) keys are optional. See https://bugs.python.org/issue38834
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
@@ -778,6 +807,8 @@ def inner(func):
# Aaaand on 3.12 we add __orig_bases__ to TypedDict
# to enable better runtime introspection.
# On 3.13 we deprecate some odd ways of creating TypedDicts.
+ # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier.
+ # PEP 728 (still pending) makes more changes.
TypedDict = typing.TypedDict
_TypedDictMeta = typing._TypedDictMeta
is_typeddict = typing.is_typeddict
@@ -785,8 +816,29 @@ def inner(func):
# 3.10.0 and later
_TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters
+ def _get_typeddict_qualifiers(annotation_type):
+ while True:
+ annotation_origin = get_origin(annotation_type)
+ if annotation_origin is Annotated:
+ annotation_args = get_args(annotation_type)
+ if annotation_args:
+ annotation_type = annotation_args[0]
+ else:
+ break
+ elif annotation_origin is Required:
+ yield Required
+ annotation_type, = get_args(annotation_type)
+ elif annotation_origin is NotRequired:
+ yield NotRequired
+ annotation_type, = get_args(annotation_type)
+ elif annotation_origin is ReadOnly:
+ yield ReadOnly
+ annotation_type, = get_args(annotation_type)
+ else:
+ break
+
class _TypedDictMeta(type):
- def __new__(cls, name, bases, ns, total=True):
+ def __new__(cls, name, bases, ns, *, total=True, closed=False):
"""Create new typed dict class object.
This method is called when TypedDict is subclassed,
@@ -829,35 +881,67 @@ def __new__(cls, name, bases, ns, total=True):
}
required_keys = set()
optional_keys = set()
+ readonly_keys = set()
+ mutable_keys = set()
+ extra_items_type = None
for base in bases:
- annotations.update(base.__dict__.get('__annotations__', {}))
- required_keys.update(base.__dict__.get('__required_keys__', ()))
- optional_keys.update(base.__dict__.get('__optional_keys__', ()))
+ base_dict = base.__dict__
+
+ annotations.update(base_dict.get('__annotations__', {}))
+ required_keys.update(base_dict.get('__required_keys__', ()))
+ optional_keys.update(base_dict.get('__optional_keys__', ()))
+ readonly_keys.update(base_dict.get('__readonly_keys__', ()))
+ mutable_keys.update(base_dict.get('__mutable_keys__', ()))
+ base_extra_items_type = base_dict.get('__extra_items__', None)
+ if base_extra_items_type is not None:
+ extra_items_type = base_extra_items_type
+
+ if closed and extra_items_type is None:
+ extra_items_type = Never
+ if closed and "__extra_items__" in own_annotations:
+ annotation_type = own_annotations.pop("__extra_items__")
+ qualifiers = set(_get_typeddict_qualifiers(annotation_type))
+ if Required in qualifiers:
+ raise TypeError(
+ "Special key __extra_items__ does not support "
+ "Required"
+ )
+ if NotRequired in qualifiers:
+ raise TypeError(
+ "Special key __extra_items__ does not support "
+ "NotRequired"
+ )
+ extra_items_type = annotation_type
annotations.update(own_annotations)
for annotation_key, annotation_type in own_annotations.items():
- annotation_origin = get_origin(annotation_type)
- if annotation_origin is Annotated:
- annotation_args = get_args(annotation_type)
- if annotation_args:
- annotation_type = annotation_args[0]
- annotation_origin = get_origin(annotation_type)
-
- if annotation_origin is Required:
+ qualifiers = set(_get_typeddict_qualifiers(annotation_type))
+
+ if Required in qualifiers:
required_keys.add(annotation_key)
- elif annotation_origin is NotRequired:
+ elif NotRequired in qualifiers:
optional_keys.add(annotation_key)
elif total:
required_keys.add(annotation_key)
else:
optional_keys.add(annotation_key)
+ if ReadOnly in qualifiers:
+ mutable_keys.discard(annotation_key)
+ readonly_keys.add(annotation_key)
+ else:
+ mutable_keys.add(annotation_key)
+ readonly_keys.discard(annotation_key)
tp_dict.__annotations__ = annotations
tp_dict.__required_keys__ = frozenset(required_keys)
tp_dict.__optional_keys__ = frozenset(optional_keys)
+ tp_dict.__readonly_keys__ = frozenset(readonly_keys)
+ tp_dict.__mutable_keys__ = frozenset(mutable_keys)
if not hasattr(tp_dict, '__total__'):
tp_dict.__total__ = total
+ tp_dict.__closed__ = closed
+ tp_dict.__extra_items__ = extra_items_type
return tp_dict
__call__ = dict # static method
@@ -871,7 +955,7 @@ def __subclasscheck__(cls, other):
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
@_ensure_subclassable(lambda bases: (_TypedDict,))
- def TypedDict(typename, fields=_marker, /, *, total=True, **kwargs):
+ def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs):
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
TypedDict creates a dictionary type such that a type checker will expect all
@@ -931,11 +1015,16 @@ class Point2D(TypedDict):
"using the functional syntax, pass an empty dictionary, e.g. "
) + example + "."
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
+ if closed is not False and closed is not True:
+ kwargs["closed"] = closed
+ closed = False
fields = kwargs
elif kwargs:
raise TypeError("TypedDict takes either a dict or keyword arguments,"
" 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 "
@@ -950,7 +1039,7 @@ class Point2D(TypedDict):
# Setting correct module is necessary to make typed dict classes pickleable.
ns['__module__'] = module
- td = _TypedDictMeta(typename, (), ns, total=total)
+ td = _TypedDictMeta(typename, (), ns, total=total, closed=closed)
td.__orig_bases__ = (TypedDict,)
return td
@@ -996,15 +1085,15 @@ def greet(name: str) -> None:
return val
-if hasattr(typing, "Required"): # 3.11+
+if hasattr(typing, "ReadOnly"): # 3.13+
get_type_hints = typing.get_type_hints
-else: # <=3.10
+else: # <=3.13
# replaces _strip_annotations()
def _strip_extras(t):
"""Strips Annotated, Required and NotRequired from a given type."""
if isinstance(t, _AnnotatedAlias):
return _strip_extras(t.__origin__)
- if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
+ if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly):
return _strip_extras(t.__args__[0])
if isinstance(t, typing._GenericAlias):
stripped_args = tuple(_strip_extras(a) for a in t.__args__)
@@ -1724,6 +1813,98 @@ def is_str(val: Union[str, float]):
PEP 647 (User-Defined Type Guards).
""")
+# 3.13+
+if hasattr(typing, 'TypeIs'):
+ TypeIs = typing.TypeIs
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ @_ExtensionsSpecialForm
+ def TypeIs(self, parameters):
+ """Special typing form used to annotate the return type of a user-defined
+ type narrower function. ``TypeIs`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeIs[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeIs`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the intersection of the type inside ``TypeGuard`` and the argument's
+ previously known type.
+
+ For example::
+
+ def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
+ return hasattr(val, '__await__')
+
+ def f(val: Union[int, Awaitable[int]]) -> int:
+ if is_awaitable(val):
+ assert_type(val, Awaitable[int])
+ else:
+ assert_type(val, int)
+
+ ``TypeIs`` also works with type variables. For more information, see
+ PEP 742 (Narrowing types with TypeIs).
+ """
+ item = typing._type_check(parameters, f'{self} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+# 3.8
+else:
+ class _TypeIsForm(_ExtensionsSpecialForm, _root=True):
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type')
+ return typing._GenericAlias(self, (item,))
+
+ TypeIs = _TypeIsForm(
+ 'TypeIs',
+ doc="""Special typing form used to annotate the return type of a user-defined
+ type narrower function. ``TypeIs`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeIs[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeIs`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the intersection of the type inside ``TypeGuard`` and the argument's
+ previously known type.
+
+ For example::
+
+ def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
+ return hasattr(val, '__await__')
+
+ def f(val: Union[int, Awaitable[int]]) -> int:
+ if is_awaitable(val):
+ assert_type(val, Awaitable[int])
+ else:
+ assert_type(val, int)
+
+ ``TypeIs`` also works with type variables. For more information, see
+ PEP 742 (Narrowing types with TypeIs).
+ """)
+
# Vendored from cpython typing._SpecialFrom
class _SpecialForm(typing._Final, _root=True):
@@ -1924,6 +2105,53 @@ class Movie(TypedDict):
""")
+if hasattr(typing, 'ReadOnly'):
+ ReadOnly = typing.ReadOnly
+elif sys.version_info[:2] >= (3, 9): # 3.9-3.12
+ @_ExtensionsSpecialForm
+ def ReadOnly(self, parameters):
+ """A special typing construct to mark an item of a TypedDict as read-only.
+
+ For example:
+
+ class Movie(TypedDict):
+ title: ReadOnly[str]
+ year: int
+
+ def mutate_movie(m: Movie) -> None:
+ m["year"] = 1992 # allowed
+ m["title"] = "The Matrix" # typechecker error
+
+ There is no runtime checking for this property.
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+else: # 3.8
+ class _ReadOnlyForm(_ExtensionsSpecialForm, _root=True):
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+ ReadOnly = _ReadOnlyForm(
+ 'ReadOnly',
+ doc="""A special typing construct to mark a key of a TypedDict as read-only.
+
+ For example:
+
+ class Movie(TypedDict):
+ title: ReadOnly[str]
+ year: int
+
+ def mutate_movie(m: Movie) -> None:
+ m["year"] = 1992 # allowed
+ m["title"] = "The Matrix" # typechecker error
+
+ There is no runtime checking for this propery.
+ """)
+
+
_UNPACK_DOC = """\
Type unpack operator.
@@ -2251,7 +2479,7 @@ def override(arg: _F, /) -> _F:
Usage:
class Base:
- def method(self) -> None: ...
+ def method(self) -> None:
pass
class Child(Base):
@@ -2281,20 +2509,17 @@ def method(self) -> None:
return arg
-if hasattr(typing, "deprecated"):
- deprecated = typing.deprecated
+if hasattr(warnings, "deprecated"):
+ deprecated = warnings.deprecated
else:
_T = typing.TypeVar("_T")
- def deprecated(
- msg: str,
- /,
- *,
- category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
- stacklevel: int = 1,
- ) -> typing.Callable[[_T], _T]:
+ class deprecated:
"""Indicate that a class, function or overload is deprecated.
+ When this decorator is applied to an object, the type checker
+ will generate a diagnostic on usage of the deprecated object.
+
Usage:
@deprecated("Use B instead")
@@ -2311,49 +2536,100 @@ def g(x: int) -> int: ...
@overload
def g(x: str) -> int: ...
- When this decorator is applied to an object, the type checker
- will generate a diagnostic on usage of the deprecated object.
-
- The warning specified by ``category`` will be emitted on use
- of deprecated objects. For functions, that happens on calls;
- for classes, on instantiation. If the ``category`` is ``None``,
- no warning is emitted. The ``stacklevel`` determines where the
+ The warning specified by *category* will be emitted at runtime
+ on use of deprecated objects. For functions, that happens on calls;
+ for classes, on instantiation and on creation of subclasses.
+ If the *category* is ``None``, no warning is emitted at runtime.
+ The *stacklevel* determines where the
warning is emitted. If it is ``1`` (the default), the warning
is emitted at the direct caller of the deprecated object; if it
is higher, it is emitted further up the stack.
+ Static type checker behavior is not affected by the *category*
+ and *stacklevel* arguments.
- The decorator sets the ``__deprecated__``
- attribute on the decorated object to the deprecation message
- passed to the decorator. If applied to an overload, the decorator
+ The deprecation message passed to the decorator is saved in the
+ ``__deprecated__`` attribute on the decorated object.
+ If applied to an overload, the decorator
must be after the ``@overload`` decorator for the attribute to
exist on the overload as returned by ``get_overloads()``.
See PEP 702 for details.
"""
- def decorator(arg: _T, /) -> _T:
+ def __init__(
+ self,
+ message: str,
+ /,
+ *,
+ category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
+ stacklevel: int = 1,
+ ) -> None:
+ if not isinstance(message, str):
+ raise TypeError(
+ "Expected an object of type str for 'message', not "
+ f"{type(message).__name__!r}"
+ )
+ self.message = message
+ self.category = category
+ self.stacklevel = stacklevel
+
+ def __call__(self, arg: _T, /) -> _T:
+ # Make sure the inner functions created below don't
+ # retain a reference to self.
+ msg = self.message
+ category = self.category
+ stacklevel = self.stacklevel
if category is None:
arg.__deprecated__ = msg
return arg
elif isinstance(arg, type):
+ import functools
+ from types import MethodType
+
original_new = arg.__new__
- has_init = arg.__init__ is not object.__init__
@functools.wraps(original_new)
def __new__(cls, *args, **kwargs):
- warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ if cls is arg:
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
if original_new is not object.__new__:
return original_new(cls, *args, **kwargs)
# Mirrors a similar check in object.__new__.
- elif not has_init and (args or kwargs):
+ elif cls.__init__ is object.__init__ and (args or kwargs):
raise TypeError(f"{cls.__name__}() takes no arguments")
else:
return original_new(cls)
arg.__new__ = staticmethod(__new__)
+
+ original_init_subclass = arg.__init_subclass__
+ # We need slightly different behavior if __init_subclass__
+ # is a bound method (likely if it was implemented in Python)
+ if isinstance(original_init_subclass, MethodType):
+ original_init_subclass = original_init_subclass.__func__
+
+ @functools.wraps(original_init_subclass)
+ def __init_subclass__(*args, **kwargs):
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ return original_init_subclass(*args, **kwargs)
+
+ arg.__init_subclass__ = classmethod(__init_subclass__)
+ # Or otherwise, which likely means it's a builtin such as
+ # object's implementation of __init_subclass__.
+ else:
+ @functools.wraps(original_init_subclass)
+ def __init_subclass__(*args, **kwargs):
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ return original_init_subclass(*args, **kwargs)
+
+ arg.__init_subclass__ = __init_subclass__
+
arg.__deprecated__ = __new__.__deprecated__ = msg
+ __init_subclass__.__deprecated__ = msg
return arg
elif callable(arg):
+ import functools
+
@functools.wraps(arg)
def wrapper(*args, **kwargs):
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
@@ -2367,8 +2643,6 @@ def wrapper(*args, **kwargs):
f"a class or callable, not {arg!r}"
)
- return decorator
-
# We have to do some monkey patching to deal with the dual nature of
# Unpack/TypeVarTuple:
@@ -2378,9 +2652,151 @@ def wrapper(*args, **kwargs):
# counting generic parameters, so that when we subscript a generic,
# the runtime doesn't try to substitute the Unpack with the subscripted type.
if not hasattr(typing, "TypeVarTuple"):
+ def _check_generic(cls, parameters, elen=_marker):
+ """Check correct count for parameters of a generic cls (internal helper).
+
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ if elen is _marker:
+ if not hasattr(cls, "__parameters__") or not cls.__parameters__:
+ raise TypeError(f"{cls} is not a generic class")
+ elen = len(cls.__parameters__)
+ alen = len(parameters)
+ if alen != elen:
+ expect_val = elen
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+ num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
+ if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
+ return
+
+ # deal with TypeVarLike defaults
+ # required TypeVarLikes cannot appear after a defaulted one.
+ if alen < elen:
+ # since we validate TypeVarLike default in _collect_type_vars
+ # or _collect_parameters we can safely check parameters[alen]
+ if getattr(parameters[alen], '__default__', None) is not None:
+ return
+
+ num_default_tv = sum(getattr(p, '__default__', None)
+ is not None for p in parameters)
+
+ elen -= num_default_tv
+
+ expect_val = f"at least {elen}"
+
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}"
+ f" for {cls}; actual {alen}, expected {expect_val}")
+else:
+ # Python 3.11+
+
+ def _check_generic(cls, parameters, elen):
+ """Check correct count for parameters of a generic cls (internal helper).
+
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ alen = len(parameters)
+ if alen != elen:
+ expect_val = elen
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+
+ # deal with TypeVarLike defaults
+ # required TypeVarLikes cannot appear after a defaulted one.
+ if alen < elen:
+ # since we validate TypeVarLike default in _collect_type_vars
+ # or _collect_parameters we can safely check parameters[alen]
+ if getattr(parameters[alen], '__default__', None) is not None:
+ return
+
+ num_default_tv = sum(getattr(p, '__default__', None)
+ is not None for p in parameters)
+
+ elen -= num_default_tv
+
+ expect_val = f"at least {elen}"
+
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments"
+ f" for {cls}; actual {alen}, expected {expect_val}")
+
+typing._check_generic = _check_generic
+
+# Python 3.11+ _collect_type_vars was renamed to _collect_parameters
+if hasattr(typing, '_collect_type_vars'):
+ def _collect_type_vars(types, typevar_types=None):
+ """Collect all type variable contained in types in order of
+ first appearance (lexicographic order). For example::
+
+ _collect_type_vars((T, List[S, T])) == (T, S)
+ """
+ if typevar_types is None:
+ typevar_types = typing.TypeVar
+ tvars = []
+ # required TypeVarLike cannot appear after TypeVarLike with default
+ default_encountered = False
+ for t in types:
+ if (
+ isinstance(t, typevar_types) and
+ t not in tvars and
+ not _is_unpack(t)
+ ):
+ if getattr(t, '__default__', None) is not None:
+ default_encountered = True
+ elif default_encountered:
+ raise TypeError(f'Type parameter {t!r} without a default'
+ ' follows type parameter with a default')
+
+ tvars.append(t)
+ if _should_collect_from_parameters(t):
+ tvars.extend([t for t in t.__parameters__ if t not in tvars])
+ return tuple(tvars)
+
typing._collect_type_vars = _collect_type_vars
- typing._check_generic = _check_generic
+else:
+ def _collect_parameters(args):
+ """Collect all type variables and parameter specifications in args
+ in order of first appearance (lexicographic order).
+ For example::
+
+ assert _collect_parameters((T, Callable[P, T])) == (T, P)
+ """
+ parameters = []
+ # required TypeVarLike cannot appear after TypeVarLike with default
+ default_encountered = False
+ for t in args:
+ if isinstance(t, type):
+ # We don't want __parameters__ descriptor of a bare Python class.
+ pass
+ elif isinstance(t, tuple):
+ # `t` might be a tuple, when `ParamSpec` is substituted with
+ # `[T, int]`, or `[int, *Ts]`, etc.
+ for x in t:
+ for collected in _collect_parameters([x]):
+ if collected not in parameters:
+ parameters.append(collected)
+ elif hasattr(t, '__typing_subst__'):
+ if t not in parameters:
+ if getattr(t, '__default__', None) is not None:
+ default_encountered = True
+ elif default_encountered:
+ raise TypeError(f'Type parameter {t!r} without a default'
+ ' follows type parameter with a default')
+
+ parameters.append(t)
+ else:
+ for x in getattr(t, '__parameters__', ()):
+ if x not in parameters:
+ parameters.append(x)
+
+ return tuple(parameters)
+
+ typing._collect_parameters = _collect_parameters
# Backport typing.NamedTuple as it exists in Python 3.13.
# In 3.11, the ability to define generic `NamedTuple`s was supported.
@@ -2437,11 +2853,35 @@ def __new__(cls, typename, bases, ns):
class_getitem = typing.Generic.__class_getitem__.__func__
nm_tpl.__class_getitem__ = classmethod(class_getitem)
# update from user namespace without overriding special namedtuple attributes
- for key in ns:
+ for key, val in ns.items():
if key in _prohibited_namedtuple_fields:
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
- elif key not in _special_namedtuple_fields and key not in nm_tpl._fields:
- setattr(nm_tpl, key, ns[key])
+ elif key not in _special_namedtuple_fields:
+ if key not in nm_tpl._fields:
+ setattr(nm_tpl, key, ns[key])
+ try:
+ set_name = type(val).__set_name__
+ except AttributeError:
+ pass
+ else:
+ try:
+ set_name(val, nm_tpl, key)
+ except BaseException as e:
+ msg = (
+ f"Error calling __set_name__ on {type(val).__name__!r} "
+ f"instance {key!r} in {typename!r}"
+ )
+ # BaseException.add_note() existed on py311,
+ # but the __set_name__ machinery didn't start
+ # using add_note() until py312.
+ # Making sure exceptions are raised in the same way
+ # as in "normal" classes seems most important here.
+ if sys.version_info >= (3, 12):
+ e.add_note(msg)
+ raise
+ else:
+ raise RuntimeError(msg) from e
+
if typing.Generic in bases:
nm_tpl.__init_subclass__()
return nm_tpl
@@ -2600,7 +3040,7 @@ def name_by_id(user_id: UserId) -> str:
num = UserId(5) + 1 # type: int
"""
- def __call__(self, obj):
+ def __call__(self, obj, /):
return obj
def __init__(self, name, tp):
diff --git a/tox.ini b/tox.ini
index 5bed0225..5be7adb8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
isolated_build = True
-envlist = py38, py39, py310, py311, py312
+envlist = py38, py39, py310, py311, py312, py313
[testenv]
changedir = src