diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b5ac6c7..78610e27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,11 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: @@ -38,7 +41,22 @@ jobs: # Python version, because typing sometimes changed between bugfix releases. # For available versions, see: # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - python-version: ["3.7", "3.7.1", "3.8", "3.8.0", "3.9", "3.9.0", "3.10", "3.10.0", "3.11", "3.11.0", "3.12", "pypy3.9"] + python-version: + - "3.7" + - "3.7.1" + - "3.8" + - "3.8.0" + - "3.9" + - "3.9.0" + - "3.10" + - "3.10.0" + - "3.11" + - "3.11.0" + - "3.12" + - "pypy3.7" + - "pypy3.8" + - "pypy3.9" + - "pypy3.10" runs-on: ubuntu-20.04 @@ -52,7 +70,6 @@ jobs: allow-prereleases: true - name: Test typing_extensions - continue-on-error: ${{ matrix.python-version == '3.12' }} run: | # Be wary of running `pip install` here, since it becomes easy for us to # accidentally pick up typing_extensions as installed by a dependency diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index cde11c14..bcb0234c 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -32,13 +32,16 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -54,7 +57,6 @@ jobs: uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} - cache: true - name: Add local version of typing_extensions as a dependency run: pdm add ./typing-extensions-latest - name: Install pydantic test dependencies @@ -70,13 +72,16 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -84,6 +89,7 @@ jobs: uses: actions/checkout@v3 with: repository: ilevkivskyi/typing_inspect + path: typing_inspect - name: Checkout typing_extensions uses: actions/checkout@v3 with: @@ -93,13 +99,15 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install typing_inspect test dependencies - run: pip install -r test-requirements.txt + run: pip install -r typing_inspect/test-requirements.txt - name: Install typing_extensions latest run: pip install ./typing-extensions-latest - name: List all installed dependencies run: pip freeze --all - name: Run typing_inspect tests - run: pytest + run: | + cd typing_inspect + pytest pyanalyze: name: pyanalyze tests @@ -107,13 +115,16 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -121,6 +132,7 @@ jobs: uses: actions/checkout@v3 with: repository: quora/pyanalyze + path: pyanalyze - name: Checkout typing_extensions uses: actions/checkout@v3 with: @@ -130,21 +142,32 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install pyanalyze test requirements - run: pip install .[tests] + run: pip install ./pyanalyze[tests] - name: Install typing_extensions latest run: pip install ./typing-extensions-latest - name: List all installed dependencies run: pip freeze --all - name: Run pyanalyze tests - run: pytest pyanalyze/ + run: | + cd pyanalyze + pytest pyanalyze/ typeguard: name: typeguard tests - if: false # TODO: unskip when typeguard's tests pass on typing_extensions>=4.6.0 + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -152,6 +175,7 @@ jobs: uses: actions/checkout@v3 with: repository: agronholm/typeguard + path: typeguard - name: Checkout typing_extensions uses: actions/checkout@v3 with: @@ -160,14 +184,17 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install typeguard test requirements - run: pip install -e .[test] + run: pip install -e ./typeguard[test] - name: Install typing_extensions latest run: pip install ./typing-extensions-latest - name: List all installed dependencies run: pip freeze --all - name: Run typeguard tests - run: pytest + run: | + cd typeguard + pytest typed-argument-parser: name: typed-argument-parser tests @@ -175,13 +202,16 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -189,6 +219,7 @@ jobs: uses: actions/checkout@v3 with: repository: swansonk14/typed-argument-parser + path: typed-argument-parser - name: Checkout typing_extensions uses: actions/checkout@v3 with: @@ -205,35 +236,41 @@ jobs: git config --global user.name "Your Name" - name: Install typed-argument-parser test requirements run: | - pip install -e . + pip install -e ./typed-argument-parser pip install pytest - name: Install typing_extensions latest run: pip install ./typing-extensions-latest - name: List all installed dependencies run: pip freeze --all - name: Run typed-argument-parser tests - run: pytest + run: | + cd typed-argument-parser + pytest - stubtest: - name: stubtest tests + mypy: + name: stubtest & mypyc tests if: >- # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout mypy for stubtest tests + - name: Checkout mypy for stubtest and mypyc tests uses: actions/checkout@v3 with: repository: python/mypy + path: mypy - name: Checkout typing_extensions uses: actions/checkout@v3 with: @@ -244,14 +281,17 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install mypy test requirements run: | + cd mypy pip install -r test-requirements.txt pip install -e . - name: Install typing_extensions latest run: pip install ./typing-extensions-latest - name: List all installed dependencies run: pip freeze --all - - name: Run stubtest tests - run: pytest ./mypy/test/teststubtest.py + - name: Run stubtest & mypyc tests + run: | + cd mypy + pytest -n 2 ./mypy/test/teststubtest.py ./mypyc/test/test_run.py ./mypyc/test/test_external.py cattrs: name: cattrs tests @@ -259,13 +299,16 @@ jobs: # if 'schedule' was the trigger, # don't run it on contributors' forks ${{ - github.repository == 'python/typing_extensions' - || github.event_name != 'schedule' + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) }} strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] runs-on: ubuntu-latest timeout-minutes: 60 steps: @@ -304,7 +347,7 @@ jobs: - pyanalyze - typeguard - typed-argument-parser - - stubtest + - mypy - cattrs if: >- @@ -318,7 +361,7 @@ jobs: || needs.pyanalyze.result == 'failure' || needs.typeguard.result == 'failure' || needs.typed-argument-parser.result == 'failure' - || needs.stubtest.result == 'failure' + || needs.mypy.result == 'failure' || needs.cattrs.result == 'failure' ) }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ecaea2ae..1e490c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,72 @@ +# Release 4.7.1 (July 2, 2023) + +- Fix support for `TypedDict`, `NamedTuple` and `is_protocol` on PyPy-3.7 and + PyPy-3.8. Patch by Alex Waygood. Note that PyPy-3.7 and PyPy-3.8 are unsupported + by the PyPy project. The next feature release of typing-extensions will + drop support for PyPy-3.7 and may also drop support for PyPy-3.8. + +# Release 4.7.0 (June 28, 2023) + +- This is expected to be the last feature release supporting Python 3.7, + which reaches its end of life on June 27, 2023. Version 4.8.0 will support + only Python 3.8.0 and up. +- Fix bug where a `typing_extensions.Protocol` class that had one or more + non-callable members would raise `TypeError` when `issubclass()` + was called against it, even if it defined a custom `__subclasshook__` + method. The correct behaviour -- which has now been restored -- is not to + raise `TypeError` in these situations if a custom `__subclasshook__` method + is defined. Patch by Alex Waygood (backporting + https://github.com/python/cpython/pull/105976). + +# Release 4.7.0rc1 (June 21, 2023) + +- Add `typing_extensions.get_protocol_members` and + `typing_extensions.is_protocol` (backport of CPython PR #104878). + Patch by Jelle Zijlstra. +- `typing_extensions` now re-exports all names in the standard library's + `typing` module, except the deprecated `ByteString`. Patch by Jelle + Zijlstra. +- Due to changes in the implementation of `typing_extensions.Protocol`, + `typing.runtime_checkable` can now be used on `typing_extensions.Protocol` + (previously, users had to use `typing_extensions.runtime_checkable` if they + were using `typing_extensions.Protocol`). +- Align the implementation of `TypedDict` with the implementation in the + standard library on Python 3.9 and higher. + `typing_extensions.TypedDict` is now a function instead of a class. The + private functions `_check_fails`, `_dict_new`, and `_typeddict_new` + have been removed. `is_typeddict` now returns `False` when called with + `TypedDict` itself as the argument. Patch by Jelle Zijlstra. +- Declare support for Python 3.12. Patch by Jelle Zijlstra. +- Fix tests on Python 3.13, which removes support for creating + `TypedDict` classes through the keyword-argument syntax. Patch by + Jelle Zijlstra. +- Fix a regression introduced in v4.6.3 that meant that + ``issubclass(object, typing_extensions.Protocol)`` would erroneously raise + ``TypeError``. Patch by Alex Waygood (backporting the CPython PR + https://github.com/python/cpython/pull/105239). +- Allow `Protocol` classes to inherit from `typing_extensions.Buffer` or + `collections.abc.Buffer`. Patch by Alex Waygood (backporting + https://github.com/python/cpython/pull/104827, by Jelle Zijlstra). +- Allow classes to inherit from both `typing.Protocol` and `typing_extensions.Protocol` + simultaneously. Since v4.6.0, this caused `TypeError` to be raised due to a + metaclass conflict. Patch by Alex Waygood. +- Backport several deprecations from CPython relating to unusual ways to + create `TypedDict`s and `NamedTuple`s. CPython PRs #105609 and #105780 + by Alex Waygood; `typing_extensions` backport by Jelle Zijlstra. + - Creating a `NamedTuple` using the functional syntax with keyword arguments + (`NT = NamedTuple("NT", a=int)`) is now deprecated. + - Creating a `NamedTuple` with zero fields using the syntax `NT = NamedTuple("NT")` + or `NT = NamedTuple("NT", None)` is now deprecated. + - Creating a `TypedDict` with zero fields using the syntax `TD = TypedDict("TD")` + or `TD = TypedDict("TD", None)` is now deprecated. +- Fix bug on Python 3.7 where a protocol `X` that had a member `a` would not be + considered an implicit subclass of an unrelated protocol `Y` that only has a + member `a`. Where the members of `X` are a superset of the members of `Y`, + `X` should always be considered a subclass of `Y` iff `Y` is a + runtime-checkable protocol that only has callable members. Patch by Alex + Waygood (backporting CPython PR + https://github.com/python/cpython/pull/105835). + # Release 4.6.3 (June 1, 2023) - Fix a regression introduced in v4.6.0 in the implementation of diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b1a093b..9d07313e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,33 @@ Starting with version 4.0.0, `typing_extensions` uses [Semantic Versioning](https://semver.org/). See the documentation for more detail. +# Type stubs + +A stub file for `typing_extensions` is maintained +[in typeshed](https://github.com/python/typeshed/blob/main/stdlib/typing_extensions.pyi). +Because of the special status that `typing_extensions` holds in the typing ecosystem, +the stubs are placed in the standard library in typeshed and distributed as +part of the stubs bundled with individual type checkers. + +# Running tests + +Testing `typing_extensions` can be tricky because many development tools depend on +`typing_extensions`, so you may end up testing some installed version of the library, +rather than your local code. + +The simplest way to run the tests locally is: + +- `cd src/` +- `python test_typing_extensions.py` + +Alternatively, you can invoke `unittest` explicitly: + +- `python -m unittest test_typing_extensions.py` + +Running these commands in the `src/` directory ensures that the local file +`typing_extensions.py` is used, instead of any other version of the library you +may have installed. + # Workflow for PyPI releases - Make sure you follow the versioning policy in the documentation @@ -50,8 +77,10 @@ for more detail. - Install the built distributions locally and test (if you were using `tox`, you already tested the source distribution). -- Run `twine upload dist/*`. - -- Tag the release. The tag should be just the version number, e.g. `4.1.1`. +- Run `twine upload dist/*`. Remember to use `__token__` as the username + and pass your API token as the password. -- `git push --tags` +- Create a new GitHub release at https://github.com/python/typing_extensions/releases/new. + Details: + - The tag should be just the version number, e.g. `4.1.1`. + - Copy the release notes from `CHANGELOG.md`. diff --git a/README.md b/README.md index ddc11882..efd3a824 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ The `typing_extensions` module serves two related purposes: - Enable experimentation with new type system PEPs before they are accepted and added to the `typing` module. +`typing_extensions` is treated specially by static type checkers such as +mypy and pyright. Objects defined in `typing_extensions` are treated the same +way as equivalent forms in `typing`. + `typing_extensions` uses [Semantic Versioning](https://semver.org/). The major version will be incremented only for backwards-incompatible changes. @@ -29,7 +33,7 @@ where `x.y` is the first version that includes all features you need. See [the documentation](https://typing-extensions.readthedocs.io/en/latest/#) for a complete listing of module contents. -## Running tests +## Contributing -To run tests, navigate into the `src/` directory and run -`test_typing_extensions.py`. +See [CONTRIBUTING.md](https://github.com/python/typing_extensions/blob/main/CONTRIBUTING.md) +for how to contribute to `typing_extensions`. diff --git a/doc/index.rst b/doc/index.rst index 6b1a6f0b..5fd2b2e8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -22,6 +22,14 @@ figured out how to deal with that possibility. Bugfixes and new typing features that don't require a PEP may be added to ``typing_extensions`` once they are merged into CPython's main branch. +``typing_extensions`` also re-exports all names from the :py:mod:`typing` module, +including those that have always been present in the module. This allows users to +import names from ``typing_extensions`` without having to remember exactly when +each object was added to :py:mod:`typing`. There are a few exceptions: +:py:class:`typing.ByteString`, which is deprecated and due to be removed in Python +3.14, is not re-exported. Similarly, the ``typing.io`` and ``typing.re`` submodules, +which are removed in Python 3.13, are excluded. + Versioning and backwards compatibility -------------------------------------- @@ -73,6 +81,57 @@ the risk of compatibility issues: attributes directly. If some information is not available through a public attribute, consider opening an issue in CPython to add such an API. +Here is an example recipe for a general-purpose function that could be used for +reasonably performant runtime introspection of typing objects. The function +will be resilient against any potential changes in ``typing_extensions`` that +alter whether an object is reimplemented in ``typing_extensions``, rather than +simply being re-exported from the :mod:`typing` module:: + + import functools + import typing + import typing_extensions + from typing import Tuple, Any + + # Use an unbounded cache for this function, for optimal performance + @functools.lru_cache(maxsize=None) + def get_typing_objects_by_name_of(name: str) -> Tuple[Any, ...]: + result = tuple( + getattr(module, name) + # You could potentially also include mypy_extensions here, + # if your library supports mypy_extensions + for module in (typing, typing_extensions) + if hasattr(module, name) + ) + if not result: + raise ValueError( + f"Neither typing nor typing_extensions has an object called {name!r}" + ) + return result + + + # Use a cache here as well, but make it a bounded cache + # (the default cache size is 128) + @functools.lru_cache() + def is_typing_name(obj: object, name: str) -> bool: + return any(obj is thing for thing in get_typing_objects_by_name_of(name)) + +Example usage:: + + >>> import typing, typing_extensions + >>> from functools import partial + >>> from typing_extensions import get_origin + >>> is_literal = partial(is_typing_name, name="Literal") + >>> is_literal(typing.Literal) + True + >>> is_literal(typing_extensions.Literal) + True + >>> is_literal(typing.Any) + False + >>> is_literal(get_origin(typing.Literal[42])) + True + >>> is_literal(get_origin(typing_extensions.Final[42])) + False + Python version support ---------------------- @@ -110,10 +169,6 @@ Special typing primitives Added to support inheritance from ``Any``. -.. data:: ClassVar - - See :py:data:`typing.ClassVar` and :pep:`526`. In ``typing`` since 3.5.3. - .. data:: Concatenate See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. @@ -161,6 +216,22 @@ Special typing primitives Support for the ``__orig_bases__`` attribute was added. + .. versionchanged:: 4.7.0 + + The undocumented keyword argument syntax for creating NamedTuple classes + (``NT = NamedTuple("NT", x=int)``) is deprecated, and will be disallowed + in Python 3.15. Use the class-based syntax or the functional syntax instead. + + .. versionchanged:: 4.7.0 + + When using the functional syntax to create a NamedTuple class, failing to + pass a value to the 'fields' parameter (``NT = NamedTuple("NT")``) is + deprecated. Passing ``None`` to the 'fields' parameter + (``NT = NamedTuple("NT", None)``) is also deprecated. Both will be + disallowed in Python 3.15. To create a NamedTuple class with zero fields, + use ``class NT(NamedTuple): pass`` or ``NT = NamedTuple("NT", [])``. + + .. data:: Never See :py:data:`typing.Never`. In ``typing`` since 3.11. @@ -178,10 +249,6 @@ Special typing primitives The improvements from Python 3.10 and 3.11 were backported. -.. data:: NoReturn - - See :py:data:`typing.NoReturn`. In ``typing`` since 3.5.4 and 3.6.2. - .. data:: NotRequired See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. @@ -231,6 +298,18 @@ Special typing primitives Backported changes to runtime-checkable protocols from Python 3.12, including :pr-cpy:`103034` and :pr-cpy:`26067`. + .. versionchanged:: 4.7.0 + + Classes can now inherit from both :py:class:`typing.Protocol` and + ``typing_extensions.Protocol`` simultaneously. Previously, this led to + :py:exc:`TypeError` being raised due to a metaclass conflict. + + It is recommended to avoid doing this if possible. Not all features and + bugfixes that ``typing_extensions.Protocol`` backports from newer Python + versions are guaranteed to work if :py:class:`typing.Protocol` is also + present in a protocol class's :py:term:`method resolution order`. See + :issue:`245` for some examples. + .. data:: Required See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11. @@ -243,10 +322,6 @@ Special typing primitives .. versionadded:: 4.0.0 -.. class:: Type - - See :py:class:`typing.Type`. In ``typing`` since 3.5.2. - .. data:: TypeAlias See :py:data:`typing.TypeAlias` and :pep:`613`. In ``typing`` since 3.10. @@ -276,6 +351,13 @@ Special typing primitives ``typing_extensions`` backport provides all of these features and bugfixes on all Python versions. + Historically, ``TypedDict`` has supported an alternative creation syntax + where the fields are supplied as keyword arguments (e.g., + ``TypedDict("TD", a=int, b=str)``). In CPython, this feature was deprecated + in Python 3.11 and removed in Python 3.13. ``typing_extensions.TypedDict`` + 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. + .. versionchanged:: 4.3.0 Added support for generic ``TypedDict``\ s. @@ -289,6 +371,21 @@ Special typing primitives Support for the ``__orig_bases__`` attribute was added. + .. versionchanged:: 4.7.0 + + ``TypedDict`` is now a function rather than a class. + This brings ``typing_extensions.TypedDict`` closer to the implementation + of :py:mod:`typing.TypedDict` on Python 3.9 and higher. + + .. versionchanged:: 4.7.0 + + When using the functional syntax to create a TypedDict class, failing to + pass a value to the 'fields' parameter (``TD = TypedDict("TD")``) is + deprecated. Passing ``None`` to the 'fields' parameter + (``TD = TypedDict("TD", None)``) is also deprecated. Both will be + disallowed in Python 3.15. To create a TypedDict class with 0 fields, + use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``. + .. class:: TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False, default=...) @@ -344,22 +441,6 @@ Special typing primitives Generic concrete collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. class:: ChainMap - - See :py:class:`typing.ChainMap`. In ``typing`` since 3.5.4 and 3.6.1. - -.. class:: Counter - - See :py:class:`typing.Counter`. In ``typing`` since 3.5.4 and 3.6.1. - -.. class:: DefaultDict - - See :py:class:`typing.DefaultDict`. In ``typing`` since 3.5.2. - -.. class:: Deque - - See :py:class:`typing.Deque`. In ``typing`` since 3.5.4 and 3.6.1. - .. class:: OrderedDict See :py:class:`typing.OrderedDict`. In ``typing`` since 3.7.2. @@ -367,26 +448,6 @@ Generic concrete collections Abstract Base Classes ~~~~~~~~~~~~~~~~~~~~~ -.. class:: AsyncContextManager - - See :py:class:`typing.AsyncContextManager`. In ``typing`` since 3.5.4 and 3.6.2. - -.. class:: AsyncGenerator - - See :py:class:`typing.AsyncGenerator`. In ``typing`` since 3.6.1. - -.. class:: AsyncIterable - - See :py:class:`typing.AsyncIterable`. In ``typing`` since 3.5.2. - -.. class:: AsyncIterator - - See :py:class:`typing.AsyncIterator`. In ``typing`` since 3.5.2. - -.. class:: Awaitable - - See :py:class:`typing.Awaitable`. In ``typing`` since 3.5.2. - .. class:: Buffer See :py:class:`collections.abc.Buffer`. Added to the standard library @@ -394,14 +455,6 @@ Abstract Base Classes .. versionadded:: 4.6.0 -.. class:: ContextManager - - See :py:class:`typing.ContextManager`. In ``typing`` since 3.5.4. - -.. class:: Coroutine - - See :py:class:`typing.Coroutine`. In ``typing`` since 3.5.3. - Protocols ~~~~~~~~~ @@ -600,6 +653,24 @@ Functions .. versionadded:: 4.2.0 +.. function:: get_protocol_members(tp) + + 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`. + + :: + + >>> from typing_extensions import Protocol, get_protocol_members + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> get_protocol_members(P) + frozenset({'a', 'b'}) + + Raise :py:exc:`TypeError` for arguments that are not Protocols. + + .. versionadded:: 4.7.0 + .. function:: get_type_hints(obj, globalns=None, localns=None, include_extras=False) See :py:func:`typing.get_type_hints`. @@ -612,6 +683,22 @@ Functions Interaction with :data:`Required` and :data:`NotRequired`. +.. function:: is_protocol(tp) + + Determine if a type is a :class:`Protocol`. This works with protocols + defined using either :py:class:`typing.Protocol` or :class:`typing_extensions.Protocol`. + + For example:: + + class P(Protocol): + def a(self) -> str: ... + b: int + + is_protocol(P) # => True + is_protocol(int) # => False + + .. versionadded:: 4.7.0 + .. function:: is_typeddict(tp) See :py:func:`typing.is_typeddict`. In ``typing`` since 3.10. @@ -622,19 +709,306 @@ Functions .. versionadded:: 4.1.0 + .. versionchanged:: 4.7.0 + + :func:`is_typeddict` now returns ``False`` when called with + :data:`TypedDict` itself as the argument, consistent with the + behavior of :py:func:`typing.is_typeddict`. + .. function:: reveal_type(obj) See :py:func:`typing.reveal_type`. In ``typing`` since 3.11. .. versionadded:: 4.1.0 -Other -~~~~~ +Pure aliases +~~~~~~~~~~~~ + +These are simply re-exported from the :mod:`typing` module on all supported +versions of Python. They are listed here for completeness. + +.. class:: AbstractSet + + See :py:class:`typing.AbstractSet`. + + .. versionadded:: 4.7.0 + +.. data:: AnyStr + + See :py:data:`typing.AnyStr`. + + .. versionadded:: 4.7.0 + +.. class:: AsyncContextManager + + See :py:class:`typing.AsyncContextManager`. In ``typing`` since 3.5.4 and 3.6.2. + +.. class:: AsyncGenerator + + See :py:class:`typing.AsyncGenerator`. In ``typing`` since 3.6.1. + +.. class:: AsyncIterable + + See :py:class:`typing.AsyncIterable`. In ``typing`` since 3.5.2. + +.. class:: AsyncIterator + + See :py:class:`typing.AsyncIterator`. In ``typing`` since 3.5.2. + +.. class:: Awaitable + + See :py:class:`typing.Awaitable`. In ``typing`` since 3.5.2. + +.. class:: BinaryIO + + See :py:class:`typing.BinaryIO`. + + .. versionadded:: 4.7.0 + +.. data:: Callable + + See :py:data:`typing.Callable`. + + .. versionadded:: 4.7.0 + +.. class:: ChainMap + + See :py:class:`typing.ChainMap`. In ``typing`` since 3.5.4 and 3.6.1. + +.. data:: ClassVar + + See :py:data:`typing.ClassVar` and :pep:`526`. In ``typing`` since 3.5.3. + +.. class:: Collection + + See :py:class:`typing.Collection`. + + .. versionadded:: 4.7.0 + +.. class:: Container + + See :py:class:`typing.Container`. + + .. versionadded:: 4.7.0 + +.. class:: ContextManager + + See :py:class:`typing.ContextManager`. In ``typing`` since 3.5.4. + +.. class:: Coroutine + + See :py:class:`typing.Coroutine`. In ``typing`` since 3.5.3. + +.. class:: Counter + + See :py:class:`typing.Counter`. In ``typing`` since 3.5.4 and 3.6.1. + +.. class:: DefaultDict + + See :py:class:`typing.DefaultDict`. In ``typing`` since 3.5.2. + +.. class:: Deque + + See :py:class:`typing.Deque`. In ``typing`` since 3.5.4 and 3.6.1. + +.. class:: Dict + + See :py:class:`typing.Dict`. + + .. versionadded:: 4.7.0 + +.. class:: ForwardRef + + See :py:class:`typing.ForwardRef`. + + .. versionadded:: 4.7.0 + +.. class:: FrozenSet + + See :py:class:`typing.FrozenSet`. + + .. versionadded:: 4.7.0 + +.. class:: Generator + + See :py:class:`typing.Generator`. + + .. versionadded:: 4.7.0 + +.. class:: Generic + + See :py:class:`typing.Generic`. + + .. versionadded:: 4.7.0 + +.. class:: Hashable + + See :py:class:`typing.Hashable`. + + .. versionadded:: 4.7.0 + +.. class:: IO + + See :py:class:`typing.IO`. + + .. versionadded:: 4.7.0 + +.. class:: ItemsView + + See :py:class:`typing.ItemsView`. + + .. versionadded:: 4.7.0 + +.. class:: Iterable + + See :py:class:`typing.Iterable`. + + .. versionadded:: 4.7.0 + +.. class:: Iterator + + See :py:class:`typing.Iterator`. + + .. versionadded:: 4.7.0 + +.. class:: KeysView + + See :py:class:`typing.KeysView`. + + .. versionadded:: 4.7.0 + +.. class:: List + + See :py:class:`typing.List`. + + .. versionadded:: 4.7.0 + +.. class:: Mapping + + See :py:class:`typing.Mapping`. + + .. versionadded:: 4.7.0 + +.. class:: MappingView + + See :py:class:`typing.MappingView`. + + .. versionadded:: 4.7.0 + +.. class:: Match + + See :py:class:`typing.Match`. + + .. versionadded:: 4.7.0 + +.. class:: MutableMapping + + See :py:class:`typing.MutableMapping`. + + .. versionadded:: 4.7.0 + +.. class:: MutableSequence + + See :py:class:`typing.MutableSequence`. + + .. versionadded:: 4.7.0 + +.. class:: MutableSet + + See :py:class:`typing.MutableSet`. + + .. versionadded:: 4.7.0 + +.. data:: NoReturn + + See :py:data:`typing.NoReturn`. In ``typing`` since 3.5.4 and 3.6.2. + +.. data:: Optional + + See :py:data:`typing.Optional`. + + .. versionadded:: 4.7.0 + +.. class:: Pattern + + See :py:class:`typing.Pattern`. + + .. versionadded:: 4.7.0 + +.. class:: Reversible + + See :py:class:`typing.Reversible`. + + .. versionadded:: 4.7.0 + +.. class:: Sequence + + See :py:class:`typing.Sequence`. + + .. versionadded:: 4.7.0 + +.. class:: Set + + See :py:class:`typing.Set`. + + .. versionadded:: 4.7.0 + +.. class:: Sized + + See :py:class:`typing.Sized`. + + .. versionadded:: 4.7.0 .. class:: Text See :py:class:`typing.Text`. In ``typing`` since 3.5.2. +.. class:: TextIO + + See :py:class:`typing.TextIO`. + + .. versionadded:: 4.7.0 + +.. data:: Tuple + + See :py:data:`typing.Tuple`. + + .. versionadded:: 4.7.0 + +.. class:: Type + + See :py:class:`typing.Type`. In ``typing`` since 3.5.2. + .. data:: TYPE_CHECKING See :py:data:`typing.TYPE_CHECKING`. In ``typing`` since 3.5.2. + +.. data:: Union + + See :py:data:`typing.Union`. + + .. versionadded:: 4.7.0 + +.. class:: ValuesView + + See :py:class:`typing.ValuesView`. + + .. versionadded:: 4.7.0 + +.. function:: cast + + See :py:func:`typing.cast`. + + .. versionadded:: 4.7.0 + +.. decorator:: no_type_check + + See :py:func:`typing.no_type_check`. + + .. versionadded:: 4.7.0 + +.. decorator:: no_type_check_decorator + + See :py:func:`typing.no_type_check_decorator`. + + .. versionadded:: 4.7.0 diff --git a/pyproject.toml b/pyproject.toml index 3858e80d..736e1e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.6.3" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" readme = "README.md" requires-python = ">=3.7" @@ -39,6 +39,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development", ] diff --git a/src/_typed_dict_test_helper.py b/src/_typed_dict_test_helper.py index 7ffc5e1d..c5582b15 100644 --- a/src/_typed_dict_test_helper.py +++ b/src/_typed_dict_test_helper.py @@ -1,7 +1,7 @@ from __future__ import annotations from typing import Generic, Optional, T -from typing_extensions import TypedDict +from typing_extensions import TypedDict, Annotated, Required # this class must not be imported into test_typing_extensions.py at top level, otherwise @@ -16,3 +16,7 @@ class Foo(TypedDict): class FooGeneric(TypedDict, Generic[T]): a: Optional[T] + + +class VeryAnnotated(TypedDict, total=False): + a: Annotated[Annotated[Annotated[Required[int], "a"], "b"], "c"] diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f9c3389c..c2ab6d7f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,6 +1,7 @@ import sys import os import abc +import gc import io import contextlib import collections @@ -36,8 +37,8 @@ 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 override, deprecated, Buffer, TypeAliasType, TypeVar -from _typed_dict_test_helper import Foo, FooGeneric +from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol +from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated # Flags used to mark tests that only apply after a specific # version of the typing module. @@ -51,6 +52,10 @@ # 3.12 changes the representation of Unpack[] (PEP 692) TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) +only_with_typing_Protocol = skipUnless( + hasattr(typing, "Protocol"), "Only relevant when typing.Protocol exists" +) + # 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 @@ -1147,10 +1152,26 @@ class NontotalMovie(TypedDict, total=False): title: Required[str] year: int +class ParentNontotalMovie(TypedDict, total=False): + title: Required[str] + +class ChildTotalMovie(ParentNontotalMovie): + year: NotRequired[int] + +class ParentDeeplyAnnotatedMovie(TypedDict): + title: Annotated[Annotated[Required[str], "foobar"], "another level"] + +class ChildDeeplyAnnotatedMovie(ParentDeeplyAnnotatedMovie): + year: NotRequired[Annotated[int, 2000]] + class AnnotatedMovie(TypedDict): title: Annotated[Required[str], "foobar"] year: NotRequired[Annotated[int, 2000]] +class WeirdlyQuotedMovie(TypedDict): + title: Annotated['Annotated[Required[str], "foobar"]', "another level"] + year: NotRequired['Annotated[int, 2000]'] + gth = get_type_hints @@ -1320,24 +1341,18 @@ def test_isinstance_collections(self): issubclass(collections.Counter, typing_extensions.Counter[str]) def test_awaitable(self): - ns = {} - exec( - "async def foo() -> typing_extensions.Awaitable[int]:\n" - " return await AwaitableWrapper(42)\n", - globals(), ns) - foo = ns['foo'] + async def foo() -> typing_extensions.Awaitable[int]: + return await AwaitableWrapper(42) + g = foo() self.assertIsInstance(g, typing_extensions.Awaitable) self.assertNotIsInstance(foo, typing_extensions.Awaitable) g.send(None) # Run foo() till completion, to avoid warning. def test_coroutine(self): - ns = {} - exec( - "async def foo():\n" - " return\n", - globals(), ns) - foo = ns['foo'] + async def foo(): + return + g = foo() self.assertIsInstance(g, typing_extensions.Coroutine) with self.assertRaises(TypeError): @@ -1457,10 +1472,10 @@ class MyCounter(typing_extensions.Counter[int]): self.assertIsInstance(d, typing_extensions.Counter) def test_async_generator(self): - ns = {} - exec("async def f():\n" - " yield 42\n", globals(), ns) - g = ns['f']() + async def f(): + yield 42 + + g = f() self.assertIsSubclass(type(g), typing_extensions.AsyncGenerator) def test_no_async_generator_instantiation(self): @@ -1478,9 +1493,8 @@ def asend(self, value): def athrow(self, typ, val=None, tb=None): pass - ns = {} - exec('async def g(): yield 0', globals(), ns) - g = ns['g'] + async def g(): yield 0 + self.assertIsSubclass(G, typing_extensions.AsyncGenerator) self.assertIsSubclass(G, typing_extensions.AsyncIterable) self.assertIsSubclass(G, collections.abc.AsyncGenerator) @@ -1757,10 +1771,7 @@ class E(C, BP): pass self.assertNotIsInstance(D(), E) self.assertNotIsInstance(E(), D) - @skipUnless( - hasattr(typing, "Protocol"), - "Test is only relevant if typing.Protocol exists" - ) + @only_with_typing_Protocol def test_runtimecheckable_on_typing_dot_Protocol(self): @runtime_checkable class Foo(typing.Protocol): @@ -1773,6 +1784,70 @@ def __init__(self): self.assertIsInstance(Bar(), Foo) self.assertNotIsInstance(object(), Foo) + @only_with_typing_Protocol + def test_typing_dot_runtimecheckable_on_Protocol(self): + @typing.runtime_checkable + class Foo(Protocol): + x: int + + class Bar: + def __init__(self): + self.x = 42 + + self.assertIsInstance(Bar(), Foo) + self.assertNotIsInstance(object(), Foo) + + @only_with_typing_Protocol + def test_typing_Protocol_and_extensions_Protocol_can_mix(self): + class TypingProto(typing.Protocol): + x: int + + class ExtensionsProto(Protocol): + y: int + + class SubProto(TypingProto, ExtensionsProto, typing.Protocol): + z: int + + class SubProto2(TypingProto, ExtensionsProto, Protocol): + z: int + + class SubProto3(ExtensionsProto, TypingProto, typing.Protocol): + z: int + + class SubProto4(ExtensionsProto, TypingProto, Protocol): + z: int + + for proto in ( + ExtensionsProto, SubProto, SubProto2, SubProto3, SubProto4 + ): + with self.subTest(proto=proto.__name__): + self.assertTrue(is_protocol(proto)) + if Protocol is not typing.Protocol: + self.assertIsInstance(proto, typing_extensions._ProtocolMeta) + self.assertIsInstance(proto.__protocol_attrs__, set) + with self.assertRaisesRegex( + TypeError, "Protocols cannot be instantiated" + ): + proto() + # check these don't raise + runtime_checkable(proto) + typing.runtime_checkable(proto) + + class Concrete(SubProto): pass + class Concrete2(SubProto2): pass + class Concrete3(SubProto3): pass + class Concrete4(SubProto4): pass + + for cls in Concrete, Concrete2, Concrete3, Concrete4: + with self.subTest(cls=cls.__name__): + self.assertFalse(is_protocol(cls)) + # Check that this doesn't raise: + self.assertIsInstance(cls(), cls) + with self.assertRaises(TypeError): + runtime_checkable(cls) + with self.assertRaises(TypeError): + typing.runtime_checkable(cls) + def test_no_instantiation(self): class P(Protocol): pass with self.assertRaises(TypeError): @@ -1938,6 +2013,154 @@ def x(self): ... with self.assertRaisesRegex(TypeError, only_classes_allowed): issubclass(1, BadPG) + def test_implicit_issubclass_between_two_protocols(self): + @runtime_checkable + class CallableMembersProto(Protocol): + def meth(self): ... + + # All the below protocols should be considered "subclasses" + # of CallableMembersProto at runtime, + # even though none of them explicitly subclass CallableMembersProto + + class IdenticalProto(Protocol): + def meth(self): ... + + class SupersetProto(Protocol): + def meth(self): ... + def meth2(self): ... + + class NonCallableMembersProto(Protocol): + meth: Callable[[], None] + + class NonCallableMembersSupersetProto(Protocol): + meth: Callable[[], None] + meth2: Callable[[str, int], bool] + + class MixedMembersProto1(Protocol): + meth: Callable[[], None] + def meth2(self): ... + + class MixedMembersProto2(Protocol): + def meth(self): ... + meth2: Callable[[str, int], bool] + + for proto in ( + IdenticalProto, SupersetProto, NonCallableMembersProto, + NonCallableMembersSupersetProto, MixedMembersProto1, MixedMembersProto2 + ): + with self.subTest(proto=proto.__name__): + self.assertIsSubclass(proto, CallableMembersProto) + + # These two shouldn't be considered subclasses of CallableMembersProto, however, + # since they don't have the `meth` protocol member + + class EmptyProtocol(Protocol): ... + class UnrelatedProtocol(Protocol): + def wut(self): ... + + self.assertNotIsSubclass(EmptyProtocol, CallableMembersProto) + self.assertNotIsSubclass(UnrelatedProtocol, CallableMembersProto) + + # These aren't protocols at all (despite having annotations), + # so they should only be considered subclasses of CallableMembersProto + # if they *actually have an attribute* matching the `meth` member + # (just having an annotation is insufficient) + + class AnnotatedButNotAProtocol: + meth: Callable[[], None] + + class NotAProtocolButAnImplicitSubclass: + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass2: + meth: Callable[[], None] + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass3: + meth: Callable[[], None] + meth2: Callable[[int, str], bool] + def meth(self): pass + def meth(self, x, y): return True + + self.assertNotIsSubclass(AnnotatedButNotAProtocol, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass2, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass3, CallableMembersProto) + + @skip_if_py312b1 + def test_issubclass_and_isinstance_on_Protocol_itself(self): + class C: + def x(self): pass + + self.assertNotIsSubclass(object, Protocol) + self.assertNotIsInstance(object(), Protocol) + + self.assertNotIsSubclass(str, Protocol) + self.assertNotIsInstance('foo', Protocol) + + self.assertNotIsSubclass(C, Protocol) + self.assertNotIsInstance(C(), Protocol) + + only_classes_allowed = r"issubclass\(\) arg 1 must be a class" + + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, Protocol) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass('foo', Protocol) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(C(), Protocol) + + T = TypeVar('T') + + @runtime_checkable + class EmptyProtocol(Protocol): pass + + @runtime_checkable + class SupportsStartsWith(Protocol): + def startswith(self, x: str) -> bool: ... + + @runtime_checkable + class SupportsX(Protocol[T]): + def x(self): ... + + for proto in EmptyProtocol, SupportsStartsWith, SupportsX: + with self.subTest(proto=proto.__name__): + self.assertIsSubclass(proto, Protocol) + + # gh-105237 / PR #105239: + # check that the presence of Protocol subclasses + # where `issubclass(X, )` evaluates to True + # doesn't influence the result of `issubclass(X, Protocol)` + + self.assertIsSubclass(object, EmptyProtocol) + self.assertIsInstance(object(), EmptyProtocol) + self.assertNotIsSubclass(object, Protocol) + self.assertNotIsInstance(object(), Protocol) + + self.assertIsSubclass(str, SupportsStartsWith) + self.assertIsInstance('foo', SupportsStartsWith) + self.assertNotIsSubclass(str, Protocol) + self.assertNotIsInstance('foo', Protocol) + + self.assertIsSubclass(C, SupportsX) + self.assertIsInstance(C(), SupportsX) + self.assertNotIsSubclass(C, Protocol) + self.assertNotIsInstance(C(), Protocol) + + @skip_if_py312b1 + def test_isinstance_checks_not_at_whim_of_gc(self): + self.addCleanup(gc.enable) + gc.disable() + + with self.assertRaisesRegex( + TypeError, + "Protocols can only inherit from other protocols" + ): + class Foo(collections.abc.Mapping, Protocol): + pass + + self.assertNotIsInstance([], collections.abc.Mapping) + def test_protocols_issubclass_non_callable(self): class C: x = 1 @@ -2490,6 +2713,50 @@ def __subclasshook__(cls, other): self.assertIsSubclass(OKClass, C) self.assertNotIsSubclass(BadClass, C) + @skipIf( + sys.version_info[:4] == (3, 12, 0, 'beta') and sys.version_info[4] < 4, + "Early betas of Python 3.12 had a bug" + ) + def test_custom_subclasshook_2(self): + @runtime_checkable + class HasX(Protocol): + # The presence of a non-callable member + # would mean issubclass() checks would fail with TypeError + # if it weren't for the custom `__subclasshook__` method + x = 1 + + @classmethod + def __subclasshook__(cls, other): + return hasattr(other, 'x') + + class Empty: pass + + class ImplementsHasX: + x = 1 + + self.assertIsInstance(ImplementsHasX(), HasX) + self.assertNotIsInstance(Empty(), HasX) + self.assertIsSubclass(ImplementsHasX, HasX) + self.assertNotIsSubclass(Empty, HasX) + + # isinstance() and issubclass() checks against this still raise TypeError, + # despite the presence of the custom __subclasshook__ method, + # as it's not decorated with @runtime_checkable + class NotRuntimeCheckable(Protocol): + @classmethod + def __subclasshook__(cls, other): + return hasattr(other, 'x') + + must_be_runtime_checkable = ( + "Instance and class checks can only be used " + "with @runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, must_be_runtime_checkable): + issubclass(object, NotRuntimeCheckable) + with self.assertRaisesRegex(TypeError, must_be_runtime_checkable): + isinstance(object(), NotRuntimeCheckable) + @skip_if_py312b1 def test_issubclass_fails_correctly(self): @runtime_checkable @@ -2744,6 +3011,28 @@ def close(self): self.assertIsSubclass(B, Custom) self.assertNotIsSubclass(A, Custom) + @skipUnless( + hasattr(collections.abc, "Buffer"), + "needs collections.abc.Buffer to exist" + ) + @skip_if_py312b1 + def test_collections_abc_buffer_protocol_allowed(self): + @runtime_checkable + class ReleasableBuffer(collections.abc.Buffer, Protocol): + def __release_buffer__(self, mv: memoryview) -> None: ... + + class C: pass + class D: + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + def __release_buffer__(self, mv: memoryview) -> None: + pass + + self.assertIsSubclass(D, ReleasableBuffer) + self.assertIsInstance(D(), ReleasableBuffer) + self.assertNotIsSubclass(C, ReleasableBuffer) + self.assertNotIsInstance(C(), ReleasableBuffer) + def test_builtin_protocol_allowlist(self): with self.assertRaises(TypeError): class CustomProtocol(TestCase, Protocol): @@ -2752,6 +3041,24 @@ class CustomProtocol(TestCase, Protocol): class CustomContextManager(typing.ContextManager, Protocol): pass + @skip_if_py312b1 + def test_typing_extensions_protocol_allowlist(self): + @runtime_checkable + class ReleasableBuffer(Buffer, Protocol): + def __release_buffer__(self, mv: memoryview) -> None: ... + + class C: pass + class D: + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + def __release_buffer__(self, mv: memoryview) -> None: + pass + + self.assertIsSubclass(D, ReleasableBuffer) + self.assertIsInstance(D(), ReleasableBuffer) + self.assertNotIsSubclass(C, ReleasableBuffer) + self.assertNotIsInstance(C(), ReleasableBuffer) + def test_non_runtime_protocol_isinstance_check(self): class P(Protocol): x: int @@ -2824,6 +3131,111 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(Y.__parameters__, ()) self.assertEqual(Y.__args__, (int, bytes, memoryview)) + def test_get_protocol_members(self): + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(object) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(object()) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Protocol) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Generic) + + class P(Protocol): + a: int + def b(self) -> str: ... + @property + def c(self) -> int: ... + + self.assertEqual(get_protocol_members(P), {'a', 'b', 'c'}) + self.assertIsInstance(get_protocol_members(P), frozenset) + self.assertIsNot(get_protocol_members(P), P.__protocol_attrs__) + + class Concrete: + a: int + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Concrete) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Concrete()) + + class ConcreteInherit(P): + a: int = 42 + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(ConcreteInherit) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(ConcreteInherit()) + + @only_with_typing_Protocol + def test_get_protocol_members_typing(self): + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(typing.Protocol) + + class P(typing.Protocol): + a: int + def b(self) -> str: ... + @property + def c(self) -> int: ... + + self.assertEqual(get_protocol_members(P), {'a', 'b', 'c'}) + self.assertIsInstance(get_protocol_members(P), frozenset) + if hasattr(P, "__protocol_attrs__"): + self.assertIsNot(get_protocol_members(P), P.__protocol_attrs__) + + class Concrete: + a: int + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Concrete) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(Concrete()) + + class ConcreteInherit(P): + a: int = 42 + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(ConcreteInherit) + with self.assertRaisesRegex(TypeError, "not a Protocol"): + get_protocol_members(ConcreteInherit()) + + def test_is_protocol(self): + self.assertTrue(is_protocol(Proto)) + self.assertTrue(is_protocol(Point)) + self.assertFalse(is_protocol(Concrete)) + self.assertFalse(is_protocol(Concrete())) + self.assertFalse(is_protocol(Generic)) + self.assertFalse(is_protocol(object)) + + # Protocol is not itself a protocol + self.assertFalse(is_protocol(Protocol)) + + @only_with_typing_Protocol + def test_is_protocol_with_typing(self): + self.assertFalse(is_protocol(typing.Protocol)) + + class TypingProto(typing.Protocol): + a: int + + self.assertTrue(is_protocol(TypingProto)) + + class Concrete(TypingProto): + a: int + + self.assertFalse(is_protocol(Concrete)) + @skip_if_py312b1 def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self): # Ensure the cache is empty, or this test won't work correctly @@ -2848,6 +3260,71 @@ class Foo(typing.Sized, Protocol): pass # before any isinstance() checks against Sized self.assertNotIsInstance(1, typing.Sized) + def test_empty_protocol_decorated_with_final(self): + @final + @runtime_checkable + class EmptyProtocol(Protocol): ... + + self.assertIsSubclass(object, EmptyProtocol) + self.assertIsInstance(object(), EmptyProtocol) + + def test_protocol_decorated_with_final_callable_members(self): + @final + @runtime_checkable + class ProtocolWithMethod(Protocol): + def startswith(self, string: str) -> bool: ... + + self.assertIsSubclass(str, ProtocolWithMethod) + self.assertNotIsSubclass(int, ProtocolWithMethod) + self.assertIsInstance('foo', ProtocolWithMethod) + self.assertNotIsInstance(42, ProtocolWithMethod) + + def test_protocol_decorated_with_final_noncallable_members(self): + @final + @runtime_checkable + class ProtocolWithNonCallableMember(Protocol): + x: int + + class Foo: + x = 42 + + only_callable_members_please = ( + r"Protocols with non-method members don't support issubclass()" + ) + + with self.assertRaisesRegex(TypeError, only_callable_members_please): + issubclass(Foo, ProtocolWithNonCallableMember) + + with self.assertRaisesRegex(TypeError, only_callable_members_please): + issubclass(int, ProtocolWithNonCallableMember) + + self.assertIsInstance(Foo(), ProtocolWithNonCallableMember) + self.assertNotIsInstance(42, ProtocolWithNonCallableMember) + + def test_protocol_decorated_with_final_mixed_members(self): + @final + @runtime_checkable + class ProtocolWithMixedMembers(Protocol): + x: int + def method(self) -> None: ... + + class Foo: + x = 42 + def method(self) -> None: ... + + only_callable_members_please = ( + r"Protocols with non-method members don't support issubclass()" + ) + + with self.assertRaisesRegex(TypeError, only_callable_members_please): + issubclass(Foo, ProtocolWithMixedMembers) + + with self.assertRaisesRegex(TypeError, only_callable_members_please): + issubclass(int, ProtocolWithMixedMembers) + + self.assertIsInstance(Foo(), ProtocolWithMixedMembers) + self.assertNotIsInstance(42, ProtocolWithMixedMembers) + class Point2DGeneric(Generic[T], TypedDict): a: T @@ -2863,8 +3340,7 @@ class BarGeneric(FooGeneric[T], total=False): class TypedDictTests(BaseTestCase): - - def test_basics_iterable_syntax(self): + def test_basics_functional_syntax(self): Emp = TypedDict('Emp', {'name': str, 'id': int}) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) @@ -2879,6 +3355,12 @@ def test_basics_iterable_syntax(self): self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) + @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) + + @skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs") def test_basics_keywords_syntax(self): with self.assertWarns(DeprecationWarning): Emp = TypedDict('Emp', name=str, id=int) @@ -2895,6 +3377,7 @@ def test_basics_keywords_syntax(self): self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) + @skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs") def test_typeddict_special_keyword_names(self): with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, @@ -2911,7 +3394,6 @@ def test_typeddict_special_keyword_names(self): self.assertEqual(a['fields'], [('bar', tuple)]) self.assertEqual(a['_fields'], {'baz', set}) - @skipIf(hasattr(typing, 'TypedDict'), "Should be tested by upstream") def test_typeddict_create_errors(self): with self.assertRaises(TypeError): TypedDict.__new__() @@ -2921,18 +3403,13 @@ def test_typeddict_create_errors(self): TypedDict('Emp', [('name', str)], None) with self.assertWarns(DeprecationWarning): - Emp = TypedDict(_typename='Emp', name=str, id=int) - self.assertEqual(Emp.__name__, 'Emp') - self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) - - with self.assertWarns(DeprecationWarning): - Emp = TypedDict('Emp', _fields={'name': str, 'id': int}) + 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, 12): + if sys.version_info >= (3, 13): self.assertEqual(TypedDict.__module__, 'typing') else: self.assertEqual(TypedDict.__module__, 'typing_extensions') @@ -2955,7 +3432,7 @@ def test_typeddict_errors(self): def test_py36_class_syntax_usage(self): self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D') self.assertEqual(LabelPoint2D.__module__, __name__) - self.assertEqual(get_type_hints(LabelPoint2D), {'x': int, 'y': int, 'label': str}) + self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str}) self.assertEqual(LabelPoint2D.__bases__, (dict,)) self.assertEqual(LabelPoint2D.__total__, True) self.assertNotIsSubclass(LabelPoint2D, typing.Sequence) @@ -2967,11 +3444,9 @@ def test_py36_class_syntax_usage(self): def test_pickle(self): global EmpD # pickle wants to reference the class by name - EmpD = TypedDict('EmpD', {"name": str, "id": int}) + EmpD = TypedDict('EmpD', {'name': str, 'id': int}) jane = EmpD({'name': 'jane', 'id': 37}) - point = Point2DGeneric(a=5.0, b=3.0) for proto in range(pickle.HIGHEST_PROTOCOL + 1): - # Test non-generic TypedDict z = pickle.dumps(jane, proto) jane2 = pickle.loads(z) self.assertEqual(jane2, jane) @@ -2979,17 +3454,20 @@ def test_pickle(self): ZZ = pickle.dumps(EmpD, proto) EmpDnew = pickle.loads(ZZ) self.assertEqual(EmpDnew({'name': 'jane', 'id': 37}), jane) - # and generic TypedDict - y = pickle.dumps(point, proto) - point2 = pickle.loads(y) - self.assertEqual(point, point2) + + def test_pickle_generic(self): + point = Point2DGeneric(a=5.0, b=3.0) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + z = pickle.dumps(point, proto) + point2 = pickle.loads(z) + self.assertEqual(point2, point) self.assertEqual(point2, {'a': 5.0, 'b': 3.0}) - YY = pickle.dumps(Point2DGeneric, proto) - Point2DGenericNew = pickle.loads(YY) + ZZ = pickle.dumps(Point2DGeneric, proto) + Point2DGenericNew = pickle.loads(ZZ) self.assertEqual(Point2DGenericNew({'a': 5.0, 'b': 3.0}), point) def test_optional(self): - EmpD = TypedDict('EmpD', {"name": str, "id": int}) + EmpD = TypedDict('EmpD', {'name': str, 'id': int}) self.assertEqual(typing.Optional[EmpD], typing.Union[None, EmpD]) self.assertNotEqual(typing.List[EmpD], typing.Tuple[EmpD]) @@ -3009,25 +3487,30 @@ def test_total(self): self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'}) def test_optional_keys(self): + class Point2Dor3D(Point2D, total=False): + z: int + assert Point2Dor3D.__required_keys__ == frozenset(['x', 'y']) assert Point2Dor3D.__optional_keys__ == frozenset(['z']) - def test_required_notrequired_keys(self): - assert NontotalMovie.__required_keys__ == frozenset({'title'}) - assert NontotalMovie.__optional_keys__ == frozenset({'year'}) + def test_keys_inheritance(self): + class BaseAnimal(TypedDict): + name: str - assert TotalMovie.__required_keys__ == frozenset({'title'}) - assert TotalMovie.__optional_keys__ == frozenset({'year'}) + class Animal(BaseAnimal, total=False): + voice: str + tail: bool + class Cat(Animal): + fur_color: str - def test_keys_inheritance(self): assert BaseAnimal.__required_keys__ == frozenset(['name']) assert BaseAnimal.__optional_keys__ == frozenset([]) - assert get_type_hints(BaseAnimal) == {'name': str} + assert BaseAnimal.__annotations__ == {'name': str} assert Animal.__required_keys__ == frozenset(['name']) assert Animal.__optional_keys__ == frozenset(['tail', 'voice']) - assert get_type_hints(Animal) == { + assert Animal.__annotations__ == { 'name': str, 'tail': bool, 'voice': str, @@ -3035,19 +3518,168 @@ def test_keys_inheritance(self): assert Cat.__required_keys__ == frozenset(['name', 'fur_color']) assert Cat.__optional_keys__ == frozenset(['tail', 'voice']) - assert get_type_hints(Cat) == { + assert Cat.__annotations__ == { 'fur_color': str, 'name': str, 'tail': bool, 'voice': str, } + def test_required_notrequired_keys(self): + self.assertEqual(NontotalMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(NontotalMovie.__optional_keys__, + frozenset({"year"})) + + self.assertEqual(TotalMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(TotalMovie.__optional_keys__, + frozenset({"year"})) + + self.assertEqual(VeryAnnotated.__required_keys__, + frozenset()) + self.assertEqual(VeryAnnotated.__optional_keys__, + frozenset({"a"})) + + self.assertEqual(AnnotatedMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(AnnotatedMovie.__optional_keys__, + frozenset({"year"})) + + self.assertEqual(WeirdlyQuotedMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(WeirdlyQuotedMovie.__optional_keys__, + frozenset({"year"})) + + self.assertEqual(ChildTotalMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(ChildTotalMovie.__optional_keys__, + frozenset({"year"})) + + self.assertEqual(ChildDeeplyAnnotatedMovie.__required_keys__, + frozenset({"title"})) + self.assertEqual(ChildDeeplyAnnotatedMovie.__optional_keys__, + frozenset({"year"})) + + def test_multiple_inheritance(self): + class One(TypedDict): + one: int + class Two(TypedDict): + two: str + class Untotal(TypedDict, total=False): + untotal: str + Inline = TypedDict('Inline', {'inline': bool}) + class Regular: + pass + + class Child(One, Two): + child: bool + self.assertEqual( + Child.__required_keys__, + frozenset(['one', 'two', 'child']), + ) + self.assertEqual( + Child.__optional_keys__, + frozenset([]), + ) + self.assertEqual( + Child.__annotations__, + {'one': int, 'two': str, 'child': bool}, + ) + + class ChildWithOptional(One, Untotal): + child: bool + self.assertEqual( + ChildWithOptional.__required_keys__, + frozenset(['one', 'child']), + ) + self.assertEqual( + ChildWithOptional.__optional_keys__, + frozenset(['untotal']), + ) + self.assertEqual( + ChildWithOptional.__annotations__, + {'one': int, 'untotal': str, 'child': bool}, + ) + + class ChildWithTotalFalse(One, Untotal, total=False): + child: bool + self.assertEqual( + ChildWithTotalFalse.__required_keys__, + frozenset(['one']), + ) + self.assertEqual( + ChildWithTotalFalse.__optional_keys__, + frozenset(['untotal', 'child']), + ) + self.assertEqual( + ChildWithTotalFalse.__annotations__, + {'one': int, 'untotal': str, 'child': bool}, + ) + + class ChildWithInlineAndOptional(Untotal, Inline): + child: bool + self.assertEqual( + ChildWithInlineAndOptional.__required_keys__, + frozenset(['inline', 'child']), + ) + self.assertEqual( + ChildWithInlineAndOptional.__optional_keys__, + frozenset(['untotal']), + ) + self.assertEqual( + ChildWithInlineAndOptional.__annotations__, + {'inline': bool, 'untotal': str, 'child': bool}, + ) + + wrong_bases = [ + (One, Regular), + (Regular, One), + (One, Two, Regular), + (Inline, Regular), + (Untotal, Regular), + ] + for bases in wrong_bases: + with self.subTest(bases=bases): + with self.assertRaisesRegex( + TypeError, + 'cannot inherit from both a TypedDict type and a non-TypedDict', + ): + class Wrong(*bases): + pass + def test_is_typeddict(self): - assert is_typeddict(Point2D) is True - assert is_typeddict(Point2Dor3D) is True - assert is_typeddict(Union[str, int]) is False + self.assertIs(is_typeddict(Point2D), True) + self.assertIs(is_typeddict(Point2Dor3D), True) + self.assertIs(is_typeddict(Union[str, int]), False) # classes, not instances - assert is_typeddict(Point2D()) is False + self.assertIs(is_typeddict(Point2D()), False) + call_based = TypedDict('call_based', {'a': int}) + self.assertIs(is_typeddict(call_based), True) + self.assertIs(is_typeddict(call_based()), False) + + T = TypeVar("T") + class BarGeneric(TypedDict, Generic[T]): + a: T + self.assertIs(is_typeddict(BarGeneric), True) + self.assertIs(is_typeddict(BarGeneric[int]), False) + self.assertIs(is_typeddict(BarGeneric()), False) + + if hasattr(typing, "TypeAliasType"): + ns = {"TypedDict": TypedDict} + exec("""if True: + class NewGeneric[T](TypedDict): + a: T + """, ns) + NewGeneric = ns["NewGeneric"] + self.assertIs(is_typeddict(NewGeneric), True) + self.assertIs(is_typeddict(NewGeneric[int]), False) + self.assertIs(is_typeddict(NewGeneric()), False) + + # The TypedDict constructor is not itself a TypedDict + self.assertIs(is_typeddict(TypedDict), False) + if hasattr(typing, "TypedDict"): + self.assertIs(is_typeddict(typing.TypedDict), False) @skipUnless(TYPING_3_8_0, "Python 3.8+ required") def test_is_typeddict_against_typeddict_from_typing(self): @@ -3086,6 +3718,24 @@ class FooBarGeneric(BarGeneric[int]): {'a': typing.Optional[T], 'b': int, 'c': str} ) + @skipUnless(TYPING_3_12_0, "PEP 695 required") + def test_pep695_generic_typeddict(self): + ns = {"TypedDict": TypedDict} + exec("""if True: + class A[T](TypedDict): + a: T + """, ns) + A = ns["A"] + T, = A.__type_params__ + self.assertIsInstance(T, TypeVar) + self.assertEqual(T.__name__, 'T') + self.assertEqual(A.__bases__, (Generic, dict)) + self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) + self.assertEqual(A.__mro__, (A, Generic, dict, object)) + self.assertEqual(A.__parameters__, (T,)) + self.assertEqual(A[str].__parameters__, ()) + self.assertEqual(A[str].__args__, (str,)) + def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T @@ -3151,11 +3801,11 @@ class Point3D(Point2DGeneric[T], Generic[T, KT]): self.assertEqual(Point3D.__total__, True) self.assertEqual(Point3D.__optional_keys__, frozenset()) self.assertEqual(Point3D.__required_keys__, frozenset(['a', 'b', 'c'])) - assert Point3D.__annotations__ == { + self.assertEqual(Point3D.__annotations__, { 'a': T, 'b': T, 'c': KT, - } + }) self.assertEqual(Point3D[int, str].__origin__, Point3D) with self.assertRaises(TypeError): @@ -3190,6 +3840,103 @@ 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.7 and 3.8.) + class TD(TypedDict): + a: T + A = TD[int] + self.assertEqual(A.__origin__, TD) + self.assertEqual(A.__parameters__, ()) + self.assertEqual(A.__args__, (int,)) + a = A(a=1) + self.assertIs(type(a), dict) + self.assertEqual(a, {'a': 1}) + + def test_orig_bases(self): + T = TypeVar('T') + + class Parent(TypedDict): + pass + + class Child(Parent): + pass + + class OtherChild(Parent): + pass + + class MixedChild(Child, OtherChild, Parent): + pass + + class GenericParent(TypedDict, Generic[T]): + pass + + class GenericChild(GenericParent[int]): + pass + + class OtherGenericChild(GenericParent[str]): + pass + + class MixedGenericChild(GenericChild, OtherGenericChild, GenericParent[float]): + pass + + class MultipleGenericBases(GenericParent[int], GenericParent[float]): + pass + + CallTypedDict = TypedDict('CallTypedDict', {}) + + self.assertEqual(Parent.__orig_bases__, (TypedDict,)) + self.assertEqual(Child.__orig_bases__, (Parent,)) + self.assertEqual(OtherChild.__orig_bases__, (Parent,)) + self.assertEqual(MixedChild.__orig_bases__, (Child, OtherChild, Parent,)) + self.assertEqual(GenericParent.__orig_bases__, (TypedDict, Generic[T])) + self.assertEqual(GenericChild.__orig_bases__, (GenericParent[int],)) + self.assertEqual(OtherGenericChild.__orig_bases__, (GenericParent[str],)) + self.assertEqual(MixedGenericChild.__orig_bases__, (GenericChild, OtherGenericChild, GenericParent[float])) + self.assertEqual(MultipleGenericBases.__orig_bases__, (GenericParent[int], GenericParent[float])) + self.assertEqual(CallTypedDict.__orig_bases__, (TypedDict,)) + + def test_zero_fields_typeddicts(self): + T1 = TypedDict("T1", {}) + class T2(TypedDict): pass + try: + ns = {"TypedDict": TypedDict} + exec("class T3[tvar](TypedDict): pass", ns) + T3 = ns["T3"] + except SyntaxError: + class T3(TypedDict): pass + S = TypeVar("S") + class T4(TypedDict, Generic[S]): pass + + expected_warning = re.escape( + "Failing to pass a value for the 'fields' parameter 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. `T5 = TypedDict('T5', {})`." + ) + with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): + T5 = TypedDict('T5') + + expected_warning = re.escape( + "Passing `None` as the 'fields' parameter 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. `T6 = TypedDict('T6', {})`." + ) + with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): + T6 = TypedDict('T6', None) + + for klass in T1, T2, T3, T4, T5, T6: + with self.subTest(klass=klass.__name__): + self.assertEqual(klass.__annotations__, {}) + self.assertEqual(klass.__required_keys__, set()) + self.assertEqual(klass.__optional_keys__, set()) + self.assertIsInstance(klass(), dict) + class AnnotatedTests(BaseTestCase): @@ -4247,6 +4994,20 @@ class CustomerModel(ModelBase, init=False): class AllTests(BaseTestCase): + def test_drop_in_for_typing(self): + # Check that the typing_extensions.__all__ is a superset of + # typing.__all__. + t_all = set(typing.__all__) + te_all = set(typing_extensions.__all__) + exceptions = {"ByteString"} + self.assertGreaterEqual(te_all, t_all - exceptions) + # Deprecated, to be removed in 3.14 + self.assertFalse(hasattr(typing_extensions, "ByteString")) + # These were never included in `typing.__all__`, + # and have been removed in Python 3.13 + self.assertNotIn('re', te_all) + self.assertNotIn('io', te_all) + def test_typing_extensions_includes_standard(self): a = typing_extensions.__all__ self.assertIn('ClassVar', a) @@ -4323,10 +5084,12 @@ def test_typing_extensions_defers_when_possible(self): exclude |= {'final', 'Any', 'NewType'} if sys.version_info < (3, 12): exclude |= { - 'Protocol', 'runtime_checkable', 'SupportsAbs', 'SupportsBytes', + 'Protocol', 'SupportsAbs', 'SupportsBytes', 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', - 'SupportsRound', 'TypedDict', 'is_typeddict', 'NamedTuple', 'Unpack', + 'SupportsRound', 'Unpack', } + if sys.version_info < (3, 13): + exclude |= {'NamedTuple', 'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): self.assertIs( @@ -4546,21 +5309,47 @@ class Group(NamedTuple): self.assertFalse(hasattr(Group, attr)) def test_namedtuple_keyword_usage(self): - LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) + with self.assertWarnsRegex( + DeprecationWarning, + "Creating NamedTuple classes using keyword arguments is deprecated" + ): + LocalEmployee = NamedTuple("LocalEmployee", name=str, age=int) + nick = LocalEmployee('Nick', 25) self.assertIsInstance(nick, tuple) self.assertEqual(nick.name, 'Nick') self.assertEqual(LocalEmployee.__name__, 'LocalEmployee') self.assertEqual(LocalEmployee._fields, ('name', 'age')) self.assertEqual(LocalEmployee.__annotations__, dict(name=str, age=int)) + with self.assertRaisesRegex( TypeError, - 'Either list of fields or keywords can be provided to NamedTuple, not both' + "Either list of fields or keywords can be provided to NamedTuple, not both" ): NamedTuple('Name', [('x', int)], y=str) + with self.assertRaisesRegex( + TypeError, + "Either list of fields or keywords can be provided to NamedTuple, not both" + ): + NamedTuple('Name', [], y=str) + + with self.assertRaisesRegex( + TypeError, + ( + r"Cannot pass `None` as the 'fields' parameter " + r"and also specify fields using keyword arguments" + ) + ): + NamedTuple('Name', None, x=int) + def test_namedtuple_special_keyword_names(self): - NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list) + with self.assertWarnsRegex( + DeprecationWarning, + "Creating NamedTuple classes using keyword arguments is deprecated" + ): + NT = NamedTuple("NT", cls=type, self=object, typename=str, fields=list) + self.assertEqual(NT.__name__, 'NT') self.assertEqual(NT._fields, ('cls', 'self', 'typename', 'fields')) a = NT(cls=str, self=42, typename='foo', fields=[('bar', tuple)]) @@ -4570,12 +5359,32 @@ def test_namedtuple_special_keyword_names(self): self.assertEqual(a.fields, [('bar', tuple)]) def test_empty_namedtuple(self): - NT = NamedTuple('NT') + expected_warning = re.escape( + "Failing to pass a value for the 'fields' parameter is deprecated " + "and will be disallowed in Python 3.15. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. `NT1 = NamedTuple('NT1', [])`." + ) + with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): + NT1 = NamedTuple('NT1') + + expected_warning = re.escape( + "Passing `None` as the 'fields' parameter is deprecated " + "and will be disallowed in Python 3.15. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. `NT2 = NamedTuple('NT2', [])`." + ) + with self.assertWarnsRegex(DeprecationWarning, fr"^{expected_warning}$"): + NT2 = NamedTuple('NT2', None) + + NT3 = NamedTuple('NT2', []) class CNT(NamedTuple): pass # empty body - for struct in [NT, CNT]: + for struct in NT1, NT2, NT3, CNT: with self.subTest(struct=struct): self.assertEqual(struct._fields, ()) self.assertEqual(struct.__annotations__, {}) @@ -4618,7 +5427,6 @@ def test_copy_and_pickle(self): self.assertIsInstance(jane2, cls) def test_docstring(self): - self.assertEqual(NamedTuple.__doc__, typing.NamedTuple.__doc__) self.assertIsInstance(NamedTuple.__doc__, str) @skipUnless(TYPING_3_8_0, "NamedTuple had a bad signature on <=3.7") diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 1b92c396..901f3b96 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -65,8 +65,10 @@ 'get_args', 'get_origin', 'get_original_bases', + 'get_protocol_members', 'get_type_hints', 'IntVar', + 'is_protocol', 'is_typeddict', 'Literal', 'NewType', @@ -85,6 +87,45 @@ 'NoReturn', 'Required', 'NotRequired', + + # Pure aliases, have always been in typing + 'AbstractSet', + 'AnyStr', + 'BinaryIO', + 'Callable', + 'Collection', + 'Container', + 'Dict', + 'ForwardRef', + 'FrozenSet', + 'Generator', + 'Generic', + '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', ] # for backward compatibility @@ -201,17 +242,19 @@ def __new__(cls, *args, **kwargs): ClassVar = typing.ClassVar + +class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + # On older versions of typing there is an internal class named "Final". # 3.8+ if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): Final = typing.Final # 3.7 else: - class _FinalForm(typing._SpecialForm, _root=True): - - def __repr__(self): - return 'typing_extensions.' + self._name - + class _FinalForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -303,14 +346,11 @@ def __eq__(self, other): def __hash__(self): return hash(frozenset(_value_and_type_iter(self.__args__))) - class _LiteralForm(typing._SpecialForm, _root=True): + class _LiteralForm(_ExtensionsSpecialForm, _root=True): def __init__(self, doc: str): self._name = 'Literal' self._doc = self.__doc__ = doc - def __repr__(self): - return 'typing_extensions.' + self._name - def __getitem__(self, parameters): if not isinstance(parameters, tuple): parameters = (parameters,) @@ -453,9 +493,10 @@ def clear_overloads(): _PROTO_ALLOWLIST = { 'collections.abc': [ 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', - 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', 'Buffer', ], 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], + 'typing_extensions': ['Buffer'], } @@ -545,7 +586,6 @@ def _caller(depth=2): # so we backport the 3.12 version of Protocol to Python <=3.11 if sys.version_info >= (3, 12): Protocol = typing.Protocol - runtime_checkable = typing.runtime_checkable else: def _allow_reckless_class_checks(depth=3): """Allow instance and class checks for special stdlib modules. @@ -558,11 +598,41 @@ def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') - class _ProtocolMeta(abc.ABCMeta): + if sys.version_info >= (3, 8): + # 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" + _typing_Protocol = typing.Protocol + _ProtocolMetaBase = type(_typing_Protocol) + else: + _typing_Protocol = _marker + _ProtocolMetaBase = abc.ABCMeta + + class _ProtocolMeta(_ProtocolMetaBase): # This metaclass is somewhat unfortunate, # 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 + # and those are slow + def __new__(mcls, name, bases, namespace, **kwargs): + if name == "Protocol" and len(bases) < 2: + pass + elif {Protocol, _typing_Protocol} & set(bases): + for base in bases: + if not ( + base in {object, typing.Generic, Protocol, _typing_Protocol} + or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, []) + or is_protocol(base) + ): + raise TypeError( + f"Protocols can only inherit from other protocols, " + f"got {base!r}" + ) + return abc.ABCMeta.__new__(mcls, name, bases, namespace, **kwargs) + def __init__(cls, *args, **kwargs): - super().__init__(*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() @@ -572,14 +642,19 @@ def __init__(cls, *args, **kwargs): ) def __subclasscheck__(cls, other): - if not isinstance(other, type): - # Same error message as for issubclass(1, int). - raise TypeError('issubclass() arg 1 must be a class') + if cls is Protocol: + return type.__subclasscheck__(cls, other) if ( getattr(cls, '_is_protocol', False) and not _allow_reckless_class_checks() ): - if not cls.__callable_proto_members_only__: + 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()" ) @@ -588,14 +663,16 @@ def __subclasscheck__(cls, other): "Instance and class checks can only be used with " "@runtime_checkable protocols" ) - return super().__subclasscheck__(other) + return abc.ABCMeta.__subclasscheck__(cls, other) def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. + if cls is Protocol: + return type.__instancecheck__(cls, instance) if not getattr(cls, "_is_protocol", False): # i.e., it's a concrete subclass of a protocol - return super().__instancecheck__(instance) + return abc.ABCMeta.__instancecheck__(cls, instance) if ( not getattr(cls, '_is_runtime_protocol', False) and @@ -604,7 +681,7 @@ def __instancecheck__(cls, instance): raise TypeError("Instance and class checks can only be used with" " @runtime_checkable protocols") - if super().__instancecheck__(instance): + if abc.ABCMeta.__instancecheck__(cls, instance): return True for attr in cls.__protocol_attrs__: @@ -623,7 +700,7 @@ def __eq__(cls, other): # Hack so that typing.Generic.__class_getitem__ # treats typing_extensions.Protocol # as equivalent to typing.Protocol on Python 3.8+ - if super().__eq__(other) is True: + if abc.ABCMeta.__eq__(cls, other) is True: return True return ( cls is Protocol and other is getattr(typing, "Protocol", object()) @@ -653,23 +730,13 @@ def _proto_hook(cls, other): if ( isinstance(annotations, collections.abc.Mapping) and attr in annotations - and issubclass(other, (typing.Generic, _ProtocolMeta)) - and getattr(other, "_is_protocol", False) + and is_protocol(other) ): break else: return NotImplemented return True - def _check_proto_bases(cls): - for base in cls.__bases__: - if not (base in (object, typing.Generic) or - base.__module__ in _PROTO_ALLOWLIST and - base.__name__ in _PROTO_ALLOWLIST[base.__module__] or - isinstance(base, _ProtocolMeta) and base._is_protocol): - raise TypeError('Protocols can only inherit from other' - f' protocols, got {repr(base)}') - if sys.version_info >= (3, 8): class Protocol(typing.Generic, metaclass=_ProtocolMeta): __doc__ = typing.Protocol.__doc__ @@ -688,13 +755,8 @@ def __init_subclass__(cls, *args, **kwargs): if '__subclasshook__' not in cls.__dict__: cls.__subclasshook__ = _proto_hook - # We have nothing more to do for non-protocols... - if not cls._is_protocol: - return - - # ... otherwise check consistency of bases, and prohibit instantiation. - _check_proto_bases(cls) - if cls.__init__ is Protocol.__init__: + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: cls.__init__ = _no_init else: @@ -784,15 +846,14 @@ def __init_subclass__(cls, *args, **kwargs): if '__subclasshook__' not in cls.__dict__: cls.__subclasshook__ = _proto_hook - # We have nothing more to do for non-protocols. - if not cls._is_protocol: - return - - # Check consistency of bases. - _check_proto_bases(cls) - if cls.__init__ is Protocol.__init__: + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: cls.__init__ = _no_init + +if sys.version_info >= (3, 8): + runtime_checkable = typing.runtime_checkable +else: def runtime_checkable(cls): """Mark a protocol class as a runtime protocol, so that it can be used with isinstance() and issubclass(). Raise TypeError @@ -892,7 +953,22 @@ def __round__(self, ndigits: int = 0) -> T_co: pass -if sys.version_info >= (3, 12): +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 sys.version_info >= (3, 13): # 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" @@ -902,117 +978,61 @@ def __round__(self, ndigits: int = 0) -> T_co: # Generic TypedDicts are also impossible using typing.TypedDict on Python <3.11. # 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. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict else: - def _check_fails(cls, other): - try: - if _caller() not in {'abc', 'functools', 'typing'}: - # Typed dicts are only for static structural subtyping. - raise TypeError('TypedDict does not support instance and class checks') - except (AttributeError, ValueError): - pass - return False - - def _dict_new(*args, **kwargs): - if not args: - raise TypeError('TypedDict.__new__(): not enough arguments') - _, args = args[0], args[1:] # allow the "cls" keyword be passed - return dict(*args, **kwargs) - - _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)' - - def _typeddict_new(*args, total=True, **kwargs): - if not args: - raise TypeError('TypedDict.__new__(): not enough arguments') - _, args = args[0], args[1:] # allow the "cls" keyword be passed - if args: - typename, args = args[0], args[1:] # allow the "_typename" keyword be passed - elif '_typename' in kwargs: - typename = kwargs.pop('_typename') - warnings.warn("Passing '_typename' as keyword argument is deprecated", - DeprecationWarning, stacklevel=2) - else: - raise TypeError("TypedDict.__new__() missing 1 required positional " - "argument: '_typename'") - if args: - try: - fields, = args # allow the "_fields" keyword be passed - except ValueError: - raise TypeError('TypedDict.__new__() takes from 2 to 3 ' - f'positional arguments but {len(args) + 2} ' - 'were given') - elif '_fields' in kwargs and len(kwargs) == 1: - fields = kwargs.pop('_fields') - warnings.warn("Passing '_fields' as keyword argument is deprecated", - DeprecationWarning, stacklevel=2) - else: - fields = None - - if fields is None: - fields = kwargs - elif kwargs: - raise TypeError("TypedDict takes either a dict or keyword arguments," - " but not both") - - if kwargs: - warnings.warn( - "The kwargs-based syntax for TypedDict definitions is deprecated, " - "may be removed in a future version, and may not be " - "understood by third-party type checkers.", - DeprecationWarning, - stacklevel=2, - ) + # 3.10.0 and later + _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters - 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 + if sys.version_info >= (3, 8): + _fake_name = "Protocol" + else: + _fake_name = "_Protocol" - return _TypedDictMeta(typename, (), ns, total=total) + class _TypedDictMeta(type): + def __new__(cls, name, bases, ns, total=True): + """Create new typed dict class object. - _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,' - ' /, *, total=True, **kwargs)') + This method is called when TypedDict is subclassed, + or when TypedDict is instantiated. This way + TypedDict supports all three syntax forms described in its docstring. + Subclasses and instances of TypedDict return actual dictionaries. + """ + for base in bases: + if type(base) is not _TypedDictMeta and base is not typing.Generic: + raise TypeError('cannot inherit from both a TypedDict type ' + 'and a non-TypedDict base class') - _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters + if any(issubclass(b, typing.Generic) for b in bases): + generic_base = (typing.Generic,) + else: + generic_base = () - class _TypedDictMeta(type): - def __init__(cls, name, bases, ns, total=True): - super().__init__(name, bases, ns) + # typing.py generally doesn't let you inherit from plain Generic, unless + # the name of the class happens to be "Protocol" (or "_Protocol" on 3.7). + tp_dict = type.__new__(_TypedDictMeta, _fake_name, (*generic_base, dict), ns) + tp_dict.__name__ = name + if tp_dict.__qualname__ == _fake_name: + tp_dict.__qualname__ = name - def __new__(cls, name, bases, ns, total=True): - # Create new typed dict class object. - # This method is called directly when TypedDict is subclassed, - # or via _typeddict_new when TypedDict is instantiated. This way - # TypedDict supports all three syntaxes described in its docstring. - # Subclasses and instances of TypedDict return actual dictionaries - # via _dict_new. - ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new - # Don't insert typing.Generic into __bases__ here, - # or Generic.__init_subclass__ will raise TypeError - # in the super().__new__() call. - # Instead, monkey-patch __bases__ onto the class after it's been created. - tp_dict = super().__new__(cls, name, (dict,), ns) - - is_generic = any(issubclass(base, typing.Generic) for base in bases) - - if is_generic: - tp_dict.__bases__ = (typing.Generic, dict) - _maybe_adjust_parameters(tp_dict) - else: - # generic TypedDicts get __orig_bases__ from Generic - tp_dict.__orig_bases__ = bases or (TypedDict,) + if not hasattr(tp_dict, '__orig_bases__'): + tp_dict.__orig_bases__ = bases annotations = {} own_annotations = ns.get('__annotations__', {}) msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" - kwds = {"module": tp_dict.__module__} if _TAKES_MODULE else {} - own_annotations = { - n: typing._type_check(tp, msg, **kwds) - for n, tp in own_annotations.items() - } + if _TAKES_MODULE: + own_annotations = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own_annotations.items() + } + else: + own_annotations = { + n: typing._type_check(tp, msg) + for n, tp in own_annotations.items() + } required_keys = set() optional_keys = set() @@ -1046,17 +1066,25 @@ def __new__(cls, name, bases, ns, total=True): tp_dict.__total__ = total return tp_dict - __instancecheck__ = __subclasscheck__ = _check_fails + __call__ = dict # static method - TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) - TypedDict.__module__ = __name__ - TypedDict.__doc__ = \ - """A simple typed name space. At runtime it is equivalent to a plain dict. + def __subclasscheck__(cls, other): + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') - TypedDict creates a dictionary type that expects all of its - instances to have a certain set of keys, with each key + __instancecheck__ = __subclasscheck__ + + _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) + + @_ensure_subclassable(lambda bases: (_TypedDict,)) + def TypedDict(__typename, __fields=_marker, *, total=True, **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 + instances to have a certain set of keys, where each key is associated with a value of a consistent type. This expectation - is not checked at runtime but is only enforced by type checkers. + is not checked at runtime. + Usage:: class Point2D(TypedDict): @@ -1071,14 +1099,66 @@ class Point2D(TypedDict): The type info can be accessed via the Point2D.__annotations__ dict, and the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. - TypedDict supports two additional equivalent forms:: + TypedDict supports an additional equivalent form:: - Point2D = TypedDict('Point2D', x=int, y=int, label=str) Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) - The class syntax is only supported in Python 3.6+, while two other - syntax forms work for Python 2.7 and 3.2+ + By default, all keys must be present in a TypedDict. It is possible + to override this by specifying totality:: + + class Point2D(TypedDict, total=False): + x: int + y: int + + This means that a Point2D TypedDict can have any of the keys omitted. A type + checker is only expected to support a literal False or True as the value of + the total argument. True is the default, and makes all items defined in the + class body be required. + + The Required and NotRequired special forms can also be used to mark + individual keys as being required or not required:: + + class Point2D(TypedDict): + x: int # the "x" key must always be present (Required is the default) + y: NotRequired[int] # the "y" key can be omitted + + 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) + __fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") + if kwargs: + 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() + 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) + td.__orig_bases__ = (TypedDict,) + return td if hasattr(typing, "_TypedDictMeta"): _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) @@ -1096,7 +1176,10 @@ class Film(TypedDict): is_typeddict(Film) # => True is_typeddict(Union[list, str]) # => False """ - return isinstance(tp, tuple(_TYPEDDICT_TYPES)) + # On 3.8, this would otherwise return True + if hasattr(typing, "TypedDict") and tp is typing.TypedDict: + return False + return isinstance(tp, _TYPEDDICT_TYPES) if hasattr(typing, "assert_type"): @@ -1366,11 +1449,7 @@ def get_args(tp): TypeAlias = typing.TypeAlias # 3.9 elif sys.version_info[:2] >= (3, 9): - class _TypeAliasForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - @_TypeAliasForm + @_ExtensionsSpecialForm def TypeAlias(self, parameters): """Special marker indicating that an assignment should be recognized as a proper type alias definition by type @@ -1385,21 +1464,19 @@ def TypeAlias(self, parameters): raise TypeError(f"{self} is not subscriptable") # 3.7-3.8 else: - class _TypeAliasForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - TypeAlias = _TypeAliasForm('TypeAlias', - doc="""Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. + TypeAlias = _ExtensionsSpecialForm( + 'TypeAlias', + doc="""Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. - For example:: + For example:: - Predicate: TypeAlias = Callable[..., bool] + Predicate: TypeAlias = Callable[..., bool] - It's invalid when used anywhere except as in the example - above.""") + It's invalid when used anywhere except as in the example + above.""" + ) def _set_default(type_param, default): @@ -1714,7 +1791,7 @@ def _concatenate_getitem(self, parameters): _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811 # 3.9 elif sys.version_info[:2] >= (3, 9): - @_TypeAliasForm + @_ExtensionsSpecialForm def Concatenate(self, parameters): """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a higher order function which adds, removes or transforms parameters of a @@ -1729,10 +1806,7 @@ def Concatenate(self, parameters): return _concatenate_getitem(self, parameters) # 3.7-8 else: - class _ConcatenateForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - + class _ConcatenateForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): return _concatenate_getitem(self, parameters) @@ -1754,11 +1828,7 @@ def __getitem__(self, parameters): TypeGuard = typing.TypeGuard # 3.9 elif sys.version_info[:2] >= (3, 9): - class _TypeGuardForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - - @_TypeGuardForm + @_ExtensionsSpecialForm def TypeGuard(self, parameters): """Special typing form used to annotate the return type of a user-defined type guard function. ``TypeGuard`` only accepts a single type argument. @@ -1806,11 +1876,7 @@ def is_str(val: Union[str, float]): return typing._GenericAlias(self, (item,)) # 3.7-3.8 else: - class _TypeGuardForm(typing._SpecialForm, _root=True): - - def __repr__(self): - return 'typing_extensions.' + self._name - + class _TypeGuardForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type') @@ -1984,10 +2050,6 @@ def int_or_str(arg: int | str) -> None: Required = typing.Required NotRequired = typing.NotRequired elif sys.version_info[:2] >= (3, 9): - class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - @_ExtensionsSpecialForm def Required(self, parameters): """A special typing construct to mark a key of a total=False TypedDict @@ -2026,10 +2088,7 @@ class Movie(TypedDict): return typing._GenericAlias(self, (item,)) else: - class _RequiredForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - + class _RequiredForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2117,14 +2176,11 @@ def _is_unpack(obj): return get_origin(obj) is Unpack elif sys.version_info[:2] >= (3, 9): - class _UnpackSpecialForm(typing._SpecialForm, _root=True): + class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): def __init__(self, getitem): super().__init__(getitem) self.__doc__ = _UNPACK_DOC - def __repr__(self): - return 'typing_extensions.' + self._name - class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar @@ -2140,10 +2196,7 @@ def _is_unpack(obj): class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar - class _UnpackForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name - + class _UnpackForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2535,7 +2588,8 @@ def wrapper(*args, **kwargs): # In 3.11, the ability to define generic `NamedTuple`s was supported. # This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8. # On 3.12, we added __orig_bases__ to call-based NamedTuples -if sys.version_info >= (3, 12): +# On 3.13, we deprecated kwargs-based NamedTuples +if sys.version_info >= (3, 13): NamedTuple = typing.NamedTuple else: def _make_nmtuple(name, types, module, defaults=()): @@ -2579,8 +2633,11 @@ def __new__(cls, typename, bases, ns): ) nm_tpl.__bases__ = bases if typing.Generic in bases: - class_getitem = typing.Generic.__class_getitem__.__func__ - nm_tpl.__class_getitem__ = classmethod(class_getitem) + if hasattr(typing, '_generic_class_getitem'): # 3.12+ + nm_tpl.__class_getitem__ = classmethod(typing._generic_class_getitem) + else: + 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: if key in _prohibited_namedtuple_fields: @@ -2591,30 +2648,87 @@ def __new__(cls, typename, bases, ns): nm_tpl.__init_subclass__() return nm_tpl - def NamedTuple(__typename, __fields=None, **kwargs): - if __fields is None: - __fields = kwargs.items() + _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + + 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. + + Usage:: + + class Employee(NamedTuple): + name: str + id: int + + This is equivalent to:: + + Employee = collections.namedtuple('Employee', ['name', 'id']) + + The resulting class has an extra __annotations__ attribute, giving a + dict that maps field names to types. (The field names are also in + the _fields attribute, which is part of the namedtuple API.) + An alternative equivalent functional syntax is also accepted:: + + Employee = NamedTuple('Employee', [('name', str), ('id', int)]) + """ + if __fields is _marker: + if kwargs: + deprecated_thing = "Creating NamedTuple classes using keyword arguments" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "Use the class-based or functional syntax instead." + ) + else: + deprecated_thing = "Failing to pass a value for the 'fields' parameter" + example = f"`{__typename} = NamedTuple({__typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." + elif __fields is None: + if kwargs: + raise TypeError( + "Cannot pass `None` as the 'fields' parameter " + "and also specify fields using keyword arguments" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + example = f"`{__typename} = NamedTuple({__typename!r}, [])`" + deprecation_msg = ( + "{name} is deprecated and will be disallowed in Python {remove}. " + "To create a NamedTuple class with 0 fields " + "using the functional syntax, " + "pass an empty list, e.g. " + ) + example + "." elif kwargs: raise TypeError("Either list of fields or keywords" " can be provided to NamedTuple, not both") + if __fields is _marker or __fields is None: + warnings.warn( + deprecation_msg.format(name=deprecated_thing, remove="3.15"), + DeprecationWarning, + stacklevel=2, + ) + __fields = kwargs.items() nt = _make_nmtuple(__typename, __fields, module=_caller()) nt.__orig_bases__ = (NamedTuple,) return nt - NamedTuple.__doc__ = typing.NamedTuple.__doc__ - _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) - # On 3.8+, alter the signature so that it matches typing.NamedTuple. # The signature of typing.NamedTuple on >=3.8 is invalid syntax in Python 3.7, # so just leave the signature as it is on 3.7. if sys.version_info >= (3, 8): - NamedTuple.__text_signature__ = '(typename, fields=None, /, **kwargs)' - - def _namedtuple_mro_entries(bases): - assert NamedTuple in bases - return (_NamedTuple,) - - NamedTuple.__mro_entries__ = _namedtuple_mro_entries + _new_signature = '(typename, fields=None, /, **kwargs)' + if isinstance(NamedTuple, _types.FunctionType): + NamedTuple.__text_signature__ = _new_signature + else: + NamedTuple.__call__.__text_signature__ = _new_signature if hasattr(collections.abc, "Buffer"): @@ -2867,3 +2981,92 @@ def __ror__(self, left): if not _is_unionable(left): return NotImplemented return typing.Union[left, self] + + +if hasattr(typing, "is_protocol"): + is_protocol = typing.is_protocol + get_protocol_members = typing.get_protocol_members +else: + def is_protocol(__tp: type) -> bool: + """Return True if the given type is a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, is_protocol + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> is_protocol(P) + True + >>> is_protocol(int) + False + """ + return ( + isinstance(__tp, type) + and getattr(__tp, '_is_protocol', False) + and __tp is not Protocol + and __tp is not getattr(typing, "Protocol", object()) + ) + + def get_protocol_members(__tp: type) -> typing.FrozenSet[str]: + """Return the set of members defined in a Protocol. + + Example:: + + >>> from typing_extensions import Protocol, get_protocol_members + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> get_protocol_members(P) + frozenset({'a', 'b'}) + + Raise a TypeError for arguments that are not Protocols. + """ + if not is_protocol(__tp): + raise TypeError(f'{__tp!r} is not a Protocol') + if hasattr(__tp, '__protocol_attrs__'): + return frozenset(__tp.__protocol_attrs__) + return frozenset(_get_protocol_attrs(__tp)) + + +# 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 +Generator = typing.Generator +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 diff --git a/tox.ini b/tox.ini index 04c4cf16..3d583efc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = True -envlist = py37, py38, py39, py310, py311 +envlist = py37, py38, py39, py310, py311, py312 [testenv] changedir = src