diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..21acaf76 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,6 @@ +# Inherits global settings from https://app.codecov.io/account/gh/python/yaml/ +# TODO: enable status checks to fail CI if coverage drops? +# https://docs.codecov.com/docs/commit-status +comment: + # https://docs.codecov.com/docs/pull-request-comments + layout: "condensed_header, diff, flags, files" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5c563144..2e4b9645 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,5 @@ updates: actions: patterns: - "*" + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1059f458..690f8c7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,50 +56,33 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} run: | - # Be wary that this does not install typing_extensions in the future - pip install coverage + # Be wary that this does not install typing_extensions in the future. + # 'toml' extra is needed to read settings from pyproject.toml on Python <3.11 + pip install 'coverage[toml]' - name: Test typing_extensions with coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} 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 cd src python --version # just to make sure we're running the right one # Run tests under coverage - export COVERAGE_FILE=.coverage_${{ matrix.python-version }} python -m coverage run -m unittest test_typing_extensions.py - - name: Test typing_extensions no coverage on pypy - if: ${{ startsWith(matrix.python-version, 'pypy') }} - 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 - cd src - python --version # just to make sure we're running the right one - python -m unittest test_typing_extensions.py + # Create xml file for Codecov + coverage xml --rcfile=../pyproject.toml --fail-under=0 - - name: Archive code coverage results - id: archive-coverage - if: ${{ !startsWith(matrix.python-version, 'pypy') }} - uses: actions/upload-artifact@v4 - with: - name: .coverage_${{ matrix.python-version }} - path: ./src/.coverage* - include-hidden-files: true - compression-level: 0 # no compression - name: Test CPython typing test suite # Test suite fails on PyPy even without typing_extensions if: ${{ !startsWith(matrix.python-version, 'pypy') }} @@ -108,9 +91,18 @@ jobs: # 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 - outputs: - # report if coverage was uploaded - cov_uploaded: ${{ steps.archive-coverage.outputs.artifact-id }} + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de + if: >- + github.repository == 'python/typing_extensions' + && (github.event_name == 'push' || github.event_name == 'pull_request') + with: + token: ${{ secrets.CODECOV_ORG_TOKEN }} + flags: ${{ matrix.python-version }} + directory: src + fail_ci_if_error: true + verbose: true create-issue-on-failure: name: Create an issue if daily tests failed @@ -130,7 +122,7 @@ jobs: issues: write steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -140,95 +132,3 @@ jobs: title: `Daily tests failed on ${new Date().toDateString()}`, body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/ci.yml", }) - - report-coverage: - name: Report coverage - - runs-on: ubuntu-latest - - needs: [tests] - - permissions: - pull-requests: write - - # Job will run even if tests failed but only if at least one artifact was uploaded - if: ${{ always() && needs.tests.outputs.cov_uploaded != '' }} - - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3" - - name: Download coverage artifacts - uses: actions/download-artifact@v4 - with: - pattern: .coverage_* - path: . - # merge only when files are named differently - merge-multiple: true - - name: Install dependencies - run: pip install coverage - - name: Combine coverage results - run: | - # List the files to see what we have - echo "Combining coverage files..." - ls -aR .coverage* - coverage combine .coverage* - echo "Creating coverage report..." - # Create xml file for further processing; Create even if below minimum - coverage xml --fail-under=0 - # Write markdown report to job summary - coverage report --fail-under=0 --format=markdown -m >> "$GITHUB_STEP_SUMMARY" - - # For future use in case we want to add a PR comment for 3rd party PRs which requires - # a workflow with elevated PR write permissions. Move below steps into a separate job. - - name: Archive code coverage report - id: cov_xml_upload - uses: actions/upload-artifact@v4 - with: - name: coverage - path: coverage.xml - - name: Code Coverage Report (console) - run: | - # Create a coverage report (console), respects fail_under in pyproject.toml - coverage report - - - name: Code Coverage Report - uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 # v1.3.0 - # Create markdown file even if coverage report fails due to fail_under - if: ${{ always() && steps.cov_xml_upload.outputs.artifact-id != '' }} - with: - filename: coverage.xml - badge: true - fail_below_min: true - format: markdown - hide_branch_rate: false - hide_complexity: true - indicators: true - output: both # console, file or both - # Note: it appears fail below min is one off, use fail_under -1 here - thresholds: '95 98' - - - name: Add link to report badge - if: ${{ always() && steps.cov_xml_upload.outputs.artifact-id != '' }} - run: | - run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}?pr=${{ github.event.pull_request.number }}" - sed -i "1s|^\(!.*\)$|[\1]($run_url)|" code-coverage-results.md - - - name: Add Coverage PR Comment - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.3 - # Create PR comment when the branch is on the repo, otherwise we lack PR write permissions - # -> need another workflow with access to secret token - if: >- - ${{ - always() - && github.event_name == 'pull_request' - && github.event.pull_request.head.repo.full_name == github.repository - && steps.cov_xml_upload.outputs.artifact-id != '' - }} - with: - hide_and_recreate: true - path: code-coverage-results.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e078218f..3d5fdcef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,11 +23,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Check package metadata @@ -43,7 +43,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python -m build - name: Store the distribution packages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: python-package-distributions path: dist/ @@ -55,15 +55,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ @@ -84,15 +84,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ @@ -112,15 +112,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.x" - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ @@ -152,11 +152,11 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: python-package-distributions path: dist/ - name: Ensure exactly one sdist and one wheel have been downloaded run: test "$(find dist/*.tar.gz | wc -l | xargs)" = 1 && test "$(find dist/*.whl | wc -l | xargs)" = 1 - name: Publish distribution to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 3a698bf7..0894c2d1 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -46,18 +46,19 @@ jobs: # 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.9", "3.10", "3.11", "3.12", "3.13"] + # Tests on 3.14 don't pass as of 2 November 2025 + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout pydantic run: git clone --depth=1 https://github.com/pydantic/pydantic.git || git clone --depth=1 https://github.com/pydantic/pydantic.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -65,7 +66,7 @@ jobs: working-directory: pydantic run: | uv add --editable ../typing-extensions-latest - uv sync --group dev + uv sync --all-packages --group testing-extra --all-extras printf "\n\nINSTALLED DEPENDENCIES ARE:\n\n" uv pip list @@ -79,18 +80,18 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout typing_inspect run: git clone --depth=1 https://github.com/ilevkivskyi/typing_inspect.git || git clone --depth=1 https://github.com/ilevkivskyi/typing_inspect.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -114,18 +115,18 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Check out pycroscope run: git clone --depth=1 https://github.com/JelleZijlstra/pycroscope.git || git clone --depth=1 https://github.com/JelleZijlstra/pycroscope.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -149,18 +150,18 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Check out typeguard run: git clone --depth=1 https://github.com/agronholm/typeguard.git || git clone --depth=1 https://github.com/agronholm/typeguard.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -186,18 +187,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # 3.9 is no longer supported. 3.14 fails some tests as of 2 November 2025 + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Check out typed-argument-parser run: git clone --depth=1 https://github.com/swansonk14/typed-argument-parser.git || git clone --depth=1 https://github.com/swansonk14/typed-argument-parser.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -228,18 +230,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # 3.14 fails a test as of 2 November 2025 + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout mypy for stubtest and mypyc tests run: git clone --depth=1 https://github.com/python/mypy.git || git clone --depth=1 https://github.com/python/mypy.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -264,18 +267,18 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout cattrs run: git clone --depth=1 https://github.com/python-attrs/cattrs.git || git clone --depth=1 https://github.com/python-attrs/cattrs.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -299,7 +302,7 @@ jobs: matrix: # PyPy is deliberately omitted here, since SQLAlchemy's tests # fail on PyPy for reasons unrelated to typing_extensions. - python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] checkout-ref: [ "main", "rel_2_0" ] # sqlalchemy tests fail when using the Ubuntu 24.04 runner # https://github.com/sqlalchemy/sqlalchemy/commit/8d73205f352e68c6603e90494494ef21027ec68f @@ -307,13 +310,13 @@ jobs: timeout-minutes: 60 steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout sqlalchemy run: git clone -b ${{ matrix.checkout-ref }} --depth=1 https://github.com/sqlalchemy/sqlalchemy.git || git clone -b ${{ matrix.checkout-ref }} --depth=1 https://github.com/sqlalchemy/sqlalchemy.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -335,16 +338,17 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + # As of 2 November 2025 a dependency is missing 3.14 wheels + python-version: [ "3.11", "3.12", "3.13" ] steps: - name: Install the latest version of uv - uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3 + uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 with: python-version: ${{ matrix.python-version }} - name: Checkout litestar run: git clone --depth=1 https://github.com/litestar-org/litestar.git || git clone --depth=1 https://github.com/litestar-org/litestar.git - name: Checkout typing_extensions - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: typing-extensions-latest persist-credentials: false @@ -353,7 +357,7 @@ jobs: run: | # litestar's python-requires means uv won't let us add typing-extensions-latest # as a requirement unless we do this - sed -i 's/^requires-python = ">=3.8/requires-python = ">=3.9/' pyproject.toml + sed -i 's/^requires-python = ">=3.8/requires-python = ">=3.10/' pyproject.toml uv add --editable ../typing-extensions-latest uv sync @@ -377,6 +381,7 @@ jobs: - mypy - cattrs - sqlalchemy + - litestar if: >- ${{ @@ -392,6 +397,7 @@ jobs: || needs.mypy.result == 'failure' || needs.cattrs.result == 'failure' || needs.sqlalchemy.result == 'failure' + || needs.litestar.result == 'failure' ) }} @@ -399,7 +405,7 @@ jobs: issues: write steps: - - uses: actions/github-script@v7 + - uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf8fd54d..2a3aedee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.14.10 hooks: - id: ruff - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,11 +17,11 @@ repos: - id: mixed-line-ending args: [--fix=lf] - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.0 + rev: v1.0.2 hooks: - id: sphinx-lint - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.36.0 hooks: - id: check-dependabot - id: check-github-workflows @@ -32,16 +32,16 @@ repos: - id: validate-pyproject additional_dependencies: ["validate-pyproject-schema-store[all]"] - repo: https://github.com/rhysd/actionlint - rev: v1.7.7 + rev: v1.7.10 hooks: - id: actionlint additional_dependencies: # actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions # and checks these with shellcheck. This is arguably its most useful feature, # but the integration only works if shellcheck is installed - - "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.10.0" + - "github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1" - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.11.0 + rev: v1.19.0 hooks: - id: zizmor - repo: meta diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e77c1f..733505a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# Unreleased + +- Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that + calling `isinstance` with `typing_extensions.Concatenate[...]` or + `typing_extensions.Unpack[...]` as the first argument could have a different + result in some situations depending on whether or not a profiling function had been + set using `sys.setprofile`. This affected both CPython and PyPy implementations. + Patch by Brian Schubert. +- Fix `__init_subclass__()` behavior in the presence of multiple inheritance involving + an `@deprecated`-decorated base class. Backport of CPython PR + [#138210](https://github.com/python/cpython/pull/138210) by Brian Schubert. +- Raise `TypeError` when attempting to subclass `typing_extensions.ParamSpec` on + Python 3.9. The `typing` implementation has always raised an error, and the + `typing_extensions` implementation has raised an error on Python 3.10+ since + `typing_extensions` v4.6.0. Patch by Brian Schubert. + # Release 4.15.0 (August 25, 2025) No user-facing changes since 4.15.0rc1. diff --git a/doc/conf.py b/doc/conf.py index db9b5185..cef3215d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -38,6 +38,27 @@ html_theme = 'alabaster' +html_theme_options = { + "description": "Backported and experimental type hints for Python", + # Make the sidebar "sticky" so that is stays visible when scrolling. + # Also makes the sidebar appear at the top of the page on mobile. + "fixed_sidebar": True, +} + +html_sidebars = { + '**': [ + 'about.html', + 'searchfield.html', + 'localtoc.html', + ] +} + +# Don't include object entries (e.g. functions, classes) in the table of contents. +toc_object_entries = False + +# Warn about all references where the target cannot be found. +nitpicky = True + class MyTranslator(HTML5Translator): """Adds a link target to name without `typing_extensions.` prefix.""" diff --git a/doc/index.rst b/doc/index.rst index 6aa95f5b..66577ef0 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -259,6 +259,13 @@ Special typing primitives .. versionadded:: 4.12.0 +.. data:: NoExtraItems + + A sentinel used when the ``extra_items`` class argument to :class:`TypedDict` is not + provided. In ``typing`` since 3.15. + + .. versionadded:: 4.13.0 + .. data:: NotRequired See :py:data:`typing.NotRequired` and :pep:`655`. In ``typing`` since 3.11. @@ -296,7 +303,7 @@ Special typing primitives ``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 + to :py:data:`types.NoneType`, and passing no value for the parameter would result in :attr:`!__default__` being set to ``None``. .. versionchanged:: 4.12.0 @@ -305,10 +312,9 @@ Special typing primitives with :py:class:`typing.ParamSpec` on Python 3.13+. .. class:: ParamSpecArgs + ParamSpecKwargs -.. class:: ParamSpecKwargs - - See :py:class:`typing.ParamSpecArgs` and :py:class:`typing.ParamSpecKwargs`. + See :py:data:`typing.ParamSpecArgs` and :py:data:`typing.ParamSpecKwargs`. In ``typing`` since 3.10. .. class:: Protocol @@ -387,12 +393,13 @@ Special typing primitives .. versionadded:: 4.10.0 -.. class:: TypedDict(dict, total=True) - - See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8. +.. class:: TypedDict(dict, total=True, closed=False, extra_items=) + See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8, but + changed and enhanced in several ways since then. ``typing_extensions`` backports various bug fixes and improvements - to ``TypedDict`` on Python 3.11 and lower. + to ``TypedDict``. + :py:class:`TypedDict` does not store runtime information about which (if any) keys are non-required in Python 3.8, and does not honor the ``total`` keyword with old-style ``TypedDict()`` in Python @@ -426,35 +433,23 @@ Special typing primitives .. 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__``. + The ``closed`` and ``extra_items`` keyword arguments introduced by + :pep:`728` and supported in Python 3.15 and newer are supported. 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. + considered closed. This reflects the ``closed`` class argument. .. 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``. + The type of the extra items allowed on the ``TypedDict``. + This attribute defaults to :data:`NoExtraItems` if the ``extra_items`` + class argument is not provided. .. versionadded:: 4.10.0 @@ -475,7 +470,7 @@ Special typing primitives ``TypedDict`` is now a function rather than a class. This brings ``typing_extensions.TypedDict`` closer to the implementation - of :py:mod:`typing.TypedDict` on Python 3.9 and higher. + of :py:class:`typing.TypedDict` on Python 3.9 and higher. .. versionchanged:: 4.7.0 @@ -495,6 +490,16 @@ Special typing primitives The keyword argument ``closed`` and the special key ``__extra_items__`` when ``closed=True`` is given were supported. + .. versionchanged:: 4.13.0 + + :pep:`728` support was updated to a newer version. Extra items are now + indicated with an ``extra_items`` class argument, not a special key + ``__extra_items__``. + + A value assigned to ``__total__`` in the class body of a + ``TypedDict`` will be overwritten by the ``total`` argument of the + ``TypedDict`` constructor. + .. class:: TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False, default=NoDefault) @@ -520,7 +525,7 @@ Special typing primitives ``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 + to :py:data:`types.NoneType`, and passing no value for the parameter would result in :attr:`!__default__` being set to ``None``. .. versionchanged:: 4.12.0 @@ -551,7 +556,7 @@ Special typing primitives ``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 + to :py:data:`types.NoneType`, and passing no value for the parameter would result in :attr:`!__default__` being set to ``None``. .. versionchanged:: 4.12.0 @@ -696,7 +701,8 @@ Decorators .. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1) - See :pep:`702`. In the :mod:`warnings` module since Python 3.13. + See :py:func:`warnings.deprecated` and :pep:`702`. In the :mod:`warnings` module + since Python 3.13. .. versionadded:: 4.5.0 @@ -792,7 +798,7 @@ Functions * Raises :exc:`TypeError` when it encounters certain objects that are not valid type hints. * Replaces type hints that evaluate to :const:`!None` with - :class:`types.NoneType`. + :data:`types.NoneType`. * Supports the :attr:`Format.FORWARDREF` and :attr:`Format.STRING` formats. @@ -809,6 +815,11 @@ Functions *format* specifies the format of the annotation and is a member of the :class:`Format` enum, defaulting to :attr:`Format.VALUE`. + .. caution:: + + This function may execute arbitrary code contained in annotations. + See :ref:`annotations-security` for more information. + .. versionadded:: 4.13.0 .. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) @@ -828,6 +839,11 @@ Functions typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + .. caution:: + + This function may execute arbitrary code contained in annotations. + See :ref:`annotations-security` for more information. + .. versionadded:: 4.13.0 .. function:: get_args(tp) @@ -895,6 +911,11 @@ Functions :py:data:`typing.Required` and :py:data:`typing.NotRequired`. ``typing_extensions`` backports these fixes. + .. caution:: + + This function may execute arbitrary code contained in annotations. + See :ref:`annotations-security` for more information. + .. versionchanged:: 4.1.0 Interaction with :data:`Required` and :data:`NotRequired`. @@ -1362,7 +1383,7 @@ versions of Python, but all are listed here for completeness. .. data:: Union - See :py:data:`typing.Union`. + See :py:class:`typing.Union`. .. versionadded:: 4.7.0 @@ -1409,3 +1430,25 @@ If you have any feedback on our security process, please `open an issue `__. To report an issue privately, use `GitHub's private reporting feature `__. + +.. _annotations-security: + +Introspection of annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some functions in this module are designed to introspect annotations at +runtime. These functions may therefore execute code contained in annotations, +which can then perform arbitrary operations. For example, +:func:`get_annotations` may call an arbitrary :term:`annotate function`, and +:meth:`evaluate_forward_ref` may call :func:`eval` on an arbitrary string. Code contained +in an annotation might make arbitrary system calls, enter an infinite loop, or perform any +other operation. This is also true for any access of the :attr:`~object.__annotations__` attribute +(as of Python 3.14), +and for various functions in the :mod:`typing` module that work with annotations, such as +:func:`typing.get_type_hints`. + +Any security issue arising from this also applies immediately after importing +code that may contain untrusted annotations: importing code can always cause arbitrary operations +to be performed. However, it is unsafe to accept strings or other input from an untrusted source and +pass them to any of the APIs for introspecting annotations, for example by editing an +``__annotations__`` dictionary or directly creating a :class:`ForwardRef` object. diff --git a/pyproject.toml b/pyproject.toml index adfed5d4..b024621d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,6 @@ ignore = [ "UP014", "UP019", "UP035", - "UP038", "UP045", # X | None instead of Optional[X] # Not relevant here "RUF012", # Use ClassVar for mutables @@ -123,6 +122,7 @@ known-first-party = ["typing_extensions", "_typed_dict_test_helper"] [tool.coverage.report] fail_under = 96 +precision = 2 show_missing = true # Omit files that are created in temporary directories during tests. # If not explicitly omitted they will result in warnings in the report. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 0986427c..f07e1eb0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -531,6 +531,14 @@ def test_pickle(self): pickled = pickle.dumps(self.bottom_type, protocol=proto) self.assertIs(self.bottom_type, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(self.bottom_type | int, Union[self.bottom_type, int]) + self.assertEqual(int | self.bottom_type, Union[int, self.bottom_type]) + + self.assertEqual(get_args(self.bottom_type | int), (self.bottom_type, int)) + self.assertEqual(get_args(int | self.bottom_type), (int, self.bottom_type)) + class NoReturnTests(BottomTypeTestsMixin, BaseTestCase): bottom_type = NoReturn @@ -805,6 +813,25 @@ class D(C, x=3): self.assertEqual(D.inited, 3) + def test_existing_init_subclass_in_sibling_base(self): + @deprecated("A will go away soon") + class A: + pass + class B: + def __init_subclass__(cls, x): + super().__init_subclass__() + cls.inited = x + + with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): + class C(A, B, x=42): + pass + self.assertEqual(C.inited, 42) + + with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): + class D(B, A, x=42): + pass + self.assertEqual(D.inited, 42) + def test_init_subclass_has_correct_cls(self): init_subclass_saw = None @@ -2210,6 +2237,39 @@ def test_or_and_ror(self): Union[typing_extensions.Generator, typing.Deque] ) + def test_setattr(self): + origin = collections.abc.Generator + alias = typing_extensions.Generator + original_name = alias._name + + def cleanup(): + for obj in origin, alias: + for attr in 'foo', '__dunder__': + try: + delattr(obj, attr) + except Exception: + pass + try: + alias._name = original_name + except Exception: + pass + + self.addCleanup(cleanup) + + # Attribute assignment on generic alias sets attribute on origin + alias.foo = 1 + self.assertEqual(alias.foo, 1) + self.assertEqual(origin.foo, 1) + # Except for dunders... + alias.__dunder__ = 2 + self.assertEqual(alias.__dunder__, 2) + self.assertRaises(AttributeError, lambda: origin.__dunder__) + + # ...and certain known attributes + alias._name = "NewName" + self.assertEqual(alias._name, "NewName") + self.assertRaises(AttributeError, lambda: origin._name) + class OtherABCTests(BaseTestCase): @@ -2379,6 +2439,16 @@ def test_error_message_when_subclassing(self): class ProUserId(UserId): ... + def test_module_with_incomplete_sys(self): + def does_not_exist(*args): + raise AttributeError + with ( + patch("sys._getframemodulename", does_not_exist, create=True), + patch("sys._getframe", does_not_exist, create=True), + ): + X = NewType("X", int) + self.assertEqual(X.__module__, None) + class Coordinate(Protocol): x: int @@ -4438,8 +4508,12 @@ def _make_td(future, class_name, annos, base, extra_names=None): child = _make_td( child_future, "Child", {"child": "int"}, "Base", {"Base": base} ) - base_anno = typing.ForwardRef("int", module="builtins") if base_future else int - child_anno = typing.ForwardRef("int", module="builtins") if child_future else int + if sys.version_info >= (3, 14): + base_anno = typing.ForwardRef("int", module="builtins", owner=base) if base_future else int + child_anno = typing.ForwardRef("int", module="builtins", owner=child) if child_future else int + else: + base_anno = typing.ForwardRef("int", module="builtins") if base_future else int + child_anno = typing.ForwardRef("int", module="builtins") if child_future else int self.assertEqual(base.__annotations__, {'base': base_anno}) self.assertEqual( child.__annotations__, {'child': child_anno, 'base': base_anno} @@ -5297,6 +5371,17 @@ class A(TypedDict): def test_dunder_dict(self): self.assertIsInstance(TypedDict.__dict__, dict) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + class TD(TypedDict): + a: int + + self.assertEqual(TD | int, Union[TD, int]) + self.assertEqual(int | TD, Union[int, TD]) + + self.assertEqual(get_args(TD | int), (TD, int)) + self.assertEqual(get_args(int | TD), (int, TD)) + class AnnotatedTests(BaseTestCase): def test_repr(self): @@ -5519,6 +5604,19 @@ def barfoo3(x: BA2): ... BA2 ) + @skipUnless(TYPING_3_11_0, "TODO: evaluate nested forward refs in Python < 3.11") + def test_get_type_hints_genericalias(self): + def foobar(x: list['X']): ... + X = Annotated[int, (1, 10)] + self.assertEqual( + get_type_hints(foobar, globals(), locals()), + {'x': list[int]} + ) + self.assertEqual( + get_type_hints(foobar, globals(), locals(), include_extras=True), + {'x': list[Annotated[int, (1, 10)]]} + ) + def test_get_type_hints_refs(self): Const = Annotated[T, "Const"] @@ -5973,6 +6071,11 @@ def run(): # The actual test: self.assertEqual(result1, result2) + def test_subclass(self): + with self.assertRaises(TypeError): + class MyParamSpec(ParamSpec): + pass + class ConcatenateTests(BaseTestCase): def test_basics(self): @@ -6131,6 +6234,47 @@ def test_is_param_expr(self): self.assertTrue(typing._is_param_expr(concat)) self.assertTrue(typing._is_param_expr(typing_concat)) + def test_isinstance_results_unaffected_by_presence_of_tracing_function(self): + # See https://github.com/python/typing_extensions/issues/661 + + 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 Concatenate + return isinstance(Concatenate[...], typing._GenericAlias) + 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 TypeGuardTests(BaseTestCase): def test_basics(self): TypeGuard[int] # OK @@ -6335,6 +6479,14 @@ def test_pickle(self): pickled = pickle.dumps(LiteralString, protocol=proto) self.assertIs(LiteralString, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(LiteralString | int, Union[LiteralString, int]) + self.assertEqual(int | LiteralString, Union[int, LiteralString]) + + self.assertEqual(get_args(LiteralString | int), (LiteralString, int)) + self.assertEqual(get_args(int | LiteralString), (int, LiteralString)) + class SelfTests(BaseTestCase): def test_basics(self): @@ -6382,6 +6534,14 @@ def test_pickle(self): pickled = pickle.dumps(Self, protocol=proto) self.assertIs(Self, pickle.loads(pickled)) + @skipUnless(TYPING_3_10_0, "PEP 604 has yet to be") + def test_or(self): + self.assertEqual(Self | int, Union[Self, int]) + self.assertEqual(int | Self, Union[int, Self]) + + self.assertEqual(get_args(Self | int), (Self, int)) + self.assertEqual(get_args(int | Self), (int, Self)) + class UnpackTests(BaseTestCase): def test_basic_plain(self): @@ -6537,6 +6697,46 @@ def test_type_var_inheritance(self): self.assertFalse(isinstance(Unpack[Ts], TypeVar)) self.assertFalse(isinstance(Unpack[Ts], typing.TypeVar)) + def test_isinstance_results_unaffected_by_presence_of_tracing_function(self): + # See https://github.com/python/typing_extensions/issues/661 + + 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 TypeVarTuple, Unpack + return isinstance(Unpack[TypeVarTuple("Ts")], 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 TypeVarTupleTests(BaseTestCase): @@ -7711,42 +7911,61 @@ class A(Generic[T, P, U]): ... self.assertEqual(A[float, [range], int].__args__, (float, (range,), int)) -class NoDefaultTests(BaseTestCase): +class SentinelTestsMixin: @skip_if_py313_beta_1 def test_pickling(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): - s = pickle.dumps(NoDefault, proto) + s = pickle.dumps(self.sentinel_type, proto) loaded = pickle.loads(s) - self.assertIs(NoDefault, loaded) + self.assertIs(self.sentinel_type, loaded) @skip_if_py313_beta_1 def test_doc(self): - self.assertIsInstance(NoDefault.__doc__, str) + self.assertIsInstance(self.sentinel_type.__doc__, str) def test_constructor(self): - self.assertIs(NoDefault, type(NoDefault)()) + self.assertIs(self.sentinel_type, type(self.sentinel_type)()) with self.assertRaises(TypeError): - type(NoDefault)(1) - - def test_repr(self): - self.assertRegex(repr(NoDefault), r'typing(_extensions)?\.NoDefault') + type(self.sentinel_type)(1) def test_no_call(self): with self.assertRaises(TypeError): - NoDefault() + self.sentinel_type() @skip_if_py313_beta_1 def test_immutable(self): with self.assertRaises(AttributeError): - NoDefault.foo = 'bar' + self.sentinel_type.foo = 'bar' with self.assertRaises(AttributeError): - NoDefault.foo + self.sentinel_type.foo # TypeError is consistent with the behavior of NoneType with self.assertRaises(TypeError): - type(NoDefault).foo = 3 + type(self.sentinel_type).foo = 3 with self.assertRaises(AttributeError): - type(NoDefault).foo + type(self.sentinel_type).foo + + +class NoDefaultTests(SentinelTestsMixin, BaseTestCase): + sentinel_type = NoDefault + + def test_repr(self): + if hasattr(typing, 'NoDefault'): + mod_name = 'typing' + else: + mod_name = "typing_extensions" + self.assertEqual(repr(NoDefault), f"{mod_name}.NoDefault") + + +class NoExtraItemsTests(SentinelTestsMixin, BaseTestCase): + sentinel_type = NoExtraItems + + def test_repr(self): + if hasattr(typing, 'NoExtraItems'): + mod_name = 'typing' + else: + mod_name = "typing_extensions" + self.assertEqual(repr(NoExtraItems), f"{mod_name}.NoExtraItems") class TypeVarInferVarianceTests(BaseTestCase): @@ -9359,6 +9578,10 @@ def test_sentinel_not_picklable(self): ): pickle.dumps(sentinel) +def load_tests(loader, tests, pattern): + import doctest + tests.addTests(doctest.DocTestSuite(typing_extensions)) + return tests if __name__ == '__main__': # pragma: no cover main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 77f33e16..20c331ee 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -148,7 +148,6 @@ 'ValuesView', 'cast', 'no_type_check', - 'no_type_check_decorator', ] # for backward compatibility @@ -160,17 +159,48 @@ # Added with bpo-45166 to 3.10.1+ and some 3.9 versions _FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ -# The functions below are modified copies of typing internal helpers. -# They are needed by _ProtocolMeta and they provide support for PEP 646. +class Sentinel: + """Create a unique sentinel object. + + *name* should be the name of the variable to which the return value shall be assigned. + *repr*, if supplied, will be used for the repr of the sentinel object. + If not provided, "" will be used. + """ + + def __init__( + self, + name: str, + repr: typing.Optional[str] = None, + ): + self._name = name + self._repr = repr if repr is not None else f'<{name}>' -class _Sentinel: def __repr__(self): - return "" + return self._repr + + if sys.version_info < (3, 11): + # The presence of this method convinces typing._type_check + # that Sentinels are types. + def __call__(self, *args, **kwargs): + raise TypeError(f"{type(self).__name__!r} object is not callable") + + # Breakpoint: https://github.com/python/cpython/pull/21515 + if sys.version_info >= (3, 10): + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __getstate__(self): + raise TypeError(f"Cannot pickle {type(self).__name__!r} object") -_marker = _Sentinel() +_marker = Sentinel("sentinel") +# The functions below are modified copies of typing internal helpers. +# They are needed by _ProtocolMeta and they provide support for PEP 646. # Breakpoint: https://github.com/python/cpython/pull/27342 if sys.version_info >= (3, 10): @@ -524,7 +554,9 @@ def _is_dunder(attr): class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True): - def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()): + def __init__(self, origin, nparams, *, defaults, inst=True, name=None): + assert nparams > 0, "`nparams` must be a positive integer" + assert defaults, "Must always specify a non-empty sequence for `defaults`" super().__init__(origin, nparams, inst=inst, name=name) self._defaults = defaults @@ -542,20 +574,14 @@ def __getitem__(self, 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 + 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") + expected = f"at least {self._nparams - len(self._defaults)}" raise TypeError( f"Too {'many' if actual_len > self._nparams else 'few'}" f" arguments for {self};" @@ -1929,6 +1955,9 @@ def __reduce__(self): def __call__(self, *args, **kwargs): pass + def __init_subclass__(cls) -> None: + raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") + # 3.9 if not hasattr(typing, 'Concatenate'): @@ -1956,7 +1985,9 @@ class _ConcatenateGenericAlias(list): __class__ = typing._GenericAlias def __init__(self, origin, args): - super().__init__(args) + # Cannot use `super().__init__` here because of the `__class__` assignment + # in the class body (https://github.com/python/typing_extensions/issues/661) + list.__init__(self, args) self.__origin__ = origin self.__args__ = args @@ -2259,10 +2290,10 @@ def f(val: Union[int, Awaitable[int]]) -> int: return typing._GenericAlias(self, (item,)) -# 3.14+? +# 3.15+? if hasattr(typing, 'TypeForm'): TypeForm = typing.TypeForm -# <=3.13 +# <=3.14 else: class _TypeFormForm(_ExtensionsSpecialForm, _root=True): # TypeForm(X) is equivalent to X but indicates to the type checker @@ -2515,7 +2546,10 @@ def __typing_is_unpacked_typevartuple__(self): def __getitem__(self, args): if self.__typing_is_unpacked_typevartuple__: return args - return super().__getitem__(args) + # Cannot use `super().__getitem__` here because of the `__class__` assignment + # in the class body on Python <=3.11 + # (https://github.com/python/typing_extensions/issues/661) + return typing._GenericAlias.__getitem__(self, args) @_UnpackSpecialForm def Unpack(self, parameters): @@ -2873,9 +2907,9 @@ def method(self) -> None: return arg -# Python 3.13.3+ contains a fix for the wrapped __new__ -# Breakpoint: https://github.com/python/cpython/pull/132160 -if sys.version_info >= (3, 13, 3): +# Python 3.13.8+ and 3.14.1+ contain a fix for the wrapped __init_subclass__ +# Breakpoint: https://github.com/python/cpython/pull/138210 +if ((3, 13, 8) <= sys.version_info < (3, 14)) or sys.version_info >= (3, 14, 1): deprecated = warnings.deprecated else: _T = typing.TypeVar("_T") @@ -2968,27 +3002,27 @@ def __new__(cls, /, *args, **kwargs): 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__ + if "__init_subclass__" in arg.__dict__: + # __init_subclass__ is directly present on the decorated class. + # Synthesize a wrapper that calls this method directly. + 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). + # Otherwise, it likely means it's a builtin such as + # object's implementation of __init_subclass__. + 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): + def __init_subclass__(cls, *args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) - return original_init_subclass(*args, **kwargs) + return super(arg, cls).__init_subclass__(*args, **kwargs) - arg.__init_subclass__ = __init_subclass__ + arg.__init_subclass__ = classmethod(__init_subclass__) arg.__deprecated__ = __new__.__deprecated__ = msg __init_subclass__.__deprecated__ = msg @@ -3829,8 +3863,8 @@ def get_protocol_members(tp: type, /) -> typing.FrozenSet[str]: >>> class P(Protocol): ... def a(self) -> str: ... ... b: int - >>> get_protocol_members(P) - frozenset({'a', 'b'}) + >>> get_protocol_members(P) == frozenset({'a', 'b'}) + True Raise a TypeError for arguments that are not Protocols. """ @@ -4207,44 +4241,6 @@ def evaluate_forward_ref( ) -class Sentinel: - """Create a unique sentinel object. - - *name* should be the name of the variable to which the return value shall be assigned. - - *repr*, if supplied, will be used for the repr of the sentinel object. - If not provided, "" will be used. - """ - - def __init__( - self, - name: str, - repr: typing.Optional[str] = None, - ): - self._name = name - self._repr = repr if repr is not None else f'<{name}>' - - def __repr__(self): - return self._repr - - if sys.version_info < (3, 11): - # The presence of this method convinces typing._type_check - # that Sentinels are types. - def __call__(self, *args, **kwargs): - raise TypeError(f"{type(self).__name__!r} object is not callable") - - # Breakpoint: https://github.com/python/cpython/pull/21515 - if sys.version_info >= (3, 10): - def __or__(self, other): - return typing.Union[self, other] - - def __ror__(self, other): - return typing.Union[other, self] - - def __getstate__(self): - raise TypeError(f"Cannot pickle {type(self).__name__!r} object") - - if sys.version_info >= (3, 14, 0, "beta"): type_repr = annotationlib.type_repr else: @@ -4302,11 +4298,16 @@ def type_repr(value): "ValuesView", "cast", "no_type_check", - "no_type_check_decorator", # This is private, but it was defined by typing_extensions for a long time # and some users rely on it. "_AnnotatedAlias", ] + +# Breakpoint: https://github.com/python/cpython/pull/133602 +if sys.version_info < (3, 15, 0): + _typing_names.append("no_type_check_decorator") + __all__.append("no_type_check_decorator") + globals().update( {name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)} )