diff --git a/.flake8 b/.flake8 index fc71a6e2..03237510 100644 --- a/.flake8 +++ b/.flake8 @@ -7,9 +7,12 @@ ignore = DW12, # code is sometimes better without this E129, + # Contradicts PEP8 nowadays + W503, # consistency with mypy W504 exclude = # tests have more relaxed formatting rules # and its own specific config in .flake8-tests src/test_typing_extensions.py, +noqa_require_code = true diff --git a/.flake8-tests b/.flake8-tests index 5a97fe89..634160ab 100644 --- a/.flake8-tests +++ b/.flake8-tests @@ -24,5 +24,8 @@ ignore = # irrelevant plugins B3, DW12, + # Contradicts PEP8 nowadays + W503, # consistency with mypy W504 +noqa_require_code = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2a04098..78610e27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,39 @@ name: Test and lint on: + schedule: + - cron: "0 2 * * *" # 2am UTC push: + branches: + - main pull_request: + workflow_dispatch: permissions: contents: read +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: tests: name: Run tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: fail-fast: false matrix: @@ -18,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-dev", "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 @@ -29,9 +67,9 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Test typing_extensions - continue-on-error: ${{ matrix.python-version == '3.12-dev' }} 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 @@ -41,6 +79,9 @@ jobs: linting: name: Lint + # no reason to run this as a cron job + if: github.event_name != 'schedule' + runs-on: ubuntu-latest steps: @@ -54,11 +95,42 @@ jobs: - name: Install dependencies run: | - pip install --upgrade pip pip install -r test-requirements.txt + # not included in test-requirements.txt as it depends on typing-extensions, + # so it's a pain to have it installed locally + pip install flake8-noqa - name: Lint implementation - run: flake8 + run: flake8 --color always - name: Lint tests - run: flake8 --config=.flake8-tests src/test_typing_extensions.py + run: flake8 --config=.flake8-tests src/test_typing_extensions.py --color always + + create-issue-on-failure: + name: Create an issue if daily tests failed + runs-on: ubuntu-latest + + needs: [tests] + + if: >- + ${{ + github.repository == 'python/typing_extensions' + && always() + && github.event_name == 'schedule' + && needs.tests.result == 'failure' + }} + + permissions: + issues: write + + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.create({ + owner: "python", + repo: "typing_extensions", + title: `Daily tests failed on ${new Date().toDateString()}`, + body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/ci.yml", + }) diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 4e270719..ad2deee1 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -2,11 +2,18 @@ name: Test packaging on: push: + branches: + - main pull_request: + workflow_dispatch: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: wheel: name: Test wheel install @@ -23,7 +30,7 @@ jobs: - name: Install pypa/build run: | # Be wary of running `pip install` here, since it becomes easy for us to - # accidentally pick up typing_extensions as installed by a dependency + # accidentally pick up typing_extensions as installed by a dependency python -m pip install --upgrade build python -m pip list @@ -53,7 +60,7 @@ jobs: - name: Install pypa/build run: | # Be wary of running `pip install` here, since it becomes easy for us to - # accidentally pick up typing_extensions as installed by a dependency + # accidentally pick up typing_extensions as installed by a dependency python -m pip install --upgrade build python -m pip list diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml new file mode 100644 index 00000000..bcb0234c --- /dev/null +++ b/.github/workflows/third_party.yml @@ -0,0 +1,382 @@ +# This workflow is a daily cron job, +# running the tests of various third-party libraries that use us. +# This helps us spot regressions early, +# and helps flag when third-party libraries are making incorrect assumptions +# that might cause them to break when we cut a new release. + +name: Third-party tests + +on: + schedule: + - cron: "30 2 * * *" # 02:30 UTC + pull_request: + paths: + - ".github/workflows/third_party.yml" + workflow_dispatch: + +permissions: + contents: read + +env: + PIP_DISABLE_PIP_VERSION_CHECK: 1 + FORCE_COLOR: 1 + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + pydantic: + name: pydantic tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout pydantic + uses: actions/checkout@v3 + with: + repository: pydantic/pydantic + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup pdm for pydantic tests + uses: pdm-project/setup-pdm@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Add local version of typing_extensions as a dependency + run: pdm add ./typing-extensions-latest + - name: Install pydantic test dependencies + run: pdm install -G testing -G email + - name: List installed dependencies + run: pdm list -vv # pdm equivalent to `pip list` + - name: Run pydantic tests + run: pdm run pytest + + typing_inspect: + name: typing_inspect tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout typing_inspect + uses: actions/checkout@v3 + with: + repository: ilevkivskyi/typing_inspect + path: typing_inspect + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install typing_inspect test dependencies + 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: | + cd typing_inspect + pytest + + pyanalyze: + name: pyanalyze tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out pyanalyze + uses: actions/checkout@v3 + with: + repository: quora/pyanalyze + path: pyanalyze + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install pyanalyze test requirements + 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: | + cd pyanalyze + pytest pyanalyze/ + + typeguard: + name: typeguard tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out typeguard + uses: actions/checkout@v3 + with: + repository: agronholm/typeguard + path: typeguard + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install typeguard test requirements + 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: | + cd typeguard + pytest + + typed-argument-parser: + name: typed-argument-parser tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out typed-argument-parser + uses: actions/checkout@v3 + with: + repository: swansonk14/typed-argument-parser + path: typed-argument-parser + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Configure git for typed-argument-parser tests + # typed-argument parser does this in their CI, + # and the tests fail unless we do this + run: | + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + - name: Install typed-argument-parser test requirements + run: | + 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: | + cd typed-argument-parser + pytest + + mypy: + name: stubtest & mypyc tests + if: >- + # if 'schedule' was the trigger, + # don't run it on contributors' forks + ${{ + github.event_name != 'schedule' + || ( + github.repository == 'python/typing_extensions' + && github.event_name == 'schedule' + ) + }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - 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: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + 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 & 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 + 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.8", "3.9", "3.10", "3.11", "pypy3.9"] + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout cattrs + uses: actions/checkout@v3 + with: + repository: python-attrs/cattrs + - name: Checkout typing_extensions + uses: actions/checkout@v3 + with: + path: typing-extensions-latest + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install pdm for cattrs + run: pip install pdm + - name: Add latest typing-extensions as a dependency + run: | + pdm remove typing-extensions + pdm add --dev ./typing-extensions-latest + - name: Install cattrs test dependencies + run: pdm install --dev -G :all + - name: List all installed dependencies + run: pdm list -vv + - name: Run cattrs tests + run: pdm run pytest tests + + create-issue-on-failure: + name: Create an issue if daily tests failed + runs-on: ubuntu-latest + + needs: + - pydantic + - typing_inspect + - pyanalyze + - typeguard + - typed-argument-parser + - mypy + - cattrs + + if: >- + ${{ + github.repository == 'python/typing_extensions' + && always() + && github.event_name == 'schedule' + && ( + needs.pydantic.result == 'failure' + || needs.typing_inspect.result == 'failure' + || needs.pyanalyze.result == 'failure' + || needs.typeguard.result == 'failure' + || needs.typed-argument-parser.result == 'failure' + || needs.mypy.result == 'failure' + || needs.cattrs.result == 'failure' + ) + }} + + permissions: + issues: write + + steps: + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + await github.rest.issues.create({ + owner: "python", + repo: "typing_extensions", + title: `Third-party tests failed on ${new Date().toDateString()}`, + body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", + }) diff --git a/CHANGELOG.md b/CHANGELOG.md index d330a0f5..1e490c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,189 @@ +# 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 + runtime-checkable protocols. The regression meant + that doing `class Foo(X, typing_extensions.Protocol)`, where `X` was a class that + had `abc.ABCMeta` as its metaclass, would then cause subsequent + `isinstance(1, X)` calls to erroneously raise `TypeError`. Patch by + Alex Waygood (backporting the CPython PR + https://github.com/python/cpython/pull/105152). +- Sync the repository's LICENSE file with that of CPython. + `typing_extensions` is distributed under the same license as + CPython itself. +- Skip a problematic test on Python 3.12.0b1. The test fails on 3.12.0b1 due to + a bug in CPython, which will be fixed in 3.12.0b2. The + `typing_extensions` test suite now passes on 3.12.0b1. + +# Release 4.6.2 (May 25, 2023) + +- Fix use of `@deprecated` on classes with `__new__` but no `__init__`. + Patch by Jelle Zijlstra. +- Fix regression in version 4.6.1 where comparing a generic class against a + runtime-checkable protocol using `isinstance()` would cause `AttributeError` + to be raised if using Python 3.7. + +# Release 4.6.1 (May 23, 2023) + +- Change deprecated `@runtime` to formal API `@runtime_checkable` in the error + message. Patch by Xuehai Pan. +- Fix regression in 4.6.0 where attempting to define a `Protocol` that was + generic over a `ParamSpec` or a `TypeVarTuple` would cause `TypeError` to be + raised. Patch by Alex Waygood. + +# Release 4.6.0 (May 22, 2023) + +- `typing_extensions` is now documented at + https://typing-extensions.readthedocs.io/en/latest/. Patch by Jelle Zijlstra. +- Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed + by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by + Jelle Zijlstra. +- Backport two CPython PRs fixing various issues with `typing.Literal`: + https://github.com/python/cpython/pull/23294 and + https://github.com/python/cpython/pull/23383. Both CPython PRs were + originally by Yurii Karabas, and both were backported to Python >=3.9.1, but + no earlier. Patch by Alex Waygood. + + A side effect of one of the changes is that equality comparisons of `Literal` + objects will now raise a `TypeError` if one of the `Literal` objects being + compared has a mutable parameter. (Using mutable parameters with `Literal` is + not supported by PEP 586 or by any major static type checkers.) +- `Literal` is now reimplemented on all Python versions <= 3.10.0. The + `typing_extensions` version does not suffer from the bug that was fixed in + https://github.com/python/cpython/pull/29334. (The CPython bugfix was + backported to CPython 3.10.1 and 3.9.8, but no earlier.) +- Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067) + (originally by Yurii Karabas), ensuring that `isinstance()` calls on + protocols raise `TypeError` when the protocol is not decorated with + `@runtime_checkable`. Patch by Alex Waygood. +- Backport several significant performance improvements to runtime-checkable + protocols that have been made in Python 3.12 (see + https://github.com/python/cpython/issues/74690 for details). Patch by Alex + Waygood. + + A side effect of one of the performance improvements is that the members of + a runtime-checkable protocol are now considered “frozen” at runtime as soon + as the class has been created. Monkey-patching attributes onto a + runtime-checkable protocol will still work, but will have no impact on + `isinstance()` checks comparing objects to the protocol. See + ["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing) + for more details. +- `isinstance()` checks against runtime-checkable protocols now use + `inspect.getattr_static()` rather than `hasattr()` to lookup whether + attributes exist (backporting https://github.com/python/cpython/pull/103034). + This means that descriptors and `__getattr__` methods are no longer + unexpectedly evaluated during `isinstance()` checks against runtime-checkable + protocols. However, it may also mean that some objects which used to be + considered instances of a runtime-checkable protocol on older versions of + `typing_extensions` may no longer be considered instances of that protocol + using the new release, and vice versa. Most users are unlikely to be affected + by this change. Patch by Alex Waygood. +- Backport the ability to define `__init__` methods on Protocol classes, a + change made in Python 3.11 (originally implemented in + https://github.com/python/cpython/pull/31628 by Adrian Garcia Badaracco). + Patch by Alex Waygood. +- Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python + <3.12. Patch by Alex Waygood. +- Add `typing_extensions` versions of `SupportsInt`, `SupportsFloat`, + `SupportsComplex`, `SupportsBytes`, `SupportsAbs` and `SupportsRound`. These + have the same semantics as the versions from the `typing` module, but + `isinstance()` checks against the `typing_extensions` versions are >10x faster + at runtime on Python <3.12. Patch by Alex Waygood. +- Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and + call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute. + Patch by Adrian Garcia Badaracco. +- Add `typing_extensions.get_original_bases`, a backport of + [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases), + introduced in Python 3.12 (CPython PR + https://github.com/python/cpython/pull/101827, originally by James + Hilton-Balfe). Patch by Alex Waygood. + + This function should always produce correct results when called on classes + constructed using features from `typing_extensions`. However, it may + produce incorrect results when called on some `NamedTuple` or `TypedDict` + classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. +- Constructing a call-based `TypedDict` using keyword arguments for the fields + now causes a `DeprecationWarning` to be emitted. This matches the behaviour + of `typing.TypedDict` on 3.11 and 3.12. +- Backport the implementation of `NewType` from 3.10 (where it is implemented + as a class rather than a function). This allows user-defined `NewType`s to be + pickled. Patch by Alex Waygood. +- Fix tests and import on Python 3.12, where `typing.TypeVar` can no longer be + subclassed. Patch by Jelle Zijlstra. +- Add `typing_extensions.TypeAliasType`, a backport of `typing.TypeAliasType` + from PEP 695. Patch by Jelle Zijlstra. +- Backport changes to the repr of `typing.Unpack` that were made in order to + implement [PEP 692](https://peps.python.org/pep-0692/) (backport of + https://github.com/python/cpython/pull/104048). Patch by Alex Waygood. + # Release 4.5.0 (February 14, 2023) - Runtime support for PEP 702, adding `typing_extensions.deprecated`. Patch diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65feb4f..9d07313e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,19 +15,49 @@ time, this may require different code for some older Python versions. `typing_extensions` may also include experimental features that are not yet part of the standard library, so that users can experiment with them before they are added to the -standard library. Such features should ideally already be specified in a PEP or draft -PEP. +standard library. Such features should already be specified in a PEP or merged into +CPython's `main` branch. `typing_extensions` supports Python versions 3.7 and up. # Versioning scheme Starting with version 4.0.0, `typing_extensions` uses -[Semantic Versioning](https://semver.org/). The major version is incremented for all -backwards-incompatible changes. +[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 + (e.g., release candidates before any feature release) + - Ensure that GitHub Actions reports no errors. - Update the version number in `typing_extensions/pyproject.toml` and in @@ -47,8 +77,10 @@ backwards-incompatible changes. - 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/LICENSE b/LICENSE index 1df6b3b8..f26bcf4d 100644 --- a/LICENSE +++ b/LICENSE @@ -2,12 +2,12 @@ A. HISTORY OF THE SOFTWARE ========================== Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands as a successor of a language called ABC. Guido remains Python's principal author, although it includes many contributions from others. In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) in Reston, Virginia where he released several versions of the software. @@ -19,7 +19,7 @@ https://www.python.org/psf/) was formed, a non-profit organization created specifically to own Python-related Intellectual Property. Zope Corporation was a sponsoring member of the PSF. -All Python releases are Open Source (see http://www.opensource.org for +All Python releases are Open Source (see https://opensource.org for the Open Source Definition). Historically, most, but not all, Python releases have also been GPL-compatible; the table below summarizes the various releases. @@ -59,6 +59,17 @@ direction to make these releases possible. B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON =============================================================== +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 -------------------------------------------- @@ -73,7 +84,7 @@ analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation; +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. @@ -252,3 +263,17 @@ FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index 6da36c37..efd3a824 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ [![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing) +[Documentation](https://typing-extensions.readthedocs.io/en/latest/#) – +[PyPI](https://pypi.org/project/typing-extensions/) + ## Overview The `typing_extensions` module serves two related purposes: @@ -12,151 +15,25 @@ 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. -New features may be added to `typing_extensions` as soon as they are specified -in a PEP that has been added to the [python/peps](https://github.com/python/peps) -repository. If the PEP is accepted, the feature will then be added to `typing` -for the next CPython release. No typing PEP has been rejected so far, so we -haven't yet figured out how to deal with that possibility. +`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`. -Starting with version 4.0.0, `typing_extensions` uses +`typing_extensions` uses [Semantic Versioning](https://semver.org/). The -major version is incremented for all backwards-incompatible changes. +major version will be incremented only for backwards-incompatible changes. Therefore, it's safe to depend on `typing_extensions` like this: `typing_extensions >=x.y, <(x+1)`, where `x.y` is the first version that includes all features you need. -`typing_extensions` supports Python versions 3.7 and higher. In the future, -support for older Python versions will be dropped some time after that version -reaches end of life. +`typing_extensions` supports Python versions 3.7 and higher. ## Included items -This module currently contains the following: - -- Experimental features - - - `override` (see [PEP 698](https://peps.python.org/pep-0698/)) - - The `default=` argument to `TypeVar`, `ParamSpec`, and `TypeVarTuple` (see [PEP 696](https://peps.python.org/pep-0696/)) - - The `infer_variance=` argument to `TypeVar` (see [PEP 695](https://peps.python.org/pep-0695/)) - - The `@deprecated` decorator (see [PEP 702](https://peps.python.org/pep-0702/)) - -- In `typing` since Python 3.11 - - - `assert_never` - - `assert_type` - - `clear_overloads` - - `@dataclass_transform()` (see [PEP 681](https://peps.python.org/pep-0681/)) - - `get_overloads` - - `LiteralString` (see [PEP 675](https://peps.python.org/pep-0675/)) - - `Never` - - `NotRequired` (see [PEP 655](https://peps.python.org/pep-0655/)) - - `reveal_type` - - `Required` (see [PEP 655](https://peps.python.org/pep-0655/)) - - `Self` (see [PEP 673](https://peps.python.org/pep-0673/)) - - `TypeVarTuple` (see [PEP 646](https://peps.python.org/pep-0646/); the `typing_extensions` version supports the `default=` argument from [PEP 696](https://peps.python.org/pep-0696/)) - - `Unpack` (see [PEP 646](https://peps.python.org/pep-0646/)) - -- In `typing` since Python 3.10 - - - `Concatenate` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `ParamSpec` (see [PEP 612](https://peps.python.org/pep-0612/); the `typing_extensions` version supports the `default=` argument from [PEP 696](https://peps.python.org/pep-0696/)) - - `ParamSpecArgs` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `ParamSpecKwargs` (see [PEP 612](https://peps.python.org/pep-0612/)) - - `TypeAlias` (see [PEP 613](https://peps.python.org/pep-0613/)) - - `TypeGuard` (see [PEP 647](https://peps.python.org/pep-0647/)) - - `is_typeddict` - -- In `typing` since Python 3.9 - - - `Annotated` (see [PEP 593](https://peps.python.org/pep-0593/)) - -- In `typing` since Python 3.8 - - - `final` (see [PEP 591](https://peps.python.org/pep-0591/)) - - `Final` (see [PEP 591](https://peps.python.org/pep-0591/)) - - `Literal` (see [PEP 586](https://peps.python.org/pep-0586/)) - - `Protocol` (see [PEP 544](https://peps.python.org/pep-0544/)) - - `runtime_checkable` (see [PEP 544](https://peps.python.org/pep-0544/)) - - `TypedDict` (see [PEP 589](https://peps.python.org/pep-0589/)) - - `get_origin` (`typing_extensions` provides this function only in Python 3.7+) - - `get_args` (`typing_extensions` provides this function only in Python 3.7+) - -- In `typing` since Python 3.7 - - - `OrderedDict` - -- In `typing` since Python 3.5 or 3.6 (see [the typing documentation](https://docs.python.org/3.10/library/typing.html) for details) - - - `AsyncContextManager` - - `AsyncGenerator` - - `AsyncIterable` - - `AsyncIterator` - - `Awaitable` - - `ChainMap` - - `ClassVar` (see [PEP 526](https://peps.python.org/pep-0526/)) - - `ContextManager` - - `Coroutine` - - `Counter` - - `DefaultDict` - - `Deque` - - `NewType` - - `NoReturn` - - `overload` - - `Text` - - `Type` - - `TYPE_CHECKING` - - `get_type_hints` - -- The following have always been present in `typing`, but the `typing_extensions` versions provide - additional features: - - - `Any` (supports inheritance since Python 3.11) - - `NamedTuple` (supports multiple inheritance with `Generic` since Python 3.11) - - `TypeVar` (see PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/)) - -# Other Notes and Limitations - -Certain objects were changed after they were added to `typing`, and -`typing_extensions` provides a backport even on newer Python versions: - -- `TypedDict` does not store runtime information - about which (if any) keys are non-required in Python 3.8, and does not - honor the `total` keyword with old-style `TypedDict()` in Python - 3.9.0 and 3.9.1. `TypedDict` also does not support multiple inheritance - with `typing.Generic` on Python <3.11. -- `get_origin` and `get_args` lack support for `Annotated` in - Python 3.8 and lack support for `ParamSpecArgs` and `ParamSpecKwargs` - in 3.9. -- `@final` was changed in Python 3.11 to set the `.__final__` attribute. -- `@overload` was changed in Python 3.11 to make function overloads - introspectable at runtime. In order to access overloads with - `typing_extensions.get_overloads()`, you must use - `@typing_extensions.overload`. -- `NamedTuple` was changed in Python 3.11 to allow for multiple inheritance - with `typing.Generic`. -- Since Python 3.11, it has been possible to inherit from `Any` at - runtime. `typing_extensions.Any` also provides this capability. -- `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, - in the draft PEPs [695](https://peps.python.org/pep-0695/) and [696](https://peps.python.org/pep-0696/), which are being considered for inclusion - in Python 3.12. - -There are a few types whose interface was modified between different -versions of typing. For example, `typing.Sequence` was modified to -subclass `typing.Reversible` as of Python 3.5.3. - -These changes are _not_ backported to prevent subtle compatibility -issues when mixing the differing implementations of modified classes. - -Certain types have incorrect runtime behavior due to limitations of older -versions of the typing module: - -- `ParamSpec` and `Concatenate` will not work with `get_args` and - `get_origin`. Certain [PEP 612](https://peps.python.org/pep-0612/) special cases in user-defined - `Generic`s are also not available. - -These types are only guaranteed to work for static type checking. +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 appropriate source directory and run -`test_typing_extensions.py`. \ No newline at end of file +See [CONTRIBUTING.md](https://github.com/python/typing_extensions/blob/main/CONTRIBUTING.md) +for how to contribute to `typing_extensions`. diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 00000000..69fa449d --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/_extensions/__init__.py b/doc/_extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/doc/_extensions/gh_link.py b/doc/_extensions/gh_link.py new file mode 100644 index 00000000..3442dbd3 --- /dev/null +++ b/doc/_extensions/gh_link.py @@ -0,0 +1,29 @@ +from docutils import nodes + + +def setup(app): + app.add_role( + "pr", autolink("https://github.com/python/typing_extensions/pull/{}", "PR #") + ) + app.add_role( + "pr-cpy", autolink("https://github.com/python/cpython/pull/{}", "CPython PR #") + ) + app.add_role( + "issue", + autolink("https://github.com/python/typing_extensions/issues/{}", "issue #"), + ) + app.add_role( + "issue-cpy", + autolink("https://github.com/python/cpython/issues/{}", "CPython issue #"), + ) + + +def autolink(pattern: str, prefix: str): + def role(name, rawtext, text: str, lineno, inliner, options=None, content=None): + if options is None: + options = {} + url = pattern.format(text) + node = nodes.reference(rawtext, f"{prefix}{text}", refuri=url, **options) + return [node], [] + + return role diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 00000000..7984bc22 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,34 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os.path +import sys + +sys.path.insert(0, os.path.abspath('.')) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'typing_extensions' +copyright = '2023, Guido van Rossum and others' +author = 'Guido van Rossum and others' +release = '4.6.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['sphinx.ext.intersphinx', '_extensions.gh_link'] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 00000000..5fd2b2e8 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,1014 @@ + +Welcome to typing_extensions's documentation! +============================================= + +``typing_extensions`` complements the standard-library :py:mod:`typing` module, +providing runtime support for type hints as specified by :pep:`484` and subsequent +PEPs. The module serves two related purposes: + +- Enable use of new type system features on older Python versions. For example, + :py:data:`typing.TypeGuard` is new in Python 3.10, but ``typing_extensions`` allows + users on previous Python versions to use it too. +- Enable experimentation with type system features proposed in new PEPs before they are accepted and + added to the :py:mod:`typing` module. + +New features may be added to ``typing_extensions`` as soon as they are specified +in a PEP that has been added to the `python/peps `_ +repository. If the PEP is accepted, the feature will then be added to the +:py:mod:`typing` module for the next CPython release. No typing PEP that +affected ``typing_extensions`` has been rejected so far, so we haven't yet +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 +-------------------------------------- + +Starting with version 4.0.0, ``typing_extensions`` uses +`Semantic Versioning `_. A changelog is +maintained `on GitHub `_. + +The major version is incremented for all backwards-incompatible changes. +Therefore, it's safe to depend +on ``typing_extensions`` like this: ``typing_extensions >=x.y, <(x+1)``, +where ``x.y`` is the first version that includes all features you need. +In view of the wide usage of ``typing_extensions`` across the ecosystem, +we are highly hesitant to break backwards compatibility, and we do not +expect to increase the major version number in the foreseeable future. + +Feature releases, with version numbers of the form 4.N.0, are made at +irregular intervals when enough new features accumulate. Before a +feature release, at least one release candidate (with a version number +of the form 4.N.0rc1) should be released to give downstream users time +to test. After at least a week of testing, the new feature version +may then be released. If necessary, additional release candidates can +be added. + +Bugfix releases, with version numbers of the form 4.N.1 or higher, +may be made if bugs are discovered after a feature release. + +Before version 4.0.0, the versioning scheme loosely followed the Python +version from which features were backported; for example, +``typing_extensions`` 3.10.0.0 was meant to reflect ``typing`` as of +Python 3.10.0. During this period, no changelog was maintained. + +Runtime use of types +~~~~~~~~~~~~~~~~~~~~ + +We aim for complete backwards compatibility in terms of the names we export: +code like ``from typing_extensions import X`` that works on one +typing-extensions release will continue to work on the next. +It is more difficult to maintain compatibility for users that introspect +types at runtime, as almost any detail can potentially break compatibility. +Users who introspect types should follow these guidelines to minimize +the risk of compatibility issues: + +- Always check for both the :mod:`typing` and ``typing_extensions`` versions + of objects, even if they are currently the same on some Python version. + Future ``typing_extensions`` releases may re-export a separate version of + the object to backport some new feature or bugfix. +- Use public APIs like :func:`get_origin` and :func:`get_original_bases` to + access internal information about types, instead of accessing private + 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 +---------------------- + +``typing_extensions`` currently supports Python versions 3.7 and higher. In the future, +support for older Python versions will be dropped some time after that version +reaches end of life. + +Module contents +--------------- + +As most of the features in ``typing_extensions`` exist in :py:mod:`typing` +in newer versions of Python, the documentation here is brief and focuses +on aspects that are specific to ``typing_extensions``, such as limitations +on specific Python versions. + +Special typing primitives +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. data:: Annotated + + See :py:data:`typing.Annotated` and :pep:`593`. In ``typing`` since 3.9. + + .. versionchanged:: 4.1.0 + + ``Annotated`` can now wrap :data:`ClassVar` and :data:`Final`. + +.. data:: Any + + See :py:data:`typing.Any`. + + Since Python 3.11, ``typing.Any`` can be used as a base class. + ``typing_extensions.Any`` supports this feature on older versions. + + .. versionadded:: 4.4.0 + + Added to support inheritance from ``Any``. + +.. data:: Concatenate + + See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. + + The backport does not support certain operations involving ``...`` as + a parameter; see :issue:`48` and :issue:`110` for details. + +.. data:: Final + + See :py:data:`typing.Final` and :pep:`591`. In ``typing`` since 3.8. + +.. data:: Literal + + See :py:data:`typing.Literal` and :pep:`586`. In ``typing`` since 3.8. + + :py:data:`typing.Literal` does not flatten or deduplicate parameters on Python <3.9.1, and a + caching bug was fixed in 3.10.1/3.9.8. The ``typing_extensions`` version + flattens and deduplicates parameters on all Python versions, and the caching + bug is also fixed on all versions. + + .. versionchanged:: 4.6.0 + + Backported the bug fixes from :pr-cpy:`29334`, :pr-cpy:`23294`, and :pr-cpy:`23383`. + +.. data:: LiteralString + + See :py:data:`typing.LiteralString` and :pep:`675`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +.. class:: NamedTuple + + See :py:class:`typing.NamedTuple`. + + ``typing_extensions`` backports several changes + to ``NamedTuple`` on Python 3.11 and lower: in 3.11, + support for generic ``NamedTuple``\ s was added, and + in 3.12, the ``__orig_bases__`` attribute was added. + + .. versionadded:: 4.3.0 + + Added to provide support for generic ``NamedTuple``\ s. + + .. versionchanged:: 4.6.0 + + 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. + + .. versionadded:: 4.1.0 + +.. class:: NewType(name, tp) + + See :py:class:`typing.NewType`. In ``typing`` since 3.5.2. + + Instances of ``NewType`` were made picklable in 3.10 and an error message was + improved in 3.11; ``typing_extensions`` backports these changes. + + .. versionchanged:: 4.6.0 + + The improvements from Python 3.10 and 3.11 were backported. + +.. data:: NotRequired + + See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. + + .. versionadded:: 4.0.0 + +.. class:: ParamSpec(name, *, default=...) + + See :py:class:`typing.ParamSpec` and :pep:`612`. In ``typing`` since 3.10. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`. + + On older Python versions, ``typing_extensions.ParamSpec`` may not work + correctly with introspection tools like :func:`get_args` and + :func:`get_origin`. Certain special cases in user-defined + :py:class:`typing.Generic`\ s are also not available (e.g., see :issue:`126`). + + .. versionchanged:: 4.4.0 + + Added support for the ``default=`` argument. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. class:: ParamSpecArgs + +.. class:: ParamSpecKwargs + + See :py:class:`typing.ParamSpecArgs` and :py:class:`typing.ParamSpecKwargs`. + In ``typing`` since 3.10. + +.. class:: Protocol + + See :py:class:`typing.Protocol` and :pep:`544`. In ``typing`` since 3.8. + + Python 3.12 improves the performance of runtime-checkable protocols; + ``typing_extensions`` backports this improvement. + + .. versionchanged:: 4.6.0 + + Backported the ability to define ``__init__`` methods on Protocol classes. + + .. versionchanged:: 4.6.0 + + 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. + + .. versionadded:: 4.0.0 + +.. data:: Self + + See :py:data:`typing.Self` and :pep:`673`. In ``typing`` since 3.11. + + .. versionadded:: 4.0.0 + +.. data:: TypeAlias + + See :py:data:`typing.TypeAlias` and :pep:`613`. In ``typing`` since 3.10. + +.. class:: TypeAliasType(name, value, *, type_params=()) + + See :py:class:`typing.TypeAliasType` and :pep:`695`. In ``typing`` since 3.12. + + .. versionadded:: 4.6.0 + +.. data:: TypeGuard + + See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. + +.. class:: TypedDict + + See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8. + + ``typing_extensions`` backports various bug fixes and improvements + to ``TypedDict`` on Python 3.11 and lower. + :py:class:`TypedDict` does not store runtime information + about which (if any) keys are non-required in Python 3.8, and does not + honor the ``total`` keyword with old-style ``TypedDict()`` in Python + 3.9.0 and 3.9.1. :py:class:`typing.TypedDict` also does not support multiple inheritance + with :py:class:`typing.Generic` on Python <3.11, and :py:class:`typing.TypedDict` classes do not + consistently have the ``__orig_bases__`` attribute on Python <3.12. The + ``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. + + .. versionchanged:: 4.6.0 + + A :py:exc:`DeprecationWarning` is now emitted when a call-based + ``TypedDict`` is constructed using keyword arguments. + + .. versionchanged:: 4.6.0 + + 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=...) + + See :py:class:`typing.TypeVar`. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`, as well as the + ``infer_variance=`` argument from :pep:`695` (also available + in Python 3.12). + + .. versionadded:: 4.4.0 + + Added in order to support the new ``default=`` and + ``infer_variance=`` arguments. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. class:: TypeVarTuple(name, *, default=...) + + See :py:class:`typing.TypeVarTuple` and :pep:`646`. In ``typing`` since 3.11. + + The ``typing_extensions`` version adds support for the + ``default=`` argument from :pep:`696`. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.4.0 + + Added support for the ``default=`` argument. + + .. versionchanged:: 4.6.0 + + The implementation was changed for compatibility with Python 3.12. + +.. data:: Unpack + + See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11. + + In Python 3.12, the ``repr()`` was changed as a result of :pep:`692`. + ``typing_extensions`` backports this change. + + Generic type aliases involving ``Unpack`` may not work correctly on + Python 3.10 and lower; see :issue:`103` for details. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.6.0 + + Backport ``repr()`` changes from Python 3.12. + +Generic concrete collections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: OrderedDict + + See :py:class:`typing.OrderedDict`. In ``typing`` since 3.7.2. + +Abstract Base Classes +~~~~~~~~~~~~~~~~~~~~~ + +.. class:: Buffer + + See :py:class:`collections.abc.Buffer`. Added to the standard library + in Python 3.12. + + .. versionadded:: 4.6.0 + +Protocols +~~~~~~~~~ + +.. class:: SupportsAbs + + See :py:class:`typing.SupportsAbs`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsBytes + + See :py:class:`typing.SupportsBytes`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsComplex + + See :py:class:`typing.SupportsComplex`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsFloat + + See :py:class:`typing.SupportsFloat`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsIndex + + See :py:class:`typing.SupportsIndex`. In ``typing`` since 3.8. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionchanged:: 4.6.0 + + Backported the performance improvements from Python 3.12. + +.. class:: SupportsInt + + See :py:class:`typing.SupportsInt`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +.. class:: SupportsRound + + See :py:class:`typing.SupportsRound`. + + ``typing_extensions`` backports a more performant version of this + protocol on Python 3.11 and lower. + + .. versionadded:: 4.6.0 + +Decorators +~~~~~~~~~~ + +.. decorator:: dataclass_transform(*, eq_default=False, order_default=False, + kw_only_default=False, frozen_default=False, + field_specifiers=(), **kwargs) + + See :py:func:`typing.dataclass_transform` and :pep:`681`. In ``typing`` since 3.11. + + Python 3.12 adds the ``frozen_default`` parameter; ``typing_extensions`` + backports this parameter. + + .. versionadded:: 4.1.0 + + .. versionchanged:: 4.2.0 + + The ``field_descriptors`` parameter was renamed to ``field_specifiers``. + For compatibility, the decorator now accepts arbitrary keyword arguments. + + .. versionchanged:: 4.5.0 + + The ``frozen_default`` parameter was added. + +.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1) + + See :pep:`702`. Experimental; not yet part of the standard library. + + .. versionadded:: 4.5.0 + +.. decorator:: final + + See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8. + + Since Python 3.11, this decorator supports runtime introspection + by setting the ``__final__`` attribute wherever possible; ``typing_extensions.final`` + backports this feature. + + .. versionchanged:: 4.1.0 + + The decorator now attempts to set the ``__final__`` attribute on decorated objects. + +.. decorator:: overload + + See :py:func:`typing.overload`. + + Since Python 3.11, this decorator supports runtime introspection + through :func:`get_overloads`; ``typing_extensions.overload`` + backports this feature. + + .. versionchanged:: 4.2.0 + + Introspection support via :func:`get_overloads` was added. + +.. decorator:: override + + See :py:func:`typing.override` and :pep:`698`. In ``typing`` since 3.12. + + .. versionadded:: 4.4.0 + + .. versionchanged:: 4.5.0 + + The decorator now attempts to set the ``__override__`` attribute on the decorated + object. + +.. decorator:: runtime_checkable + + See :py:func:`typing.runtime_checkable`. In ``typing`` since 3.8. + + In Python 3.12, the performance of runtime-checkable protocols was + improved, and ``typing_extensions`` backports these performance + improvements. + +Functions +~~~~~~~~~ + +.. function:: assert_never(arg) + + See :py:func:`typing.assert_never`. In ``typing`` since 3.11. + + .. versionadded:: 4.1.0 + +.. function:: assert_type(val, typ) + + See :py:func:`typing.assert_type`. In ``typing`` since 3.11. + + .. versionadded:: 4.2.0 + +.. function:: clear_overloads() + + See :py:func:`typing.clear_overloads`. In ``typing`` since 3.11. + + .. versionadded:: 4.2.0 + +.. function:: get_args(tp) + + See :py:func:`typing.get_args`. In ``typing`` since 3.8. + + This function was changed in 3.9 and 3.10 to deal with :data:`Annotated` + and :class:`ParamSpec` correctly; ``typing_extensions`` backports these + fixes. + +.. function:: get_origin(tp) + + See :py:func:`typing.get_origin`. In ``typing`` since 3.8. + + This function was changed in 3.9 and 3.10 to deal with :data:`Annotated` + and :class:`ParamSpec` correctly; ``typing_extensions`` backports these + fixes. + +.. function:: get_original_bases(cls) + + See :py:func:`types.get_original_bases`. Added to the standard library + in Python 3.12. + + This function should always produce correct results when called on classes + constructed using features from ``typing_extensions``. However, it may + produce incorrect results when called on some :py:class:`NamedTuple` or + :py:class:`TypedDict` classes on Python <=3.11. + + .. versionadded:: 4.6.0 + +.. function:: get_overloads(func) + + See :py:func:`typing.get_overloads`. In ``typing`` since 3.11. + + Before Python 3.11, this works only with overloads created through + :func:`overload`, not with :py:func:`typing.overload`. + + .. 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`. + + In Python 3.11, this function was changed to support the new + :py:data:`typing.Required` and :py:data:`typing.NotRequired`. + ``typing_extensions`` backports these fixes. + + .. versionchanged:: 4.1.0 + + 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. + + On versions where :class:`TypedDict` is not the same as + :py:class:`typing.TypedDict`, this function recognizes + ``TypedDict`` classes created through either mechanism. + + .. 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 + +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/doc/make.bat b/doc/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/pyproject.toml b/pyproject.toml index 41bf2bed..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.5.0" +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", ] @@ -46,7 +47,7 @@ classifiers = [ Home = "https://github.com/python/typing_extensions" Repository = "https://github.com/python/typing_extensions" Changes = "https://github.com/python/typing_extensions/blob/main/CHANGELOG.md" -Documentation = "https://typing.readthedocs.io/" +Documentation = "https://typing-extensions.readthedocs.io/" "Bug Tracker" = "https://github.com/python/typing_extensions/issues" "Q & A" = "https://github.com/python/typing/discussions" @@ -57,5 +58,5 @@ name = "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" email = "levkivskyi@gmail.com" [tool.flit.sdist] -include = ["CHANGELOG.md", "README.md", "*/*test*.py"] +include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"] exclude = [] 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 208382a0..c2ab6d7f 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,37 +1,44 @@ import sys import os import abc +import gc +import io import contextlib import collections from collections import defaultdict import collections.abc import copy from functools import lru_cache +import importlib import inspect import pickle +import re import subprocess +import tempfile +import textwrap import types +from pathlib import Path from unittest import TestCase, main, skipUnless, skipIf from unittest.mock import patch -from test import ann_module, ann_module2, ann_module3 import typing -from typing import TypeVar, Optional, Union, AnyStr +from typing import Optional, Union, AnyStr from typing import T, KT, VT # Not in __all__. -from typing import Tuple, List, Dict, Iterable, Iterator, Callable +from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable from typing import Generic from typing import no_type_check +import warnings + import typing_extensions from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString -from typing_extensions import assert_type, get_type_hints, get_origin, get_args +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 -from _typed_dict_test_helper import Foo, FooGeneric -import warnings +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. @@ -42,10 +49,123 @@ # 3.11 makes runtime type checks (_type_check) more lenient. TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0) +# 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 +ANN_MODULE_SOURCE = '''\ +from typing import Optional +from functools import wraps + +__annotations__[1] = 2 + +class C: + + x = 5; y: Optional['C'] = None + +from typing import Tuple +x: int = 5; y: str = x; f: Tuple[int, int] + +class M(type): + + __annotations__['123'] = 123 + o: type = object + +(pars): bool = True + +class D(C): + j: str = 'hi'; k: str= 'bye' + +from types import new_class +h_class = new_class('H', (C,)) +j_class = new_class('J') + +class F(): + z: int = 5 + def __init__(self, x): + pass + +class Y(F): + def __init__(self): + super(F, self).__init__(123) + +class Meta(type): + def __new__(meta, name, bases, namespace): + return super().__new__(meta, name, bases, namespace) + +class S(metaclass = Meta): + x: str = 'something' + y: str = 'something else' + +def foo(x: int = 10): + def bar(y: List[str]): + x: str = 'yes' + bar() + +def dec(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper +''' + +ANN_MODULE_2_SOURCE = '''\ +from typing import no_type_check, ClassVar + +i: int = 1 +j: int +x: float = i/10 + +def f(): + class C: ... + return C() + +f().new_attr: object = object() + +class C: + def __init__(self, x: int) -> None: + self.x = x + +c = C(5) +c.new_attr: int = 10 + +__annotations__ = {} + + +@no_type_check +class NTC: + def meth(self, param: complex) -> None: + ... + +class CV: + var: ClassVar['CV'] + +CV.var = CV() +''' + +ANN_MODULE_3_SOURCE = '''\ +def f_bad_ann(): + __annotations__[1] = 2 + +class C_OK: + def __init__(self, x: int) -> None: + self.x: no_such_name = x # This one is OK as proposed by Guido + +class D_bad_ann: + def __init__(self, x: int) -> None: + sfel.y: int = 0 + +def g_bad_ann(): + no_such_name.attr: int = 0 +''' + class BaseTestCase(TestCase): def assertIsSubclass(self, cls, class_or_tuple, msg=None): @@ -238,9 +358,11 @@ class A: with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): A() - with self.assertRaises(TypeError): - A(42) + with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): + with self.assertRaises(TypeError): + A(42) + def test_class_with_init(self): @deprecated("HasInit will go away soon") class HasInit: def __init__(self, x): @@ -250,6 +372,7 @@ def __init__(self, x): instance = HasInit(42) self.assertEqual(instance.x, 42) + def test_class_with_new(self): has_new_called = False @deprecated("HasNew will go away soon") @@ -266,6 +389,8 @@ def __init__(self, x) -> None: instance = HasNew(42) self.assertEqual(instance.x, 42) self.assertTrue(has_new_called) + + def test_class_with_inherited_new(self): new_base_called = False class NewBase: @@ -286,6 +411,23 @@ class HasInheritedNew(NewBase): self.assertEqual(instance.x, 42) self.assertTrue(new_base_called) + def test_class_with_new_but_no_init(self): + new_called = False + + @deprecated("HasNewNoInit will go away soon") + class HasNewNoInit: + def __new__(cls, x): + nonlocal new_called + new_called = True + obj = super().__new__(cls) + obj.x = x + return obj + + with self.assertWarnsRegex(DeprecationWarning, "HasNewNoInit will go away soon"): + instance = HasNewNoInit(42) + self.assertEqual(instance.x, 42) + self.assertTrue(new_called) + def test_function(self): @deprecated("b will go away soon") def b(): @@ -375,8 +517,13 @@ def test_repr(self): else: mod_name = 'typing_extensions' self.assertEqual(repr(Any), f"{mod_name}.Any") - if sys.version_info < (3, 11): # skip for now on 3.11+ see python/cpython#95987 - self.assertEqual(repr(self.SubclassesAny), "") + + @skipIf(sys.version_info[:3] == (3, 11, 0), "A bug was fixed in 3.11.1") + def test_repr_on_Any_subclass(self): + self.assertEqual( + repr(self.SubclassesAny), + f"" + ) def test_instantiation(self): with self.assertRaises(TypeError): @@ -573,7 +720,7 @@ def test_no_isinstance(self): class IntVarTests(BaseTestCase): def test_valid(self): - T_ints = IntVar("T_ints") # noqa + T_ints = IntVar("T_ints") def test_invalid(self): with self.assertRaises(TypeError): @@ -581,7 +728,7 @@ def test_invalid(self): with self.assertRaises(TypeError): T_ints = IntVar("T_ints", bound=int) with self.assertRaises(TypeError): - T_ints = IntVar("T_ints", covariant=True) # noqa + T_ints = IntVar("T_ints", covariant=True) class LiteralTests(BaseTestCase): @@ -591,6 +738,13 @@ def test_basics(self): Literal["x", "y", "z"] Literal[None] + def test_enum(self): + import enum + class My(enum.Enum): + A = 'A' + + self.assertEqual(Literal[My.A].__args__, (My.A,)) + def test_illegal_parameters_do_not_raise_runtime_errors(self): # Type checkers should reject these types, but we do not # raise errors at runtime to maintain maximum flexibility @@ -606,7 +760,8 @@ def test_literals_inside_other_types(self): List[Literal[("foo", "bar", "baz")]] def test_repr(self): - if hasattr(typing, 'Literal'): + # we backport various bugfixes that were added in 3.10.1 and earlier + if sys.version_info >= (3, 10, 1): mod_name = 'typing' else: mod_name = 'typing_extensions' @@ -615,6 +770,7 @@ def test_repr(self): self.assertEqual(repr(Literal[int]), mod_name + ".Literal[int]") self.assertEqual(repr(Literal), mod_name + ".Literal") self.assertEqual(repr(Literal[None]), mod_name + ".Literal[None]") + self.assertEqual(repr(Literal[1, 2, 3, 3]), mod_name + ".Literal[1, 2, 3]") def test_cannot_init(self): with self.assertRaises(TypeError): @@ -646,6 +802,113 @@ def test_no_multiple_subscripts(self): with self.assertRaises(TypeError): Literal[1][1] + def test_equal(self): + self.assertNotEqual(Literal[0], Literal[False]) + self.assertNotEqual(Literal[True], Literal[1]) + self.assertNotEqual(Literal[1], Literal[2]) + self.assertNotEqual(Literal[1, True], Literal[1]) + self.assertNotEqual(Literal[1, True], Literal[1, 1]) + self.assertNotEqual(Literal[1, 2], Literal[True, 2]) + self.assertEqual(Literal[1], Literal[1]) + self.assertEqual(Literal[1, 2], Literal[2, 1]) + self.assertEqual(Literal[1, 2, 3], Literal[1, 2, 3, 3]) + + def test_hash(self): + self.assertEqual(hash(Literal[1]), hash(Literal[1])) + self.assertEqual(hash(Literal[1, 2]), hash(Literal[2, 1])) + self.assertEqual(hash(Literal[1, 2, 3]), hash(Literal[1, 2, 3, 3])) + + def test_args(self): + self.assertEqual(Literal[1, 2, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, 2, 3, 3].__args__, (1, 2, 3)) + self.assertEqual(Literal[1, Literal[2], Literal[3, 4]].__args__, (1, 2, 3, 4)) + # Mutable arguments will not be deduplicated + self.assertEqual(Literal[[], []].__args__, ([], [])) + + def test_union_of_literals(self): + self.assertEqual(Union[Literal[1], Literal[2]].__args__, + (Literal[1], Literal[2])) + self.assertEqual(Union[Literal[1], Literal[1]], + Literal[1]) + + self.assertEqual(Union[Literal[False], Literal[0]].__args__, + (Literal[False], Literal[0])) + self.assertEqual(Union[Literal[True], Literal[1]].__args__, + (Literal[True], Literal[1])) + + import enum + class Ints(enum.IntEnum): + A = 0 + B = 1 + + self.assertEqual(Union[Literal[Ints.A], Literal[Ints.B]].__args__, + (Literal[Ints.A], Literal[Ints.B])) + + self.assertEqual(Union[Literal[Ints.A], Literal[Ints.A]], + Literal[Ints.A]) + self.assertEqual(Union[Literal[Ints.B], Literal[Ints.B]], + Literal[Ints.B]) + + self.assertEqual(Union[Literal[0], Literal[Ints.A], Literal[False]].__args__, + (Literal[0], Literal[Ints.A], Literal[False])) + self.assertEqual(Union[Literal[1], Literal[Ints.B], Literal[True]].__args__, + (Literal[1], Literal[Ints.B], Literal[True])) + + @skipUnless(TYPING_3_10_0, "Python 3.10+ required") + def test_or_type_operator_with_Literal(self): + self.assertEqual((Literal[1] | Literal[2]).__args__, + (Literal[1], Literal[2])) + + self.assertEqual((Literal[0] | Literal[False]).__args__, + (Literal[0], Literal[False])) + self.assertEqual((Literal[1] | Literal[True]).__args__, + (Literal[1], Literal[True])) + + self.assertEqual(Literal[1] | Literal[1], Literal[1]) + self.assertEqual(Literal['a'] | Literal['a'], Literal['a']) + + import enum + class Ints(enum.IntEnum): + A = 0 + B = 1 + + self.assertEqual(Literal[Ints.A] | Literal[Ints.A], Literal[Ints.A]) + self.assertEqual(Literal[Ints.B] | Literal[Ints.B], Literal[Ints.B]) + + self.assertEqual((Literal[Ints.B] | Literal[Ints.A]).__args__, + (Literal[Ints.B], Literal[Ints.A])) + + self.assertEqual((Literal[0] | Literal[Ints.A]).__args__, + (Literal[0], Literal[Ints.A])) + self.assertEqual((Literal[1] | Literal[Ints.B]).__args__, + (Literal[1], Literal[Ints.B])) + + def test_flatten(self): + l1 = Literal[Literal[1], Literal[2], Literal[3]] + l2 = Literal[Literal[1, 2], 3] + l3 = Literal[Literal[1, 2, 3]] + for lit in l1, l2, l3: + self.assertEqual(lit, Literal[1, 2, 3]) + self.assertEqual(lit.__args__, (1, 2, 3)) + + def test_does_not_flatten_enum(self): + import enum + class Ints(enum.IntEnum): + A = 1 + B = 2 + + literal = Literal[ + Literal[Ints.A], + Literal[Ints.B], + Literal[1], + Literal[2], + ] + self.assertEqual(literal.__args__, (Ints.A, Ints.B, 1, 2)) + + def test_caching_of_Literal_respects_type(self): + self.assertIs(type(Literal[1].__args__[0]), int) + self.assertIs(type(Literal[True].__args__[0]), bool) + class MethodHolder: @classmethod @@ -841,7 +1104,7 @@ def __str__(self): def __add__(self, other): return 0 -@runtime +@runtime_checkable class HasCallProtocol(Protocol): __call__: typing.Callable @@ -889,37 +1152,67 @@ 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 class GetTypeHintTests(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "ann_module.py").write_text(ANN_MODULE_SOURCE) + Path(tempdir, "ann_module2.py").write_text(ANN_MODULE_2_SOURCE) + Path(tempdir, "ann_module3.py").write_text(ANN_MODULE_3_SOURCE) + cls.ann_module = importlib.import_module("ann_module") + cls.ann_module2 = importlib.import_module("ann_module2") + cls.ann_module3 = importlib.import_module("ann_module3") + sys.path.pop() + + @classmethod + def tearDownClass(cls): + for modname in "ann_module", "ann_module2", "ann_module3": + delattr(cls, modname) + del sys.modules[modname] + def test_get_type_hints_modules(self): ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str} - if (TYPING_3_11_0 - or (TYPING_3_10_0 and sys.version_info.releaselevel in {'candidate', 'final'})): - # More tests were added in 3.10rc1. - ann_module_type_hints['u'] = int | float - self.assertEqual(gth(ann_module), ann_module_type_hints) - self.assertEqual(gth(ann_module2), {}) - self.assertEqual(gth(ann_module3), {}) + self.assertEqual(gth(self.ann_module), ann_module_type_hints) + self.assertEqual(gth(self.ann_module2), {}) + self.assertEqual(gth(self.ann_module3), {}) def test_get_type_hints_classes(self): - self.assertEqual(gth(ann_module.C, ann_module.__dict__), - {'y': Optional[ann_module.C]}) - self.assertIsInstance(gth(ann_module.j_class), dict) - self.assertEqual(gth(ann_module.M), {'123': 123, 'o': type}) - self.assertEqual(gth(ann_module.D), - {'j': str, 'k': str, 'y': Optional[ann_module.C]}) - self.assertEqual(gth(ann_module.Y), {'z': int}) - self.assertEqual(gth(ann_module.h_class), - {'y': Optional[ann_module.C]}) - self.assertEqual(gth(ann_module.S), {'x': str, 'y': str}) - self.assertEqual(gth(ann_module.foo), {'x': int}) + self.assertEqual(gth(self.ann_module.C, self.ann_module.__dict__), + {'y': Optional[self.ann_module.C]}) + self.assertIsInstance(gth(self.ann_module.j_class), dict) + self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type}) + self.assertEqual(gth(self.ann_module.D), + {'j': str, 'k': str, 'y': Optional[self.ann_module.C]}) + self.assertEqual(gth(self.ann_module.Y), {'z': int}) + self.assertEqual(gth(self.ann_module.h_class), + {'y': Optional[self.ann_module.C]}) + self.assertEqual(gth(self.ann_module.S), {'x': str, 'y': str}) + self.assertEqual(gth(self.ann_module.foo), {'x': int}) self.assertEqual(gth(NoneAndForward, globals()), {'parent': NoneAndForward, 'meaning': type(None)}) @@ -927,10 +1220,10 @@ def test_respect_no_type_check(self): @no_type_check class NoTpCheck: class Inn: - def __init__(self, x: 'not a type'): ... # noqa + def __init__(self, x: 'not a type'): ... self.assertTrue(NoTpCheck.__no_type_check__) self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__) - self.assertEqual(gth(ann_module2.NTC.meth), {}) + self.assertEqual(gth(self.ann_module2.NTC.meth), {}) class ABase(Generic[T]): def meth(x: int): ... @no_type_check @@ -938,8 +1231,8 @@ class Der(ABase): ... self.assertEqual(gth(ABase.meth), {'x': int}) def test_get_type_hints_ClassVar(self): - self.assertEqual(gth(ann_module2.CV, ann_module2.__dict__), - {'var': ClassVar[ann_module2.CV]}) + self.assertEqual(gth(self.ann_module2.CV, self.ann_module2.__dict__), + {'var': ClassVar[self.ann_module2.CV]}) self.assertEqual(gth(B, globals()), {'y': int, 'x': ClassVar[Optional[B]], 'b': int}) self.assertEqual(gth(CSub, globals()), @@ -1048,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): @@ -1185,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): @@ -1206,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) @@ -1293,29 +1579,96 @@ def foo(a: A) -> Optional[BaseException]: class NewTypeTests(BaseTestCase): + @classmethod + def setUpClass(cls): + global UserId + UserId = NewType('UserId', int) + cls.UserName = NewType(cls.__qualname__ + '.UserName', str) + + @classmethod + def tearDownClass(cls): + global UserId + del UserId + del cls.UserName def test_basic(self): - UserId = NewType('UserId', int) - UserName = NewType('UserName', str) self.assertIsInstance(UserId(5), int) - self.assertIsInstance(UserName('Joe'), str) + self.assertIsInstance(self.UserName('Joe'), str) self.assertEqual(UserId(5) + 1, 6) def test_errors(self): - UserId = NewType('UserId', int) - UserName = NewType('UserName', str) with self.assertRaises(TypeError): issubclass(UserId, int) with self.assertRaises(TypeError): - class D(UserName): + class D(UserId): pass + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + for cls in (int, self.UserName): + with self.subTest(cls=cls): + self.assertEqual(UserId | cls, Union[UserId, cls]) + self.assertEqual(cls | UserId, Union[cls, UserId]) + + self.assertEqual(get_args(UserId | cls), (UserId, cls)) + self.assertEqual(get_args(cls | UserId), (cls, UserId)) + + def test_special_attrs(self): + self.assertEqual(UserId.__name__, 'UserId') + self.assertEqual(UserId.__qualname__, 'UserId') + self.assertEqual(UserId.__module__, __name__) + self.assertEqual(UserId.__supertype__, int) + + UserName = self.UserName + self.assertEqual(UserName.__name__, 'UserName') + self.assertEqual(UserName.__qualname__, + self.__class__.__qualname__ + '.UserName') + self.assertEqual(UserName.__module__, __name__) + self.assertEqual(UserName.__supertype__, str) + + def test_repr(self): + self.assertEqual(repr(UserId), f'{__name__}.UserId') + self.assertEqual(repr(self.UserName), + f'{__name__}.{self.__class__.__qualname__}.UserName') + + def test_pickle(self): + UserAge = NewType('UserAge', float) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + pickled = pickle.dumps(UserId, proto) + loaded = pickle.loads(pickled) + self.assertIs(loaded, UserId) + + pickled = pickle.dumps(self.UserName, proto) + loaded = pickle.loads(pickled) + self.assertIs(loaded, self.UserName) + + with self.assertRaises(pickle.PicklingError): + pickle.dumps(UserAge, proto) + + def test_missing__name__(self): + code = ("import typing_extensions\n" + "NT = typing_extensions.NewType('NT', int)\n" + ) + exec(code, {}) + + def test_error_message_when_subclassing(self): + with self.assertRaisesRegex( + TypeError, + re.escape( + "Cannot subclass an instance of NewType. Perhaps you were looking for: " + "`ProUserId = NewType('ProUserId', UserId)`" + ) + ): + class ProUserId(UserId): + ... + class Coordinate(Protocol): x: int y: int -@runtime +@runtime_checkable class Point(Coordinate, Protocol): label: str @@ -1330,11 +1683,11 @@ class XAxis(Protocol): class YAxis(Protocol): y: int -@runtime +@runtime_checkable class Position(XAxis, YAxis, Protocol): pass -@runtime +@runtime_checkable class Proto(Protocol): attr: int @@ -1357,10 +1710,18 @@ class NT(NamedTuple): y: int +skip_if_py312b1 = skipIf( + sys.version_info == (3, 12, 0, 'beta', 1), + "CPython had bugs in 3.12.0b1" +) + + class ProtocolTests(BaseTestCase): + def test_runtime_alias(self): + self.assertIs(runtime, runtime_checkable) def test_basic_protocol(self): - @runtime + @runtime_checkable class P(Protocol): def meth(self): pass @@ -1378,7 +1739,7 @@ def f(): self.assertNotIsInstance(f, P) def test_everything_implements_empty_protocol(self): - @runtime + @runtime_checkable class Empty(Protocol): pass class C: pass def f(): @@ -1410,6 +1771,83 @@ class E(C, BP): pass self.assertNotIsInstance(D(), E) self.assertNotIsInstance(E(), D) + @only_with_typing_Protocol + def test_runtimecheckable_on_typing_dot_Protocol(self): + @runtime_checkable + class Foo(typing.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_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): @@ -1427,8 +1865,34 @@ class PG(Protocol[T]): pass class CG(PG[T]): pass self.assertIsInstance(CG[int](), CG) + def test_protocol_defining_init_does_not_get_overridden(self): + # check that P.__init__ doesn't get clobbered + # see https://bugs.python.org/issue44807 + + class P(Protocol): + x: int + def __init__(self, x: int) -> None: + self.x = x + class C: pass + + c = C() + P.__init__(c, 1) + self.assertEqual(c.x, 1) + + def test_concrete_class_inheriting_init_from_protocol(self): + class P(Protocol): + x: int + def __init__(self, x: int) -> None: + self.x = x + + class C(P): pass + + c = C(1) + self.assertIsInstance(c, C) + self.assertEqual(c.x, 1) + def test_cannot_instantiate_abstract(self): - @runtime + @runtime_checkable class P(Protocol): @abc.abstractmethod def ameth(self) -> int: @@ -1446,7 +1910,7 @@ def test_subprotocols_extending(self): class P1(Protocol): def meth1(self): pass - @runtime + @runtime_checkable class P2(P1, Protocol): def meth2(self): pass @@ -1475,7 +1939,7 @@ def meth1(self): class P2(Protocol): def meth2(self): pass - @runtime + @runtime_checkable class P(P1, P2, Protocol): pass class C: @@ -1498,10 +1962,10 @@ def meth2(self): def test_protocols_issubclass(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): def x(self): ... - @runtime + @runtime_checkable class PG(Protocol[T]): def x(self): ... class BadP(Protocol): @@ -1513,67 +1977,521 @@ def x(self): ... self.assertIsSubclass(C, P) self.assertIsSubclass(C, PG) self.assertIsSubclass(BadP, PG) - with self.assertRaises(TypeError): + + no_subscripted_generics = ( + "Subscripted generics cannot be used with class and instance checks" + ) + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(C, PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(C, PG[C]) - with self.assertRaises(TypeError): + + only_runtime_checkable_protocols = ( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols): issubclass(C, BadP) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols): issubclass(C, BadPG) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(P, PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): issubclass(PG, PG[int]) + only_classes_allowed = r"issubclass\(\) arg 1 must be a class" + + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, P) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, PG) + with self.assertRaisesRegex(TypeError, only_classes_allowed): + issubclass(1, BadP) + 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 - @runtime + + @runtime_checkable class PNonCall(Protocol): x = 1 - with self.assertRaises(TypeError): + + non_callable_members_illegal = ( + "Protocols with non-method members don't support issubclass()" + ) + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) PNonCall.register(C) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(C, PNonCall) + self.assertIsInstance(C(), PNonCall) + # check that non-protocol subclasses are not affected class D(PNonCall): ... + self.assertNotIsSubclass(C, D) self.assertNotIsInstance(C(), D) D.register(C) self.assertIsSubclass(C, D) self.assertIsInstance(C(), D) - with self.assertRaises(TypeError): + + with self.assertRaisesRegex(TypeError, non_callable_members_illegal): issubclass(D, PNonCall) + def test_no_weird_caching_with_issubclass_after_isinstance(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: + def __init__(self) -> None: + self.x = 42 + + self.assertIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): + issubclass(Eggs, Spam) + + def test_no_weird_caching_with_issubclass_after_isinstance_2(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: ... + + self.assertNotIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): + issubclass(Eggs, Spam) + + def test_no_weird_caching_with_issubclass_after_isinstance_3(self): + @runtime_checkable + class Spam(Protocol): + x: int + + class Eggs: + def __getattr__(self, attr): + if attr == "x": + return 42 + raise AttributeError(attr) + + self.assertNotIsInstance(Eggs(), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaisesRegex( + TypeError, + "Protocols with non-method members don't support issubclass()" + ): + issubclass(Eggs, Spam) + def test_protocols_isinstance(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): def meth(x): ... - @runtime + @runtime_checkable class PG(Protocol[T]): def meth(x): ... + @runtime_checkable + class WeirdProto(Protocol): + meth = str.maketrans + @runtime_checkable + class WeirdProto2(Protocol): + meth = lambda *args, **kwargs: None # noqa: E731 + class CustomCallable: + def __call__(self, *args, **kwargs): + pass + @runtime_checkable + class WeirderProto(Protocol): + meth = CustomCallable() class BadP(Protocol): def meth(x): ... class BadPG(Protocol[T]): def meth(x): ... class C: def meth(x): ... - self.assertIsInstance(C(), P) - self.assertIsInstance(C(), PG) - with self.assertRaises(TypeError): + class C2: + def __init__(self): + self.meth = lambda: None + for klass in C, C2: + for proto in P, PG, WeirdProto, WeirdProto2, WeirderProto: + with self.subTest(klass=klass.__name__, proto=proto.__name__): + self.assertIsInstance(klass(), proto) + + no_subscripted_generics = ( + "Subscripted generics cannot be used with class and instance checks" + ) + + with self.assertRaisesRegex(TypeError, no_subscripted_generics): isinstance(C(), PG[T]) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, no_subscripted_generics): isinstance(C(), PG[C]) - with self.assertRaises(TypeError): + + only_runtime_checkable_msg = ( + "Instance and class checks can only be used " + "with @runtime_checkable protocols" + ) + + with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg): isinstance(C(), BadP) - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg): isinstance(C(), BadPG) + def test_protocols_isinstance_properties_and_descriptors(self): + class C: + @property + def attr(self): + return 42 + + class CustomDescriptor: + def __get__(self, obj, objtype=None): + return 42 + + class D: + attr = CustomDescriptor() + + # Check that properties set on superclasses + # are still found by the isinstance() logic + class E(C): ... + class F(D): ... + + class Empty: ... + + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + @runtime_checkable + class MethodP(Protocol): + def attr(self): ... + + @runtime_checkable + class MethodPG(Protocol[T]): + def attr(self) -> T: ... + + for protocol_class in P, P1, PG, PG1, MethodP, MethodPG: + for klass in C, D, E, F: + with self.subTest( + klass=klass.__name__, + protocol_class=protocol_class.__name__ + ): + self.assertIsInstance(klass(), protocol_class) + + with self.subTest(klass="Empty", protocol_class=protocol_class.__name__): + self.assertNotIsInstance(Empty(), protocol_class) + + class BadP(Protocol): + @property + def attr(self): ... + + class BadP1(Protocol): + attr: int + + class BadPG(Protocol[T]): + @property + def attr(self): ... + + class BadPG1(Protocol[T]): + attr: T + + cases = ( + PG[T], PG[C], PG1[T], PG1[C], MethodPG[T], + MethodPG[C], BadP, BadP1, BadPG, BadPG1 + ) + + for obj in cases: + for klass in C, D, E, F, Empty: + with self.subTest(klass=klass.__name__, obj=obj): + with self.assertRaises(TypeError): + isinstance(klass(), obj) + + def test_protocols_isinstance_not_fooled_by_custom_dir(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class CustomDirWithX: + x = 10 + def __dir__(self): + return [] + + class CustomDirWithoutX: + def __dir__(self): + return ["x"] + + self.assertIsInstance(CustomDirWithX(), HasX) + self.assertNotIsInstance(CustomDirWithoutX(), HasX) + + def test_protocols_isinstance_attribute_access_with_side_effects(self): + class C: + @property + def attr(self): + raise AttributeError('no') + + class CustomDescriptor: + def __get__(self, obj, objtype=None): + raise RuntimeError("NO") + + class D: + attr = CustomDescriptor() + + # Check that properties set on superclasses + # are still found by the isinstance() logic + class E(C): ... + class F(D): ... + + class WhyWouldYouDoThis: + def __getattr__(self, name): + raise RuntimeError("wut") + + T = TypeVar('T') + + @runtime_checkable + class P(Protocol): + @property + def attr(self): ... + + @runtime_checkable + class P1(Protocol): + attr: int + + @runtime_checkable + class PG(Protocol[T]): + @property + def attr(self): ... + + @runtime_checkable + class PG1(Protocol[T]): + attr: T + + @runtime_checkable + class MethodP(Protocol): + def attr(self): ... + + @runtime_checkable + class MethodPG(Protocol[T]): + def attr(self) -> T: ... + + for protocol_class in P, P1, PG, PG1, MethodP, MethodPG: + for klass in C, D, E, F: + with self.subTest( + klass=klass.__name__, + protocol_class=protocol_class.__name__ + ): + self.assertIsInstance(klass(), protocol_class) + + with self.subTest( + klass="WhyWouldYouDoThis", + protocol_class=protocol_class.__name__ + ): + self.assertNotIsInstance(WhyWouldYouDoThis(), protocol_class) + + def test_protocols_isinstance___slots__(self): + # As per the consensus in https://github.com/python/typing/issues/1367, + # this is desirable behaviour + @runtime_checkable + class HasX(Protocol): + x: int + + class HasNothingButSlots: + __slots__ = ("x",) + + self.assertIsInstance(HasNothingButSlots(), HasX) + def test_protocols_isinstance_py36(self): class APoint: def __init__(self, x, y, label): @@ -1607,10 +2525,10 @@ class Bad: pass def test_protocols_isinstance_init(self): T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol): x = 1 - @runtime + @runtime_checkable class PG(Protocol[T]): x = 1 class C: @@ -1619,8 +2537,94 @@ def __init__(self, x): self.assertIsInstance(C(1), P) self.assertIsInstance(C(1), PG) + def test_protocols_isinstance_monkeypatching(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class Foo: ... + + f = Foo() + self.assertNotIsInstance(f, HasX) + f.x = 42 + self.assertIsInstance(f, HasX) + del f.x + self.assertNotIsInstance(f, HasX) + + @skip_if_py312b1 + def test_runtime_checkable_generic_non_protocol(self): + # Make sure this doesn't raise AttributeError + with self.assertRaisesRegex( + TypeError, + "@runtime_checkable can be only applied to protocol classes", + ): + @runtime_checkable + class Foo(Generic[T]): ... + + def test_runtime_checkable_generic(self): + @runtime_checkable + class Foo(Protocol[T]): + def meth(self) -> T: ... + + class Impl: + def meth(self) -> int: ... + + self.assertIsSubclass(Impl, Foo) + + class NotImpl: + def method(self) -> int: ... + + self.assertNotIsSubclass(NotImpl, Foo) + + if sys.version_info >= (3, 12): + exec(textwrap.dedent( + """ + @skip_if_py312b1 + def test_pep695_generics_can_be_runtime_checkable(self): + @runtime_checkable + class HasX(Protocol): + x: int + + class Bar[T]: + x: T + def __init__(self, x): + self.x = x + + class Capybara[T]: + y: str + def __init__(self, y): + self.y = y + + self.assertIsInstance(Bar(1), HasX) + self.assertNotIsInstance(Capybara('a'), HasX) + """ + )) + + @skip_if_py312b1 + def test_protocols_isinstance_generic_classes(self): + T = TypeVar("T") + + class Foo(Generic[T]): + x: T + + def __init__(self, x): + self.x = x + + class Bar(Foo[int]): + ... + + @runtime_checkable + class HasX(Protocol): + x: int + + foo = Foo(1) + self.assertIsInstance(foo, HasX) + + bar = Bar(2) + self.assertIsInstance(bar, HasX) + def test_protocols_support_register(self): - @runtime + @runtime_checkable class P(Protocol): x = 1 class PM(Protocol): @@ -1633,7 +2637,7 @@ class C: pass self.assertIsInstance(C(), D) def test_none_on_non_callable_doesnt_block_implementation(self): - @runtime + @runtime_checkable class P(Protocol): x = 1 class A: @@ -1647,7 +2651,7 @@ def __init__(self): self.assertIsInstance(C(), P) def test_none_on_callable_blocks_implementation(self): - @runtime + @runtime_checkable class P(Protocol): def x(self): ... class A: @@ -1663,16 +2667,16 @@ def __init__(self): def test_non_protocol_subclasses(self): class P(Protocol): x = 1 - @runtime + @runtime_checkable class PR(Protocol): def meth(self): pass class NonP(P): x = 1 class NonPR(PR): pass - class C: + class C(metaclass=abc.ABCMeta): x = 1 - class D: - def meth(self): pass + class D(metaclass=abc.ABCMeta): # noqa: B024 + def meth(self): pass # noqa: B027 self.assertNotIsInstance(C(), NonP) self.assertNotIsInstance(D(), NonPR) self.assertNotIsSubclass(C, NonP) @@ -1680,6 +2684,20 @@ def meth(self): pass self.assertIsInstance(NonPR(), PR) self.assertIsSubclass(NonPR, PR) + self.assertNotIn("__protocol_attrs__", vars(NonP)) + self.assertNotIn("__protocol_attrs__", vars(NonPR)) + self.assertNotIn("__callable_proto_members_only__", vars(NonP)) + self.assertNotIn("__callable_proto_members_only__", vars(NonPR)) + + acceptable_extra_attrs = { + '_is_protocol', '_is_runtime_protocol', '__parameters__', + '__init__', '__annotations__', '__subclasshook__', + } + self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs) + self.assertLessEqual( + vars(NonPR).keys(), vars(D).keys() | acceptable_extra_attrs + ) + def test_custom_subclasshook(self): class P(Protocol): x = 1 @@ -1695,18 +2713,63 @@ 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 + @runtime_checkable class P(Protocol): x = 1 class C: pass - with self.assertRaises(TypeError): + with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"): issubclass(C(), P) def test_defining_generic_protocols(self): T = TypeVar('T') S = TypeVar('S') - @runtime + @runtime_checkable class PR(Protocol[T, S]): def meth(self): pass class P(PR[int, T], Protocol[T]): @@ -1730,7 +2793,7 @@ class C(PR[int, T]): pass def test_defining_generic_protocols_old_style(self): T = TypeVar('T') S = TypeVar('S') - @runtime + @runtime_checkable class PR(Protocol, Generic[T, S]): def meth(self): pass class P(PR[int, str], Protocol): @@ -1747,7 +2810,7 @@ class P1(Protocol, Generic[T]): def bar(self, x: T) -> str: ... class P2(Generic[T], Protocol): def bar(self, x: T) -> str: ... - @runtime + @runtime_checkable class PSub(P1[str], Protocol): x = 1 class Test: @@ -1759,6 +2822,48 @@ def bar(self, x: str) -> str: with self.assertRaises(TypeError): PR[int, ClassVar] + if hasattr(typing, "TypeAliasType"): + exec(textwrap.dedent( + """ + def test_pep695_generic_protocol_callable_members(self): + @runtime_checkable + class Foo[T](Protocol): + def meth(self, x: T) -> None: ... + + class Bar[T]: + def meth(self, x: T) -> None: ... + + self.assertIsInstance(Bar(), Foo) + self.assertIsSubclass(Bar, Foo) + + @runtime_checkable + class SupportsTrunc[T](Protocol): + def __trunc__(self) -> T: ... + + self.assertIsInstance(0.0, SupportsTrunc) + self.assertIsSubclass(float, SupportsTrunc) + + def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self): + @runtime_checkable + class Spam[T](Protocol): + x: T + + class Eggs[T]: + def __init__(self, x: T) -> None: + self.x = x + + self.assertIsInstance(Eggs(42), Spam) + + # gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta, + # TypeError wouldn't be raised here, + # as the cached result of the isinstance() check immediately above + # would mean the issubclass() call would short-circuit + # before we got to the "raise TypeError" line + with self.assertRaises(TypeError): + issubclass(Eggs, Spam) + """ + )) + def test_init_called(self): T = TypeVar('T') class P(Protocol[T]): pass @@ -1804,7 +2909,7 @@ class P(Protocol[T]): pass self.assertIs(P[int].__origin__, P) def test_generic_protocols_special_from_protocol(self): - @runtime + @runtime_checkable class PR(Protocol): x = 1 class P(Protocol): @@ -1818,11 +2923,7 @@ def meth(self): self.assertTrue(P._is_protocol) self.assertTrue(PR._is_protocol) self.assertTrue(PG._is_protocol) - if hasattr(typing, 'Protocol'): - self.assertFalse(P._is_runtime_protocol) - else: - with self.assertRaises(AttributeError): - self.assertFalse(P._is_runtime_protocol) + self.assertFalse(P._is_runtime_protocol) self.assertTrue(PR._is_runtime_protocol) self.assertTrue(PG[int]._is_protocol) self.assertEqual(typing_extensions._get_protocol_attrs(P), {'meth'}) @@ -1832,17 +2933,17 @@ def meth(self): def test_no_runtime_deco_on_nominal(self): with self.assertRaises(TypeError): - @runtime + @runtime_checkable class C: pass class Proto(Protocol): x = 1 with self.assertRaises(TypeError): - @runtime + @runtime_checkable class Concrete(Proto): pass def test_none_treated_correctly(self): - @runtime + @runtime_checkable class P(Protocol): x: int = None class B(object): pass @@ -1859,8 +2960,8 @@ def __init__(self): class DI: def __init__(self): self.x = None - self.assertIsInstance(C(), P) - self.assertIsInstance(D(), P) + self.assertIsInstance(CI(), P) + self.assertIsInstance(DI(), P) def test_protocols_in_unions(self): class P(Protocol): @@ -1873,7 +2974,7 @@ def test_protocols_pickleable(self): global P, CP # pickle wants to reference the class by name T = TypeVar('T') - @runtime + @runtime_checkable class P(Protocol[T]): x = 1 class CP(P[int]): @@ -1910,6 +3011,61 @@ 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): + pass + + 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 + + with self.assertRaisesRegex(TypeError, "@runtime_checkable"): + isinstance(1, P) + def test_no_init_same_for_different_protocol_implementations(self): class CustomProtocolWithoutInitA(Protocol): pass @@ -1919,6 +3075,256 @@ class CustomProtocolWithoutInitB(Protocol): self.assertEqual(CustomProtocolWithoutInitA.__init__, CustomProtocolWithoutInitB.__init__) + def test_protocol_generic_over_paramspec(self): + P = ParamSpec("P") + T = TypeVar("T") + T2 = TypeVar("T2") + + class MemoizedFunc(Protocol[P, T, T2]): + cache: typing.Dict[T2, T] + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ... + + self.assertEqual(MemoizedFunc.__parameters__, (P, T, T2)) + self.assertTrue(MemoizedFunc._is_protocol) + + with self.assertRaises(TypeError): + MemoizedFunc[[int, str, str]] + + if sys.version_info >= (3, 10): + # These unfortunately don't pass on <=3.9, + # due to typing._type_check on older Python versions + X = MemoizedFunc[[int, str, str], T, T2] + self.assertEqual(X.__parameters__, (T, T2)) + self.assertEqual(X.__args__, ((int, str, str), T, T2)) + + Y = X[bytes, memoryview] + self.assertEqual(Y.__parameters__, ()) + self.assertEqual(Y.__args__, ((int, str, str), bytes, memoryview)) + + def test_protocol_generic_over_typevartuple(self): + Ts = TypeVarTuple("Ts") + T = TypeVar("T") + T2 = TypeVar("T2") + + class MemoizedFunc(Protocol[Unpack[Ts], T, T2]): + cache: typing.Dict[T2, T] + def __call__(self, *args: Unpack[Ts]) -> T: ... + + self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) + self.assertTrue(MemoizedFunc._is_protocol) + + things = "arguments" if sys.version_info >= (3, 11) else "parameters" + + # A bug was fixed in 3.11.1 + # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) + # That means this assertion doesn't pass on 3.11.0, + # but it passes on all other Python versions + if sys.version_info[:3] != (3, 11, 0): + with self.assertRaisesRegex(TypeError, f"Too few {things}"): + MemoizedFunc[int] + + X = MemoizedFunc[int, T, T2] + self.assertEqual(X.__parameters__, (T, T2)) + self.assertEqual(X.__args__, (int, T, T2)) + + Y = X[bytes, memoryview] + 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 + collections.abc.Sized._abc_registry_clear() + + class Foo(collections.abc.Sized, Protocol): pass + + # CPython gh-105144: this previously raised TypeError + # if a Protocol subclass of Sized had been created + # before any isinstance() checks against Sized + self.assertNotIsInstance(1, collections.abc.Sized) + + @skip_if_py312b1 + def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self): + # Ensure the cache is empty, or this test won't work correctly + collections.abc.Sized._abc_registry_clear() + + class Foo(typing.Sized, Protocol): pass + + # CPython gh-105144: this previously raised TypeError + # if a Protocol subclass of Sized had been created + # 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 @@ -1934,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) @@ -1950,8 +3355,15 @@ 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): - Emp = TypedDict('Emp', name=str, id=int) + with self.assertWarns(DeprecationWarning): + Emp = TypedDict('Emp', name=str, id=int) self.assertIsSubclass(Emp, dict) self.assertIsSubclass(Emp, typing.MutableMapping) self.assertNotIsSubclass(Emp, collections.abc.Sequence) @@ -1965,9 +3377,11 @@ 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): - TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, - fields=list, _fields=dict) + with self.assertWarns(DeprecationWarning): + TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, + fields=list, _fields=dict) self.assertEqual(TD.__name__, 'TD') self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, '_typename': int, 'fields': list, '_fields': dict}) @@ -1980,28 +3394,22 @@ 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__() with self.assertRaises(TypeError): TypedDict() with self.assertRaises(TypeError): - 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}) + TypedDict('Emp', [('name', str)], None) 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 hasattr(typing, "Required"): + if sys.version_info >= (3, 13): self.assertEqual(TypedDict.__module__, 'typing') else: self.assertEqual(TypedDict.__module__, 'typing_extensions') @@ -2014,7 +3422,7 @@ def test_typeddict_errors(self): issubclass(dict, Emp) if not TYPING_3_11_0: - with self.assertRaises(TypeError): + with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning): TypedDict('Hi', x=1) with self.assertRaises(TypeError): TypedDict('Hi', [('x', int), ('y', 1)]) @@ -2024,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) @@ -2036,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) @@ -2048,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]) @@ -2078,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, @@ -2104,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): @@ -2155,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 @@ -2220,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): @@ -2259,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): @@ -2368,16 +4046,6 @@ class C: get_type_hints(C, globals())["const"], Annotated[Final[int], "Const"] ) - def test_hash_eq(self): - self.assertEqual(len({Annotated[int, 4, 5], Annotated[int, 4, 5]}), 1) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[int, 5, 4]) - self.assertNotEqual(Annotated[int, 4, 5], Annotated[str, 4, 5]) - self.assertNotEqual(Annotated[int, 4], Annotated[int, 4, 4]) - self.assertEqual( - {Annotated[int, 4, 5], Annotated[int, 4, 5], Annotated[T, 4, 5]}, - {Annotated[int, 4, 5], Annotated[T, 4, 5]} - ) - def test_cannot_subclass(self): with self.assertRaisesRegex(TypeError, "Cannot subclass .*Annotated"): class C(Annotated): @@ -2528,6 +4196,49 @@ def test_get_type_hints_typeddict(self): 'year': NotRequired[Annotated[int, 2000]], } + 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,)) + class TypeAliasTests(BaseTestCase): def test_canonical_usage_with_variable_annotation(self): @@ -2577,6 +4288,7 @@ def test_basic_plain(self): P = ParamSpec('P') self.assertEqual(P, P) self.assertIsInstance(P, ParamSpec) + self.assertEqual(P.__name__, 'P') # Should be hashable hash(P) @@ -2584,6 +4296,7 @@ def test_repr(self): P = ParamSpec('P') P_co = ParamSpec('P_co', covariant=True) P_contra = ParamSpec('P_contra', contravariant=True) + P_infer = ParamSpec('P_infer', infer_variance=True) P_2 = ParamSpec('P_2') self.assertEqual(repr(P), '~P') self.assertEqual(repr(P_2), '~P_2') @@ -2592,6 +4305,30 @@ def test_repr(self): # just follow CPython. self.assertEqual(repr(P_co), '+P_co') self.assertEqual(repr(P_contra), '-P_contra') + # On other versions we use typing.ParamSpec, but it is not aware of + # infer_variance=. Not worth creating our own version of ParamSpec + # for this. + if hasattr(typing, 'TypeAliasType') or not hasattr(typing, 'ParamSpec'): + self.assertEqual(repr(P_infer), 'P_infer') + else: + self.assertEqual(repr(P_infer), '~P_infer') + + def test_variance(self): + P_co = ParamSpec('P_co', covariant=True) + P_contra = ParamSpec('P_contra', contravariant=True) + P_infer = ParamSpec('P_infer', infer_variance=True) + + self.assertIs(P_co.__covariant__, True) + self.assertIs(P_co.__contravariant__, False) + self.assertIs(P_co.__infer_variance__, False) + + self.assertIs(P_contra.__covariant__, False) + self.assertIs(P_contra.__contravariant__, True) + self.assertIs(P_contra.__infer_variance__, False) + + self.assertIs(P_infer.__covariant__, False) + self.assertIs(P_infer.__contravariant__, False) + self.assertIs(P_infer.__infer_variance__, True) def test_valid_uses(self): P = ParamSpec('P') @@ -2603,7 +4340,6 @@ def test_valid_uses(self): self.assertEqual(C2.__args__, (P, T)) self.assertEqual(C2.__parameters__, (P, T)) - # Test collections.abc.Callable too. if sys.version_info[:2] >= (3, 9): # Note: no tests for Callable.__parameters__ here @@ -2648,13 +4384,18 @@ def test_user_generics(self): class X(Generic[T, P]): pass - G1 = X[int, P_2] - self.assertEqual(G1.__args__, (int, P_2)) - self.assertEqual(G1.__parameters__, (P_2,)) + class Y(Protocol[T, P]): + pass + + for klass in X, Y: + with self.subTest(klass=klass.__name__): + G1 = klass[int, P_2] + self.assertEqual(G1.__args__, (int, P_2)) + self.assertEqual(G1.__parameters__, (P_2,)) - G2 = X[int, Concatenate[int, P_2]] - self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) - self.assertEqual(G2.__parameters__, (P_2,)) + G2 = klass[int, Concatenate[int, P_2]] + self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) + self.assertEqual(G2.__parameters__, (P_2,)) # The following are some valid uses cases in PEP 612 that don't work: # These do not work in 3.9, _type_check blocks the list and ellipsis. @@ -2667,6 +4408,9 @@ class X(Generic[T, P]): class Z(Generic[P]): pass + class ProtoZ(Protocol[P]): + pass + def test_pickle(self): global P, P_co, P_contra, P_default P = ParamSpec('P') @@ -2919,10 +4663,7 @@ def test_basic_plain(self): def test_repr(self): Ts = TypeVarTuple('Ts') - if TYPING_3_11_0: - self.assertEqual(repr(Unpack[Ts]), '*Ts') - else: - self.assertEqual(repr(Unpack[Ts]), 'typing_extensions.Unpack[Ts]') + self.assertEqual(repr(Unpack[Ts]), f'{Unpack.__module__}.Unpack[Ts]') def test_cannot_subclass_vars(self): with self.assertRaises(TypeError): @@ -2976,31 +4717,49 @@ def test_concatenation(self): self.assertEqual(Tuple[int, Unpack[Xs], str].__args__, (int, Unpack[Xs], str)) class C(Generic[Unpack[Xs]]): pass - self.assertEqual(C[int, Unpack[Xs]].__args__, (int, Unpack[Xs])) - self.assertEqual(C[Unpack[Xs], int].__args__, (Unpack[Xs], int)) - self.assertEqual(C[int, Unpack[Xs], str].__args__, - (int, Unpack[Xs], str)) + class D(Protocol[Unpack[Xs]]): pass + for klass in C, D: + with self.subTest(klass=klass.__name__): + self.assertEqual(klass[int, Unpack[Xs]].__args__, (int, Unpack[Xs])) + self.assertEqual(klass[Unpack[Xs], int].__args__, (Unpack[Xs], int)) + self.assertEqual(klass[int, Unpack[Xs], str].__args__, + (int, Unpack[Xs], str)) def test_class(self): Ts = TypeVarTuple('Ts') class C(Generic[Unpack[Ts]]): pass - self.assertEqual(C[int].__args__, (int,)) - self.assertEqual(C[int, str].__args__, (int, str)) + class D(Protocol[Unpack[Ts]]): pass + + for klass in C, D: + with self.subTest(klass=klass.__name__): + self.assertEqual(klass[int].__args__, (int,)) + self.assertEqual(klass[int, str].__args__, (int, str)) with self.assertRaises(TypeError): class C(Generic[Unpack[Ts], int]): pass + with self.assertRaises(TypeError): + class D(Protocol[Unpack[Ts], int]): pass + T1 = TypeVar('T') T2 = TypeVar('T') class C(Generic[T1, T2, Unpack[Ts]]): pass - self.assertEqual(C[int, str].__args__, (int, str)) - self.assertEqual(C[int, str, float].__args__, (int, str, float)) - self.assertEqual(C[int, str, float, bool].__args__, (int, str, float, bool)) - # TODO This should probably also fail on 3.11, pending changes to CPython. - if not TYPING_3_11_0: - with self.assertRaises(TypeError): - C[int] + class D(Protocol[T1, T2, Unpack[Ts]]): pass + for klass in C, D: + with self.subTest(klass=klass.__name__): + self.assertEqual(klass[int, str].__args__, (int, str)) + self.assertEqual(klass[int, str, float].__args__, (int, str, float)) + self.assertEqual( + klass[int, str, float, bool].__args__, (int, str, float, bool) + ) + # A bug was fixed in 3.11.1 + # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) + # That means this assertion doesn't pass on 3.11.0, + # but it passes on all other Python versions + if sys.version_info[:3] != (3, 11, 0): + with self.assertRaises(TypeError): + klass[int] class TypeVarTupleTests(BaseTestCase): @@ -3044,7 +4803,10 @@ def test_args_and_parameters(self): Ts = TypeVarTuple('Ts') t = Tuple[tuple(Ts)] - self.assertEqual(t.__args__, (Unpack[Ts],)) + if sys.version_info >= (3, 11): + self.assertEqual(t.__args__, (typing.Unpack[Ts],)) + else: + self.assertEqual(t.__args__, (Unpack[Ts],)) self.assertEqual(t.__parameters__, (Ts,)) def test_pickle(self): @@ -3136,7 +4898,10 @@ def cached(self): ... class RevealTypeTests(BaseTestCase): def test_reveal_type(self): obj = object() - self.assertIs(obj, reveal_type(obj)) + + with contextlib.redirect_stderr(io.StringIO()) as stderr: + self.assertIs(obj, reveal_type(obj)) + self.assertEqual("Runtime type is 'object'", stderr.getvalue().strip()) class DataclassTransformTests(BaseTestCase): @@ -3229,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) @@ -3291,18 +5070,26 @@ def test_typing_extensions_defers_when_possible(self): 'overload', 'ParamSpec', 'Text', - 'TypedDict', 'TypeVar', 'TypeVarTuple', 'TYPE_CHECKING', 'Final', 'get_type_hints', - 'is_typeddict', } if sys.version_info < (3, 10): exclude |= {'get_args', 'get_origin'} + if sys.version_info < (3, 10, 1): + exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'NamedTuple', 'Any'} + exclude |= {'final', 'Any', 'NewType'} + if sys.version_info < (3, 12): + exclude |= { + 'Protocol', 'SupportsAbs', 'SupportsBytes', + 'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt', + '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( @@ -3348,7 +5135,6 @@ def __add__(self, other): return 0 -@skipIf(TYPING_3_11_0, "These invariants should all be tested upstream on 3.11+") class NamedTupleTests(BaseTestCase): class NestedEmployee(NamedTuple): name: str @@ -3486,9 +5272,12 @@ class Y(Generic[T], NamedTuple): a = A(3) self.assertIs(type(a), G) + self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - with self.assertRaisesRegex(TypeError, 'Too many parameters'): + things = "arguments" if sys.version_info >= (3, 11) else "parameters" + + with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") @@ -3520,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)]) @@ -3544,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__, {}) @@ -3592,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") @@ -3607,8 +5441,8 @@ def test_signature_on_37(self): @skipUnless(TYPING_3_9_0, "NamedTuple was a class on 3.8 and lower") def test_same_as_typing_NamedTuple_39_plus(self): self.assertEqual( - set(dir(NamedTuple)), - set(dir(typing.NamedTuple)) | {"__text_signature__"} + set(dir(NamedTuple)) - {"__text_signature__"}, + set(dir(typing.NamedTuple)) ) self.assertIs(type(NamedTuple), type(typing.NamedTuple)) @@ -3619,11 +5453,175 @@ def test_same_as_typing_NamedTuple_38_minus(self): self.NestedEmployee._field_types ) + def test_orig_bases(self): + T = TypeVar('T') + + class SimpleNamedTuple(NamedTuple): + pass + + class GenericNamedTuple(NamedTuple, Generic[T]): + pass + + self.assertEqual(SimpleNamedTuple.__orig_bases__, (NamedTuple,)) + self.assertEqual(GenericNamedTuple.__orig_bases__, (NamedTuple, Generic[T])) + + CallNamedTuple = NamedTuple('CallNamedTuple', []) + + self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,)) + + +class TypeVarTests(BaseTestCase): + def test_basic_plain(self): + T = TypeVar('T') + # T equals itself. + self.assertEqual(T, T) + # T is an instance of TypeVar + self.assertIsInstance(T, TypeVar) + self.assertEqual(T.__name__, 'T') + self.assertEqual(T.__constraints__, ()) + self.assertIs(T.__bound__, None) + self.assertIs(T.__covariant__, False) + self.assertIs(T.__contravariant__, False) + self.assertIs(T.__infer_variance__, False) + + def test_attributes(self): + T_bound = TypeVar('T_bound', bound=int) + self.assertEqual(T_bound.__name__, 'T_bound') + self.assertEqual(T_bound.__constraints__, ()) + self.assertIs(T_bound.__bound__, int) + + T_constraints = TypeVar('T_constraints', int, str) + self.assertEqual(T_constraints.__name__, 'T_constraints') + self.assertEqual(T_constraints.__constraints__, (int, str)) + self.assertIs(T_constraints.__bound__, None) + + T_co = TypeVar('T_co', covariant=True) + self.assertEqual(T_co.__name__, 'T_co') + self.assertIs(T_co.__covariant__, True) + self.assertIs(T_co.__contravariant__, False) + self.assertIs(T_co.__infer_variance__, False) + + T_contra = TypeVar('T_contra', contravariant=True) + self.assertEqual(T_contra.__name__, 'T_contra') + self.assertIs(T_contra.__covariant__, False) + self.assertIs(T_contra.__contravariant__, True) + self.assertIs(T_contra.__infer_variance__, False) + + T_infer = TypeVar('T_infer', infer_variance=True) + self.assertEqual(T_infer.__name__, 'T_infer') + self.assertIs(T_infer.__covariant__, False) + self.assertIs(T_infer.__contravariant__, False) + self.assertIs(T_infer.__infer_variance__, True) + + def test_typevar_instance_type_error(self): + T = TypeVar('T') + with self.assertRaises(TypeError): + isinstance(42, T) + + def test_typevar_subclass_type_error(self): + T = TypeVar('T') + with self.assertRaises(TypeError): + issubclass(int, T) + with self.assertRaises(TypeError): + issubclass(T, int) + + def test_constrained_error(self): + with self.assertRaises(TypeError): + X = TypeVar('X', int) + X + + def test_union_unique(self): + X = TypeVar('X') + Y = TypeVar('Y') + self.assertNotEqual(X, Y) + self.assertEqual(Union[X], X) + self.assertNotEqual(Union[X], Union[X, Y]) + self.assertEqual(Union[X, X], X) + self.assertNotEqual(Union[X, int], Union[X]) + self.assertNotEqual(Union[X, int], Union[int]) + self.assertEqual(Union[X, int].__args__, (X, int)) + self.assertEqual(Union[X, int].__parameters__, (X,)) + self.assertIs(Union[X, int].__origin__, Union) + + if hasattr(types, "UnionType"): + def test_or(self): + X = TypeVar('X') + # use a string because str doesn't implement + # __or__/__ror__ itself + self.assertEqual(X | "x", Union[X, "x"]) + self.assertEqual("x" | X, Union["x", X]) + # make sure the order is correct + self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x"))) + self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X)) + + def test_union_constrained(self): + A = TypeVar('A', str, bytes) + self.assertNotEqual(Union[A, str], Union[A]) + + def test_repr(self): + self.assertEqual(repr(T), '~T') + self.assertEqual(repr(KT), '~KT') + self.assertEqual(repr(VT), '~VT') + self.assertEqual(repr(AnyStr), '~AnyStr') + T_co = TypeVar('T_co', covariant=True) + self.assertEqual(repr(T_co), '+T_co') + T_contra = TypeVar('T_contra', contravariant=True) + self.assertEqual(repr(T_contra), '-T_contra') + + def test_no_redefinition(self): + self.assertNotEqual(TypeVar('T'), TypeVar('T')) + self.assertNotEqual(TypeVar('T', int, str), TypeVar('T', int, str)) + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class V(TypeVar): pass + T = TypeVar("T") + with self.assertRaises(TypeError): + class V(T): pass + + def test_cannot_instantiate_vars(self): + with self.assertRaises(TypeError): + TypeVar('A')() + + def test_bound_errors(self): + with self.assertRaises(TypeError): + TypeVar('X', bound=Union) + with self.assertRaises(TypeError): + TypeVar('X', str, float, bound=Employee) + with self.assertRaisesRegex(TypeError, + r"Bound must be a type\. Got \(1, 2\)\."): + TypeVar('X', bound=(1, 2)) + + # Technically we could run it on later versions of 3.7 and 3.8, + # but that's not worth the effort. + @skipUnless(TYPING_3_9_0, "Fix was not backported") + def test_missing__name__(self): + # See bpo-39942 + code = ("import typing\n" + "T = typing.TypeVar('T')\n" + ) + exec(code, {}) + + def test_no_bivariant(self): + with self.assertRaises(ValueError): + TypeVar('T', covariant=True, contravariant=True) + + def test_cannot_combine_explicit_and_infer(self): + with self.assertRaises(ValueError): + TypeVar('T', covariant=True, infer_variance=True) + with self.assertRaises(ValueError): + TypeVar('T', contravariant=True, infer_variance=True) + class TypeVarLikeDefaultsTests(BaseTestCase): def test_typevar(self): T = typing_extensions.TypeVar('T', default=int) + typing_T = typing.TypeVar('T') self.assertEqual(T.__default__, int) + self.assertIsInstance(T, typing_extensions.TypeVar) + self.assertIsInstance(T, typing.TypeVar) + self.assertIsInstance(typing_T, typing.TypeVar) + self.assertIsInstance(typing_T, typing_extensions.TypeVar) class A(Generic[T]): ... Alias = Optional[T] @@ -3637,6 +5635,12 @@ def test_typevar_none(self): def test_paramspec(self): P = ParamSpec('P', default=(str, int)) self.assertEqual(P.__default__, (str, int)) + self.assertIsInstance(P, ParamSpec) + if hasattr(typing, "ParamSpec"): + self.assertIsInstance(P, typing.ParamSpec) + typing_P = typing.ParamSpec('P') + self.assertIsInstance(typing_P, typing.ParamSpec) + self.assertIsInstance(typing_P, ParamSpec) class A(Generic[P]): ... Alias = typing.Callable[P, None] @@ -3644,6 +5648,12 @@ class A(Generic[P]): ... def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) + self.assertIsInstance(Ts, TypeVarTuple) + if hasattr(typing, "TypeVarTuple"): + self.assertIsInstance(Ts, typing.TypeVarTuple) + typing_Ts = typing.TypeVarTuple('Ts') + self.assertIsInstance(typing_Ts, typing.TypeVarTuple) + self.assertIsInstance(typing_Ts, TypeVarTuple) class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] @@ -3687,5 +5697,236 @@ def test_pickle(self): self.assertEqual(z.__infer_variance__, typevar.__infer_variance__) +class BufferTests(BaseTestCase): + def test(self): + self.assertIsInstance(memoryview(b''), Buffer) + self.assertIsInstance(bytearray(), Buffer) + self.assertIsInstance(b"x", Buffer) + self.assertNotIsInstance(1, Buffer) + + self.assertIsSubclass(bytearray, Buffer) + self.assertIsSubclass(memoryview, Buffer) + self.assertIsSubclass(bytes, Buffer) + self.assertNotIsSubclass(int, Buffer) + + class MyRegisteredBuffer: + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + + # On 3.12, collections.abc.Buffer does a structural compatibility check + if TYPING_3_12_0: + self.assertIsInstance(MyRegisteredBuffer(), Buffer) + self.assertIsSubclass(MyRegisteredBuffer, Buffer) + else: + self.assertNotIsInstance(MyRegisteredBuffer(), Buffer) + self.assertNotIsSubclass(MyRegisteredBuffer, Buffer) + Buffer.register(MyRegisteredBuffer) + self.assertIsInstance(MyRegisteredBuffer(), Buffer) + self.assertIsSubclass(MyRegisteredBuffer, Buffer) + + class MySubclassedBuffer(Buffer): + def __buffer__(self, flags: int) -> memoryview: + return memoryview(b'') + + self.assertIsInstance(MySubclassedBuffer(), Buffer) + self.assertIsSubclass(MySubclassedBuffer, Buffer) + + +class GetOriginalBasesTests(BaseTestCase): + def test_basics(self): + T = TypeVar('T') + class A: pass + class B(Generic[T]): pass + class C(B[int]): pass + class D(B[str], float): pass + self.assertEqual(get_original_bases(A), (object,)) + self.assertEqual(get_original_bases(B), (Generic[T],)) + self.assertEqual(get_original_bases(C), (B[int],)) + self.assertEqual(get_original_bases(int), (object,)) + self.assertEqual(get_original_bases(D), (B[str], float)) + + with self.assertRaisesRegex(TypeError, "Expected an instance of type"): + get_original_bases(object()) + + @skipUnless(TYPING_3_9_0, "PEP 585 is yet to be") + def test_builtin_generics(self): + class E(list[T]): pass + class F(list[int]): pass + + self.assertEqual(get_original_bases(E), (list[T],)) + self.assertEqual(get_original_bases(F), (list[int],)) + + def test_namedtuples(self): + # On 3.12, this should work well with typing.NamedTuple and typing_extensions.NamedTuple + # On lower versions, it will only work fully with typing_extensions.NamedTuple + if sys.version_info >= (3, 12): + namedtuple_classes = (typing.NamedTuple, typing_extensions.NamedTuple) + else: + namedtuple_classes = (typing_extensions.NamedTuple,) + + for NamedTuple in namedtuple_classes: # noqa: F402 + with self.subTest(cls=NamedTuple): + class ClassBasedNamedTuple(NamedTuple): + x: int + + class GenericNamedTuple(NamedTuple, Generic[T]): + x: T + + CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)]) + + self.assertIs( + get_original_bases(ClassBasedNamedTuple)[0], NamedTuple + ) + self.assertEqual( + get_original_bases(GenericNamedTuple), + (NamedTuple, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedNamedTuple)[0], NamedTuple + ) + + def test_typeddicts(self): + # On 3.12, this should work well with typing.TypedDict and typing_extensions.TypedDict + # On lower versions, it will only work fully with typing_extensions.TypedDict + if sys.version_info >= (3, 12): + typeddict_classes = (typing.TypedDict, typing_extensions.TypedDict) + else: + typeddict_classes = (typing_extensions.TypedDict,) + + for TypedDict in typeddict_classes: # noqa: F402 + with self.subTest(cls=TypedDict): + class ClassBasedTypedDict(TypedDict): + x: int + + class GenericTypedDict(TypedDict, Generic[T]): + x: T + + CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int}) + + self.assertIs( + get_original_bases(ClassBasedTypedDict)[0], + TypedDict + ) + self.assertEqual( + get_original_bases(GenericTypedDict), + (TypedDict, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedTypedDict)[0], + TypedDict + ) + + +class TypeAliasTypeTests(BaseTestCase): + def test_attributes(self): + Simple = TypeAliasType("Simple", int) + self.assertEqual(Simple.__name__, "Simple") + self.assertIs(Simple.__value__, int) + self.assertEqual(Simple.__type_params__, ()) + self.assertEqual(Simple.__parameters__, ()) + + T = TypeVar("T") + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + self.assertEqual(ListOrSetT.__name__, "ListOrSetT") + self.assertEqual(ListOrSetT.__value__, Union[List[T], Set[T]]) + self.assertEqual(ListOrSetT.__type_params__, (T,)) + self.assertEqual(ListOrSetT.__parameters__, (T,)) + + Ts = TypeVarTuple("Ts") + Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)) + self.assertEqual(Variadic.__name__, "Variadic") + self.assertEqual(Variadic.__value__, Tuple[int, Unpack[Ts]]) + self.assertEqual(Variadic.__type_params__, (Ts,)) + self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) + + def test_cannot_set_attributes(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(AttributeError, "readonly attribute"): + Simple.__name__ = "NewName" + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__value__ = str + with self.assertRaisesRegex( + AttributeError, + "attribute '__type_params__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__type_params__ = (T,) + with self.assertRaisesRegex( + AttributeError, + "attribute '__parameters__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__parameters__ = (T,) + with self.assertRaisesRegex( + AttributeError, + "attribute '__module__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__module__ = 42 + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): + Simple.some_attribute = "not allowed" + + def test_cannot_delete_attributes(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(AttributeError, "readonly attribute"): + del Simple.__name__ + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): + del Simple.__value__ + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): + del Simple.some_attribute + + def test_or(self): + Alias = TypeAliasType("Alias", int) + if sys.version_info >= (3, 10): + self.assertEqual(Alias | int, Union[Alias, int]) + self.assertEqual(Alias | None, Union[Alias, None]) + self.assertEqual(Alias | (int | str), Union[Alias, int | str]) + self.assertEqual(Alias | list[float], Union[Alias, list[float]]) + else: + with self.assertRaises(TypeError): + Alias | int + # Rejected on all versions + with self.assertRaises(TypeError): + Alias | "Ref" + + def test_getitem(self): + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + subscripted = ListOrSetT[int] + self.assertEqual(get_args(subscripted), (int,)) + self.assertIs(get_origin(subscripted), ListOrSetT) + with self.assertRaises(TypeError): + subscripted[str] + + still_generic = ListOrSetT[Iterable[T]] + self.assertEqual(get_args(still_generic), (Iterable[T],)) + self.assertIs(get_origin(still_generic), ListOrSetT) + fully_subscripted = still_generic[float] + self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) + self.assertIs(get_origin(fully_subscripted), ListOrSetT) + + def test_pickle(self): + global Alias + Alias = TypeAliasType("Alias", int) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + pickled = pickle.dumps(Alias, proto) + unpickled = pickle.loads(pickled) + self.assertIs(unpickled, Alias) + + def test_no_instance_subclassing(self): + with self.assertRaises(TypeError): + class MyAlias(TypeAliasType): + pass + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6ae0c34c..901f3b96 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -9,7 +9,6 @@ import typing import warnings - __all__ = [ # Super-special typing primitives. 'Any', @@ -33,6 +32,7 @@ 'Coroutine', 'AsyncGenerator', 'AsyncContextManager', + 'Buffer', 'ChainMap', # Concrete collection types. @@ -45,7 +45,13 @@ 'TypedDict', # Structural checks, a.k.a. protocols. + 'SupportsAbs', + 'SupportsBytes', + 'SupportsComplex', + 'SupportsFloat', 'SupportsIndex', + 'SupportsInt', + 'SupportsRound', # One-off things. 'Annotated', @@ -58,8 +64,11 @@ 'final', 'get_args', 'get_origin', + 'get_original_bases', + 'get_protocol_members', 'get_type_hints', 'IntVar', + 'is_protocol', 'is_typeddict', 'Literal', 'NewType', @@ -71,12 +80,52 @@ 'runtime_checkable', 'Text', 'TypeAlias', + 'TypeAliasType', 'TypeGuard', 'TYPE_CHECKING', 'Never', '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 @@ -86,7 +135,13 @@ # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. -_marker = object() + +class _Sentinel: + def __repr__(self): + return "" + + +_marker = _Sentinel() def _check_generic(cls, parameters, elen=_marker): @@ -187,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.') @@ -260,21 +317,67 @@ def IntVar(name): return typing.TypeVar(name) -# 3.8+: -if hasattr(typing, 'Literal'): +# A Literal bug was fixed in 3.11.0, 3.10.1 and 3.9.8 +if sys.version_info >= (3, 10, 1): Literal = typing.Literal -# 3.7: else: - class _LiteralForm(typing._SpecialForm, _root=True): + def _flatten_literal_params(parameters): + """An internal helper for Literal creation: flatten Literals among parameters""" + params = [] + for p in parameters: + if isinstance(p, _LiteralGenericAlias): + params.extend(p.__args__) + else: + params.append(p) + return tuple(params) - def __repr__(self): - return 'typing_extensions.' + self._name + def _value_and_type_iter(params): + for p in params: + yield p, type(p) + + class _LiteralGenericAlias(typing._GenericAlias, _root=True): + def __eq__(self, other): + if not isinstance(other, _LiteralGenericAlias): + return NotImplemented + these_args_deduped = set(_value_and_type_iter(self.__args__)) + other_args_deduped = set(_value_and_type_iter(other.__args__)) + return these_args_deduped == other_args_deduped + + def __hash__(self): + return hash(frozenset(_value_and_type_iter(self.__args__))) + + class _LiteralForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, doc: str): + self._name = 'Literal' + self._doc = self.__doc__ = doc def __getitem__(self, parameters): - return typing._GenericAlias(self, parameters) + if not isinstance(parameters, tuple): + parameters = (parameters,) + + parameters = _flatten_literal_params(parameters) - Literal = _LiteralForm('Literal', - doc="""A type that can be used to indicate to type checkers + val_type_pairs = list(_value_and_type_iter(parameters)) + try: + deduped_pairs = set(val_type_pairs) + except TypeError: + # unhashable parameters + pass + else: + # similar logic to typing._deduplicate on Python 3.9+ + if len(deduped_pairs) < len(val_type_pairs): + new_parameters = [] + for pair in val_type_pairs: + if pair in deduped_pairs: + new_parameters.append(pair[0]) + deduped_pairs.remove(pair) + assert not deduped_pairs, deduped_pairs + parameters = tuple(new_parameters) + + return _LiteralGenericAlias(self, parameters) + + Literal = _LiteralForm(doc="""\ + A type that can be used to indicate to type checkers that the corresponding value has a value literally equivalent to the provided parameter. For example: @@ -288,7 +391,7 @@ def __getitem__(self, parameters): instead of a type.""") -_overload_dummy = typing._overload_dummy # noqa +_overload_dummy = typing._overload_dummy if hasattr(typing, "get_overloads"): # 3.11+ @@ -383,40 +486,55 @@ def clear_overloads(): Counter = typing.Counter ChainMap = typing.ChainMap AsyncGenerator = typing.AsyncGenerator -NewType = typing.NewType Text = typing.Text TYPE_CHECKING = typing.TYPE_CHECKING -_PROTO_WHITELIST = ['Callable', 'Awaitable', - 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', - 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', - 'ContextManager', 'AsyncContextManager'] +_PROTO_ALLOWLIST = { + 'collections.abc': [ + 'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', 'Buffer', + ], + 'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'], + 'typing_extensions': ['Buffer'], +} + + +_EXCLUDED_ATTRS = { + "__abstractmethods__", "__annotations__", "__weakref__", "_is_protocol", + "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__", + "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__", + "__subclasshook__", "__orig_class__", "__init__", "__new__", + "__protocol_attrs__", "__callable_proto_members_only__", +} + +if sys.version_info < (3, 8): + _EXCLUDED_ATTRS |= { + "_gorg", "__next_in_mro__", "__extra__", "__tree_hash__", "__args__", + "__origin__" + } + +if sys.version_info >= (3, 9): + _EXCLUDED_ATTRS.add("__class_getitem__") + +if sys.version_info >= (3, 12): + _EXCLUDED_ATTRS.add("__type_params__") + +_EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS) def _get_protocol_attrs(cls): attrs = set() for base in cls.__mro__[:-1]: # without object - if base.__name__ in ('Protocol', 'Generic'): + if base.__name__ in {'Protocol', 'Generic'}: continue annotations = getattr(base, '__annotations__', {}) - for attr in list(base.__dict__.keys()) + list(annotations.keys()): - if (not attr.startswith('_abc_') and attr not in ( - '__abstractmethods__', '__annotations__', '__weakref__', - '_is_protocol', '_is_runtime_protocol', '__dict__', - '__args__', '__slots__', - '__next_in_mro__', '__parameters__', '__origin__', - '__orig_bases__', '__extra__', '__tree_hash__', - '__doc__', '__subclasshook__', '__init__', '__new__', - '__module__', '_MutableMapping__marker', '_gorg')): + for attr in (*base.__dict__, *annotations): + if (not attr.startswith('_abc_') and attr not in _EXCLUDED_ATTRS): attrs.add(attr) return attrs -def _is_callable_members_only(cls): - return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) - - def _maybe_adjust_parameters(cls): """Helper function used in Protocol.__init_subclass__ and _TypedDictMeta.__new__. @@ -426,7 +544,7 @@ def _maybe_adjust_parameters(cls): """ tvars = [] if '__orig_bases__' in cls.__dict__: - tvars = typing._collect_type_vars(cls.__orig_bases__) + tvars = _collect_type_vars(cls.__orig_bases__) # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. # If found, tvars must be a subset of it. # If not found, tvars is it. @@ -457,168 +575,284 @@ def _maybe_adjust_parameters(cls): cls.__parameters__ = tuple(tvars) -# 3.8+ -if hasattr(typing, 'Protocol'): +def _caller(depth=2): + try: + return sys._getframe(depth).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): # For platforms without _getframe() + return None + + +# The performance of runtime-checkable protocols is significantly improved on Python 3.12, +# so we backport the 3.12 version of Protocol to Python <=3.11 +if sys.version_info >= (3, 12): Protocol = typing.Protocol -# 3.7 else: + def _allow_reckless_class_checks(depth=3): + """Allow instance and class checks for special stdlib modules. + The abc and functools modules indiscriminately call isinstance() and + issubclass() on the whole MRO of a user class, which may contain protocols. + """ + return _caller(depth) in {'abc', 'functools', None} def _no_init(self, *args, **kwargs): if type(self)._is_protocol: raise TypeError('Protocols cannot be instantiated') - class _ProtocolMeta(abc.ABCMeta): # noqa: B024 - # This metaclass is a bit unfortunate and exists only because of the lack - # of __instancehook__. + 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): + abc.ABCMeta.__init__(cls, *args, **kwargs) + if getattr(cls, "_is_protocol", False): + cls.__protocol_attrs__ = _get_protocol_attrs(cls) + # PEP 544 prohibits using issubclass() + # with protocols that have non-method members. + cls.__callable_proto_members_only__ = all( + callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__ + ) + + def __subclasscheck__(cls, other): + if cls is Protocol: + return type.__subclasscheck__(cls, other) + if ( + getattr(cls, '_is_protocol', False) + and not _allow_reckless_class_checks() + ): + if not isinstance(other, type): + # Same error message as for issubclass(1, int). + raise TypeError('issubclass() arg 1 must be a class') + if ( + not cls.__callable_proto_members_only__ + and cls.__dict__.get("__subclasshook__") is _proto_hook + ): + raise TypeError( + "Protocols with non-method members don't support issubclass()" + ) + if not getattr(cls, '_is_runtime_protocol', False): + raise TypeError( + "Instance and class checks can only be used with " + "@runtime_checkable protocols" + ) + return abc.ABCMeta.__subclasscheck__(cls, other) + def __instancecheck__(cls, instance): # We need this method for situations where attributes are # assigned in __init__. - if ((not getattr(cls, '_is_protocol', False) or - _is_callable_members_only(cls)) and - issubclass(instance.__class__, cls)): + 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 abc.ABCMeta.__instancecheck__(cls, instance) + + if ( + not getattr(cls, '_is_runtime_protocol', False) and + not _allow_reckless_class_checks() + ): + raise TypeError("Instance and class checks can only be used with" + " @runtime_checkable protocols") + + if abc.ABCMeta.__instancecheck__(cls, instance): return True - if cls._is_protocol: - if all(hasattr(instance, attr) and - (not callable(getattr(cls, attr, None)) or - getattr(instance, attr) is not None) - for attr in _get_protocol_attrs(cls)): - return True - return super().__instancecheck__(instance) - - class Protocol(metaclass=_ProtocolMeta): - # There is quite a lot of overlapping code with typing.Generic. - # Unfortunately it is hard to avoid this while these live in two different - # modules. The duplicated code will be removed when Protocol is moved to typing. - """Base class for protocol classes. Protocol classes are defined as:: - - class Proto(Protocol): - def meth(self) -> int: - ... - Such classes are primarily used with static type checkers that recognize - structural subtyping (static duck-typing), for example:: + for attr in cls.__protocol_attrs__: + try: + val = inspect.getattr_static(instance, attr) + except AttributeError: + break + if val is None and callable(getattr(cls, attr, None)): + break + else: + return True - class C: - def meth(self) -> int: - return 0 + return False - def func(x: Proto) -> int: - return x.meth() + 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 abc.ABCMeta.__eq__(cls, other) is True: + return True + return ( + cls is Protocol and other is getattr(typing, "Protocol", object()) + ) - func(C()) # Passes static type check + # This has to be defined, or the abc-module cache + # complains about classes with this metaclass being unhashable, + # if we define only __eq__! + def __hash__(cls) -> int: + return type.__hash__(cls) + + @classmethod + def _proto_hook(cls, other): + if not cls.__dict__.get('_is_protocol', False): + return NotImplemented + + for attr in cls.__protocol_attrs__: + for base in other.__mro__: + # Check if the members appears in the class dictionary... + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + + # ...or in annotations, if it is a sub-protocol. + annotations = getattr(base, '__annotations__', {}) + if ( + isinstance(annotations, collections.abc.Mapping) + and attr in annotations + and is_protocol(other) + ): + break + else: + return NotImplemented + return True - See PEP 544 for details. Protocol classes decorated with - @typing_extensions.runtime act as simple-minded runtime protocol that checks - only the presence of given attributes, ignoring their type signatures. + if sys.version_info >= (3, 8): + class Protocol(typing.Generic, metaclass=_ProtocolMeta): + __doc__ = typing.Protocol.__doc__ + __slots__ = () + _is_protocol = True + _is_runtime_protocol = False - Protocol classes can be generic, they are defined as:: + def __init_subclass__(cls, *args, **kwargs): + super().__init_subclass__(*args, **kwargs) - class GenProto(Protocol[T]): - def meth(self) -> T: - ... - """ - __slots__ = () - _is_protocol = True + # Determine if this is a protocol or a concrete subclass. + if not cls.__dict__.get('_is_protocol', False): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) - def __new__(cls, *args, **kwds): - if cls is Protocol: - raise TypeError("Type Protocol cannot be instantiated; " - "it can only be used as a base class") - return super().__new__(cls) + # Set (or override) the protocol subclass hook. + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook - @typing._tp_cache - def __class_getitem__(cls, params): - if not isinstance(params, tuple): - params = (params,) - if not params and cls is not typing.Tuple: - raise TypeError( - f"Parameter list to {cls.__qualname__}[...] cannot be empty") - msg = "Parameters to generic types must be types." - params = tuple(typing._type_check(p, msg) for p in params) # noqa - if cls is Protocol: - # Generic can only be subscripted with unique type variables. - if not all(isinstance(p, typing.TypeVar) for p in params): - i = 0 - while isinstance(params[i], typing.TypeVar): - i += 1 - raise TypeError( - "Parameters to Protocol[...] must all be type variables." - f" Parameter {i + 1} is {params[i]}") - if len(set(params)) != len(params): - raise TypeError( - "Parameters to Protocol[...] must all be unique") - else: - # Subscripting a regular Generic subclass. - _check_generic(cls, params, len(cls.__parameters__)) - return typing._GenericAlias(cls, params) + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: + cls.__init__ = _no_init - def __init_subclass__(cls, *args, **kwargs): - if '__orig_bases__' in cls.__dict__: - error = typing.Generic in cls.__orig_bases__ - else: - error = typing.Generic in cls.__bases__ - if error: - raise TypeError("Cannot inherit from plain Generic") - _maybe_adjust_parameters(cls) + else: + class Protocol(metaclass=_ProtocolMeta): + # There is quite a lot of overlapping code with typing.Generic. + # Unfortunately it is hard to avoid this on Python <3.8, + # as the typing module on Python 3.7 doesn't let us subclass typing.Generic! + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol): + def meth(self) -> int: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime_checkable act + as simple-minded runtime-checkable protocols that check + only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto(Protocol[T]): + def meth(self) -> T: + ... + """ + __slots__ = () + _is_protocol = True + _is_runtime_protocol = False + + def __new__(cls, *args, **kwds): + if cls is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can only be used as a base class") + return super().__new__(cls) + + @typing._tp_cache + def __class_getitem__(cls, params): + if not isinstance(params, tuple): + params = (params,) + if not params and cls is not typing.Tuple: + raise TypeError( + f"Parameter list to {cls.__qualname__}[...] cannot be empty") + msg = "Parameters to generic types must be types." + params = tuple(typing._type_check(p, msg) for p in params) + if cls is Protocol: + # Generic can only be subscripted with unique type variables. + if not all(isinstance(p, typing.TypeVar) for p in params): + i = 0 + while isinstance(params[i], typing.TypeVar): + i += 1 + raise TypeError( + "Parameters to Protocol[...] must all be type variables." + f" Parameter {i + 1} is {params[i]}") + if len(set(params)) != len(params): + raise TypeError( + "Parameters to Protocol[...] must all be unique") + else: + # Subscripting a regular Generic subclass. + _check_generic(cls, params, len(cls.__parameters__)) + return typing._GenericAlias(cls, params) - # Determine if this is a protocol or a concrete subclass. - if not cls.__dict__.get('_is_protocol', None): - cls._is_protocol = any(b is Protocol for b in cls.__bases__) + def __init_subclass__(cls, *args, **kwargs): + if '__orig_bases__' in cls.__dict__: + error = typing.Generic in cls.__orig_bases__ + else: + error = typing.Generic in cls.__bases__ + if error: + raise TypeError("Cannot inherit from plain Generic") + _maybe_adjust_parameters(cls) - # Set (or override) the protocol subclass hook. - def _proto_hook(other): + # Determine if this is a protocol or a concrete subclass. if not cls.__dict__.get('_is_protocol', None): - return NotImplemented - if not getattr(cls, '_is_runtime_protocol', False): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: - return NotImplemented - raise TypeError("Instance and class checks can only be used with" - " @runtime protocols") - if not _is_callable_members_only(cls): - if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: - return NotImplemented - raise TypeError("Protocols with non-method members" - " don't support issubclass()") - if not isinstance(other, type): - # Same error as for issubclass(1, int) - raise TypeError('issubclass() arg 1 must be a class') - for attr in _get_protocol_attrs(cls): - for base in other.__mro__: - if attr in base.__dict__: - if base.__dict__[attr] is None: - return NotImplemented - break - annotations = getattr(base, '__annotations__', {}) - if (isinstance(annotations, typing.Mapping) and - attr in annotations and - isinstance(other, _ProtocolMeta) and - other._is_protocol): - break - else: - return NotImplemented - return True - if '__subclasshook__' not in cls.__dict__: - cls.__subclasshook__ = _proto_hook + cls._is_protocol = any(b is Protocol for b in cls.__bases__) - # We have nothing more to do for non-protocols. - if not cls._is_protocol: - return + # Set (or override) the protocol subclass hook. + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook - # Check consistency of bases. - for base in cls.__bases__: - if not (base in (object, typing.Generic) or - base.__module__ == 'collections.abc' and - base.__name__ in _PROTO_WHITELIST or - isinstance(base, _ProtocolMeta) and base._is_protocol): - raise TypeError('Protocols can only inherit from other' - f' protocols, got {repr(base)}') - cls.__init__ = _no_init + # Prohibit instantiation for protocol classes + if cls._is_protocol and cls.__init__ is Protocol.__init__: + cls.__init__ = _no_init -# 3.8+ -if hasattr(typing, 'runtime_checkable'): +if sys.version_info >= (3, 8): runtime_checkable = typing.runtime_checkable -# 3.7 else: def runtime_checkable(cls): """Mark a protocol class as a runtime protocol, so that it @@ -628,7 +862,10 @@ def runtime_checkable(cls): This allows a simple-minded structural check very similar to the one-offs in collections.abc such as Hashable. """ - if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: + if not ( + (isinstance(cls, _ProtocolMeta) or issubclass(cls, typing.Generic)) + and getattr(cls, "_is_protocol", False) + ): raise TypeError('@runtime_checkable can be only applied to protocol classes,' f' got {cls!r}') cls._is_runtime_protocol = True @@ -639,11 +876,52 @@ def runtime_checkable(cls): runtime = runtime_checkable -# 3.8+ -if hasattr(typing, 'SupportsIndex'): +# Our version of runtime-checkable protocols is faster on Python 3.7-3.11 +if sys.version_info >= (3, 12): + SupportsInt = typing.SupportsInt + SupportsFloat = typing.SupportsFloat + SupportsComplex = typing.SupportsComplex + SupportsBytes = typing.SupportsBytes SupportsIndex = typing.SupportsIndex -# 3.7 + SupportsAbs = typing.SupportsAbs + SupportsRound = typing.SupportsRound else: + @runtime_checkable + class SupportsInt(Protocol): + """An ABC with one abstract method __int__.""" + __slots__ = () + + @abc.abstractmethod + def __int__(self) -> int: + pass + + @runtime_checkable + class SupportsFloat(Protocol): + """An ABC with one abstract method __float__.""" + __slots__ = () + + @abc.abstractmethod + def __float__(self) -> float: + pass + + @runtime_checkable + class SupportsComplex(Protocol): + """An ABC with one abstract method __complex__.""" + __slots__ = () + + @abc.abstractmethod + def __complex__(self) -> complex: + pass + + @runtime_checkable + class SupportsBytes(Protocol): + """An ABC with one abstract method __bytes__.""" + __slots__ = () + + @abc.abstractmethod + def __bytes__(self) -> bytes: + pass + @runtime_checkable class SupportsIndex(Protocol): __slots__ = () @@ -652,8 +930,45 @@ class SupportsIndex(Protocol): def __index__(self) -> int: pass + @runtime_checkable + class SupportsAbs(Protocol[T_co]): + """ + An ABC with one abstract method __abs__ that is covariant in its return type. + """ + __slots__ = () -if hasattr(typing, "Required"): + @abc.abstractmethod + def __abs__(self) -> T_co: + pass + + @runtime_checkable + class SupportsRound(Protocol[T_co]): + """ + An ABC with one abstract method __round__ that is covariant in its return type. + """ + __slots__ = () + + @abc.abstractmethod + def __round__(self, ndigits: int = 0) -> T_co: + pass + + +def _ensure_subclassable(mro_entries): + def inner(func): + if sys.implementation.name == "pypy" and sys.version_info < (3, 9): + cls_dict = { + "__call__": staticmethod(func), + "__mro_entries__": staticmethod(mro_entries) + } + t = type(func.__name__, (), cls_dict) + return functools.update_wrapper(t(), func) + else: + func.__mro_entries__ = mro_entries + return func + return inner + + +if 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" @@ -661,108 +976,63 @@ def __index__(self) -> int: # The standard library TypedDict below Python 3.11 does not store runtime # information about optional and required keys when using Required or NotRequired. # 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 sys._getframe(1).f_globals['__name__'] 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') - import warnings - 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') - import warnings - 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") + # 3.10.0 and later + _TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters - ns = {'__annotations__': dict(fields)} - try: - # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - pass + 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) - - if any(issubclass(base, typing.Generic) for base in bases): - tp_dict.__bases__ = (typing.Generic, dict) - _maybe_adjust_parameters(tp_dict) + 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() @@ -796,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): @@ -821,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) @@ -846,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"): @@ -872,9 +1205,6 @@ def greet(name: str) -> None: if hasattr(typing, "Required"): get_type_hints = typing.get_type_hints else: - import functools - import types - # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" @@ -887,12 +1217,12 @@ def _strip_extras(t): if stripped_args == t.__args__: return t return t.copy_with(stripped_args) - if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias): + if hasattr(_types, "GenericAlias") and isinstance(t, _types.GenericAlias): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t - return types.GenericAlias(t.__origin__, stripped_args) - if hasattr(types, "UnionType") and isinstance(t, types.UnionType): + return _types.GenericAlias(t.__origin__, stripped_args) + if hasattr(_types, "UnionType") and isinstance(t, _types.UnionType): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t @@ -1119,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 @@ -1138,59 +1464,77 @@ 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 = _ExtensionsSpecialForm( + 'TypeAlias', + doc="""Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example + above.""" + ) - TypeAlias = _TypeAliasForm('TypeAlias', - doc="""Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - For example:: +def _set_default(type_param, default): + if isinstance(default, (tuple, list)): + type_param.__default__ = tuple((typing._type_check(d, "Default must be a type") + for d in default)) + elif default != _marker: + type_param.__default__ = typing._type_check(default, "Default must be a type") + else: + type_param.__default__ = None - Predicate: TypeAlias = Callable[..., bool] - It's invalid when used anywhere except as in the example - above.""") +def _set_module(typevarlike): + # for pickling: + def_mod = _caller(depth=3) + if def_mod != 'typing_extensions': + typevarlike.__module__ = def_mod class _DefaultMixin: """Mixin for TypeVarLike defaults.""" __slots__ = () + __init__ = _set_default - def __init__(self, default): - if isinstance(default, (tuple, list)): - self.__default__ = tuple((typing._type_check(d, "Default must be a type") - for d in default)) - elif default != _marker: - self.__default__ = typing._type_check(default, "Default must be a type") - else: - self.__default__ = None + +# Classes using this metaclass must provide a _backported_typevarlike ClassVar +class _TypeVarLikeMeta(type): + def __instancecheck__(cls, __instance: Any) -> bool: + return isinstance(__instance, cls._backported_typevarlike) # Add default and infer_variance parameters from PEP 696 and 695 -class TypeVar(typing.TypeVar, _DefaultMixin, _root=True): +class TypeVar(metaclass=_TypeVarLikeMeta): """Type variable.""" - __module__ = 'typing' + _backported_typevarlike = typing.TypeVar - def __init__(self, name, *constraints, bound=None, - covariant=False, contravariant=False, - default=_marker, infer_variance=False): - super().__init__(name, *constraints, bound=bound, covariant=covariant, - contravariant=contravariant) - _DefaultMixin.__init__(self, default) - self.__infer_variance__ = infer_variance + def __new__(cls, name, *constraints, bound=None, + covariant=False, contravariant=False, + default=_marker, infer_variance=False): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, can pass infer_variance to typing.TypeVar + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant, + infer_variance=infer_variance) + else: + typevar = typing.TypeVar(name, *constraints, bound=bound, + covariant=covariant, contravariant=contravariant) + if infer_variance and (covariant or contravariant): + raise ValueError("Variance cannot be specified with infer_variance.") + typevar.__infer_variance__ = infer_variance + _set_default(typevar, default) + _set_module(typevar) + return typevar - # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None - if def_mod != 'typing_extensions': - self.__module__ = def_mod + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type") # Python 3.10+ has PEP 612 @@ -1258,25 +1602,33 @@ def __eq__(self, other): # 3.10+ if hasattr(typing, 'ParamSpec'): - # Add default Parameter - PEP 696 - class ParamSpec(typing.ParamSpec, _DefaultMixin, _root=True): - """Parameter specification variable.""" + # Add default parameter - PEP 696 + class ParamSpec(metaclass=_TypeVarLikeMeta): + """Parameter specification.""" + + _backported_typevarlike = typing.ParamSpec + + def __new__(cls, name, *, bound=None, + covariant=False, contravariant=False, + infer_variance=False, default=_marker): + if hasattr(typing, "TypeAliasType"): + # PEP 695 implemented, can pass infer_variance to typing.TypeVar + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant, + infer_variance=infer_variance) + else: + paramspec = typing.ParamSpec(name, bound=bound, + covariant=covariant, + contravariant=contravariant) + paramspec.__infer_variance__ = infer_variance - __module__ = 'typing' + _set_default(paramspec, default) + _set_module(paramspec) + return paramspec - def __init__(self, name, *, bound=None, covariant=False, contravariant=False, - default=_marker): - super().__init__(name, bound=bound, covariant=covariant, - contravariant=contravariant) - _DefaultMixin.__init__(self, default) - - # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None - if def_mod != 'typing_extensions': - self.__module__ = def_mod + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") # 3.7-3.9 else: @@ -1341,11 +1693,12 @@ def kwargs(self): return ParamSpecKwargs(self) def __init__(self, name, *, bound=None, covariant=False, contravariant=False, - default=_marker): + infer_variance=False, default=_marker): super().__init__([self]) self.__name__ = name self.__covariant__ = bool(covariant) self.__contravariant__ = bool(contravariant) + self.__infer_variance__ = bool(infer_variance) if bound: self.__bound__ = typing._type_check(bound, 'Bound must be a type.') else: @@ -1353,15 +1706,14 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False, _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod def __repr__(self): - if self.__covariant__: + if self.__infer_variance__: + prefix = '' + elif self.__covariant__: prefix = '+' elif self.__contravariant__: prefix = '-' @@ -1436,10 +1788,10 @@ def _concatenate_getitem(self, parameters): # 3.10+ if hasattr(typing, 'Concatenate'): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa + _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 @@ -1454,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) @@ -1479,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. @@ -1531,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') @@ -1709,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 @@ -1751,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.') @@ -1793,28 +2127,65 @@ class Movie(TypedDict): """) -if hasattr(typing, "Unpack"): # 3.11+ +_UNPACK_DOC = """\ +Type unpack operator. + +The type unpack operator takes the child types from some container type, +such as `tuple[int, str]` or a `TypeVarTuple`, and 'pulls them out'. For +example: + + # For some generic class `Foo`: + Foo[Unpack[tuple[int, str]]] # Equivalent to Foo[int, str] + + Ts = TypeVarTuple('Ts') + # Specifies that `Bar` is generic in an arbitrary number of types. + # (Think of `Ts` as a tuple of an arbitrary number of individual + # `TypeVar`s, which the `Unpack` is 'pulling out' directly into the + # `Generic[]`.) + class Bar(Generic[Unpack[Ts]]): ... + Bar[int] # Valid + Bar[int, str] # Also valid + +From Python 3.11, this can also be done using the `*` operator: + + Foo[*tuple[int, str]] + class Bar(Generic[*Ts]): ... + +The operator can also be used along with a `TypedDict` to annotate +`**kwargs` in a function signature. For instance: + + class Movie(TypedDict): + name: str + year: int + + # This function expects two keyword arguments - *name* of type `str` and + # *year* of type `int`. + def foo(**kwargs: Unpack[Movie]): ... + +Note that there is only some runtime checking of this operator. Not +everything the runtime allows may be accepted by static type checkers. + +For more information, see PEP 646 and PEP 692. +""" + + +if sys.version_info >= (3, 12): # PEP 692 changed the repr of Unpack[] Unpack = typing.Unpack + + def _is_unpack(obj): + return get_origin(obj) is Unpack + elif sys.version_info[:2] >= (3, 9): - class _UnpackSpecialForm(typing._SpecialForm, _root=True): - def __repr__(self): - return 'typing_extensions.' + self._name + class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): + def __init__(self, getitem): + super().__init__(getitem) + self.__doc__ = _UNPACK_DOC class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar @_UnpackSpecialForm def Unpack(self, parameters): - """A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """ item = typing._type_check(parameters, f'{self._name} accepts only a single type.') return _UnpackAlias(self, (item,)) @@ -1825,27 +2196,13 @@ 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.') return _UnpackAlias(self, (item,)) - Unpack = _UnpackForm( - 'Unpack', - doc="""A special typing construct to unpack a variadic type. For example: - - Shape = TypeVarTuple('Shape') - Batch = NewType('Batch', int) - - def add_batch_axis( - x: Array[Unpack[Shape]] - ) -> Array[Batch, Unpack[Shape]]: ... - - """) + Unpack = _UnpackForm('Unpack', doc=_UNPACK_DOC) def _is_unpack(obj): return isinstance(obj, _UnpackAlias) @@ -1853,21 +2210,20 @@ def _is_unpack(obj): if hasattr(typing, "TypeVarTuple"): # 3.11+ - # Add default Parameter - PEP 696 - class TypeVarTuple(typing.TypeVarTuple, _DefaultMixin, _root=True): + # Add default parameter - PEP 696 + class TypeVarTuple(metaclass=_TypeVarLikeMeta): """Type variable tuple.""" - def __init__(self, name, *, default=_marker): - super().__init__(name) - _DefaultMixin.__init__(self, default) + _backported_typevarlike = typing.TypeVarTuple - # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None - if def_mod != 'typing_extensions': - self.__module__ = def_mod + def __new__(cls, name, *, default=_marker): + tvt = typing.TypeVarTuple(name) + _set_default(tvt, default) + _set_module(tvt) + return tvt + + def __init_subclass__(self, *args, **kwds): + raise TypeError("Cannot subclass special typing classes") else: class TypeVarTuple(_DefaultMixin): @@ -1925,10 +2281,7 @@ def __init__(self, name, *, default=_marker): _DefaultMixin.__init__(self, default) # for pickling: - try: - def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): - def_mod = None + def_mod = _caller() if def_mod != 'typing_extensions': self.__module__ = def_mod @@ -2163,7 +2516,15 @@ def g(x: str) -> int: ... When this decorator is applied to an object, the type checker will generate a diagnostic on usage of the deprecated object. - No runtime warning is issued. The decorator sets the ``__deprecated__`` + The warning specified by ``category`` will be emitted on use + of deprecated objects. For functions, that happens on calls; + for classes, on instantiation. If the ``category`` is ``None``, + no warning is emitted. The ``stacklevel`` determines where the + warning is emitted. If it is ``1`` (the default), the warning + is emitted at the direct caller of the deprecated object; if it + is higher, it is emitted further up the stack. + + The decorator sets the ``__deprecated__`` attribute on the decorated object to the deprecation message passed to the decorator. If applied to an overload, the decorator must be after the ``@overload`` decorator for the attribute to @@ -2183,11 +2544,11 @@ def decorator(__arg: _T) -> _T: @functools.wraps(original_new) def __new__(cls, *args, **kwargs): warnings.warn(__msg, category=category, stacklevel=stacklevel + 1) - # Mirrors a similar check in object.__new__. - if not has_init and (args or kwargs): - raise TypeError(f"{cls.__name__}() takes no arguments") if original_new is not object.__new__: return original_new(cls, *args, **kwargs) + # Mirrors a similar check in object.__new__. + elif not has_init and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") else: return original_new(cls) @@ -2223,18 +2584,14 @@ def wrapper(*args, **kwargs): typing._check_generic = _check_generic -# Backport typing.NamedTuple as it exists in Python 3.11. +# Backport typing.NamedTuple as it exists in Python 3.12. # 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. -if sys.version_info >= (3, 11): +# On 3.12, we added __orig_bases__ to call-based NamedTuples +# On 3.13, we deprecated kwargs-based NamedTuples +if sys.version_info >= (3, 13): NamedTuple = typing.NamedTuple else: - def _caller(): - try: - return sys._getframe(2).f_globals.get('__name__', '__main__') - except (AttributeError, ValueError): # For platforms without _getframe() - return None - def _make_nmtuple(name, types, module, defaults=()): fields = [n for n, t in types] annotations = {n: typing._type_check(t, f"field {n} annotation must be a type") @@ -2276,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: @@ -2288,25 +2648,425 @@ 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") - return _make_nmtuple(__typename, __fields, module=_caller()) - - NamedTuple.__doc__ = typing.NamedTuple.__doc__ - _NamedTuple = type.__new__(_NamedTupleMeta, 'NamedTuple', (), {}) + 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 # 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)' + _new_signature = '(typename, fields=None, /, **kwargs)' + if isinstance(NamedTuple, _types.FunctionType): + NamedTuple.__text_signature__ = _new_signature + else: + NamedTuple.__call__.__text_signature__ = _new_signature - def _namedtuple_mro_entries(bases): - assert NamedTuple in bases - return (_NamedTuple,) - NamedTuple.__mro_entries__ = _namedtuple_mro_entries +if hasattr(collections.abc, "Buffer"): + Buffer = collections.abc.Buffer +else: + class Buffer(abc.ABC): + """Base class for classes that implement the buffer protocol. + + The buffer protocol allows Python objects to expose a low-level + memory buffer interface. Before Python 3.12, it is not possible + to implement the buffer protocol in pure Python code, or even + to check whether a class implements the buffer protocol. In + Python 3.12 and higher, the ``__buffer__`` method allows access + to the buffer protocol from Python code, and the + ``collections.abc.Buffer`` ABC allows checking whether a class + implements the buffer protocol. + + To indicate support for the buffer protocol in earlier versions, + inherit from this ABC, either in a stub file or at runtime, + or use ABC registration. This ABC provides no methods, because + there is no Python-accessible methods shared by pre-3.12 buffer + classes. It is useful primarily for static checks. + + """ + + # As a courtesy, register the most common stdlib buffer classes. + Buffer.register(memoryview) + Buffer.register(bytearray) + Buffer.register(bytes) + + +# Backport of types.get_original_bases, available on 3.12+ in CPython +if hasattr(_types, "get_original_bases"): + get_original_bases = _types.get_original_bases +else: + def get_original_bases(__cls): + """Return the class's "original" bases prior to modification by `__mro_entries__`. + + Examples:: + + from typing import TypeVar, Generic + from typing_extensions import NamedTuple, TypedDict + + T = TypeVar("T") + class Foo(Generic[T]): ... + class Bar(Foo[int], float): ... + class Baz(list[str]): ... + Eggs = NamedTuple("Eggs", [("a", int), ("b", str)]) + Spam = TypedDict("Spam", {"a": int, "b": str}) + + assert get_original_bases(Bar) == (Foo[int], float) + assert get_original_bases(Baz) == (list[str],) + assert get_original_bases(Eggs) == (NamedTuple,) + assert get_original_bases(Spam) == (TypedDict,) + assert get_original_bases(int) == (object,) + """ + try: + return __cls.__orig_bases__ + except AttributeError: + try: + return __cls.__bases__ + except AttributeError: + raise TypeError( + f'Expected an instance of type, not {type(__cls).__name__!r}' + ) from None + + +# NewType is a class on Python 3.10+, making it pickleable +# The error message for subclassing instances of NewType was improved on 3.11+ +if sys.version_info >= (3, 11): + NewType = typing.NewType +else: + class NewType: + """NewType creates simple unique types with almost zero + runtime overhead. NewType(name, tp) is considered a subtype of tp + by static type checkers. At runtime, NewType(name, tp) returns + a dummy callable that simply returns its argument. Usage:: + UserId = NewType('UserId', int) + def name_by_id(user_id: UserId) -> str: + ... + UserId('user') # Fails type check + name_by_id(42) # Fails type check + name_by_id(UserId(42)) # OK + num = UserId(5) + 1 # type: int + """ + + def __call__(self, obj): + return obj + + def __init__(self, name, tp): + self.__qualname__ = name + if '.' in name: + name = name.rpartition('.')[-1] + self.__name__ = name + self.__supertype__ = tp + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __mro_entries__(self, bases): + # We defined __mro_entries__ to get a better error message + # if a user attempts to subclass a NewType instance. bpo-46170 + supercls_name = self.__name__ + + class Dummy: + def __init_subclass__(cls): + subcls_name = cls.__name__ + raise TypeError( + f"Cannot subclass an instance of NewType. " + f"Perhaps you were looking for: " + f"`{subcls_name} = NewType({subcls_name!r}, {supercls_name})`" + ) + + return (Dummy,) + + def __repr__(self): + return f'{self.__module__}.{self.__qualname__}' + + def __reduce__(self): + return self.__qualname__ + + if sys.version_info >= (3, 10): + # PEP 604 methods + # It doesn't make sense to have these methods on Python <3.10 + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + +if hasattr(typing, "TypeAliasType"): + TypeAliasType = typing.TypeAliasType +else: + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + + class TypeAliasType: + """Create named, parameterized type aliases. + + This provides a backport of the new `type` statement in Python 3.12: + + type ListOrSet[T] = list[T] | set[T] + + is equivalent to: + + T = TypeVar("T") + ListOrSet = TypeAliasType("ListOrSet", list[T] | set[T], type_params=(T,)) + + The name ListOrSet can then be used as an alias for the type it refers to. + + The type_params argument should contain all the type parameters used + in the value of the type alias. If the alias is not generic, this + argument is omitted. + + Static type checkers should only support type aliases declared using + TypeAliasType that follow these rules: + + - The first argument (the name) must be a string literal. + - The TypeAliasType instance must be immediately assigned to a variable + of the same name. (For example, 'X = TypeAliasType("Y", int)' is invalid, + as is 'X, Y = TypeAliasType("X", int), TypeAliasType("Y", int)'). + + """ + + def __init__(self, name: str, value, *, type_params=()): + if not isinstance(name, str): + raise TypeError("TypeAliasType name must be a string") + self.__value__ = value + self.__type_params__ = type_params + + parameters = [] + for type_param in type_params: + if isinstance(type_param, TypeVarTuple): + parameters.extend(type_param) + else: + parameters.append(type_param) + self.__parameters__ = tuple(parameters) + def_mod = _caller() + if def_mod != 'typing_extensions': + self.__module__ = def_mod + # Setting this attribute closes the TypeAliasType from further modification + self.__name__ = name + + def __setattr__(self, __name: str, __value: object) -> None: + if hasattr(self, "__name__"): + self._raise_attribute_error(__name) + super().__setattr__(__name, __value) + + def __delattr__(self, __name: str) -> Never: + self._raise_attribute_error(__name) + + def _raise_attribute_error(self, name: str) -> Never: + # Match the Python 3.12 error messages exactly + if name == "__name__": + raise AttributeError("readonly attribute") + elif name in {"__value__", "__type_params__", "__parameters__", "__module__"}: + raise AttributeError( + f"attribute '{name}' of 'typing.TypeAliasType' objects " + "is not writable" + ) + else: + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute '{name}'" + ) + + def __repr__(self) -> str: + return self.__name__ + + def __getitem__(self, parameters): + if not isinstance(parameters, tuple): + parameters = (parameters,) + parameters = [ + typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ] + return typing._GenericAlias(self, tuple(parameters)) + + def __reduce__(self): + return self.__name__ + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + "type 'typing_extensions.TypeAliasType' is not an acceptable base type" + ) + + # The presence of this method convinces typing._type_check + # that TypeAliasTypes are types. + def __call__(self): + raise TypeError("Type alias is not callable") + + if sys.version_info >= (3, 10): + def __or__(self, right): + # For forward compatibility with 3.12, reject Unions + # that are not accepted by the built-in Union. + if not _is_unionable(right): + return NotImplemented + return typing.Union[self, right] + + 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/test-requirements.txt b/test-requirements.txt index 05c4c918..675b2c5d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,2 @@ flake8 flake8-bugbear -flake8-pyi>=22.8.0 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