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