diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 03237510..00000000
--- a/.flake8
+++ /dev/null
@@ -1,18 +0,0 @@
-[flake8]
-
-max-line-length = 90
-ignore =
- # irrelevant plugins
- B3,
- DW12,
- # code is sometimes better without this
- E129,
- # Contradicts PEP8 nowadays
- W503,
- # consistency with mypy
- W504
-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
deleted file mode 100644
index 634160ab..00000000
--- a/.flake8-tests
+++ /dev/null
@@ -1,31 +0,0 @@
-# This configuration is specific to test_*.py; you need to invoke it
-# by specifically naming this config, like this:
-#
-# $ flake8 --config=.flake8-tests [SOURCES]
-#
-# This will be possibly merged in the future.
-
-[flake8]
-max-line-length = 100
-ignore =
- # temporary ignores until we sort it out
- B017,
- E302,
- E303,
- E306,
- E501,
- E701,
- E704,
- F722,
- F811,
- F821,
- F841,
- W503,
- # irrelevant plugins
- B3,
- DW12,
- # Contradicts PEP8 nowadays
- W503,
- # consistency with mypy
- W504
-noqa_require_code = true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..5c563144
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: monthly
+ groups:
+ actions:
+ patterns:
+ - "*"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e221022c..9f062801 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,6 +13,7 @@ permissions:
contents: read
env:
+ FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
concurrency:
@@ -51,6 +52,7 @@ jobs:
- "3.11"
- "3.11.0"
- "3.12"
+ - "3.13"
- "pypy3.8"
- "pypy3.9"
- "pypy3.10"
@@ -58,10 +60,10 @@ jobs:
runs-on: ubuntu-20.04
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
@@ -73,6 +75,15 @@ jobs:
cd src
python -m unittest test_typing_extensions.py
+ - name: Test CPython typing test suite
+ # Test suite fails on PyPy even without typing_extensions
+ if: ${{ !startsWith(matrix.python-version, 'pypy') }}
+ run: |
+ cd src
+ # Run the typing test suite from CPython with typing_extensions installed,
+ # because we monkeypatch typing under some circumstances.
+ python -c 'import typing_extensions; import test.__main__' test_typing -v
+
linting:
name: Lint
@@ -82,26 +93,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: "3"
cache: "pip"
cache-dependency-path: "test-requirements.txt"
-
- name: Install dependencies
- run: |
- pip install -r test-requirements.txt
- # not included in test-requirements.txt as it depends on typing-extensions,
- # so it's a pain to have it installed locally
- pip install flake8-noqa
-
+ run: pip install -r test-requirements.txt
- name: Lint implementation
- run: flake8 --color always
-
- - name: Lint tests
- run: flake8 --config=.flake8-tests src/test_typing_extensions.py --color always
+ run: ruff check
create-issue-on-failure:
name: Create an issue if daily tests failed
@@ -121,7 +123,7 @@ jobs:
issues: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
deleted file mode 100644
index ad2deee1..00000000
--- a/.github/workflows/package.yml
+++ /dev/null
@@ -1,76 +0,0 @@
-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
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Set up Python
- uses: actions/setup-python@v2
- with:
- python-version: 3
-
- - 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
- python -m pip install --upgrade build
- python -m pip list
-
- - name: Build and install wheel
- run: |
- python -m build .
- export path_to_file=$(find dist -type f -name "typing_extensions-*.whl")
- echo "::notice::Installing wheel: $path_to_file"
- pip install -vvv $path_to_file
- python -m pip list
-
- - name: Attempt to import typing_extensions
- run: python -c "import typing_extensions; print(typing_extensions.__all__)"
-
- sdist:
- name: Test sdist install
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
-
- - name: Set up Python
- uses: actions/setup-python@v2
- with:
- python-version: 3
-
- - 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
- python -m pip install --upgrade build
- python -m pip list
-
- - name: Build and install sdist
- run: |
- python -m build .
- export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz")
- echo "::notice::Installing sdist: $path_to_file"
- pip install -vvv $path_to_file
- python -m pip list
-
- - name: Attempt to import typing_extensions
- run: python -c "import typing_extensions; print(typing_extensions.__all__)"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 00000000..47704723
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,149 @@
+# Based on
+# https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+
+name: Test builds and publish Python distribution to PyPI
+
+on:
+ release:
+ types: [published]
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build distribution
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ - name: Check package metadata
+ run: python scripts/check_package.py ${{ github.ref }}
+ - 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
+ python -m pip install --upgrade build
+ python -m pip list
+ - name: Build a binary wheel and a source tarball
+ run: python -m build
+ - name: Store the distribution packages
+ uses: actions/upload-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+
+ test-wheel:
+ name: Test wheel
+ needs:
+ - build
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Install wheel
+ run: |
+ export path_to_file=$(find dist -type f -name "typing_extensions-*.whl")
+ echo "::notice::Installing wheel: $path_to_file"
+ python -m pip install --user $path_to_file
+ python -m pip list
+ - name: Run typing_extensions tests against installed package
+ run: rm src/typing_extensions.py && python src/test_typing_extensions.py
+
+ test-sdist:
+ name: Test source distribution
+ needs:
+ - build
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Unpack and test source distribution
+ run: |
+ export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz")
+ echo "::notice::Unpacking source distribution: $path_to_file"
+ tar xzf $path_to_file -C dist/
+ cd ${path_to_file%.tar.gz}/src
+ python test_typing_extensions.py
+
+ test-sdist-installed:
+ name: Test installed source distribution
+ needs:
+ - build
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.x"
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Install source distribution
+ run: |
+ export path_to_file=$(find dist -type f -name "typing_extensions-*.tar.gz")
+ echo "::notice::Installing source distribution: $path_to_file"
+ python -m pip install --user $path_to_file
+ python -m pip list
+ - name: Run typing_extensions tests against installed package
+ run: rm src/typing_extensions.py && python src/test_typing_extensions.py
+
+ publish-to-pypi:
+ name: >-
+ Publish Python distribution to PyPI
+ if: github.event_name == 'release' # only publish to PyPI on releases
+ needs:
+ - test-sdist
+ - test-sdist-installed
+ - test-wheel
+ - build
+ runs-on: ubuntu-latest
+ environment:
+ name: publish
+ url: https://pypi.org/p/typing-extensions
+ permissions:
+ id-token: write # IMPORTANT: mandatory for trusted publishing
+
+ steps:
+ - name: Download all the dists
+ uses: actions/download-artifact@v4
+ with:
+ name: python-package-distributions
+ path: dist/
+ - name: Ensure exactly one sdist and one wheel have been downloaded
+ run: test $(ls dist/*.tar.gz | wc -l) = 1 && test $(ls dist/*.whl | wc -l) = 1
+ - name: Publish distribution to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml
index b318e333..8424d8fe 100644
--- a/.github/workflows/third_party.yml
+++ b/.github/workflows/third_party.yml
@@ -41,12 +41,15 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"]
+ # PyPy is deliberately omitted here,
+ # since pydantic's tests intermittently segfault on PyPy,
+ # and it's nothing to do with typing_extensions
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout pydantic
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: pydantic/pydantic
- name: Edit pydantic pyproject.toml
@@ -54,13 +57,14 @@ jobs:
# as a requirement unless we do this
run: sed -i 's/^requires-python = .*/requires-python = ">=3.8"/' pyproject.toml
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup pdm for pydantic tests
- uses: pdm-project/setup-pdm@v3
+ uses: pdm-project/setup-pdm@v4
with:
python-version: ${{ matrix.python-version }}
+ allow-python-prereleases: true
- name: Add local version of typing_extensions as a dependency
run: pdm add ./typing-extensions-latest
- name: Install pydantic test dependencies
@@ -90,24 +94,28 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout typing_inspect
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: ilevkivskyi/typing_inspect
path: typing_inspect
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install typing_inspect test dependencies
- run: pip install -r typing_inspect/test-requirements.txt
+ run: |
+ cd typing_inspect
+ uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typing_inspect tests
run: |
cd typing_inspect
@@ -133,25 +141,29 @@ jobs:
timeout-minutes: 60
steps:
- name: Check out pyanalyze
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: quora/pyanalyze
path: pyanalyze
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install pyanalyze test requirements
- run: pip install ./pyanalyze[tests]
+ run: |
+ cd pyanalyze
+ uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run pyanalyze tests
run: |
cd pyanalyze
@@ -172,30 +184,34 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out typeguard
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: agronholm/typeguard
path: typeguard
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install typeguard test requirements
- run: pip install -e ./typeguard[test]
+ run: |
+ cd typeguard
+ uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typeguard tests
run: |
cd typeguard
@@ -216,23 +232,25 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Check out typed-argument-parser
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: swansonk14/typed-argument-parser
path: typed-argument-parser
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Configure git for typed-argument-parser tests
# typed-argument parser does this in their CI,
# and the tests fail unless we do this
@@ -241,12 +259,13 @@ jobs:
git config --global user.name "Your Name"
- name: Install typed-argument-parser test requirements
run: |
- pip install -e ./typed-argument-parser
- pip install pytest
+ cd typed-argument-parser
+ uv pip install --system "typed-argument-parser @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
+ uv pip install --system pytest --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run typed-argument-parser tests
run: |
cd typed-argument-parser
@@ -272,28 +291,30 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout mypy for stubtest and mypyc tests
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python/mypy
path: mypy
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
+ - name: Install uv
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install mypy test requirements
run: |
cd mypy
- pip install -r test-requirements.txt
- pip install -e .
+ uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD)
+ uv pip install --system -e .
- name: Install typing_extensions latest
- run: pip install ./typing-extensions-latest
+ run: uv pip install --system "typing-extensions @ ./typing-extensions-latest"
- name: List all installed dependencies
- run: pip freeze --all
+ run: uv pip freeze
- name: Run stubtest & mypyc tests
run: |
cd mypy
@@ -314,20 +335,20 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11", "pypy3.9"]
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout cattrs
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
repository: python-attrs/cattrs
- name: Checkout typing_extensions
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
with:
path: typing-extensions-latest
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install pdm for cattrs
@@ -376,7 +397,7 @@ jobs:
issues: write
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
diff --git a/.gitignore b/.gitignore
index 0ad58f48..ee36fe77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ venv*/
.tox/
.venv*/
.vscode/
+.python-version
*.swp
*.pyc
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..60419be8
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,13 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.12"
+
+sphinx:
+ configuration: doc/conf.py
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58f0487d..90f5b682 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,165 @@
+# Release 4.12.2 (June 7, 2024)
+
+- Fix regression in v4.12.0 where specialization of certain
+ generics with an overridden `__eq__` method would raise errors.
+ Patch by Jelle Zijlstra.
+- Fix tests so they pass on 3.13.0b2
+
+# Release 4.12.1 (June 1, 2024)
+
+- Preliminary changes for compatibility with the draft implementation
+ of PEP 649 in Python 3.14. Patch by Jelle Zijlstra.
+- Fix regression in v4.12.0 where nested `Annotated` types would cause
+ `TypeError` to be raised if the nested `Annotated` type had unhashable
+ metadata. Patch by Alex Waygood.
+
+# Release 4.12.0 (May 23, 2024)
+
+This release is mostly the same as 4.12.0rc1 but fixes one more
+longstanding bug.
+
+- Fix incorrect behaviour of `typing_extensions.ParamSpec` on Python 3.8 and
+ 3.9 that meant that
+ `isinstance(typing_extensions.ParamSpec("P"), typing.TypeVar)` would have a
+ different result in some situations depending on whether or not a profiling
+ function had been set using `sys.setprofile`. Patch by Alex Waygood.
+
+# Release 4.12.0rc1 (May 16, 2024)
+
+This release focuses on compatibility with the upcoming release of
+Python 3.13. Most changes are related to the implementation of type
+parameter defaults (PEP 696).
+
+Thanks to all of the people who contributed patches, especially Alex
+Waygood, who did most of the work adapting typing-extensions to the
+CPython PEP 696 implementation.
+
+Full changelog:
+
+- Improve the implementation of type parameter defaults (PEP 696)
+ - Backport the `typing.NoDefault` sentinel object from Python 3.13.
+ TypeVars, ParamSpecs and TypeVarTuples without default values now have
+ their `__default__` attribute set to this sentinel value.
+ - TypeVars, ParamSpecs and TypeVarTuples now have a `has_default()`
+ method, matching `typing.TypeVar`, `typing.ParamSpec` and
+ `typing.TypeVarTuple` on Python 3.13+.
+ - TypeVars, ParamSpecs and TypeVarTuples with `default=None` passed to
+ their constructors now have their `__default__` attribute set to `None`
+ at runtime rather than `types.NoneType`.
+ - Fix most tests for `TypeVar`, `ParamSpec` and `TypeVarTuple` on Python
+ 3.13.0b1 and newer.
+ - Backport CPython PR [#118774](https://github.com/python/cpython/pull/118774),
+ allowing type parameters without default values to follow those with
+ default values in some type parameter lists. Patch by Alex Waygood,
+ backporting a CPython PR by Jelle Zijlstra.
+ - It is now disallowed to use a `TypeVar` with a default value after a
+ `TypeVarTuple` in a type parameter list. This matches the CPython
+ implementation of PEP 696 on Python 3.13+.
+ - Fix bug in PEP-696 implementation where a default value for a `ParamSpec`
+ would be cast to a tuple if a list was provided.
+ Patch by Alex Waygood.
+- Fix `Protocol` tests on Python 3.13.0a6 and newer. 3.13.0a6 adds a new
+ `__static_attributes__` attribute to all classes in Python,
+ which broke some assumptions made by the implementation of
+ `typing_extensions.Protocol`. Similarly, 3.13.0b1 adds the new
+ `__firstlineno__` attribute to all classes.
+- Fix `AttributeError` when using `typing_extensions.runtime_checkable`
+ in combination with `typing.Protocol` on Python 3.12.2 or newer.
+ Patch by Alex Waygood.
+- At runtime, `assert_never` now includes the repr of the argument
+ in the `AssertionError`. Patch by Hashem, backporting of the original
+ fix https://github.com/python/cpython/pull/91720 by Jelle Zijlstra.
+- The second and third parameters of `typing_extensions.Generator`,
+ and the second parameter of `typing_extensions.AsyncGenerator`,
+ now default to `None`. This matches the behaviour of `typing.Generator`
+ and `typing.AsyncGenerator` on Python 3.13+.
+- `typing_extensions.ContextManager` and
+ `typing_extensions.AsyncContextManager` now have an optional second
+ parameter, which defaults to `Optional[bool]`. The new parameter
+ signifies the return type of the `__(a)exit__` method, matching
+ `typing.ContextManager` and `typing.AsyncContextManager` on Python
+ 3.13+.
+- Backport `types.CapsuleType` from Python 3.13.
+- Releases are now made using [Trusted Publishers](https://docs.pypi.org/trusted-publishers/)
+ improving the security of the release process. Patch by Jelle Zijlstra.
+
+# Release 4.12.0a1 and 4.12.0a2 (May 16, 2024)
+
+These releases primarily test a revised release workflow. If all goes
+well, release 4.12.0rc1 will follow soon.
+
+# Release 4.11.0 (April 5, 2024)
+
+This feature release provides improvements to various recently
+added features, most importantly type parameter defaults (PEP 696).
+
+There are no changes since 4.11.0rc1.
+
+# Release 4.11.0rc1 (March 24, 2024)
+
+- Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra.
+- Fix the runtime behavior of type parameters with defaults (PEP 696).
+ Patch by Nadir Chowdhury.
+- Fix minor discrepancy between error messages produced by `typing`
+ and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra.
+- When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation.
+
+# Release 4.10.0 (February 24, 2024)
+
+This feature release adds support for PEP 728 (TypedDict with extra
+items) and PEP 742 (``TypeIs``).
+
+There are no changes since 4.10.0rc1.
+
+# Release 4.10.0rc1 (February 17, 2024)
+
+- Add support for PEP 728, supporting the `closed` keyword argument and the
+ special `__extra_items__` key for TypedDict. Patch by Zixuan James Li.
+- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch
+ by Jelle Zijlstra.
+- Drop runtime error when a read-only `TypedDict` item overrides a mutable
+ one. Type checkers should still flag this as an error. Patch by Jelle
+ Zijlstra.
+- Speedup `issubclass()` checks against simple runtime-checkable protocols by
+ around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex
+ Waygood).
+- Fix a regression in the implementation of protocols where `typing.Protocol`
+ classes that were not marked as `@runtime_checkable` would be unnecessarily
+ introspected, potentially causing exceptions to be raised if the protocol had
+ problematic members. Patch by Alex Waygood, backporting
+ https://github.com/python/cpython/pull/113401.
+
+# Release 4.9.0 (December 9, 2023)
+
+This feature release adds `typing_extensions.ReadOnly`, as specified
+by PEP 705, and makes various other improvements, especially to
+`@typing_extensions.deprecated()`.
+
+There are no changes since 4.9.0rc1.
+
+# Release 4.9.0rc1 (November 29, 2023)
+
+- Add support for PEP 705, adding `typing_extensions.ReadOnly`. Patch
+ by Jelle Zijlstra.
+- All parameters on `NewType.__call__` are now positional-only. This means that
+ the signature of `typing_extensions.NewType.__call__` now exactly matches the
+ signature of `typing.NewType.__call__`. Patch by Alex Waygood.
+- Fix bug with using `@deprecated` on a mixin class. Inheriting from a
+ deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.
+- `@deprecated` now gives a better error message if you pass a non-`str`
+ argument to the `msg` parameter. Patch by Alex Waygood.
+- `@deprecated` is now implemented as a class for better introspectability.
+ Patch by Jelle Zijlstra.
+- Exclude `__match_args__` from `Protocol` members.
+ Backport of https://github.com/python/cpython/pull/110683 by Nikita Sobolev.
+- When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__`
+ is called on all objects that define `__set_name__` and exist in the values
+ of the `NamedTuple` class's class dictionary. Patch by Alex Waygood,
+ backporting https://github.com/python/cpython/pull/111876.
+- Improve the error message when trying to call `issubclass()` against a
+ `Protocol` that has non-method members. Patch by Alex Waygood (backporting
+ https://github.com/python/cpython/pull/112344, by Randolph Scholz).
+
# Release 4.8.0 (September 17, 2023)
No changes since 4.8.0rc1.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a118a40e..1b030d56 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -61,24 +61,10 @@ may have installed.
- Update the version number in `typing_extensions/pyproject.toml` and in
`typing_extensions/CHANGELOG.md`.
-- Make sure your environment is up to date
-
- - `git checkout main`
- - `git pull`
- - `python -m pip install --upgrade build twine`
-
-- Build the source and wheel distributions:
-
- - `rm -rf dist/`
- - `python -m build .`
-
-- Install the built distributions locally and test (if you were using `tox`, you already
- tested the source distribution).
-
-- Run `twine upload dist/*`. Remember to use `__token__` as the username
- and pass your API token as the password.
-
- 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`.
+
+- Release automation will finish the release. You'll have to manually
+ approve the last step before upload.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..efd1d6a3
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,10 @@
+# Security Policy
+
+## Supported Versions
+
+Only the latest release is supported.
+
+## Reporting a Vulnerability
+
+To report an issue, go to https://github.com/python/typing_extensions/security.
+We commit to respond to any issue within 14 days and promptly release any fixes.
diff --git a/doc/conf.py b/doc/conf.py
index 7984bc22..42273604 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -6,6 +6,9 @@
import os.path
import sys
+from docutils.nodes import Element
+from sphinx.writers.html5 import HTML5Translator
+
sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
@@ -26,9 +29,22 @@
intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)}
+add_module_names = False
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'alabaster'
-html_static_path = ['_static']
+
+
+class MyTranslator(HTML5Translator):
+ """Adds a link target to name without `typing_extensions.` prefix."""
+ def visit_desc_signature(self, node: Element) -> None:
+ desc_name = node.get("fullname")
+ if desc_name:
+ self.body.append(f'')
+ super().visit_desc_signature(node)
+
+
+def setup(app):
+ app.set_translator('html', MyTranslator)
diff --git a/doc/index.rst b/doc/index.rst
index 28b795a3..3f0d2d44 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,3 +1,4 @@
+.. module:: typing_extensions
Welcome to typing_extensions's documentation!
=============================================
@@ -252,13 +253,19 @@ Special typing primitives
The improvements from Python 3.10 and 3.11 were backported.
+.. data:: NoDefault
+
+ See :py:class:`typing.NoDefault`. In ``typing`` since 3.13.0.
+
+ .. versionadded:: 4.12.0
+
.. data:: NotRequired
See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11.
.. versionadded:: 4.0.0
-.. class:: ParamSpec(name, *, default=...)
+.. class:: ParamSpec(name, *, default=NoDefault)
See :py:class:`typing.ParamSpec` and :pep:`612`. In ``typing`` since 3.10.
@@ -283,6 +290,20 @@ Special typing primitives
Passing an ellipsis literal (``...``) to *default* now works on Python
3.10 and lower.
+ .. versionchanged:: 4.12.0
+
+ The :attr:`!__default__` attribute is now set to ``None`` if
+ ``default=None`` is passed, and to :data:`NoDefault` if no value is passed.
+
+ Previously, passing ``None`` would result in :attr:`!__default__` being set
+ to :py:class:`types.NoneType`, and passing no value for the parameter would
+ result in :attr:`!__default__` being set to ``None``.
+
+ .. versionchanged:: 4.12.0
+
+ ParamSpecs now have a ``has_default()`` method, for compatibility
+ with :py:class:`typing.ParamSpec` on Python 3.13+.
+
.. class:: ParamSpecArgs
.. class:: ParamSpecKwargs
@@ -318,6 +339,12 @@ Special typing primitives
present in a protocol class's :py:term:`method resolution order`. See
:issue:`245` for some examples.
+.. data:: ReadOnly
+
+ See :pep:`705`. Indicates that a :class:`TypedDict` item may not be modified.
+
+ .. versionadded:: 4.9.0
+
.. data:: Required
See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11.
@@ -344,7 +371,13 @@ Special typing primitives
See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10.
-.. class:: TypedDict
+.. data:: TypeIs
+
+ See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing.
+
+ .. versionadded:: 4.10.0
+
+.. class:: TypedDict(dict, total=True)
See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8.
@@ -366,6 +399,55 @@ Special typing primitives
raises a :py:exc:`DeprecationWarning` when this syntax is used in Python 3.12
or lower and fails with a :py:exc:`TypeError` in Python 3.13 and higher.
+ ``typing_extensions`` supports the experimental :data:`ReadOnly` qualifier
+ proposed by :pep:`705`. It is reflected in the following attributes:
+
+ .. attribute:: __readonly_keys__
+
+ A :py:class:`frozenset` containing the names of all read-only keys. Keys
+ are read-only if they carry the :data:`ReadOnly` qualifier.
+
+ .. versionadded:: 4.9.0
+
+ .. attribute:: __mutable_keys__
+
+ A :py:class:`frozenset` containing the names of all mutable keys. Keys
+ are mutable if they do not carry the :data:`ReadOnly` qualifier.
+
+ .. versionadded:: 4.9.0
+
+ The experimental ``closed`` keyword argument and the special key
+ ``__extra_items__`` proposed in :pep:`728` are supported.
+
+ When ``closed`` is unspecified or ``closed=False`` is given,
+ ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a
+ special key that does not show up in ``__readonly_keys__``,
+ ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or
+ ``__annotations__``.
+
+ For runtime introspection, two attributes can be looked at:
+
+ .. attribute:: __closed__
+
+ A boolean flag indicating whether the current ``TypedDict`` is
+ considered closed. This is not inherited by the ``TypedDict``'s
+ subclasses.
+
+ .. versionadded:: 4.10.0
+
+ .. attribute:: __extra_items__
+
+ The type annotation of the extra items allowed on the ``TypedDict``.
+ This attribute defaults to ``None`` on a TypedDict that has itself and
+ all its bases non-closed. This default is different from ``type(None)``
+ that represents ``__extra_items__: None`` defined on a closed
+ ``TypedDict``.
+
+ If ``__extra_items__`` is not defined or inherited on a closed
+ ``TypedDict``, this defaults to ``Never``.
+
+ .. versionadded:: 4.10.0
+
.. versionchanged:: 4.3.0
Added support for generic ``TypedDict``\ s.
@@ -394,8 +476,17 @@ Special typing primitives
disallowed in Python 3.15. To create a TypedDict class with 0 fields,
use ``class TD(TypedDict): pass`` or ``TD = TypedDict("TD", {})``.
+ .. versionchanged:: 4.9.0
+
+ Support for the :data:`ReadOnly` qualifier was added.
+
+ .. versionchanged:: 4.10.0
+
+ The keyword argument ``closed`` and the special key ``__extra_items__``
+ when ``closed=True`` is given were supported.
+
.. class:: TypeVar(name, *constraints, bound=None, covariant=False,
- contravariant=False, infer_variance=False, default=...)
+ contravariant=False, infer_variance=False, default=NoDefault)
See :py:class:`typing.TypeVar`.
@@ -413,7 +504,21 @@ Special typing primitives
The implementation was changed for compatibility with Python 3.12.
-.. class:: TypeVarTuple(name, *, default=...)
+ .. versionchanged:: 4.12.0
+
+ The :attr:`!__default__` attribute is now set to ``None`` if
+ ``default=None`` is passed, and to :data:`NoDefault` if no value is passed.
+
+ Previously, passing ``None`` would result in :attr:`!__default__` being set
+ to :py:class:`types.NoneType`, and passing no value for the parameter would
+ result in :attr:`!__default__` being set to ``None``.
+
+ .. versionchanged:: 4.12.0
+
+ TypeVars now have a ``has_default()`` method, for compatibility
+ with :py:class:`typing.TypeVar` on Python 3.13+.
+
+.. class:: TypeVarTuple(name, *, default=NoDefault)
See :py:class:`typing.TypeVarTuple` and :pep:`646`. In ``typing`` since 3.11.
@@ -430,6 +535,26 @@ Special typing primitives
The implementation was changed for compatibility with Python 3.12.
+ .. versionchanged:: 4.12.0
+
+ The :attr:`!__default__` attribute is now set to ``None`` if
+ ``default=None`` is passed, and to :data:`NoDefault` if no value is passed.
+
+ Previously, passing ``None`` would result in :attr:`!__default__` being set
+ to :py:class:`types.NoneType`, and passing no value for the parameter would
+ result in :attr:`!__default__` being set to ``None``.
+
+ .. versionchanged:: 4.12.0
+
+ TypeVarTuples now have a ``has_default()`` method, for compatibility
+ with :py:class:`typing.TypeVarTuple` on Python 3.13+.
+
+ .. versionchanged:: 4.12.0
+
+ It is now disallowed to use a `TypeVar` with a default value after a
+ `TypeVarTuple` in a type parameter list. This matches the CPython
+ implementation of PEP 696 on Python 3.13+.
+
.. data:: Unpack
See :py:data:`typing.Unpack` and :pep:`646`. In ``typing`` since 3.11.
@@ -549,10 +674,15 @@ Decorators
.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1)
- See :pep:`702`. Experimental; not yet part of the standard library.
+ See :pep:`702`. In the :mod:`warnings` module since Python 3.13.
.. versionadded:: 4.5.0
+ .. versionchanged:: 4.9.0
+
+ Inheriting from a deprecated class now also raises a runtime
+ :py:exc:`DeprecationWarning`.
+
.. decorator:: final
See :py:func:`typing.final` and :pep:`591`. In ``typing`` since 3.8.
@@ -684,6 +814,11 @@ Functions
Interaction with :data:`Required` and :data:`NotRequired`.
+ .. versionchanged:: 4.11.0
+
+ When ``include_extra=False``, ``get_type_hints()`` now strips
+ :data:`ReadOnly` from the annotation.
+
.. function:: is_protocol(tp)
Determine if a type is a :class:`Protocol`. This works with protocols
@@ -753,11 +888,25 @@ Annotation metadata
The documentation string passed to :class:`Doc`.
+Capsule objects
+~~~~~~~~~~~~~~~
+
+.. class:: CapsuleType
+
+ The type of :py:ref:`capsule objects `.
+ See :py:class:`types.CapsuleType`, where it has existed since Python 3.13.
+
+ Note that this may not exist on all implementations of Python; it is only
+ guaranteed to exist on CPython.
+
+ .. versionadded:: 4.12.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.
+Most of these are simply re-exported from the :mod:`typing` module on all supported
+versions of Python, but all are listed here for completeness.
.. class:: AbstractSet
@@ -775,10 +924,19 @@ versions of Python. They are listed here for completeness.
See :py:class:`typing.AsyncContextManager`. In ``typing`` since 3.5.4 and 3.6.2.
+ .. versionchanged:: 4.12.0
+
+ ``AsyncContextManager`` now has an optional second parameter, defaulting to
+ ``Optional[bool]``, signifying the return type of the ``__aexit__`` method.
+
.. class:: AsyncGenerator
See :py:class:`typing.AsyncGenerator`. In ``typing`` since 3.6.1.
+ .. versionchanged:: 4.12.0
+
+ The second type parameter is now optional (it defaults to ``None``).
+
.. class:: AsyncIterable
See :py:class:`typing.AsyncIterable`. In ``typing`` since 3.5.2.
@@ -827,6 +985,11 @@ versions of Python. They are listed here for completeness.
See :py:class:`typing.ContextManager`. In ``typing`` since 3.5.4.
+ .. versionchanged:: 4.12.0
+
+ ``ContextManager`` now has an optional second parameter, defaulting to
+ ``Optional[bool]``, signifying the return type of the ``__exit__`` method.
+
.. class:: Coroutine
See :py:class:`typing.Coroutine`. In ``typing`` since 3.5.3.
@@ -867,6 +1030,11 @@ versions of Python. They are listed here for completeness.
.. versionadded:: 4.7.0
+ .. versionchanged:: 4.12.0
+
+ The second type and third type parameters are now optional
+ (they both default to ``None``).
+
.. class:: Generic
See :py:class:`typing.Generic`.
@@ -1048,3 +1216,23 @@ versions of Python. They are listed here for completeness.
See :py:func:`typing.no_type_check_decorator`.
.. versionadded:: 4.7.0
+
+Security
+--------
+
+``typing_extensions`` is among the most widely used packages in the
+Python ecosystem. Therefore, we take security seriously and strive
+to use a transparent, secure release process.
+
+We commit to the following in order to keep the package secure in the
+future:
+
+* ``typing_extensions`` will never include any native extensions, only
+ pure Python code.
+* ``typing_extensions`` will not have any third-party dependencies.
+* We will follow best practices for a secure release process.
+
+If you have any feedback on our security process, please `open an issue
+`__. To report
+an issue privately, use `GitHub's private reporting feature
+`__.
diff --git a/pyproject.toml b/pyproject.toml
index b71e6d01..3388d553 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi"
# Project metadata
[project]
name = "typing_extensions"
-version = "4.8.0"
+version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
readme = "README.md"
requires-python = ">=3.8"
@@ -39,6 +39,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development",
]
@@ -59,3 +60,42 @@ email = "levkivskyi@gmail.com"
[tool.flit.sdist]
include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"]
exclude = []
+
+[tool.ruff]
+line-length = 90
+target-version = "py38"
+
+[tool.ruff.lint]
+select = [
+ "B",
+ "C4",
+ "E",
+ "F",
+ "I",
+ "ISC001",
+ "PGH004",
+ "RUF",
+ "SIM201",
+ "SIM202",
+ "UP",
+ "W",
+]
+
+# Ignore various "modernization" rules that tell you off for importing/using
+# deprecated things from the typing module, etc.
+ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP038"]
+
+[tool.ruff.lint.per-file-ignores]
+"!src/typing_extensions.py" = [
+ "B018",
+ "B024",
+ "C4",
+ "E302",
+ "E306",
+ "E501",
+ "E701",
+]
+
+[tool.ruff.lint.isort]
+extra-standard-library = ["tomllib"]
+known-first-party = ["typing_extensions", "_typed_dict_test_helper"]
diff --git a/scripts/check_package.py b/scripts/check_package.py
new file mode 100644
index 00000000..f52df411
--- /dev/null
+++ b/scripts/check_package.py
@@ -0,0 +1,60 @@
+import argparse
+import re
+import sys
+import tomllib
+from pathlib import Path
+
+
+class ValidationError(Exception):
+ pass
+
+
+def check(github_ref: str | None) -> None:
+ pyproject = Path(__file__).parent.parent / "pyproject.toml"
+ if not pyproject.exists():
+ raise ValidationError("pyproject.toml not found")
+ with pyproject.open("rb") as f:
+ data = tomllib.load(f)
+ pyproject_version = data["project"]["version"]
+
+ if github_ref is not None and github_ref.startswith("refs/tags/"):
+ version = github_ref.removeprefix("refs/tags/")
+ if version != pyproject_version:
+ raise ValidationError(
+ f"Version mismatch: GitHub ref is {version}, "
+ f"but pyproject.toml is {pyproject_version}"
+ )
+
+ requires_python = data["project"]["requires-python"]
+ assert sys.version_info[0] == 3, "Rewrite this script when Python 4 comes out"
+ match = re.fullmatch(r">=3\.(\d+)", requires_python)
+ if not match:
+ raise ValidationError(f"Invalid requires-python: {requires_python!r}")
+ lowest_minor = int(match.group(1))
+
+ description = data["project"]["description"]
+ if not description.endswith(f"3.{lowest_minor}+"):
+ raise ValidationError(f"Description should mention Python 3.{lowest_minor}+")
+
+ classifiers = set(data["project"]["classifiers"])
+ for should_be_supported in range(lowest_minor, sys.version_info[1] + 1):
+ if (
+ f"Programming Language :: Python :: 3.{should_be_supported}"
+ not in classifiers
+ ):
+ raise ValidationError(
+ f"Missing classifier for Python 3.{should_be_supported}"
+ )
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser("Script to check the package metadata")
+ parser.add_argument(
+ "github_ref", type=str, help="The current GitHub ref", nargs="?"
+ )
+ args = parser.parse_args()
+ try:
+ check(args.github_ref)
+ except ValidationError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/_typed_dict_test_helper.py b/src/_typed_dict_test_helper.py
index c5582b15..73cf9199 100644
--- a/src/_typed_dict_test_helper.py
+++ b/src/_typed_dict_test_helper.py
@@ -1,7 +1,8 @@
from __future__ import annotations
from typing import Generic, Optional, T
-from typing_extensions import TypedDict, Annotated, Required
+
+from typing_extensions import Annotated, Required, TypedDict
# this class must not be imported into test_typing_extensions.py at top level, otherwise
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 97717bce..2f98765b 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -1,45 +1,102 @@
-import sys
-import os
import abc
-import gc
-import io
-import contextlib
import collections
-from collections import defaultdict
import collections.abc
+import contextlib
import copy
-from functools import lru_cache
+import gc
import importlib
import inspect
+import io
import pickle
import re
import subprocess
+import sys
import tempfile
import textwrap
import types
-from pathlib import Path
-from unittest import TestCase, main, skipUnless, skipIf
-from unittest.mock import patch
import typing
-from typing import Optional, Union, AnyStr
-from typing import T, KT, VT # Not in __all__.
-from typing import Tuple, List, Set, Dict, Iterable, Iterator, Callable
-from typing import Generic
-from typing import no_type_check
import warnings
+from collections import defaultdict
+from functools import lru_cache
+from pathlib import Path
+from unittest import TestCase, main, skipIf, skipUnless
+from unittest.mock import patch
import typing_extensions
-from typing_extensions import NoReturn, Any, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict, Self
-from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
-from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
-from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict
-from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
-from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
-from typing_extensions import clear_overloads, get_overloads, overload
-from typing_extensions import NamedTuple
-from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol
-from typing_extensions import Doc
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
+from typing_extensions import (
+ Annotated,
+ Any,
+ AnyStr,
+ AsyncContextManager,
+ AsyncIterator,
+ Awaitable,
+ Buffer,
+ Callable,
+ ClassVar,
+ Concatenate,
+ Dict,
+ Doc,
+ Final,
+ Generic,
+ IntVar,
+ Iterable,
+ Iterator,
+ List,
+ Literal,
+ LiteralString,
+ NamedTuple,
+ Never,
+ NewType,
+ NoDefault,
+ NoReturn,
+ NotRequired,
+ Optional,
+ ParamSpec,
+ ParamSpecArgs,
+ ParamSpecKwargs,
+ Protocol,
+ ReadOnly,
+ Required,
+ Self,
+ Set,
+ Tuple,
+ Type,
+ TypeAlias,
+ TypeAliasType,
+ TypedDict,
+ TypeGuard,
+ TypeIs,
+ TypeVar,
+ TypeVarTuple,
+ Union,
+ Unpack,
+ assert_never,
+ assert_type,
+ clear_overloads,
+ dataclass_transform,
+ deprecated,
+ final,
+ get_args,
+ get_origin,
+ get_original_bases,
+ get_overloads,
+ get_protocol_members,
+ get_type_hints,
+ is_protocol,
+ is_typeddict,
+ no_type_check,
+ overload,
+ override,
+ reveal_type,
+ runtime,
+ runtime_checkable,
+)
+
+NoneType = type(None)
+T = TypeVar("T")
+KT = TypeVar("KT")
+VT = TypeVar("VT")
# Flags used to mark tests that only apply after a specific
# version of the typing module.
@@ -52,15 +109,27 @@
# 3.12 changes the representation of Unpack[] (PEP 692)
TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0)
+# 3.13 drops support for the keyword argument syntax of TypedDict
+TYPING_3_13_0 = sys.version_info[:3] >= (3, 13, 0)
+
# https://github.com/python/cpython/pull/27017 was backported into some 3.9 and 3.10
# versions, but not all
HAS_FORWARD_MODULE = "module" in inspect.signature(typing._type_check).parameters
+skip_if_py313_beta_1 = skipIf(
+ sys.version_info[:5] == (3, 13, 0, 'beta', 1),
+ "Bugfixes will be released in 3.13.0b2"
+)
+
ANN_MODULE_SOURCE = '''\
-from typing import Optional
+import sys
+from typing import List, Optional
from functools import wraps
-__annotations__[1] = 2
+try:
+ __annotations__[1] = 2
+except NameError:
+ assert sys.version_info >= (3, 14)
class C:
@@ -70,8 +139,10 @@ class C:
x: int = 5; y: str = x; f: Tuple[int, int]
class M(type):
-
- __annotations__['123'] = 123
+ try:
+ __annotations__['123'] = 123
+ except NameError:
+ assert sys.version_info >= (3, 14)
o: type = object
(pars): bool = True
@@ -166,14 +237,14 @@ def g_bad_ann():
class BaseTestCase(TestCase):
def assertIsSubclass(self, cls, class_or_tuple, msg=None):
if not issubclass(cls, class_or_tuple):
- message = f'{cls!r} is not a subclass of {repr(class_or_tuple)}'
+ message = f'{cls!r} is not a subclass of {class_or_tuple!r}'
if msg is not None:
message += f' : {msg}'
raise self.failureException(message)
def assertNotIsSubclass(self, cls, class_or_tuple, msg=None):
if issubclass(cls, class_or_tuple):
- message = f'{cls!r} is a subclass of {repr(class_or_tuple)}'
+ message = f'{cls!r} is a subclass of {class_or_tuple!r}'
if msg is not None:
message += f' : {msg}'
raise self.failureException(message)
@@ -213,7 +284,7 @@ def test_cannot_subclass(self):
class A(self.bottom_type):
pass
with self.assertRaises(TypeError):
- class A(type(self.bottom_type)):
+ class B(type(self.bottom_type)):
pass
def test_cannot_instantiate(self):
@@ -275,6 +346,19 @@ def test_exception(self):
with self.assertRaises(AssertionError):
assert_never(None)
+ value = "some value"
+ with self.assertRaisesRegex(AssertionError, value):
+ assert_never(value)
+
+ # Make sure a huge value doesn't get printed in its entirety
+ huge_value = "a" * 10000
+ with self.assertRaises(AssertionError) as cm:
+ assert_never(huge_value)
+ self.assertLess(
+ len(cm.exception.args[0]),
+ typing_extensions._ASSERT_NEVER_REPR_MAX_LENGTH * 2,
+ )
+
class OverrideTests(BaseTestCase):
def test_override(self):
@@ -302,7 +386,6 @@ def static_method_good_order():
def static_method_bad_order():
return 42
-
self.assertIsSubclass(Derived, Base)
instance = Derived()
self.assertEqual(instance.normal_method(), 42)
@@ -418,6 +501,93 @@ def __new__(cls, x):
self.assertEqual(instance.x, 42)
self.assertTrue(new_called)
+ def test_mixin_class(self):
+ @deprecated("Mixin will go away soon")
+ class Mixin:
+ pass
+
+ class Base:
+ def __init__(self, a) -> None:
+ self.a = a
+
+ with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
+ class Child(Base, Mixin):
+ pass
+
+ instance = Child(42)
+ self.assertEqual(instance.a, 42)
+
+ def test_existing_init_subclass(self):
+ @deprecated("C will go away soon")
+ class C:
+ def __init_subclass__(cls) -> None:
+ cls.inited = True
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ C()
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ class D(C):
+ pass
+
+ self.assertTrue(D.inited)
+ self.assertIsInstance(D(), D) # no deprecation
+
+ def test_existing_init_subclass_in_base(self):
+ class Base:
+ def __init_subclass__(cls, x) -> None:
+ cls.inited = x
+
+ @deprecated("C will go away soon")
+ class C(Base, x=42):
+ pass
+
+ self.assertEqual(C.inited, 42)
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ C()
+
+ with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
+ class D(C, x=3):
+ pass
+
+ self.assertEqual(D.inited, 3)
+
+ def test_init_subclass_has_correct_cls(self):
+ init_subclass_saw = None
+
+ @deprecated("Base will go away soon")
+ class Base:
+ def __init_subclass__(cls) -> None:
+ nonlocal init_subclass_saw
+ init_subclass_saw = cls
+
+ self.assertIsNone(init_subclass_saw)
+
+ with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
+ class C(Base):
+ pass
+
+ self.assertIs(init_subclass_saw, C)
+
+ def test_init_subclass_with_explicit_classmethod(self):
+ init_subclass_saw = None
+
+ @deprecated("Base will go away soon")
+ class Base:
+ @classmethod
+ def __init_subclass__(cls) -> None:
+ nonlocal init_subclass_saw
+ init_subclass_saw = cls
+
+ self.assertIsNone(init_subclass_saw)
+
+ with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
+ class C(Base):
+ pass
+
+ self.assertIs(init_subclass_saw, C)
+
def test_function(self):
@deprecated("b will go away soon")
def b():
@@ -480,6 +650,29 @@ def d():
warnings.simplefilter("error")
d()
+ def test_only_strings_allowed(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ "Expected an object of type str for 'message', not 'type'"
+ ):
+ @deprecated
+ class Foo: ...
+
+ with self.assertRaisesRegex(
+ TypeError,
+ "Expected an object of type str for 'message', not 'function'"
+ ):
+ @deprecated
+ def foo(): ...
+
+ def test_no_retained_references_to_wrapper_instance(self):
+ @deprecated('depr')
+ def d(): pass
+
+ self.assertFalse(any(
+ isinstance(cell.cell_contents, deprecated) for cell in d.__closure__
+ ))
+
class AnyTests(BaseTestCase):
def test_can_subclass(self):
@@ -555,7 +748,7 @@ def test_cannot_subclass(self):
class C(type(ClassVar)):
pass
with self.assertRaises(TypeError):
- class C(type(ClassVar[int])):
+ class D(type(ClassVar[int])):
pass
def test_cannot_init(self):
@@ -596,7 +789,7 @@ def test_cannot_subclass(self):
class C(type(Final)):
pass
with self.assertRaises(TypeError):
- class C(type(Final[int])):
+ class D(type(Final[int])):
pass
def test_cannot_init(self):
@@ -630,18 +823,18 @@ def test_repr(self):
mod_name = 'typing'
else:
mod_name = 'typing_extensions'
- self.assertEqual(repr(Required), mod_name + '.Required')
+ self.assertEqual(repr(Required), f'{mod_name}.Required')
cv = Required[int]
- self.assertEqual(repr(cv), mod_name + '.Required[int]')
+ self.assertEqual(repr(cv), f'{mod_name}.Required[int]')
cv = Required[Employee]
- self.assertEqual(repr(cv), mod_name + '.Required[%s.Employee]' % __name__)
+ self.assertEqual(repr(cv), f'{mod_name}.Required[{__name__}.Employee]')
def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class C(type(Required)):
pass
with self.assertRaises(TypeError):
- class C(type(Required[int])):
+ class D(type(Required[int])):
pass
def test_cannot_init(self):
@@ -675,18 +868,18 @@ def test_repr(self):
mod_name = 'typing'
else:
mod_name = 'typing_extensions'
- self.assertEqual(repr(NotRequired), mod_name + '.NotRequired')
+ self.assertEqual(repr(NotRequired), f'{mod_name}.NotRequired')
cv = NotRequired[int]
- self.assertEqual(repr(cv), mod_name + '.NotRequired[int]')
+ self.assertEqual(repr(cv), f'{mod_name}.NotRequired[int]')
cv = NotRequired[Employee]
- self.assertEqual(repr(cv), mod_name + '.NotRequired[%s.Employee]' % __name__)
+ self.assertEqual(repr(cv), f'{mod_name}.NotRequired[{ __name__}.Employee]')
def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class C(type(NotRequired)):
pass
with self.assertRaises(TypeError):
- class C(type(NotRequired[int])):
+ class D(type(NotRequired[int])):
pass
def test_cannot_init(self):
@@ -706,15 +899,15 @@ def test_no_isinstance(self):
class IntVarTests(BaseTestCase):
def test_valid(self):
- T_ints = IntVar("T_ints")
+ IntVar("T_ints")
def test_invalid(self):
with self.assertRaises(TypeError):
- T_ints = IntVar("T_ints", int)
+ IntVar("T_ints", int)
with self.assertRaises(TypeError):
- T_ints = IntVar("T_ints", bound=int)
+ IntVar("T_ints", bound=int)
with self.assertRaises(TypeError):
- T_ints = IntVar("T_ints", covariant=True)
+ IntVar("T_ints", covariant=True)
class LiteralTests(BaseTestCase):
@@ -737,7 +930,7 @@ def test_illegal_parameters_do_not_raise_runtime_errors(self):
Literal[int]
Literal[Literal[1, 2], Literal[4, 5]]
Literal[3j + 2, ..., ()]
- Literal[b"foo", u"bar"]
+ Literal[b"foo", "bar"]
Literal[{"foo": 3, "bar": 4}]
Literal[T]
@@ -1061,7 +1254,6 @@ async def __aexit__(self, etype, eval, tb):
return None
-
class A:
y: float
class B(A):
@@ -1182,7 +1374,10 @@ def tearDownClass(cls):
del sys.modules[modname]
def test_get_type_hints_modules(self):
- ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str}
+ if sys.version_info >= (3, 14):
+ ann_module_type_hints = {'f': Tuple[int, int], 'x': int, 'y': str}
+ else:
+ ann_module_type_hints = {1: 2, 'f': Tuple[int, int], 'x': int, 'y': str}
self.assertEqual(gth(self.ann_module), ann_module_type_hints)
self.assertEqual(gth(self.ann_module2), {})
self.assertEqual(gth(self.ann_module3), {})
@@ -1191,7 +1386,10 @@ def test_get_type_hints_classes(self):
self.assertEqual(gth(self.ann_module.C, self.ann_module.__dict__),
{'y': Optional[self.ann_module.C]})
self.assertIsInstance(gth(self.ann_module.j_class), dict)
- self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type})
+ if sys.version_info >= (3, 14):
+ self.assertEqual(gth(self.ann_module.M), {'o': type})
+ else:
+ self.assertEqual(gth(self.ann_module.M), {'123': 123, 'o': type})
self.assertEqual(gth(self.ann_module.D),
{'j': str, 'k': str, 'y': Optional[self.ann_module.C]})
self.assertEqual(gth(self.ann_module.Y), {'z': int})
@@ -1206,7 +1404,7 @@ def test_respect_no_type_check(self):
@no_type_check
class NoTpCheck:
class Inn:
- def __init__(self, x: 'not a type'): ...
+ def __init__(self, x: 'not a type'): ... # noqa: F722 # (yes, there's a syntax error in this annotation, that's the point)
self.assertTrue(NoTpCheck.__no_type_check__)
self.assertTrue(NoTpCheck.Inn.__init__.__no_type_check__)
self.assertEqual(gth(self.ann_module2.NTC.meth), {})
@@ -1457,12 +1655,92 @@ class MyCounter(typing_extensions.Counter[int]):
self.assertIsInstance(d, collections.Counter)
self.assertIsInstance(d, typing_extensions.Counter)
- def test_async_generator(self):
- async def f():
+
+# These are a separate TestCase class,
+# as (unlike most collections.abc aliases in typing_extensions),
+# these are reimplemented on Python <=3.12 so that we can provide
+# default values for the second and third parameters
+class GeneratorTests(BaseTestCase):
+
+ def test_generator_basics(self):
+ def foo():
yield 42
+ g = foo()
+
+ self.assertIsInstance(g, typing_extensions.Generator)
+ self.assertNotIsInstance(foo, typing_extensions.Generator)
+ self.assertIsSubclass(type(g), typing_extensions.Generator)
+ self.assertNotIsSubclass(type(foo), typing_extensions.Generator)
+
+ parameterized = typing_extensions.Generator[int, str, None]
+ with self.assertRaises(TypeError):
+ isinstance(g, parameterized)
+ with self.assertRaises(TypeError):
+ issubclass(type(g), parameterized)
+
+ def test_generator_default(self):
+ g1 = typing_extensions.Generator[int]
+ g2 = typing_extensions.Generator[int, None, None]
+ self.assertEqual(get_args(g1), (int, type(None), type(None)))
+ self.assertEqual(get_args(g1), get_args(g2))
+
+ g3 = typing_extensions.Generator[int, float]
+ g4 = typing_extensions.Generator[int, float, None]
+ self.assertEqual(get_args(g3), (int, float, type(None)))
+ self.assertEqual(get_args(g3), get_args(g4))
+
+ def test_no_generator_instantiation(self):
+ with self.assertRaises(TypeError):
+ typing_extensions.Generator()
+ with self.assertRaises(TypeError):
+ typing_extensions.Generator[T, T, T]()
+ with self.assertRaises(TypeError):
+ typing_extensions.Generator[int, int, int]()
+ def test_subclassing_generator(self):
+ class G(typing_extensions.Generator[int, int, None]):
+ def send(self, value):
+ pass
+ def throw(self, typ, val=None, tb=None):
+ pass
+
+ def g(): yield 0
+
+ self.assertIsSubclass(G, typing_extensions.Generator)
+ self.assertIsSubclass(G, typing_extensions.Iterable)
+ self.assertIsSubclass(G, collections.abc.Generator)
+ self.assertIsSubclass(G, collections.abc.Iterable)
+ self.assertNotIsSubclass(type(g), G)
+
+ instance = G()
+ self.assertIsInstance(instance, typing_extensions.Generator)
+ self.assertIsInstance(instance, typing_extensions.Iterable)
+ self.assertIsInstance(instance, collections.abc.Generator)
+ self.assertIsInstance(instance, collections.abc.Iterable)
+ self.assertNotIsInstance(type(g), G)
+ self.assertNotIsInstance(g, G)
+
+ def test_async_generator_basics(self):
+ async def f():
+ yield 42
g = f()
+
+ self.assertIsInstance(g, typing_extensions.AsyncGenerator)
self.assertIsSubclass(type(g), typing_extensions.AsyncGenerator)
+ self.assertNotIsInstance(f, typing_extensions.AsyncGenerator)
+ self.assertNotIsSubclass(type(f), typing_extensions.AsyncGenerator)
+
+ parameterized = typing_extensions.AsyncGenerator[int, str]
+ with self.assertRaises(TypeError):
+ isinstance(g, parameterized)
+ with self.assertRaises(TypeError):
+ issubclass(type(g), parameterized)
+
+ def test_async_generator_default(self):
+ ag1 = typing_extensions.AsyncGenerator[int]
+ ag2 = typing_extensions.AsyncGenerator[int, None]
+ self.assertEqual(get_args(ag1), (int, type(None)))
+ self.assertEqual(get_args(ag1), get_args(ag2))
def test_no_async_generator_instantiation(self):
with self.assertRaises(TypeError):
@@ -1495,6 +1773,68 @@ async def g(): yield 0
self.assertNotIsInstance(type(g), G)
self.assertNotIsInstance(g, G)
+ def test_subclassing_subclasshook(self):
+
+ class Base(typing_extensions.Generator):
+ @classmethod
+ def __subclasshook__(cls, other):
+ if other.__name__ == 'Foo':
+ return True
+ else:
+ return False
+
+ class C(Base): ...
+ class Foo: ...
+ class Bar: ...
+ self.assertIsSubclass(Foo, Base)
+ self.assertIsSubclass(Foo, C)
+ self.assertNotIsSubclass(Bar, C)
+
+ def test_subclassing_register(self):
+
+ class A(typing_extensions.Generator): ...
+ class B(A): ...
+
+ class C: ...
+ A.register(C)
+ self.assertIsSubclass(C, A)
+ self.assertNotIsSubclass(C, B)
+
+ class D: ...
+ B.register(D)
+ self.assertIsSubclass(D, A)
+ self.assertIsSubclass(D, B)
+
+ class M: ...
+ collections.abc.Generator.register(M)
+ self.assertIsSubclass(M, typing_extensions.Generator)
+
+ def test_collections_as_base(self):
+
+ class M(collections.abc.Generator): ...
+ self.assertIsSubclass(M, typing_extensions.Generator)
+ self.assertIsSubclass(M, typing_extensions.Iterable)
+
+ class S(collections.abc.AsyncGenerator): ...
+ self.assertIsSubclass(S, typing_extensions.AsyncGenerator)
+ self.assertIsSubclass(S, typing_extensions.AsyncIterator)
+
+ class A(collections.abc.Generator, metaclass=abc.ABCMeta): ...
+ class B: ...
+ A.register(B)
+ self.assertIsSubclass(B, typing_extensions.Generator)
+
+ @skipIf(sys.version_info < (3, 10), "PEP 604 has yet to be")
+ def test_or_and_ror(self):
+ self.assertEqual(
+ typing_extensions.Generator | typing_extensions.AsyncGenerator,
+ Union[typing_extensions.Generator, typing_extensions.AsyncGenerator]
+ )
+ self.assertEqual(
+ typing_extensions.Generator | typing.Deque,
+ Union[typing_extensions.Generator, typing.Deque]
+ )
+
class OtherABCTests(BaseTestCase):
@@ -1507,6 +1847,12 @@ def manager():
self.assertIsInstance(cm, typing_extensions.ContextManager)
self.assertNotIsInstance(42, typing_extensions.ContextManager)
+ def test_contextmanager_type_params(self):
+ cm1 = typing_extensions.ContextManager[int]
+ self.assertEqual(get_args(cm1), (int, typing.Optional[bool]))
+ cm2 = typing_extensions.ContextManager[int, None]
+ self.assertEqual(get_args(cm2), (int, NoneType))
+
def test_async_contextmanager(self):
class NotACM:
pass
@@ -1518,11 +1864,20 @@ def manager():
cm = manager()
self.assertNotIsInstance(cm, typing_extensions.AsyncContextManager)
- self.assertEqual(typing_extensions.AsyncContextManager[int].__args__, (int,))
+ self.assertEqual(
+ typing_extensions.AsyncContextManager[int].__args__,
+ (int, typing.Optional[bool])
+ )
with self.assertRaises(TypeError):
isinstance(42, typing_extensions.AsyncContextManager[int])
with self.assertRaises(TypeError):
- typing_extensions.AsyncContextManager[int, str]
+ typing_extensions.AsyncContextManager[int, str, float]
+
+ def test_asynccontextmanager_type_params(self):
+ cm1 = typing_extensions.AsyncContextManager[int]
+ self.assertEqual(get_args(cm1), (int, typing.Optional[bool]))
+ cm2 = typing_extensions.AsyncContextManager[int, None]
+ self.assertEqual(get_args(cm2), (int, NoneType))
class TypeTests(BaseTestCase):
@@ -1747,10 +2102,10 @@ class BP(Protocol): pass
class P(C, Protocol):
pass
with self.assertRaises(TypeError):
- class P(Protocol, C):
+ class Q(Protocol, C):
pass
with self.assertRaises(TypeError):
- class P(BP, C, Protocol):
+ class R(BP, C, Protocol):
pass
class D(BP, C): pass
class E(C, BP): pass
@@ -2063,7 +2418,7 @@ class NotAProtocolButAnImplicitSubclass3:
meth: Callable[[], None]
meth2: Callable[[int, str], bool]
def meth(self): pass
- def meth(self, x, y): return True
+ def meth2(self, x, y): return True
self.assertNotIsSubclass(AnnotatedButNotAProtocol, CallableMembersProto)
self.assertIsSubclass(NotAProtocolButAnImplicitSubclass, CallableMembersProto)
@@ -2506,6 +2861,39 @@ class Bad: pass
self.assertNotIsInstance(Other(), Concrete)
self.assertIsInstance(NT(1, 2), Position)
+ def test_runtime_checkable_with_match_args(self):
+ @runtime_checkable
+ class P_regular(Protocol):
+ x: int
+ y: int
+
+ @runtime_checkable
+ class P_match(Protocol):
+ __match_args__ = ("x", "y")
+ x: int
+ y: int
+
+ class Regular:
+ def __init__(self, x: int, y: int):
+ self.x = x
+ self.y = y
+
+ class WithMatch:
+ __match_args__ = ("x", "y", "z")
+ def __init__(self, x: int, y: int, z: int):
+ self.x = x
+ self.y = y
+ self.z = z
+
+ class Nope: ...
+
+ self.assertIsInstance(Regular(1, 2), P_regular)
+ self.assertIsInstance(Regular(1, 2), P_match)
+ self.assertIsInstance(WithMatch(1, 2, 3), P_regular)
+ self.assertIsInstance(WithMatch(1, 2, 3), P_match)
+ self.assertNotIsInstance(Nope(), P_regular)
+ self.assertNotIsInstance(Nope(), P_match)
+
def test_protocols_isinstance_init(self):
T = TypeVar('T')
@runtime_checkable
@@ -2658,7 +3046,7 @@ class NonP(P):
class NonPR(PR): pass
class C(metaclass=abc.ABCMeta):
x = 1
- class D(metaclass=abc.ABCMeta): # noqa: B024
+ class D(metaclass=abc.ABCMeta):
def meth(self): pass # noqa: B027
self.assertNotIsInstance(C(), NonP)
self.assertNotIsInstance(D(), NonPR)
@@ -2669,12 +3057,12 @@ def meth(self): pass # noqa: B027
self.assertNotIn("__protocol_attrs__", vars(NonP))
self.assertNotIn("__protocol_attrs__", vars(NonPR))
- self.assertNotIn("__callable_proto_members_only__", vars(NonP))
- self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonP))
+ self.assertNotIn("__non_callable_proto_members__", vars(NonPR))
acceptable_extra_attrs = {
'_is_protocol', '_is_runtime_protocol', '__parameters__',
- '__init__', '__annotations__', '__subclasshook__',
+ '__init__', '__annotations__', '__subclasshook__', '__annotate__'
}
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
self.assertLessEqual(
@@ -2743,11 +3131,26 @@ def __subclasshook__(cls, other):
@skip_if_py312b1
def test_issubclass_fails_correctly(self):
@runtime_checkable
- class P(Protocol):
+ class NonCallableMembers(Protocol):
x = 1
+
+ class NotRuntimeCheckable(Protocol):
+ def callable_member(self) -> int: ...
+
+ @runtime_checkable
+ class RuntimeCheckable(Protocol):
+ def callable_member(self) -> int: ...
+
class C: pass
- with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
- issubclass(C(), P)
+
+ # These three all exercise different code paths,
+ # but should result in the same error message:
+ for protocol in NonCallableMembers, NotRuntimeCheckable, RuntimeCheckable:
+ with self.subTest(proto_name=protocol.__name__):
+ with self.assertRaisesRegex(
+ TypeError, r"issubclass\(\) arg 1 must be a class"
+ ):
+ issubclass(C(), protocol)
def test_defining_generic_protocols(self):
T = TypeVar('T')
@@ -2861,11 +3264,11 @@ def test_protocols_bad_subscripts(self):
with self.assertRaises(TypeError):
class P(Protocol[T, T]): pass
with self.assertRaises(TypeError):
- class P(Protocol[int]): pass
+ class P2(Protocol[int]): pass
with self.assertRaises(TypeError):
- class P(Protocol[T], Protocol[S]): pass
+ class P3(Protocol[T], Protocol[S]): pass
with self.assertRaises(TypeError):
- class P(typing.Mapping[T, S], Protocol[T]): pass
+ class P4(typing.Mapping[T, S], Protocol[T]): pass
def test_generic_protocols_repr(self):
T = TypeVar('T')
@@ -2929,7 +3332,7 @@ def test_none_treated_correctly(self):
@runtime_checkable
class P(Protocol):
x: int = None
- class B(object): pass
+ class B: pass
self.assertNotIsInstance(B(), P)
class C:
x = 1
@@ -3096,7 +3499,7 @@ def __call__(self, *args: Unpack[Ts]) -> T: ...
self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2))
self.assertTrue(MemoizedFunc._is_protocol)
- things = "arguments" if sys.version_info >= (3, 11) else "parameters"
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
# A bug was fixed in 3.11.1
# (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3)
@@ -3306,6 +3709,68 @@ def method(self) -> None: ...
self.assertIsInstance(Foo(), ProtocolWithMixedMembers)
self.assertNotIsInstance(42, ProtocolWithMixedMembers)
+ def test_protocol_issubclass_error_message(self):
+ @runtime_checkable
+ class Vec2D(Protocol):
+ x: float
+ y: float
+
+ def square_norm(self) -> float:
+ return self.x ** 2 + self.y ** 2
+
+ self.assertEqual(Vec2D.__protocol_attrs__, {'x', 'y', 'square_norm'})
+ expected_error_message = (
+ "Protocols with non-method members don't support issubclass()."
+ " Non-method members: 'x', 'y'."
+ )
+ with self.assertRaisesRegex(TypeError, re.escape(expected_error_message)):
+ issubclass(int, Vec2D)
+
+ def test_nonruntime_protocol_interaction_with_evil_classproperty(self):
+ class classproperty:
+ def __get__(self, instance, type):
+ raise RuntimeError("NO")
+
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ # recognised as a protocol attr,
+ # but not actually accessed by the protocol metaclass
+ # (which would raise RuntimeError) for non-runtime protocols.
+ # See gh-113320
+ self.assertEqual(get_protocol_members(Commentable), {"evil"})
+
+ def test_runtime_protocol_interaction_with_evil_classproperty(self):
+ class CustomError(Exception): pass
+
+ class classproperty:
+ def __get__(self, instance, type):
+ raise CustomError
+
+ with self.assertRaises(TypeError) as cm:
+ @runtime_checkable
+ class Commentable(Protocol):
+ evil = classproperty()
+
+ exc = cm.exception
+ self.assertEqual(
+ exc.args[0],
+ "Failed to determine whether protocol member 'evil' is a method member"
+ )
+ self.assertIs(type(exc.__cause__), CustomError)
+
+ def test_extensions_runtimecheckable_on_typing_Protocol(self):
+ @runtime_checkable
+ class Functor(typing.Protocol):
+ def foo(self) -> None: ...
+
+ self.assertNotIsSubclass(object, Functor)
+
+ class Bar:
+ def foo(self): pass
+
+ self.assertIsSubclass(Bar, Functor)
+
class Point2DGeneric(Generic[T], TypedDict):
a: T
@@ -3338,8 +3803,8 @@ def test_basics_functional_syntax(self):
@skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13")
def test_keywords_syntax_raises_on_3_13(self):
- with self.assertRaises(TypeError):
- Emp = TypedDict('Emp', name=str, id=int)
+ with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning):
+ TypedDict('Emp', name=str, id=int)
@skipIf(sys.version_info >= (3, 13), "3.13 removes support for kwargs")
def test_basics_keywords_syntax(self):
@@ -3383,17 +3848,9 @@ def test_typeddict_create_errors(self):
with self.assertRaises(TypeError):
TypedDict('Emp', [('name', str)], None)
- with self.assertWarns(DeprecationWarning):
- Emp = TypedDict('Emp', name=str, id=int)
- self.assertEqual(Emp.__name__, 'Emp')
- self.assertEqual(Emp.__annotations__, {'name': str, 'id': int})
-
def test_typeddict_errors(self):
Emp = TypedDict('Emp', {'name': str, 'id': int})
- if sys.version_info >= (3, 13):
- self.assertEqual(TypedDict.__module__, 'typing')
- else:
- self.assertEqual(TypedDict.__module__, 'typing_extensions')
+ self.assertEqual(TypedDict.__module__, 'typing_extensions')
jim = Emp(name='Jim', id=1)
with self.assertRaises(TypeError):
isinstance({}, Emp)
@@ -3613,6 +4070,24 @@ class ChildWithInlineAndOptional(Untotal, Inline):
{'inline': bool, 'untotal': str, 'child': bool},
)
+ class Closed(TypedDict, closed=True):
+ __extra_items__: None
+
+ class Unclosed(TypedDict, closed=False):
+ ...
+
+ class ChildUnclosed(Closed, Unclosed):
+ ...
+
+ self.assertFalse(ChildUnclosed.__closed__)
+ self.assertEqual(ChildUnclosed.__extra_items__, type(None))
+
+ class ChildClosed(Unclosed, Closed):
+ ...
+
+ self.assertFalse(ChildClosed.__closed__)
+ self.assertEqual(ChildClosed.__extra_items__, type(None))
+
wrong_bases = [
(One, Regular),
(Regular, One),
@@ -3770,7 +4245,6 @@ class C(B[int]):
with self.assertRaises(TypeError):
C[str]
-
class Point3D(Point2DGeneric[T], Generic[T, KT]):
c: KT
@@ -3917,6 +4391,207 @@ class T4(TypedDict, Generic[S]): pass
self.assertEqual(klass.__optional_keys__, set())
self.assertIsInstance(klass(), dict)
+ def test_readonly_inheritance(self):
+ class Base1(TypedDict):
+ a: ReadOnly[int]
+
+ class Child1(Base1):
+ b: str
+
+ self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
+
+ class Base2(TypedDict):
+ a: ReadOnly[int]
+
+ class Child2(Base2):
+ b: str
+
+ self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
+
+ def test_make_mutable_key_readonly(self):
+ class Base(TypedDict):
+ a: int
+
+ self.assertEqual(Base.__readonly_keys__, frozenset())
+ self.assertEqual(Base.__mutable_keys__, frozenset({'a'}))
+
+ class Child(Base):
+ a: ReadOnly[int] # type checker error, but allowed at runtime
+
+ self.assertEqual(Child.__readonly_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__mutable_keys__, frozenset())
+
+ def test_can_make_readonly_key_mutable(self):
+ class Base(TypedDict):
+ a: ReadOnly[int]
+
+ class Child(Base):
+ a: int
+
+ self.assertEqual(Child.__readonly_keys__, frozenset())
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_combine_qualifiers(self):
+ class AllTheThings(TypedDict):
+ a: Annotated[Required[ReadOnly[int]], "why not"]
+ b: Required[Annotated[ReadOnly[int], "why not"]]
+ c: ReadOnly[NotRequired[Annotated[int, "why not"]]]
+ d: NotRequired[Annotated[int, "why not"]]
+
+ self.assertEqual(AllTheThings.__required_keys__, frozenset({'a', 'b'}))
+ self.assertEqual(AllTheThings.__optional_keys__, frozenset({'c', 'd'}))
+ self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'}))
+ self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'}))
+
+ self.assertEqual(
+ get_type_hints(AllTheThings, include_extras=False),
+ {'a': int, 'b': int, 'c': int, 'd': int},
+ )
+ self.assertEqual(
+ get_type_hints(AllTheThings, include_extras=True),
+ {
+ 'a': Annotated[Required[ReadOnly[int]], 'why not'],
+ 'b': Required[Annotated[ReadOnly[int], 'why not']],
+ 'c': ReadOnly[NotRequired[Annotated[int, 'why not']]],
+ 'd': NotRequired[Annotated[int, 'why not']],
+ },
+ )
+
+ def test_extra_keys_non_readonly(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: str
+
+ class Child(Base):
+ a: NotRequired[int]
+
+ self.assertEqual(Child.__required_keys__, frozenset({}))
+ self.assertEqual(Child.__optional_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_extra_keys_readonly(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: ReadOnly[str]
+
+ class Child(Base):
+ a: NotRequired[str]
+
+ self.assertEqual(Child.__required_keys__, frozenset({}))
+ self.assertEqual(Child.__optional_keys__, frozenset({'a'}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a'}))
+
+ def test_extra_key_required(self):
+ with self.assertRaisesRegex(
+ TypeError,
+ "Special key __extra_items__ does not support Required"
+ ):
+ TypedDict("A", {"__extra_items__": Required[int]}, closed=True)
+
+ with self.assertRaisesRegex(
+ TypeError,
+ "Special key __extra_items__ does not support NotRequired"
+ ):
+ TypedDict("A", {"__extra_items__": NotRequired[int]}, closed=True)
+
+ def test_regular_extra_items(self):
+ class ExtraReadOnly(TypedDict):
+ __extra_items__: ReadOnly[str]
+
+ self.assertEqual(ExtraReadOnly.__required_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraReadOnly.__optional_keys__, frozenset({}))
+ self.assertEqual(ExtraReadOnly.__readonly_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraReadOnly.__mutable_keys__, frozenset({}))
+ self.assertEqual(ExtraReadOnly.__extra_items__, None)
+ self.assertFalse(ExtraReadOnly.__closed__)
+
+ class ExtraRequired(TypedDict):
+ __extra_items__: Required[str]
+
+ self.assertEqual(ExtraRequired.__required_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraRequired.__optional_keys__, frozenset({}))
+ self.assertEqual(ExtraRequired.__readonly_keys__, frozenset({}))
+ self.assertEqual(ExtraRequired.__mutable_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraRequired.__extra_items__, None)
+ self.assertFalse(ExtraRequired.__closed__)
+
+ class ExtraNotRequired(TypedDict):
+ __extra_items__: NotRequired[str]
+
+ self.assertEqual(ExtraNotRequired.__required_keys__, frozenset({}))
+ self.assertEqual(ExtraNotRequired.__optional_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraNotRequired.__readonly_keys__, frozenset({}))
+ self.assertEqual(ExtraNotRequired.__mutable_keys__, frozenset({'__extra_items__'}))
+ self.assertEqual(ExtraNotRequired.__extra_items__, None)
+ self.assertFalse(ExtraNotRequired.__closed__)
+
+ def test_closed_inheritance(self):
+ class Base(TypedDict, closed=True):
+ __extra_items__: ReadOnly[Union[str, None]]
+
+ self.assertEqual(Base.__required_keys__, frozenset({}))
+ self.assertEqual(Base.__optional_keys__, frozenset({}))
+ self.assertEqual(Base.__readonly_keys__, frozenset({}))
+ self.assertEqual(Base.__mutable_keys__, frozenset({}))
+ self.assertEqual(Base.__annotations__, {})
+ self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]])
+ self.assertTrue(Base.__closed__)
+
+ class Child(Base):
+ a: int
+ __extra_items__: int
+
+ self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(Child.__optional_keys__, frozenset({}))
+ self.assertEqual(Child.__readonly_keys__, frozenset({}))
+ self.assertEqual(Child.__mutable_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(Child.__annotations__, {"__extra_items__": int, "a": int})
+ self.assertEqual(Child.__extra_items__, ReadOnly[Union[str, None]])
+ self.assertFalse(Child.__closed__)
+
+ class GrandChild(Child, closed=True):
+ __extra_items__: str
+
+ self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(GrandChild.__optional_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__readonly_keys__, frozenset({}))
+ self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a', "__extra_items__"}))
+ self.assertEqual(GrandChild.__annotations__, {"__extra_items__": int, "a": int})
+ self.assertEqual(GrandChild.__extra_items__, str)
+ self.assertTrue(GrandChild.__closed__)
+
+ def test_implicit_extra_items(self):
+ class Base(TypedDict):
+ a: int
+
+ self.assertEqual(Base.__extra_items__, None)
+ self.assertFalse(Base.__closed__)
+
+ class ChildA(Base, closed=True):
+ ...
+
+ self.assertEqual(ChildA.__extra_items__, Never)
+ self.assertTrue(ChildA.__closed__)
+
+ class ChildB(Base, closed=True):
+ __extra_items__: None
+
+ self.assertEqual(ChildB.__extra_items__, type(None))
+ self.assertTrue(ChildB.__closed__)
+
+ @skipIf(
+ TYPING_3_13_0,
+ "The keyword argument alternative to define a "
+ "TypedDict type using the functional syntax is no longer supported"
+ )
+ def test_backwards_compatibility(self):
+ with self.assertWarns(DeprecationWarning):
+ TD = TypedDict("TD", closed=int)
+ self.assertFalse(TD.__closed__)
+ self.assertEqual(TD.__annotations__, {"closed": int})
+
class AnnotatedTests(BaseTestCase):
@@ -4094,6 +4769,14 @@ def test_annotated_in_other_types(self):
X = List[Annotated[T, 5]]
self.assertEqual(X[int], List[Annotated[int, 5]])
+ def test_nested_annotated_with_unhashable_metadata(self):
+ X = Annotated[
+ List[Annotated[str, {"unhashable_metadata"}]],
+ "metadata"
+ ]
+ self.assertEqual(X.__origin__, List[Annotated[str, {"unhashable_metadata"}]])
+ self.assertEqual(X.__metadata__, ("metadata",))
+
class GetTypeHintsTests(BaseTestCase):
def test_get_type_hints(self):
@@ -4217,7 +4900,7 @@ def test_canonical_usage_with_variable_annotation(self):
exec('Alias: TypeAlias = Employee', globals(), ns)
def test_canonical_usage_with_type_comment(self):
- Alias: TypeAlias = Employee
+ Alias: TypeAlias = Employee # noqa: F841
def test_cannot_instantiate(self):
with self.assertRaises(TypeError):
@@ -4240,7 +4923,7 @@ class C(TypeAlias):
pass
with self.assertRaises(TypeError):
- class C(type(TypeAlias)):
+ class D(type(TypeAlias)):
pass
def test_repr(self):
@@ -4387,7 +5070,7 @@ def test_pickle(self):
P = ParamSpec('P')
P_co = ParamSpec('P_co', covariant=True)
P_contra = ParamSpec('P_contra', contravariant=True)
- P_default = ParamSpec('P_default', default=int)
+ P_default = ParamSpec('P_default', default=[int])
for proto in range(pickle.HIGHEST_PROTOCOL):
with self.subTest(f'Pickle protocol {proto}'):
for paramspec in (P, P_co, P_contra, P_default):
@@ -4411,6 +5094,48 @@ def test_eq(self):
# won't be the same.
self.assertNotEqual(hash(ParamSpec('P')), hash(P))
+ def test_isinstance_results_unaffected_by_presence_of_tracing_function(self):
+ # See https://github.com/python/typing_extensions/issues/318
+
+ code = textwrap.dedent(
+ """\
+ import sys, typing
+
+ def trace_call(*args):
+ return trace_call
+
+ def run():
+ sys.modules.pop("typing_extensions", None)
+ from typing_extensions import ParamSpec
+ return isinstance(ParamSpec("P"), typing.TypeVar)
+
+ isinstance_result_1 = run()
+ sys.setprofile(trace_call)
+ isinstance_result_2 = run()
+ sys.stdout.write(f"{isinstance_result_1} {isinstance_result_2}")
+ """
+ )
+
+ # Run this in an isolated process or it pollutes the environment
+ # and makes other tests fail:
+ try:
+ proc = subprocess.run(
+ [sys.executable, "-c", code], check=True, capture_output=True, text=True,
+ )
+ except subprocess.CalledProcessError as exc:
+ print("stdout", exc.stdout, sep="\n")
+ print("stderr", exc.stderr, sep="\n")
+ raise
+
+ # Sanity checks that assert the test is working as expected
+ self.assertIsInstance(proc.stdout, str)
+ result1, result2 = proc.stdout.split(" ")
+ self.assertIn(result1, {"True", "False"})
+ self.assertIn(result2, {"True", "False"})
+
+ # The actual test:
+ self.assertEqual(result1, result2)
+
class ConcatenateTests(BaseTestCase):
def test_basics(self):
@@ -4427,11 +5152,15 @@ def test_valid_uses(self):
C1 = Callable[Concatenate[int, P], int]
C2 = Callable[Concatenate[int, T, P], T]
+ self.assertEqual(C1.__origin__, C2.__origin__)
+ self.assertNotEqual(C1, C2)
# Test collections.abc.Callable too.
if sys.version_info[:2] >= (3, 9):
C3 = collections.abc.Callable[Concatenate[int, P], int]
C4 = collections.abc.Callable[Concatenate[int, T, P], T]
+ self.assertEqual(C3.__origin__, C4.__origin__)
+ self.assertNotEqual(C3, C4)
def test_invalid_uses(self):
P = ParamSpec('P')
@@ -4501,7 +5230,7 @@ def test_cannot_subclass(self):
class C(type(TypeGuard)):
pass
with self.assertRaises(TypeError):
- class C(type(TypeGuard[int])):
+ class D(type(TypeGuard[int])):
pass
def test_cannot_init(self):
@@ -4519,6 +5248,50 @@ def test_no_isinstance(self):
issubclass(int, TypeGuard)
+class TypeIsTests(BaseTestCase):
+ def test_basics(self):
+ TypeIs[int] # OK
+ self.assertEqual(TypeIs[int], TypeIs[int])
+
+ def foo(arg) -> TypeIs[int]: ...
+ self.assertEqual(gth(foo), {'return': TypeIs[int]})
+
+ def test_repr(self):
+ if hasattr(typing, 'TypeIs'):
+ mod_name = 'typing'
+ else:
+ mod_name = 'typing_extensions'
+ self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs')
+ cv = TypeIs[int]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]')
+ cv = TypeIs[Employee]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]')
+ cv = TypeIs[Tuple[int]]
+ self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]')
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(TypeIs)):
+ pass
+ with self.assertRaises(TypeError):
+ class D(type(TypeIs[int])):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ TypeIs()
+ with self.assertRaises(TypeError):
+ type(TypeIs)()
+ with self.assertRaises(TypeError):
+ type(TypeIs[Optional[int]])()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, TypeIs[int])
+ with self.assertRaises(TypeError):
+ issubclass(int, TypeIs)
+
+
class LiteralStringTests(BaseTestCase):
def test_basics(self):
class Foo:
@@ -4536,7 +5309,7 @@ def test_repr(self):
mod_name = 'typing'
else:
mod_name = 'typing_extensions'
- self.assertEqual(repr(LiteralString), '{}.LiteralString'.format(mod_name))
+ self.assertEqual(repr(LiteralString), f'{mod_name}.LiteralString')
def test_cannot_subscript(self):
with self.assertRaises(TypeError):
@@ -4547,7 +5320,7 @@ def test_cannot_subclass(self):
class C(type(LiteralString)):
pass
with self.assertRaises(TypeError):
- class C(LiteralString):
+ class D(LiteralString):
pass
def test_cannot_init(self):
@@ -4590,7 +5363,7 @@ def test_repr(self):
mod_name = 'typing'
else:
mod_name = 'typing_extensions'
- self.assertEqual(repr(Self), '{}.Self'.format(mod_name))
+ self.assertEqual(repr(Self), f'{mod_name}.Self')
def test_cannot_subscript(self):
with self.assertRaises(TypeError):
@@ -4849,7 +5622,7 @@ def stmethod(): ...
def prop(self): ...
@final
- @lru_cache() # noqa: B019
+ @lru_cache # noqa: B019
def cached(self): ...
# Use getattr_static because the descriptor returns the
@@ -5036,28 +5809,27 @@ def test_all_names_in___all__(self):
self.assertLessEqual(exclude, actual_names)
def test_typing_extensions_defers_when_possible(self):
- exclude = {
- 'dataclass_transform',
- 'overload',
- 'ParamSpec',
- 'TypeVar',
- 'TypeVarTuple',
- 'get_type_hints',
- }
+ exclude = set()
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', 'Any', 'NewType'}
+ exclude |= {'final', 'Any', 'NewType', 'overload'}
if sys.version_info < (3, 12):
exclude |= {
- 'Protocol', 'SupportsAbs', 'SupportsBytes',
+ 'SupportsAbs', 'SupportsBytes',
'SupportsComplex', 'SupportsFloat', 'SupportsIndex', 'SupportsInt',
- 'SupportsRound', 'Unpack',
+ 'SupportsRound', 'Unpack', 'dataclass_transform',
}
if sys.version_info < (3, 13):
- exclude |= {'NamedTuple', 'TypedDict', 'is_typeddict'}
+ exclude |= {
+ 'NamedTuple', 'Protocol', 'runtime_checkable', 'Generator',
+ 'AsyncGenerator', 'ContextManager', 'AsyncContextManager',
+ 'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints',
+ }
+ if not typing_extensions._PEP_728_IMPLEMENTED:
+ exclude |= {'TypedDict', 'is_typeddict'}
for item in typing_extensions.__all__:
if item not in exclude and hasattr(typing, item):
self.assertIs(
@@ -5065,8 +5837,7 @@ def test_typing_extensions_defers_when_possible(self):
getattr(typing, item))
def test_typing_extensions_compiles_with_opt(self):
- file_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
- 'typing_extensions.py')
+ file_path = typing_extensions.__file__
try:
subprocess.check_output(f'{sys.executable} -OO {file_path}',
stderr=subprocess.STDOUT,
@@ -5092,17 +5863,6 @@ def double(self):
return 2 * self.x
-class XRepr(NamedTuple):
- x: int
- y: int = 1
-
- def __str__(self):
- return f'{self.x} -> {self.y}'
-
- def __add__(self, other):
- return 0
-
-
class NamedTupleTests(BaseTestCase):
class NestedEmployee(NamedTuple):
name: str
@@ -5194,11 +5954,11 @@ class X(NamedTuple, A):
TypeError,
'can only inherit from a NamedTuple type and Generic'
):
- class X(NamedTuple, tuple):
+ class Y(NamedTuple, tuple):
x: int
with self.assertRaisesRegex(TypeError, 'duplicate base class'):
- class X(NamedTuple, NamedTuple):
+ class Z(NamedTuple, NamedTuple):
x: int
class A(NamedTuple):
@@ -5207,7 +5967,7 @@ class A(NamedTuple):
TypeError,
'can only inherit from a NamedTuple type and Generic'
):
- class X(NamedTuple, A):
+ class XX(NamedTuple, A):
y: str
def test_generic(self):
@@ -5236,8 +5996,7 @@ class Y(Generic[T], NamedTuple):
self.assertIsInstance(a, G)
self.assertEqual(a.x, 3)
- things = "arguments" if sys.version_info >= (3, 11) else "parameters"
-
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
with self.assertRaisesRegex(TypeError, f'Too many {things}'):
G[int, str]
@@ -5418,6 +6177,128 @@ class GenericNamedTuple(NamedTuple, Generic[T]):
self.assertEqual(CallNamedTuple.__orig_bases__, (NamedTuple,))
+ def test_setname_called_on_values_in_class_dictionary(self):
+ class Vanilla:
+ def __set_name__(self, owner, name):
+ self.name = name
+
+ class Foo(NamedTuple):
+ attr = Vanilla()
+
+ foo = Foo()
+ self.assertEqual(len(foo), 0)
+ self.assertNotIn('attr', Foo._fields)
+ self.assertIsInstance(foo.attr, Vanilla)
+ self.assertEqual(foo.attr.name, "attr")
+
+ class Bar(NamedTuple):
+ attr: Vanilla = Vanilla()
+
+ bar = Bar()
+ self.assertEqual(len(bar), 1)
+ self.assertIn('attr', Bar._fields)
+ self.assertIsInstance(bar.attr, Vanilla)
+ self.assertEqual(bar.attr.name, "attr")
+
+ @skipIf(
+ TYPING_3_12_0,
+ "__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
+ )
+ def test_setname_raises_the_same_as_on_other_classes_py311_minus(self):
+ class CustomException(BaseException): pass
+
+ class Annoying:
+ def __set_name__(self, owner, name):
+ raise CustomException
+
+ annoying = Annoying()
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NormalClass:
+ attr = annoying
+ normal_exception = cm.exception
+
+ with self.assertRaises(RuntimeError) as cm:
+ class NamedTupleClass(NamedTuple):
+ attr = annoying
+ namedtuple_exception = cm.exception
+
+ self.assertIs(type(namedtuple_exception), RuntimeError)
+ self.assertIs(type(namedtuple_exception), type(normal_exception))
+ self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args))
+ self.assertEqual(
+ namedtuple_exception.args[0],
+ normal_exception.args[0].replace("NormalClass", "NamedTupleClass")
+ )
+
+ self.assertIs(type(namedtuple_exception.__cause__), CustomException)
+ self.assertIs(
+ type(namedtuple_exception.__cause__), type(normal_exception.__cause__)
+ )
+ self.assertEqual(
+ namedtuple_exception.__cause__.args, normal_exception.__cause__.args
+ )
+
+ @skipUnless(
+ TYPING_3_12_0,
+ "__set_name__ behaviour changed on py312+ to use BaseException.add_note()"
+ )
+ def test_setname_raises_the_same_as_on_other_classes_py312_plus(self):
+ class CustomException(BaseException): pass
+
+ class Annoying:
+ def __set_name__(self, owner, name):
+ raise CustomException
+
+ annoying = Annoying()
+
+ with self.assertRaises(CustomException) as cm:
+ class NormalClass:
+ attr = annoying
+ normal_exception = cm.exception
+
+ with self.assertRaises(CustomException) as cm:
+ class NamedTupleClass(NamedTuple):
+ attr = annoying
+ namedtuple_exception = cm.exception
+
+ expected_note = (
+ "Error calling __set_name__ on 'Annoying' instance "
+ "'attr' in 'NamedTupleClass'"
+ )
+
+ self.assertIs(type(namedtuple_exception), CustomException)
+ self.assertIs(type(namedtuple_exception), type(normal_exception))
+ self.assertEqual(namedtuple_exception.args, normal_exception.args)
+
+ self.assertEqual(len(namedtuple_exception.__notes__), 1)
+ self.assertEqual(
+ len(namedtuple_exception.__notes__), len(normal_exception.__notes__)
+ )
+
+ self.assertEqual(namedtuple_exception.__notes__[0], expected_note)
+ self.assertEqual(
+ namedtuple_exception.__notes__[0],
+ normal_exception.__notes__[0].replace("NormalClass", "NamedTupleClass")
+ )
+
+ def test_strange_errors_when_accessing_set_name_itself(self):
+ class CustomException(Exception): pass
+
+ class Meta(type):
+ def __getattribute__(self, attr):
+ if attr == "__set_name__":
+ raise CustomException
+ return object.__getattribute__(self, attr)
+
+ class VeryAnnoying(metaclass=Meta): pass
+
+ very_annoying = VeryAnnoying()
+
+ with self.assertRaises(CustomException):
+ class Foo(NamedTuple):
+ attr = very_annoying
+
class TypeVarTests(BaseTestCase):
def test_basic_plain(self):
@@ -5526,7 +6407,7 @@ def test_cannot_subclass(self):
class V(TypeVar): pass
T = TypeVar("T")
with self.assertRaises(TypeError):
- class V(T): pass
+ class W(T): pass
def test_cannot_instantiate_vars(self):
with self.assertRaises(TypeError):
@@ -5573,17 +6454,20 @@ def test_typevar(self):
self.assertIsInstance(typing_T, typing_extensions.TypeVar)
class A(Generic[T]): ...
- Alias = Optional[T]
+ self.assertEqual(Optional[T].__args__, (T, type(None)))
def test_typevar_none(self):
U = typing_extensions.TypeVar('U')
U_None = typing_extensions.TypeVar('U_None', default=None)
- self.assertEqual(U.__default__, None)
- self.assertEqual(U_None.__default__, type(None))
+ self.assertIs(U.__default__, NoDefault)
+ self.assertFalse(U.has_default())
+ self.assertEqual(U_None.__default__, None)
+ self.assertTrue(U_None.has_default())
def test_paramspec(self):
- P = ParamSpec('P', default=(str, int))
- self.assertEqual(P.__default__, (str, int))
+ P = ParamSpec('P', default=[str, int])
+ self.assertEqual(P.__default__, [str, int])
+ self.assertTrue(P.has_default())
self.assertIsInstance(P, ParamSpec)
if hasattr(typing, "ParamSpec"):
self.assertIsInstance(P, typing.ParamSpec)
@@ -5592,15 +6476,25 @@ def test_paramspec(self):
self.assertIsInstance(typing_P, ParamSpec)
class A(Generic[P]): ...
- Alias = typing.Callable[P, None]
+ self.assertEqual(typing.Callable[P, None].__args__, (P, type(None)))
P_default = ParamSpec('P_default', default=...)
self.assertIs(P_default.__default__, ...)
+ self.assertTrue(P_default.has_default())
+
+ def test_paramspec_none(self):
+ U = ParamSpec('U')
+ U_None = ParamSpec('U_None', default=None)
+ self.assertIs(U.__default__, NoDefault)
+ self.assertFalse(U.has_default())
+ self.assertIs(U_None.__default__, None)
+ self.assertTrue(U_None.has_default())
def test_typevartuple(self):
Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]])
self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])
self.assertIsInstance(Ts, TypeVarTuple)
+ self.assertTrue(Ts.has_default())
if hasattr(typing, "TypeVarTuple"):
self.assertIsInstance(Ts, typing.TypeVarTuple)
typing_Ts = typing.TypeVarTuple('Ts')
@@ -5608,7 +6502,75 @@ def test_typevartuple(self):
self.assertIsInstance(typing_Ts, TypeVarTuple)
class A(Generic[Unpack[Ts]]): ...
- Alias = Optional[Unpack[Ts]]
+ self.assertEqual(Optional[Unpack[Ts]].__args__, (Unpack[Ts], type(None)))
+
+ @skipIf(
+ sys.version_info < (3, 11, 1),
+ "Not yet backported for older versions of Python"
+ )
+ def test_typevartuple_specialization(self):
+ T = TypeVar("T")
+ Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]])
+ self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])
+ class A(Generic[T, Unpack[Ts]]): ...
+ self.assertEqual(A[float].__args__, (float, str, int))
+ self.assertEqual(A[float, range].__args__, (float, range))
+ self.assertEqual(A[float, Unpack[tuple[int, ...]]].__args__, (float, Unpack[tuple[int, ...]]))
+
+ @skipIf(
+ sys.version_info < (3, 11, 1),
+ "Not yet backported for older versions of Python"
+ )
+ def test_typevar_and_typevartuple_specialization(self):
+ T = TypeVar("T")
+ U = TypeVar("U", default=float)
+ Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]])
+ self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])
+ class A(Generic[T, U, Unpack[Ts]]): ...
+ self.assertEqual(A[int].__args__, (int, float, str, int))
+ self.assertEqual(A[int, str].__args__, (int, str, str, int))
+ self.assertEqual(A[int, str, range].__args__, (int, str, range))
+ self.assertEqual(A[int, str, Unpack[tuple[int, ...]]].__args__, (int, str, Unpack[tuple[int, ...]]))
+
+ def test_no_default_after_typevar_tuple(self):
+ T = TypeVar("T", default=int)
+ Ts = TypeVarTuple("Ts")
+ Ts_default = TypeVarTuple("Ts_default", default=Unpack[Tuple[str, int]])
+
+ with self.assertRaises(TypeError):
+ class X(Generic[Unpack[Ts], T]): ...
+
+ with self.assertRaises(TypeError):
+ class Y(Generic[Unpack[Ts_default], T]): ...
+
+ def test_typevartuple_none(self):
+ U = TypeVarTuple('U')
+ U_None = TypeVarTuple('U_None', default=None)
+ self.assertIs(U.__default__, NoDefault)
+ self.assertFalse(U.has_default())
+ self.assertIs(U_None.__default__, None)
+ self.assertTrue(U_None.has_default())
+
+ def test_no_default_after_non_default(self):
+ DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
+ T = TypeVar('T')
+
+ with self.assertRaises(TypeError):
+ Generic[DefaultStrT, T]
+
+ def test_need_more_params(self):
+ DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str)
+ T = typing_extensions.TypeVar('T')
+ U = typing_extensions.TypeVar('U')
+
+ class A(Generic[T, U, DefaultStrT]): ...
+ A[int, bool]
+ A[int, bool, str]
+
+ with self.assertRaises(
+ TypeError, msg="Too few arguments for .+; actual 1, expected at least 2"
+ ):
+ A[int]
def test_pickle(self):
global U, U_co, U_contra, U_default # pickle wants to reference the class by name
@@ -5625,6 +6587,133 @@ def test_pickle(self):
self.assertEqual(z.__bound__, typevar.__bound__)
self.assertEqual(z.__default__, typevar.__default__)
+ def test_strange_defaults_are_allowed(self):
+ # Leave it to type checkers to check whether strange default values
+ # should be allowed or disallowed
+ def not_a_type(): ...
+
+ for typevarlike_cls in TypeVar, ParamSpec, TypeVarTuple:
+ for default in not_a_type, 42, bytearray(), (int, not_a_type, 42):
+ with self.subTest(typevarlike_cls=typevarlike_cls, default=default):
+ T = typevarlike_cls("T", default=default)
+ self.assertEqual(T.__default__, default)
+
+ @skip_if_py313_beta_1
+ def test_allow_default_after_non_default_in_alias(self):
+ T_default = TypeVar('T_default', default=int)
+ T = TypeVar('T')
+ Ts = TypeVarTuple('Ts')
+
+ a1 = Callable[[T_default], T]
+ self.assertEqual(a1.__args__, (T_default, T))
+
+ if sys.version_info >= (3, 9):
+ a2 = dict[T_default, T]
+ self.assertEqual(a2.__args__, (T_default, T))
+
+ a3 = typing.Dict[T_default, T]
+ self.assertEqual(a3.__args__, (T_default, T))
+
+ a4 = Callable[[Unpack[Ts]], T]
+ self.assertEqual(a4.__args__, (Unpack[Ts], T))
+
+ @skipIf(
+ typing_extensions.Protocol is typing.Protocol,
+ "Test currently fails with the CPython version of Protocol and that's not our fault"
+ )
+ def test_generic_with_broken_eq(self):
+ # See https://github.com/python/typing_extensions/pull/422 for context
+ class BrokenEq(type):
+ def __eq__(self, other):
+ if other is typing_extensions.Protocol:
+ raise TypeError("I'm broken")
+ return False
+
+ class G(Generic[T], metaclass=BrokenEq):
+ pass
+
+ alias = G[int]
+ self.assertIs(get_origin(alias), G)
+ self.assertEqual(get_args(alias), (int,))
+
+ @skipIf(
+ sys.version_info < (3, 11, 1),
+ "Not yet backported for older versions of Python"
+ )
+ def test_paramspec_specialization(self):
+ T = TypeVar("T")
+ P = ParamSpec('P', default=[str, int])
+ self.assertEqual(P.__default__, [str, int])
+ class A(Generic[T, P]): ...
+ self.assertEqual(A[float].__args__, (float, (str, int)))
+ self.assertEqual(A[float, [range]].__args__, (float, (range,)))
+
+ @skipIf(
+ sys.version_info < (3, 11, 1),
+ "Not yet backported for older versions of Python"
+ )
+ def test_typevar_and_paramspec_specialization(self):
+ T = TypeVar("T")
+ U = TypeVar("U", default=float)
+ P = ParamSpec('P', default=[str, int])
+ self.assertEqual(P.__default__, [str, int])
+ class A(Generic[T, U, P]): ...
+ self.assertEqual(A[float].__args__, (float, float, (str, int)))
+ self.assertEqual(A[float, int].__args__, (float, int, (str, int)))
+ self.assertEqual(A[float, int, [range]].__args__, (float, int, (range,)))
+
+ @skipIf(
+ sys.version_info < (3, 11, 1),
+ "Not yet backported for older versions of Python"
+ )
+ def test_paramspec_and_typevar_specialization(self):
+ T = TypeVar("T")
+ P = ParamSpec('P', default=[str, int])
+ U = TypeVar("U", default=float)
+ self.assertEqual(P.__default__, [str, int])
+ class A(Generic[T, P, U]): ...
+ self.assertEqual(A[float].__args__, (float, (str, int), float))
+ self.assertEqual(A[float, [range]].__args__, (float, (range,), float))
+ self.assertEqual(A[float, [range], int].__args__, (float, (range,), int))
+
+
+class NoDefaultTests(BaseTestCase):
+ @skip_if_py313_beta_1
+ def test_pickling(self):
+ for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+ s = pickle.dumps(NoDefault, proto)
+ loaded = pickle.loads(s)
+ self.assertIs(NoDefault, loaded)
+
+ @skip_if_py313_beta_1
+ def test_doc(self):
+ self.assertIsInstance(NoDefault.__doc__, str)
+
+ def test_constructor(self):
+ self.assertIs(NoDefault, type(NoDefault)())
+ with self.assertRaises(TypeError):
+ type(NoDefault)(1)
+
+ def test_repr(self):
+ self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault')
+
+ def test_no_call(self):
+ with self.assertRaises(TypeError):
+ NoDefault()
+
+ @skip_if_py313_beta_1
+ def test_immutable(self):
+ with self.assertRaises(AttributeError):
+ NoDefault.foo = 'bar'
+ with self.assertRaises(AttributeError):
+ NoDefault.foo
+
+ # TypeError is consistent with the behavior of NoneType
+ with self.assertRaises(TypeError):
+ type(NoDefault).foo = 3
+ with self.assertRaises(AttributeError):
+ type(NoDefault).foo
+
class TypeVarInferVarianceTests(BaseTestCase):
def test_typevar(self):
@@ -5934,5 +7023,15 @@ def test_pickle(self):
self.assertEqual(doc_info, pickle.loads(pickled))
+@skipUnless(
+ hasattr(typing_extensions, "CapsuleType"),
+ "CapsuleType is not available on all Python implementations"
+)
+class CapsuleTypeTests(BaseTestCase):
+ def test_capsule_type(self):
+ import _datetime
+ self.assertIsInstance(_datetime.datetime_CAPI, typing_extensions.CapsuleType)
+
+
if __name__ == '__main__':
main()
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index c96bf90f..dec429ca 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -1,6 +1,7 @@
import abc
import collections
import collections.abc
+import contextlib
import functools
import inspect
import operator
@@ -83,9 +84,11 @@
'TypeAlias',
'TypeAliasType',
'TypeGuard',
+ 'TypeIs',
'TYPE_CHECKING',
'Never',
'NoReturn',
+ 'ReadOnly',
'Required',
'NotRequired',
@@ -114,6 +117,7 @@
'MutableMapping',
'MutableSequence',
'MutableSet',
+ 'NoDefault',
'Optional',
'Pattern',
'Reversible',
@@ -132,6 +136,7 @@
# for backward compatibility
PEP_560 = True
GenericMeta = type
+_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta")
# The functions below are modified copies of typing internal helpers.
# They are needed by _ProtocolMeta and they provide support for PEP 646.
@@ -145,27 +150,6 @@ def __repr__(self):
_marker = _Sentinel()
-def _check_generic(cls, parameters, elen=_marker):
- """Check correct count for parameters of a generic cls (internal helper).
- This gives a nice error message in case of count mismatch.
- """
- if not elen:
- raise TypeError(f"{cls} is not a generic class")
- if elen is _marker:
- if not hasattr(cls, "__parameters__") or not cls.__parameters__:
- raise TypeError(f"{cls} is not a generic class")
- elen = len(cls.__parameters__)
- alen = len(parameters)
- if alen != elen:
- if hasattr(cls, "__parameters__"):
- parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
- num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
- if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
- return
- raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
- f" actual {alen}, expected {elen}")
-
-
if sys.version_info >= (3, 10):
def _should_collect_from_parameters(t):
return isinstance(
@@ -179,27 +163,6 @@ def _should_collect_from_parameters(t):
return isinstance(t, typing._GenericAlias) and not t._special
-def _collect_type_vars(types, typevar_types=None):
- """Collect all type variable contained in types in order of
- first appearance (lexicographic order). For example::
-
- _collect_type_vars((T, List[S, T])) == (T, S)
- """
- if typevar_types is None:
- typevar_types = typing.TypeVar
- tvars = []
- for t in types:
- if (
- isinstance(t, typevar_types) and
- t not in tvars and
- not _is_unpack(t)
- ):
- tvars.append(t)
- if _should_collect_from_parameters(t):
- tvars.extend([t for t in t.__parameters__ if t not in tvars])
- return tuple(tvars)
-
-
NoReturn = typing.NoReturn
# Some unconstrained type variables. These are used by the container types.
@@ -446,17 +409,96 @@ def clear_overloads():
AsyncIterable = typing.AsyncIterable
AsyncIterator = typing.AsyncIterator
Deque = typing.Deque
-ContextManager = typing.ContextManager
-AsyncContextManager = typing.AsyncContextManager
DefaultDict = typing.DefaultDict
OrderedDict = typing.OrderedDict
Counter = typing.Counter
ChainMap = typing.ChainMap
-AsyncGenerator = typing.AsyncGenerator
Text = typing.Text
TYPE_CHECKING = typing.TYPE_CHECKING
+if sys.version_info >= (3, 13, 0, "beta"):
+ from typing import AsyncContextManager, AsyncGenerator, ContextManager, Generator
+else:
+ def _is_dunder(attr):
+ return attr.startswith('__') and attr.endswith('__')
+
+ # Python <3.9 doesn't have typing._SpecialGenericAlias
+ _special_generic_alias_base = getattr(
+ typing, "_SpecialGenericAlias", typing._GenericAlias
+ )
+
+ class _SpecialGenericAlias(_special_generic_alias_base, _root=True):
+ def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()):
+ if _special_generic_alias_base is typing._GenericAlias:
+ # Python <3.9
+ self.__origin__ = origin
+ self._nparams = nparams
+ super().__init__(origin, nparams, special=True, inst=inst, name=name)
+ else:
+ # Python >= 3.9
+ super().__init__(origin, nparams, inst=inst, name=name)
+ self._defaults = defaults
+
+ def __setattr__(self, attr, val):
+ allowed_attrs = {'_name', '_inst', '_nparams', '_defaults'}
+ if _special_generic_alias_base is typing._GenericAlias:
+ # Python <3.9
+ allowed_attrs.add("__origin__")
+ if _is_dunder(attr) or attr in allowed_attrs:
+ object.__setattr__(self, attr, val)
+ else:
+ setattr(self.__origin__, attr, val)
+
+ @typing._tp_cache
+ def __getitem__(self, params):
+ if not isinstance(params, tuple):
+ params = (params,)
+ msg = "Parameters to generic types must be types."
+ params = tuple(typing._type_check(p, msg) for p in params)
+ if (
+ self._defaults
+ and len(params) < self._nparams
+ and len(params) + len(self._defaults) >= self._nparams
+ ):
+ params = (*params, *self._defaults[len(params) - self._nparams:])
+ actual_len = len(params)
+
+ if actual_len != self._nparams:
+ if self._defaults:
+ expected = f"at least {self._nparams - len(self._defaults)}"
+ else:
+ expected = str(self._nparams)
+ if not self._nparams:
+ raise TypeError(f"{self} is not a generic class")
+ raise TypeError(
+ f"Too {'many' if actual_len > self._nparams else 'few'}"
+ f" arguments for {self};"
+ f" actual {actual_len}, expected {expected}"
+ )
+ return self.copy_with(params)
+
+ _NoneType = type(None)
+ Generator = _SpecialGenericAlias(
+ collections.abc.Generator, 3, defaults=(_NoneType, _NoneType)
+ )
+ AsyncGenerator = _SpecialGenericAlias(
+ collections.abc.AsyncGenerator, 2, defaults=(_NoneType,)
+ )
+ ContextManager = _SpecialGenericAlias(
+ contextlib.AbstractContextManager,
+ 2,
+ name="ContextManager",
+ defaults=(typing.Optional[bool],)
+ )
+ AsyncContextManager = _SpecialGenericAlias(
+ contextlib.AbstractAsyncContextManager,
+ 2,
+ name="AsyncContextManager",
+ defaults=(typing.Optional[bool],)
+ )
+
+
_PROTO_ALLOWLIST = {
'collections.abc': [
'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable',
@@ -467,22 +509,11 @@ def clear_overloads():
}
-_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__",
+_EXCLUDED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | {
+ "__match_args__", "__protocol_attrs__", "__non_callable_proto_members__",
+ "__final__",
}
-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()
@@ -503,9 +534,9 @@ def _caller(depth=2):
return None
-# The performance of runtime-checkable protocols is significantly improved on Python 3.12,
-# so we backport the 3.12 version of Protocol to Python <=3.11
-if sys.version_info >= (3, 12):
+# `__match_args__` attribute was removed from protocol members in 3.13,
+# we want to backport this change to older Python versions.
+if sys.version_info >= (3, 13):
Protocol = typing.Protocol
else:
def _allow_reckless_class_checks(depth=3):
@@ -519,6 +550,22 @@ def _no_init(self, *args, **kwargs):
if type(self)._is_protocol:
raise TypeError('Protocols cannot be instantiated')
+ def _type_check_issubclass_arg_1(arg):
+ """Raise TypeError if `arg` is not an instance of `type`
+ in `issubclass(arg, )`.
+
+ In most cases, this is verified by type.__subclasscheck__.
+ Checking it again unnecessarily would slow down issubclass() checks,
+ so, we don't perform this check unless we absolutely have to.
+
+ For various error paths, however,
+ we want to ensure that *this* error message is shown to the user
+ where relevant, rather than a typing.py-specific error message.
+ """
+ if not isinstance(arg, type):
+ # Same error message as for issubclass(1, int).
+ raise TypeError('issubclass() arg 1 must be a class')
+
# Inheriting from typing._ProtocolMeta isn't actually desirable,
# but is necessary to allow typing.Protocol and typing_extensions.Protocol
# to mix without getting TypeErrors about "metaclass conflict"
@@ -549,11 +596,6 @@ def __init__(cls, *args, **kwargs):
abc.ABCMeta.__init__(cls, *args, **kwargs)
if getattr(cls, "_is_protocol", False):
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
- # PEP 544 prohibits using issubclass()
- # with protocols that have non-method members.
- cls.__callable_proto_members_only__ = all(
- callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
- )
def __subclasscheck__(cls, other):
if cls is Protocol:
@@ -562,21 +604,23 @@ def __subclasscheck__(cls, other):
getattr(cls, '_is_protocol', False)
and not _allow_reckless_class_checks()
):
- if not isinstance(other, type):
- # Same error message as for issubclass(1, int).
- raise TypeError('issubclass() arg 1 must be a class')
- if (
- not cls.__callable_proto_members_only__
- and cls.__dict__.get("__subclasshook__") is _proto_hook
- ):
- raise TypeError(
- "Protocols with non-method members don't support issubclass()"
- )
if not getattr(cls, '_is_runtime_protocol', False):
+ _type_check_issubclass_arg_1(other)
raise TypeError(
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
+ if (
+ # this attribute is set by @runtime_checkable:
+ cls.__non_callable_proto_members__
+ and cls.__dict__.get("__subclasshook__") is _proto_hook
+ ):
+ _type_check_issubclass_arg_1(other)
+ non_method_attrs = sorted(cls.__non_callable_proto_members__)
+ raise TypeError(
+ "Protocols with non-method members don't support issubclass()."
+ f" Non-method members: {str(non_method_attrs)[1:-1]}."
+ )
return abc.ABCMeta.__subclasscheck__(cls, other)
def __instancecheck__(cls, instance):
@@ -603,7 +647,8 @@ def __instancecheck__(cls, instance):
val = inspect.getattr_static(instance, attr)
except AttributeError:
break
- if val is None and callable(getattr(cls, attr, None)):
+ # this attribute is set by @runtime_checkable:
+ if val is None and attr not in cls.__non_callable_proto_members__:
break
else:
return True
@@ -671,8 +716,63 @@ def __init_subclass__(cls, *args, **kwargs):
cls.__init__ = _no_init
+if sys.version_info >= (3, 13):
+ runtime_checkable = typing.runtime_checkable
+else:
+ def runtime_checkable(cls):
+ """Mark a protocol class as a runtime protocol.
+
+ Such protocol can be used with isinstance() and issubclass().
+ Raise TypeError if applied to a non-protocol class.
+ This allows a simple-minded structural check very similar to
+ one trick ponies in collections.abc such as Iterable.
+
+ For example::
+
+ @runtime_checkable
+ class Closable(Protocol):
+ def close(self): ...
+
+ assert isinstance(open('/some/file'), Closable)
+
+ Warning: this will check only the presence of the required methods,
+ not their type signatures!
+ """
+ if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False):
+ raise TypeError(f'@runtime_checkable can be only applied to protocol classes,'
+ f' got {cls!r}')
+ cls._is_runtime_protocol = True
+
+ # typing.Protocol classes on <=3.11 break if we execute this block,
+ # because typing.Protocol classes on <=3.11 don't have a
+ # `__protocol_attrs__` attribute, and this block relies on the
+ # `__protocol_attrs__` attribute. Meanwhile, typing.Protocol classes on 3.12.2+
+ # break if we *don't* execute this block, because *they* assume that all
+ # protocol classes have a `__non_callable_proto_members__` attribute
+ # (which this block sets)
+ if isinstance(cls, _ProtocolMeta) or sys.version_info >= (3, 12, 2):
+ # PEP 544 prohibits using issubclass()
+ # with protocols that have non-method members.
+ # See gh-113320 for why we compute this attribute here,
+ # rather than in `_ProtocolMeta.__init__`
+ cls.__non_callable_proto_members__ = set()
+ for attr in cls.__protocol_attrs__:
+ try:
+ is_callable = callable(getattr(cls, attr, None))
+ except Exception as e:
+ raise TypeError(
+ f"Failed to determine whether protocol member {attr!r} "
+ "is a method member"
+ ) from e
+ else:
+ if not is_callable:
+ cls.__non_callable_proto_members__.add(attr)
+
+ return cls
+
+
# The "runtime" alias exists for backwards compatibility.
-runtime = runtime_checkable = typing.runtime_checkable
+runtime = runtime_checkable
# Our version of runtime-checkable protocols is faster on Python 3.8-3.11
@@ -767,7 +867,11 @@ def inner(func):
return inner
-if sys.version_info >= (3, 13):
+# Update this to something like >=3.13.0b1 if and when
+# PEP 728 is implemented in CPython
+_PEP_728_IMPLEMENTED = False
+
+if _PEP_728_IMPLEMENTED:
# The standard library TypedDict in Python 3.8 does not store runtime information
# about which (if any) keys are optional. See https://bugs.python.org/issue38834
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
@@ -778,6 +882,8 @@ def inner(func):
# Aaaand on 3.12 we add __orig_bases__ to TypedDict
# to enable better runtime introspection.
# On 3.13 we deprecate some odd ways of creating TypedDicts.
+ # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier.
+ # PEP 728 (still pending) makes more changes.
TypedDict = typing.TypedDict
_TypedDictMeta = typing._TypedDictMeta
is_typeddict = typing.is_typeddict
@@ -785,8 +891,29 @@ def inner(func):
# 3.10.0 and later
_TAKES_MODULE = "module" in inspect.signature(typing._type_check).parameters
+ def _get_typeddict_qualifiers(annotation_type):
+ while True:
+ annotation_origin = get_origin(annotation_type)
+ if annotation_origin is Annotated:
+ annotation_args = get_args(annotation_type)
+ if annotation_args:
+ annotation_type = annotation_args[0]
+ else:
+ break
+ elif annotation_origin is Required:
+ yield Required
+ annotation_type, = get_args(annotation_type)
+ elif annotation_origin is NotRequired:
+ yield NotRequired
+ annotation_type, = get_args(annotation_type)
+ elif annotation_origin is ReadOnly:
+ yield ReadOnly
+ annotation_type, = get_args(annotation_type)
+ else:
+ break
+
class _TypedDictMeta(type):
- def __new__(cls, name, bases, ns, total=True):
+ def __new__(cls, name, bases, ns, *, total=True, closed=False):
"""Create new typed dict class object.
This method is called when TypedDict is subclassed,
@@ -815,7 +942,13 @@ def __new__(cls, name, bases, ns, total=True):
tp_dict.__orig_bases__ = bases
annotations = {}
- own_annotations = ns.get('__annotations__', {})
+ if "__annotations__" in ns:
+ own_annotations = ns["__annotations__"]
+ elif "__annotate__" in ns:
+ # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
+ own_annotations = ns["__annotate__"](1)
+ else:
+ own_annotations = {}
msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
if _TAKES_MODULE:
own_annotations = {
@@ -829,35 +962,67 @@ def __new__(cls, name, bases, ns, total=True):
}
required_keys = set()
optional_keys = set()
+ readonly_keys = set()
+ mutable_keys = set()
+ extra_items_type = None
for base in bases:
- annotations.update(base.__dict__.get('__annotations__', {}))
- required_keys.update(base.__dict__.get('__required_keys__', ()))
- optional_keys.update(base.__dict__.get('__optional_keys__', ()))
+ base_dict = base.__dict__
+
+ annotations.update(base_dict.get('__annotations__', {}))
+ required_keys.update(base_dict.get('__required_keys__', ()))
+ optional_keys.update(base_dict.get('__optional_keys__', ()))
+ readonly_keys.update(base_dict.get('__readonly_keys__', ()))
+ mutable_keys.update(base_dict.get('__mutable_keys__', ()))
+ base_extra_items_type = base_dict.get('__extra_items__', None)
+ if base_extra_items_type is not None:
+ extra_items_type = base_extra_items_type
+
+ if closed and extra_items_type is None:
+ extra_items_type = Never
+ if closed and "__extra_items__" in own_annotations:
+ annotation_type = own_annotations.pop("__extra_items__")
+ qualifiers = set(_get_typeddict_qualifiers(annotation_type))
+ if Required in qualifiers:
+ raise TypeError(
+ "Special key __extra_items__ does not support "
+ "Required"
+ )
+ if NotRequired in qualifiers:
+ raise TypeError(
+ "Special key __extra_items__ does not support "
+ "NotRequired"
+ )
+ extra_items_type = annotation_type
annotations.update(own_annotations)
for annotation_key, annotation_type in own_annotations.items():
- annotation_origin = get_origin(annotation_type)
- if annotation_origin is Annotated:
- annotation_args = get_args(annotation_type)
- if annotation_args:
- annotation_type = annotation_args[0]
- annotation_origin = get_origin(annotation_type)
-
- if annotation_origin is Required:
+ qualifiers = set(_get_typeddict_qualifiers(annotation_type))
+
+ if Required in qualifiers:
required_keys.add(annotation_key)
- elif annotation_origin is NotRequired:
+ elif NotRequired in qualifiers:
optional_keys.add(annotation_key)
elif total:
required_keys.add(annotation_key)
else:
optional_keys.add(annotation_key)
+ if ReadOnly in qualifiers:
+ mutable_keys.discard(annotation_key)
+ readonly_keys.add(annotation_key)
+ else:
+ mutable_keys.add(annotation_key)
+ readonly_keys.discard(annotation_key)
tp_dict.__annotations__ = annotations
tp_dict.__required_keys__ = frozenset(required_keys)
tp_dict.__optional_keys__ = frozenset(optional_keys)
+ tp_dict.__readonly_keys__ = frozenset(readonly_keys)
+ tp_dict.__mutable_keys__ = frozenset(mutable_keys)
if not hasattr(tp_dict, '__total__'):
tp_dict.__total__ = total
+ tp_dict.__closed__ = closed
+ tp_dict.__extra_items__ = extra_items_type
return tp_dict
__call__ = dict # static method
@@ -871,7 +1036,7 @@ def __subclasscheck__(cls, other):
_TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {})
@_ensure_subclassable(lambda bases: (_TypedDict,))
- def TypedDict(typename, fields=_marker, /, *, total=True, **kwargs):
+ def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs):
"""A simple typed namespace. At runtime it is equivalent to a plain dict.
TypedDict creates a dictionary type such that a type checker will expect all
@@ -931,11 +1096,16 @@ class Point2D(TypedDict):
"using the functional syntax, pass an empty dictionary, e.g. "
) + example + "."
warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2)
+ if closed is not False and closed is not True:
+ kwargs["closed"] = closed
+ closed = False
fields = kwargs
elif kwargs:
raise TypeError("TypedDict takes either a dict or keyword arguments,"
" but not both")
if kwargs:
+ if sys.version_info >= (3, 13):
+ raise TypeError("TypedDict takes no keyword arguments")
warnings.warn(
"The kwargs-based syntax for TypedDict definitions is deprecated "
"in Python 3.11, will be removed in Python 3.13, and may not be "
@@ -950,7 +1120,7 @@ class Point2D(TypedDict):
# Setting correct module is necessary to make typed dict classes pickleable.
ns['__module__'] = module
- td = _TypedDictMeta(typename, (), ns, total=total)
+ td = _TypedDictMeta(typename, (), ns, total=total, closed=closed)
td.__orig_bases__ = (TypedDict,)
return td
@@ -996,15 +1166,15 @@ def greet(name: str) -> None:
return val
-if hasattr(typing, "Required"): # 3.11+
+if hasattr(typing, "ReadOnly"): # 3.13+
get_type_hints = typing.get_type_hints
-else: # <=3.10
+else: # <=3.13
# replaces _strip_annotations()
def _strip_extras(t):
"""Strips Annotated, Required and NotRequired from a given type."""
if isinstance(t, _AnnotatedAlias):
return _strip_extras(t.__origin__)
- if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired):
+ if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly):
return _strip_extras(t.__args__[0])
if isinstance(t, typing._GenericAlias):
stripped_args = tuple(_strip_extras(a) for a in t.__args__)
@@ -1101,7 +1271,7 @@ def __repr__(self):
def __reduce__(self):
return operator.getitem, (
- Annotated, (self.__origin__,) + self.__metadata__
+ Annotated, (self.__origin__, *self.__metadata__)
)
def __eq__(self, other):
@@ -1227,7 +1397,7 @@ def get_args(tp):
get_args(Callable[[], T][int]) == ([], int)
"""
if isinstance(tp, _AnnotatedAlias):
- return (tp.__origin__,) + tp.__metadata__
+ return (tp.__origin__, *tp.__metadata__)
if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)):
if getattr(tp, "_special", False):
return ()
@@ -1273,17 +1443,37 @@ def TypeAlias(self, parameters):
)
+if hasattr(typing, "NoDefault"):
+ NoDefault = typing.NoDefault
+else:
+ class NoDefaultTypeMeta(type):
+ def __setattr__(cls, attr, value):
+ # TypeError is consistent with the behavior of NoneType
+ raise TypeError(
+ f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}"
+ )
+
+ class NoDefaultType(metaclass=NoDefaultTypeMeta):
+ """The type of the NoDefault singleton."""
+
+ __slots__ = ()
+
+ def __new__(cls):
+ return globals().get("NoDefault") or object.__new__(cls)
+
+ def __repr__(self):
+ return "typing_extensions.NoDefault"
+
+ def __reduce__(self):
+ return "NoDefault"
+
+ NoDefault = NoDefaultType()
+ del NoDefaultType, NoDefaultTypeMeta
+
+
def _set_default(type_param, default):
- 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:
- if isinstance(type_param, ParamSpec) and default is ...: # ... not valid <3.11
- type_param.__default__ = default
- else:
- type_param.__default__ = typing._type_check(default, "Default must be a type")
- else:
- type_param.__default__ = None
+ type_param.has_default = lambda: default is not NoDefault
+ type_param.__default__ = default
def _set_module(typevarlike):
@@ -1306,32 +1496,46 @@ 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(metaclass=_TypeVarLikeMeta):
- """Type variable."""
+if _PEP_696_IMPLEMENTED:
+ from typing import TypeVar
+else:
+ # Add default and infer_variance parameters from PEP 696 and 695
+ class TypeVar(metaclass=_TypeVarLikeMeta):
+ """Type variable."""
- _backported_typevarlike = typing.TypeVar
+ _backported_typevarlike = typing.TypeVar
- def __new__(cls, name, *constraints, bound=None,
- covariant=False, contravariant=False,
- default=_marker, infer_variance=False):
- if hasattr(typing, "TypeAliasType"):
- # PEP 695 implemented (3.12+), 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
+ def __new__(cls, name, *constraints, bound=None,
+ covariant=False, contravariant=False,
+ default=NoDefault, infer_variance=False):
+ if hasattr(typing, "TypeAliasType"):
+ # PEP 695 implemented (3.12+), 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
- def __init_subclass__(cls) -> None:
- raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type")
+ _set_default(typevar, default)
+ _set_module(typevar)
+
+ def _tvar_prepare_subst(alias, args):
+ if (
+ typevar.has_default()
+ and alias.__parameters__.index(typevar) == len(args)
+ ):
+ args += (typevar.__default__,)
+ return args
+
+ typevar.__typing_prepare_subst__ = _tvar_prepare_subst
+ return typevar
+
+ def __init_subclass__(cls) -> None:
+ raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type")
# Python 3.10+ has PEP 612
@@ -1396,8 +1600,12 @@ def __eq__(self, other):
return NotImplemented
return self.__origin__ == other.__origin__
+
+if _PEP_696_IMPLEMENTED:
+ from typing import ParamSpec
+
# 3.10+
-if hasattr(typing, 'ParamSpec'):
+elif hasattr(typing, 'ParamSpec'):
# Add default parameter - PEP 696
class ParamSpec(metaclass=_TypeVarLikeMeta):
@@ -1407,7 +1615,7 @@ class ParamSpec(metaclass=_TypeVarLikeMeta):
def __new__(cls, name, *, bound=None,
covariant=False, contravariant=False,
- infer_variance=False, default=_marker):
+ infer_variance=False, default=NoDefault):
if hasattr(typing, "TypeAliasType"):
# PEP 695 implemented, can pass infer_variance to typing.TypeVar
paramspec = typing.ParamSpec(name, bound=bound,
@@ -1422,6 +1630,24 @@ def __new__(cls, name, *, bound=None,
_set_default(paramspec, default)
_set_module(paramspec)
+
+ def _paramspec_prepare_subst(alias, args):
+ params = alias.__parameters__
+ i = params.index(paramspec)
+ if i == len(args) and paramspec.has_default():
+ args = [*args, paramspec.__default__]
+ if i >= len(args):
+ raise TypeError(f"Too few arguments for {alias}")
+ # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
+ if len(params) == 1 and not typing._is_param_expr(args[0]):
+ assert i == 0
+ args = (args,)
+ # Convert lists to tuples to help other libraries cache the results.
+ elif isinstance(args[i], list):
+ args = (*args[:i], tuple(args[i]), *args[i + 1:])
+ return args
+
+ paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst
return paramspec
def __init_subclass__(cls) -> None:
@@ -1490,8 +1716,8 @@ def kwargs(self):
return ParamSpecKwargs(self)
def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
- infer_variance=False, default=_marker):
- super().__init__([self])
+ infer_variance=False, default=NoDefault):
+ list.__init__(self, [self])
self.__name__ = name
self.__covariant__ = bool(covariant)
self.__contravariant__ = bool(contravariant)
@@ -1585,7 +1811,7 @@ def _concatenate_getitem(self, parameters):
# 3.10+
if hasattr(typing, 'Concatenate'):
Concatenate = typing.Concatenate
- _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811
+ _ConcatenateGenericAlias = typing._ConcatenateGenericAlias
# 3.9
elif sys.version_info[:2] >= (3, 9):
@_ExtensionsSpecialForm
@@ -1724,6 +1950,98 @@ def is_str(val: Union[str, float]):
PEP 647 (User-Defined Type Guards).
""")
+# 3.13+
+if hasattr(typing, 'TypeIs'):
+ TypeIs = typing.TypeIs
+# 3.9
+elif sys.version_info[:2] >= (3, 9):
+ @_ExtensionsSpecialForm
+ def TypeIs(self, parameters):
+ """Special typing form used to annotate the return type of a user-defined
+ type narrower function. ``TypeIs`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeIs[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeIs`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the intersection of the type inside ``TypeGuard`` and the argument's
+ previously known type.
+
+ For example::
+
+ def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
+ return hasattr(val, '__await__')
+
+ def f(val: Union[int, Awaitable[int]]) -> int:
+ if is_awaitable(val):
+ assert_type(val, Awaitable[int])
+ else:
+ assert_type(val, int)
+
+ ``TypeIs`` also works with type variables. For more information, see
+ PEP 742 (Narrowing types with TypeIs).
+ """
+ item = typing._type_check(parameters, f'{self} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+# 3.8
+else:
+ class _TypeIsForm(_ExtensionsSpecialForm, _root=True):
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type')
+ return typing._GenericAlias(self, (item,))
+
+ TypeIs = _TypeIsForm(
+ 'TypeIs',
+ doc="""Special typing form used to annotate the return type of a user-defined
+ type narrower function. ``TypeIs`` only accepts a single type argument.
+ At runtime, functions marked this way should return a boolean.
+
+ ``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
+ type checkers to determine a more precise type of an expression within a
+ program's code flow. Usually type narrowing is done by analyzing
+ conditional code flow and applying the narrowing to a block of code. The
+ conditional expression here is sometimes referred to as a "type guard".
+
+ Sometimes it would be convenient to use a user-defined boolean function
+ as a type guard. Such a function should use ``TypeIs[...]`` as its
+ return type to alert static type checkers to this intention.
+
+ Using ``-> TypeIs`` tells the static type checker that for a given
+ function:
+
+ 1. The return value is a boolean.
+ 2. If the return value is ``True``, the type of its argument
+ is the intersection of the type inside ``TypeGuard`` and the argument's
+ previously known type.
+
+ For example::
+
+ def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
+ return hasattr(val, '__await__')
+
+ def f(val: Union[int, Awaitable[int]]) -> int:
+ if is_awaitable(val):
+ assert_type(val, Awaitable[int])
+ else:
+ assert_type(val, int)
+
+ ``TypeIs`` also works with type variables. For more information, see
+ PEP 742 (Narrowing types with TypeIs).
+ """)
+
# Vendored from cpython typing._SpecialFrom
class _SpecialForm(typing._Final, _root=True):
@@ -1924,6 +2242,53 @@ class Movie(TypedDict):
""")
+if hasattr(typing, 'ReadOnly'):
+ ReadOnly = typing.ReadOnly
+elif sys.version_info[:2] >= (3, 9): # 3.9-3.12
+ @_ExtensionsSpecialForm
+ def ReadOnly(self, parameters):
+ """A special typing construct to mark an item of a TypedDict as read-only.
+
+ For example:
+
+ class Movie(TypedDict):
+ title: ReadOnly[str]
+ year: int
+
+ def mutate_movie(m: Movie) -> None:
+ m["year"] = 1992 # allowed
+ m["title"] = "The Matrix" # typechecker error
+
+ There is no runtime checking for this property.
+ """
+ item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+else: # 3.8
+ class _ReadOnlyForm(_ExtensionsSpecialForm, _root=True):
+ def __getitem__(self, parameters):
+ item = typing._type_check(parameters,
+ f'{self._name} accepts only a single type.')
+ return typing._GenericAlias(self, (item,))
+
+ ReadOnly = _ReadOnlyForm(
+ 'ReadOnly',
+ doc="""A special typing construct to mark a key of a TypedDict as read-only.
+
+ For example:
+
+ class Movie(TypedDict):
+ title: ReadOnly[str]
+ year: int
+
+ def mutate_movie(m: Movie) -> None:
+ m["year"] = 1992 # allowed
+ m["title"] = "The Matrix" # typechecker error
+
+ There is no runtime checking for this propery.
+ """)
+
+
_UNPACK_DOC = """\
Type unpack operator.
@@ -1981,6 +2346,17 @@ def __init__(self, getitem):
class _UnpackAlias(typing._GenericAlias, _root=True):
__class__ = typing.TypeVar
+ @property
+ def __typing_unpacked_tuple_args__(self):
+ assert self.__origin__ is Unpack
+ assert len(self.__args__) == 1
+ arg, = self.__args__
+ if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)):
+ if arg.__origin__ is not tuple:
+ raise TypeError("Unpack[...] must be used with a tuple type")
+ return arg.__args__
+ return None
+
@_UnpackSpecialForm
def Unpack(self, parameters):
item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
@@ -2005,7 +2381,20 @@ def _is_unpack(obj):
return isinstance(obj, _UnpackAlias)
-if hasattr(typing, "TypeVarTuple"): # 3.11+
+if _PEP_696_IMPLEMENTED:
+ from typing import TypeVarTuple
+
+elif hasattr(typing, "TypeVarTuple"): # 3.11+
+
+ def _unpack_args(*args):
+ newargs = []
+ for arg in args:
+ subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
+ if subargs is not None and not (subargs and subargs[-1] is ...):
+ newargs.extend(subargs)
+ else:
+ newargs.append(arg)
+ return newargs
# Add default parameter - PEP 696
class TypeVarTuple(metaclass=_TypeVarLikeMeta):
@@ -2013,10 +2402,57 @@ class TypeVarTuple(metaclass=_TypeVarLikeMeta):
_backported_typevarlike = typing.TypeVarTuple
- def __new__(cls, name, *, default=_marker):
+ def __new__(cls, name, *, default=NoDefault):
tvt = typing.TypeVarTuple(name)
_set_default(tvt, default)
_set_module(tvt)
+
+ def _typevartuple_prepare_subst(alias, args):
+ params = alias.__parameters__
+ typevartuple_index = params.index(tvt)
+ for param in params[typevartuple_index + 1:]:
+ if isinstance(param, TypeVarTuple):
+ raise TypeError(
+ f"More than one TypeVarTuple parameter in {alias}"
+ )
+
+ alen = len(args)
+ plen = len(params)
+ left = typevartuple_index
+ right = plen - typevartuple_index - 1
+ var_tuple_index = None
+ fillarg = None
+ for k, arg in enumerate(args):
+ if not isinstance(arg, type):
+ subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
+ if subargs and len(subargs) == 2 and subargs[-1] is ...:
+ if var_tuple_index is not None:
+ raise TypeError(
+ "More than one unpacked "
+ "arbitrary-length tuple argument"
+ )
+ var_tuple_index = k
+ fillarg = subargs[0]
+ if var_tuple_index is not None:
+ left = min(left, var_tuple_index)
+ right = min(right, alen - var_tuple_index - 1)
+ elif left + right > alen:
+ raise TypeError(f"Too few arguments for {alias};"
+ f" actual {alen}, expected at least {plen - 1}")
+ if left == alen - right and tvt.has_default():
+ replacement = _unpack_args(tvt.__default__)
+ else:
+ replacement = args[left: alen - right]
+
+ return (
+ *args[:left],
+ *([fillarg] * (typevartuple_index - left)),
+ replacement,
+ *([fillarg] * (plen - right - left - typevartuple_index - 1)),
+ *args[alen - right:],
+ )
+
+ tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst
return tvt
def __init_subclass__(self, *args, **kwds):
@@ -2073,7 +2509,7 @@ def get_shape(self) -> Tuple[*Ts]:
def __iter__(self):
yield self.__unpacked__
- def __init__(self, name, *, default=_marker):
+ def __init__(self, name, *, default=NoDefault):
self.__name__ = name
_DefaultMixin.__init__(self, default)
@@ -2124,6 +2560,12 @@ def reveal_type(obj: T, /) -> T:
return obj
+if hasattr(typing, "_ASSERT_NEVER_REPR_MAX_LENGTH"): # 3.11+
+ _ASSERT_NEVER_REPR_MAX_LENGTH = typing._ASSERT_NEVER_REPR_MAX_LENGTH
+else: # <=3.10
+ _ASSERT_NEVER_REPR_MAX_LENGTH = 100
+
+
if hasattr(typing, "assert_never"): # 3.11+
assert_never = typing.assert_never
else: # <=3.10
@@ -2147,7 +2589,10 @@ def int_or_str(arg: int | str) -> None:
At runtime, this throws an exception when called.
"""
- raise AssertionError("Expected code to be unreachable")
+ value = repr(arg)
+ if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH:
+ value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...'
+ raise AssertionError(f"Expected code to be unreachable, but got: {value}")
if sys.version_info >= (3, 12): # 3.12+
@@ -2251,7 +2696,7 @@ def override(arg: _F, /) -> _F:
Usage:
class Base:
- def method(self) -> None: ...
+ def method(self) -> None:
pass
class Child(Base):
@@ -2281,20 +2726,17 @@ def method(self) -> None:
return arg
-if hasattr(typing, "deprecated"):
- deprecated = typing.deprecated
+if hasattr(warnings, "deprecated"):
+ deprecated = warnings.deprecated
else:
_T = typing.TypeVar("_T")
- def deprecated(
- msg: str,
- /,
- *,
- category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
- stacklevel: int = 1,
- ) -> typing.Callable[[_T], _T]:
+ class deprecated:
"""Indicate that a class, function or overload is deprecated.
+ When this decorator is applied to an object, the type checker
+ will generate a diagnostic on usage of the deprecated object.
+
Usage:
@deprecated("Use B instead")
@@ -2311,49 +2753,100 @@ def g(x: int) -> int: ...
@overload
def g(x: str) -> int: ...
- When this decorator is applied to an object, the type checker
- will generate a diagnostic on usage of the deprecated object.
-
- The warning specified by ``category`` will be emitted on use
- of deprecated objects. For functions, that happens on calls;
- for classes, on instantiation. If the ``category`` is ``None``,
- no warning is emitted. The ``stacklevel`` determines where the
+ The warning specified by *category* will be emitted at runtime
+ on use of deprecated objects. For functions, that happens on calls;
+ for classes, on instantiation and on creation of subclasses.
+ If the *category* is ``None``, no warning is emitted at runtime.
+ The *stacklevel* determines where the
warning is emitted. If it is ``1`` (the default), the warning
is emitted at the direct caller of the deprecated object; if it
is higher, it is emitted further up the stack.
+ Static type checker behavior is not affected by the *category*
+ and *stacklevel* arguments.
- The decorator sets the ``__deprecated__``
- attribute on the decorated object to the deprecation message
- passed to the decorator. If applied to an overload, the decorator
+ The deprecation message passed to the decorator is saved in the
+ ``__deprecated__`` attribute on the decorated object.
+ If applied to an overload, the decorator
must be after the ``@overload`` decorator for the attribute to
exist on the overload as returned by ``get_overloads()``.
See PEP 702 for details.
"""
- def decorator(arg: _T, /) -> _T:
+ def __init__(
+ self,
+ message: str,
+ /,
+ *,
+ category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
+ stacklevel: int = 1,
+ ) -> None:
+ if not isinstance(message, str):
+ raise TypeError(
+ "Expected an object of type str for 'message', not "
+ f"{type(message).__name__!r}"
+ )
+ self.message = message
+ self.category = category
+ self.stacklevel = stacklevel
+
+ def __call__(self, arg: _T, /) -> _T:
+ # Make sure the inner functions created below don't
+ # retain a reference to self.
+ msg = self.message
+ category = self.category
+ stacklevel = self.stacklevel
if category is None:
arg.__deprecated__ = msg
return arg
elif isinstance(arg, type):
+ import functools
+ from types import MethodType
+
original_new = arg.__new__
- has_init = arg.__init__ is not object.__init__
@functools.wraps(original_new)
def __new__(cls, *args, **kwargs):
- warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ if cls is arg:
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
if original_new is not object.__new__:
return original_new(cls, *args, **kwargs)
# Mirrors a similar check in object.__new__.
- elif not has_init and (args or kwargs):
+ elif cls.__init__ is object.__init__ and (args or kwargs):
raise TypeError(f"{cls.__name__}() takes no arguments")
else:
return original_new(cls)
arg.__new__ = staticmethod(__new__)
+
+ original_init_subclass = arg.__init_subclass__
+ # We need slightly different behavior if __init_subclass__
+ # is a bound method (likely if it was implemented in Python)
+ if isinstance(original_init_subclass, MethodType):
+ original_init_subclass = original_init_subclass.__func__
+
+ @functools.wraps(original_init_subclass)
+ def __init_subclass__(*args, **kwargs):
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ return original_init_subclass(*args, **kwargs)
+
+ arg.__init_subclass__ = classmethod(__init_subclass__)
+ # Or otherwise, which likely means it's a builtin such as
+ # object's implementation of __init_subclass__.
+ else:
+ @functools.wraps(original_init_subclass)
+ def __init_subclass__(*args, **kwargs):
+ warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
+ return original_init_subclass(*args, **kwargs)
+
+ arg.__init_subclass__ = __init_subclass__
+
arg.__deprecated__ = __new__.__deprecated__ = msg
+ __init_subclass__.__deprecated__ = msg
return arg
elif callable(arg):
+ import functools
+
@functools.wraps(arg)
def wrapper(*args, **kwargs):
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
@@ -2367,8 +2860,6 @@ def wrapper(*args, **kwargs):
f"a class or callable, not {arg!r}"
)
- return decorator
-
# We have to do some monkey patching to deal with the dual nature of
# Unpack/TypeVarTuple:
@@ -2378,10 +2869,222 @@ def wrapper(*args, **kwargs):
# counting generic parameters, so that when we subscript a generic,
# the runtime doesn't try to substitute the Unpack with the subscripted type.
if not hasattr(typing, "TypeVarTuple"):
- typing._collect_type_vars = _collect_type_vars
+ def _check_generic(cls, parameters, elen=_marker):
+ """Check correct count for parameters of a generic cls (internal helper).
+
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ if elen is _marker:
+ if not hasattr(cls, "__parameters__") or not cls.__parameters__:
+ raise TypeError(f"{cls} is not a generic class")
+ elen = len(cls.__parameters__)
+ alen = len(parameters)
+ if alen != elen:
+ expect_val = elen
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+ num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters)
+ if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples):
+ return
+
+ # deal with TypeVarLike defaults
+ # required TypeVarLikes cannot appear after a defaulted one.
+ if alen < elen:
+ # since we validate TypeVarLike default in _collect_type_vars
+ # or _collect_parameters we can safely check parameters[alen]
+ if (
+ getattr(parameters[alen], '__default__', NoDefault)
+ is not NoDefault
+ ):
+ return
+
+ num_default_tv = sum(getattr(p, '__default__', NoDefault)
+ is not NoDefault for p in parameters)
+
+ elen -= num_default_tv
+
+ expect_val = f"at least {elen}"
+
+ things = "arguments" if sys.version_info >= (3, 10) else "parameters"
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}"
+ f" for {cls}; actual {alen}, expected {expect_val}")
+else:
+ # Python 3.11+
+
+ def _check_generic(cls, parameters, elen):
+ """Check correct count for parameters of a generic cls (internal helper).
+
+ This gives a nice error message in case of count mismatch.
+ """
+ if not elen:
+ raise TypeError(f"{cls} is not a generic class")
+ alen = len(parameters)
+ if alen != elen:
+ expect_val = elen
+ if hasattr(cls, "__parameters__"):
+ parameters = [p for p in cls.__parameters__ if not _is_unpack(p)]
+
+ # deal with TypeVarLike defaults
+ # required TypeVarLikes cannot appear after a defaulted one.
+ if alen < elen:
+ # since we validate TypeVarLike default in _collect_type_vars
+ # or _collect_parameters we can safely check parameters[alen]
+ if (
+ getattr(parameters[alen], '__default__', NoDefault)
+ is not NoDefault
+ ):
+ return
+
+ num_default_tv = sum(getattr(p, '__default__', NoDefault)
+ is not NoDefault for p in parameters)
+
+ elen -= num_default_tv
+
+ expect_val = f"at least {elen}"
+
+ raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments"
+ f" for {cls}; actual {alen}, expected {expect_val}")
+
+if not _PEP_696_IMPLEMENTED:
typing._check_generic = _check_generic
+def _has_generic_or_protocol_as_origin() -> bool:
+ try:
+ frame = sys._getframe(2)
+ # - Catch AttributeError: not all Python implementations have sys._getframe()
+ # - Catch ValueError: maybe we're called from an unexpected module
+ # and the call stack isn't deep enough
+ except (AttributeError, ValueError):
+ return False # err on the side of leniency
+ else:
+ # If we somehow get invoked from outside typing.py,
+ # also err on the side of leniency
+ if frame.f_globals.get("__name__") != "typing":
+ return False
+ origin = frame.f_locals.get("origin")
+ # Cannot use "in" because origin may be an object with a buggy __eq__ that
+ # throws an error.
+ return origin is typing.Generic or origin is Protocol or origin is typing.Protocol
+
+
+_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)}
+
+
+def _is_unpacked_typevartuple(x) -> bool:
+ if get_origin(x) is not Unpack:
+ return False
+ args = get_args(x)
+ return (
+ bool(args)
+ and len(args) == 1
+ and type(args[0]) in _TYPEVARTUPLE_TYPES
+ )
+
+
+# Python 3.11+ _collect_type_vars was renamed to _collect_parameters
+if hasattr(typing, '_collect_type_vars'):
+ def _collect_type_vars(types, typevar_types=None):
+ """Collect all type variable contained in types in order of
+ first appearance (lexicographic order). For example::
+
+ _collect_type_vars((T, List[S, T])) == (T, S)
+ """
+ if typevar_types is None:
+ typevar_types = typing.TypeVar
+ tvars = []
+
+ # A required TypeVarLike cannot appear after a TypeVarLike with a default
+ # if it was a direct call to `Generic[]` or `Protocol[]`
+ enforce_default_ordering = _has_generic_or_protocol_as_origin()
+ default_encountered = False
+
+ # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
+ type_var_tuple_encountered = False
+
+ for t in types:
+ if _is_unpacked_typevartuple(t):
+ type_var_tuple_encountered = True
+ elif isinstance(t, typevar_types) and t not in tvars:
+ if enforce_default_ordering:
+ has_default = getattr(t, '__default__', NoDefault) is not NoDefault
+ if has_default:
+ if type_var_tuple_encountered:
+ raise TypeError('Type parameter with a default'
+ ' follows TypeVarTuple')
+ default_encountered = True
+ elif default_encountered:
+ raise TypeError(f'Type parameter {t!r} without a default'
+ ' follows type parameter with a default')
+
+ tvars.append(t)
+ if _should_collect_from_parameters(t):
+ tvars.extend([t for t in t.__parameters__ if t not in tvars])
+ return tuple(tvars)
+
+ typing._collect_type_vars = _collect_type_vars
+else:
+ def _collect_parameters(args):
+ """Collect all type variables and parameter specifications in args
+ in order of first appearance (lexicographic order).
+
+ For example::
+
+ assert _collect_parameters((T, Callable[P, T])) == (T, P)
+ """
+ parameters = []
+
+ # A required TypeVarLike cannot appear after a TypeVarLike with default
+ # if it was a direct call to `Generic[]` or `Protocol[]`
+ enforce_default_ordering = _has_generic_or_protocol_as_origin()
+ default_encountered = False
+
+ # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
+ type_var_tuple_encountered = False
+
+ for t in args:
+ if isinstance(t, type):
+ # We don't want __parameters__ descriptor of a bare Python class.
+ pass
+ elif isinstance(t, tuple):
+ # `t` might be a tuple, when `ParamSpec` is substituted with
+ # `[T, int]`, or `[int, *Ts]`, etc.
+ for x in t:
+ for collected in _collect_parameters([x]):
+ if collected not in parameters:
+ parameters.append(collected)
+ elif hasattr(t, '__typing_subst__'):
+ if t not in parameters:
+ if enforce_default_ordering:
+ has_default = (
+ getattr(t, '__default__', NoDefault) is not NoDefault
+ )
+
+ if type_var_tuple_encountered and has_default:
+ raise TypeError('Type parameter with a default'
+ ' follows TypeVarTuple')
+
+ if has_default:
+ default_encountered = True
+ elif default_encountered:
+ raise TypeError(f'Type parameter {t!r} without a default'
+ ' follows type parameter with a default')
+
+ parameters.append(t)
+ else:
+ if _is_unpacked_typevartuple(t):
+ type_var_tuple_encountered = True
+ for x in getattr(t, '__parameters__', ()):
+ if x not in parameters:
+ parameters.append(x)
+
+ return tuple(parameters)
+
+ if not _PEP_696_IMPLEMENTED:
+ typing._collect_parameters = _collect_parameters
+
# Backport typing.NamedTuple as it exists in Python 3.13.
# In 3.11, the ability to define generic `NamedTuple`s was supported.
# This was explicitly disallowed in 3.9-3.10, and only half-worked in <=3.8.
@@ -2414,7 +3117,13 @@ def __new__(cls, typename, bases, ns):
raise TypeError(
'can only inherit from a NamedTuple type and Generic')
bases = tuple(tuple if base is _NamedTuple else base for base in bases)
- types = ns.get('__annotations__', {})
+ if "__annotations__" in ns:
+ types = ns["__annotations__"]
+ elif "__annotate__" in ns:
+ # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
+ types = ns["__annotate__"](1)
+ else:
+ types = {}
default_names = []
for field_name in types:
if field_name in ns:
@@ -2437,11 +3146,35 @@ def __new__(cls, typename, bases, ns):
class_getitem = typing.Generic.__class_getitem__.__func__
nm_tpl.__class_getitem__ = classmethod(class_getitem)
# update from user namespace without overriding special namedtuple attributes
- for key in ns:
+ for key, val in ns.items():
if key in _prohibited_namedtuple_fields:
raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
- elif key not in _special_namedtuple_fields and key not in nm_tpl._fields:
- setattr(nm_tpl, key, ns[key])
+ elif key not in _special_namedtuple_fields:
+ if key not in nm_tpl._fields:
+ setattr(nm_tpl, key, ns[key])
+ try:
+ set_name = type(val).__set_name__
+ except AttributeError:
+ pass
+ else:
+ try:
+ set_name(val, nm_tpl, key)
+ except BaseException as e:
+ msg = (
+ f"Error calling __set_name__ on {type(val).__name__!r} "
+ f"instance {key!r} in {typename!r}"
+ )
+ # BaseException.add_note() existed on py311,
+ # but the __set_name__ machinery didn't start
+ # using add_note() until py312.
+ # Making sure exceptions are raised in the same way
+ # as in "normal" classes seems most important here.
+ if sys.version_info >= (3, 12):
+ e.add_note(msg)
+ raise
+ else:
+ raise RuntimeError(msg) from e
+
if typing.Generic in bases:
nm_tpl.__init_subclass__()
return nm_tpl
@@ -2522,7 +3255,7 @@ class Employee(NamedTuple):
if hasattr(collections.abc, "Buffer"):
Buffer = collections.abc.Buffer
else:
- class Buffer(abc.ABC):
+ class Buffer(abc.ABC): # noqa: B024
"""Base class for classes that implement the buffer protocol.
The buffer protocol allows Python objects to expose a low-level
@@ -2600,7 +3333,7 @@ def name_by_id(user_id: UserId) -> str:
num = UserId(5) + 1 # type: int
"""
- def __call__(self, obj):
+ def __call__(self, obj, /):
return obj
def __init__(self, name, tp):
@@ -2849,6 +3582,23 @@ def __eq__(self, other: object) -> bool:
return self.documentation == other.documentation
+_CapsuleType = getattr(_types, "CapsuleType", None)
+
+if _CapsuleType is None:
+ try:
+ import _socket
+ except ImportError:
+ pass
+ else:
+ _CAPI = getattr(_socket, "CAPI", None)
+ if _CAPI is not None:
+ _CapsuleType = type(_CAPI)
+
+if _CapsuleType is not None:
+ CapsuleType = _CapsuleType
+ __all__.append("CapsuleType")
+
+
# 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
@@ -2862,7 +3612,6 @@ def __eq__(self, other: object) -> bool:
Dict = typing.Dict
ForwardRef = typing.ForwardRef
FrozenSet = typing.FrozenSet
-Generator = typing.Generator
Generic = typing.Generic
Hashable = typing.Hashable
IO = typing.IO
diff --git a/test-requirements.txt b/test-requirements.txt
index 675b2c5d..7242d3b5 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1 @@
-flake8
-flake8-bugbear
+ruff==0.4.5
diff --git a/tox.ini b/tox.ini
index 5bed0225..5be7adb8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
isolated_build = True
-envlist = py38, py39, py310, py311, py312
+envlist = py38, py39, py310, py311, py312, py313
[testenv]
changedir = src