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/workflows/ci.yml b/.github/workflows/ci.yml index e9d69774..3df842da 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: @@ -23,16 +24,11 @@ jobs: tests: name: Run tests + # if 'schedule' was the trigger, + # don't run it on contributors' forks if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' strategy: fail-fast: false @@ -42,21 +38,21 @@ jobs: # For available versions, see: # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json python-version: - - "3.8" - - "3.8.0" - "3.9" - - "3.9.0" + - "3.9.12" - "3.10" - - "3.10.0" + - "3.10.4" - "3.11" - "3.11.0" - "3.12" + - "3.12.0" - "3.13" - - "pypy3.8" + - "3.13.0" + - "3.14-dev" - "pypy3.9" - "pypy3.10" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -72,6 +68,7 @@ jobs: # 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 - name: Test CPython typing test suite @@ -99,19 +96,10 @@ jobs: 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 diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 8424d8fe..a15735b0 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -26,90 +26,77 @@ concurrency: cancel-in-progress: true jobs: + skip-schedule-on-fork: + name: Check for schedule trigger on fork + runs-on: ubuntu-latest + # if 'schedule' was the trigger, + # don't run it on contributors' forks + if: >- + github.repository == 'python/typing_extensions' + || github.event_name != 'schedule' + steps: + - run: true + pydantic: name: pydantic tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: # 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"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout pydantic - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 with: - repository: pydantic/pydantic - - name: Edit pydantic pyproject.toml - # pydantic's python-requires means pdm won't let us add typing-extensions-latest - # as a requirement unless we do this - run: sed -i 's/^requires-python = .*/requires-python = ">=3.8"/' pyproject.toml + python-version: ${{ matrix.python-version }} + allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - 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 with: path: typing-extensions-latest - - name: Setup pdm for pydantic tests - 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 + run: cd pydantic; uv add --editable ../typing-extensions-latest - name: Install pydantic test dependencies - run: pdm install -G testing -G email + run: cd pydantic; uv sync --group dev - name: List installed dependencies - run: pdm list -vv # pdm equivalent to `pip list` + run: cd pydantic; uv pip list - name: Run pydantic tests - run: pdm run pytest + run: cd pydantic; uv run pytest typing_inspect: name: typing_inspect tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout typing_inspect - uses: actions/checkout@v4 - with: - repository: ilevkivskyi/typing_inspect - path: typing_inspect - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python 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: 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 + with: + path: typing-extensions-latest - name: Install typing_inspect test dependencies run: | + set -x 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 @@ -121,34 +108,16 @@ jobs: cd typing_inspect pytest - pyanalyze: - name: pyanalyze tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + pycroscope: + name: pycroscope tests + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out pyanalyze - uses: actions/checkout@v4 - with: - repository: quora/pyanalyze - path: pyanalyze - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -156,47 +125,36 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - - name: Install pyanalyze test requirements + - 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 + with: + path: typing-extensions-latest + - name: Install pycroscope test requirements 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) + set -x + cd pycroscope + uv pip install --system 'pycroscope[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies run: uv pip freeze - - name: Run pyanalyze tests + - name: Run pycroscope tests run: | - cd pyanalyze - pytest pyanalyze/ + cd pycroscope + pytest pycroscope/ typeguard: name: typeguard tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out typeguard - uses: actions/checkout@v4 - with: - repository: agronholm/typeguard - path: typeguard - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -204,10 +162,17 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - 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 + with: + path: typing-extensions-latest - name: Install typeguard test requirements run: | + set -x cd typeguard - uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system "typeguard @ ." --group test --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies @@ -215,42 +180,31 @@ jobs: - name: Run typeguard tests run: | cd typeguard + export PYTHON_COLORS=0 # A test fails if tracebacks are colorized pytest typed-argument-parser: name: typed-argument-parser tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Check out typed-argument-parser - uses: actions/checkout@v4 - with: - repository: swansonk14/typed-argument-parser - path: typed-argument-parser - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python 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: 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 + with: + path: typing-extensions-latest - name: Configure git for typed-argument-parser tests # typed-argument parser does this in their CI, # and the tests fail unless we do this @@ -259,6 +213,7 @@ jobs: git config --global user.name "Your Name" - name: Install typed-argument-parser test requirements run: | + set -x 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) @@ -273,32 +228,14 @@ jobs: mypy: name: stubtest & mypyc tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout mypy for stubtest and mypyc tests - uses: actions/checkout@v4 - with: - repository: python/mypy - path: mypy - - name: Checkout typing_extensions - uses: actions/checkout@v4 - with: - path: typing-extensions-latest - name: Setup Python uses: actions/setup-python@v5 with: @@ -306,8 +243,15 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - 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 + with: + path: typing-extensions-latest - name: Install mypy test requirements run: | + set -x cd mypy 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 . @@ -322,47 +266,109 @@ jobs: cattrs: name: cattrs tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + needs: skip-schedule-on-fork strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] runs-on: ubuntu-latest timeout-minutes: 60 steps: - - name: Checkout cattrs - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 with: - repository: python-attrs/cattrs + 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 with: path: typing-extensions-latest - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - name: Install pdm for cattrs run: pip install pdm - name: Add latest typing-extensions as a dependency run: | + cd cattrs pdm remove typing-extensions - pdm add --dev ./typing-extensions-latest + pdm add --dev ../typing-extensions-latest + pdm update --group=docs pendulum # pinned version in lockfile is incompatible with py313 as of 2025/05/05 + pdm sync --clean - name: Install cattrs test dependencies - run: pdm install --dev -G :all + run: cd cattrs; pdm install --dev -G :all - name: List all installed dependencies - run: pdm list -vv + run: cd cattrs; pdm list -vv - name: Run cattrs tests - run: pdm run pytest tests + run: cd cattrs; pdm run pytest tests + + sqlalchemy: + name: sqlalchemy tests + needs: skip-schedule-on-fork + strategy: + fail-fast: false + 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" ] + checkout-ref: [ "main", "rel_2_0" ] + # sqlalchemy tests fail when using the Ubuntu 24.04 runner + # https://github.com/sqlalchemy/sqlalchemy/commit/8d73205f352e68c6603e90494494ef21027ec68f + runs-on: ubuntu-22.04 + timeout-minutes: 60 + steps: + - name: Setup Python + 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: 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 + with: + path: typing-extensions-latest + - name: Install sqlalchemy test dependencies + run: uv pip install --system tox setuptools + - name: List installed dependencies + # Note: tox installs SQLAlchemy and its dependencies in a different isolated + # environment before running the tests. To see the dependencies installed + # in the test environment, look for the line 'freeze> python -m pip freeze --all' + # in the output of the test step below. + run: uv pip list + - name: Run sqlalchemy tests + run: | + cd sqlalchemy + tox -e github-nocext \ + --force-dep "typing-extensions @ file://$(pwd)/../typing-extensions-latest" \ + -- -q --nomemory --notimingintensive + + + litestar: + name: litestar tests + needs: skip-schedule-on-fork + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] + steps: + - name: Setup Python + uses: actions/setup-python@v5 + 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 + with: + path: typing-extensions-latest + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Run litestar tests + run: uv run --with=../typing-extensions-latest -- python -m pytest tests/unit/test_typing.py tests/unit/test_dto + working-directory: litestar create-issue-on-failure: name: Create an issue if daily tests failed @@ -371,11 +377,12 @@ jobs: needs: - pydantic - typing_inspect - - pyanalyze + - pycroscope - typeguard - typed-argument-parser - mypy - cattrs + - sqlalchemy if: >- ${{ @@ -385,11 +392,12 @@ jobs: && ( needs.pydantic.result == 'failure' || needs.typing_inspect.result == 'failure' - || needs.pyanalyze.result == 'failure' + || needs.pycroscope.result == 'failure' || needs.typeguard.result == 'failure' || needs.typed-argument-parser.result == 'failure' || needs.mypy.result == 'failure' || needs.cattrs.result == 'failure' + || needs.sqlalchemy.result == 'failure' ) }} @@ -405,5 +413,5 @@ jobs: owner: "python", repo: "typing_extensions", title: `Third-party tests failed on ${new Date().toDateString()}`, - body: "Runs listed here: https://github.com/python/typing_extensions/actions/workflows/third_party.yml", + body: "Run listed here: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9cd298..92a19a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,114 @@ +# Unreleased + +- Drop support for Python 3.8 (including PyPy-3.8). Patch by [Victorien Plot](https://github.com/Viicos). +- Do not attempt to re-export names that have been removed from `typing`, + anticipating the removal of `typing.no_type_check_decorator` in Python 3.15. + Patch by Jelle Zijlstra. +- Update `typing_extensions.Format` and `typing_extensions.evaluate_forward_ref` to align + with changes in Python 3.14. Patch by Jelle Zijlstra. +- Fix tests for Python 3.14. Patch by Jelle Zijlstra. + +New features: + +- Add support for inline typed dictionaries ([PEP 764](https://peps.python.org/pep-0764/)). + Patch by [Victorien Plot](https://github.com/Viicos). +- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by + Sebastian Rittau. +- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)). Patch by + [Victorien Plot](https://github.com/Viicos). + +# Release 4.13.2 (April 10, 2025) + +- Fix `TypeError` when taking the union of `typing_extensions.TypeAliasType` and a + `typing.TypeAliasType` on Python 3.12 and 3.13. + Patch by [Joren Hammudoglu](https://github.com/jorenham). +- Backport from CPython PR [#132160](https://github.com/python/cpython/pull/132160) + to avoid having user arguments shadowed in generated `__new__` by + `@typing_extensions.deprecated`. + Patch by [Victorien Plot](https://github.com/Viicos). + +# Release 4.13.1 (April 3, 2025) + +Bugfixes: + +- Fix regression in 4.13.0 on Python 3.10.2 causing a `TypeError` when using `Concatenate`. + Patch by [Daraan](https://github.com/Daraan). +- Fix `TypeError` when using `evaluate_forward_ref` on Python 3.10.1-2 and 3.9.8-10. + Patch by [Daraan](https://github.com/Daraan). + +# Release 4.13.0 (March 25, 2025) + +No user-facing changes since 4.13.0rc1. + +# Release 4.13.0rc1 (March 18, 2025) + +New features: + +- Add `typing_extensions.TypeForm` from PEP 747. Patch by + Jelle Zijlstra. +- Add `typing_extensions.get_annotations`, a backport of + `inspect.get_annotations` that adds features specified + by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. +- Backport `evaluate_forward_ref` from CPython PR + [#119891](https://github.com/python/cpython/pull/119891) to evaluate `ForwardRef`s. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. + +Bugfixes and changed features: + +- Update PEP 728 implementation to a newer version of the PEP. Patch by Jelle Zijlstra. +- Copy the coroutine status of functions and methods wrapped + with `@typing_extensions.deprecated`. Patch by Sebastian Rittau. +- Fix bug where `TypeAliasType` instances could be subscripted even + where they were not generic. Patch by [Daraan](https://github.com/Daraan). +- Fix bug where a subscripted `TypeAliasType` instance did not have all + attributes of the original `TypeAliasType` instance on older Python versions. + Patch by [Daraan](https://github.com/Daraan) and Alex Waygood. +- Fix bug where subscripted `TypeAliasType` instances (and some other + subscripted objects) had wrong parameters if they were directly + subscripted with an `Unpack` object. + Patch by [Daraan](https://github.com/Daraan). +- Backport to Python 3.10 the ability to substitute `...` in generic `Callable` + aliases that have a `Concatenate` special form as their argument. + Patch by [Daraan](https://github.com/Daraan). +- Extended the `Concatenate` backport for Python 3.8-3.10 to now accept + `Ellipsis` as an argument. Patch by [Daraan](https://github.com/Daraan). +- Fix backport of `get_type_hints` to reflect Python 3.11+ behavior which does not add + `Union[..., NoneType]` to annotations that have a `None` default value anymore. + This fixes wrapping of `Annotated` in an unwanted `Optional` in such cases. + Patch by [Daraan](https://github.com/Daraan). +- Fix error in subscription of `Unpack` aliases causing nested Unpacks + to not be resolved correctly. Patch by [Daraan](https://github.com/Daraan). +- Backport CPython PR [#124795](https://github.com/python/cpython/pull/124795): + fix `TypeAliasType` not raising an error on non-tuple inputs for `type_params`. + Patch by [Daraan](https://github.com/Daraan). +- Fix that lists and `...` could not be used for parameter expressions for `TypeAliasType` + instances before Python 3.11. + Patch by [Daraan](https://github.com/Daraan). +- Fix error on Python 3.10 when using `typing.Concatenate` and + `typing_extensions.Concatenate` together. Patch by [Daraan](https://github.com/Daraan). +- Backport of CPython PR [#109544](https://github.com/python/cpython/pull/109544) + to reflect Python 3.13+ behavior: A value assigned to `__total__` in the class body of a + `TypedDict` will be overwritten by the `total` argument of the `TypedDict` constructor. + Patch by [Daraan](https://github.com/Daraan), backporting a CPython PR by Jelle Zijlstra. +- `isinstance(typing_extensions.Unpack[...], TypeVar)` now evaluates to `False` on Python 3.11 + and newer, but remains `True` on versions before 3.11. + Patch by [Daraan](https://github.com/Daraan). + +# 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 diff --git a/doc/conf.py b/doc/conf.py index 40d3c6b7..db9b5185 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -5,8 +5,9 @@ import os.path import sys -from sphinx.writers.html5 import HTML5Translator + from docutils.nodes import Element +from sphinx.writers.html5 import HTML5Translator sys.path.insert(0, os.path.abspath('.')) @@ -26,7 +27,9 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} +# This should usually point to /3, unless there is a necessity to link to +# features in future versions of Python. +intersphinx_mapping = {'py': ('https://docs.python.org/3.14', None)} add_module_names = False diff --git a/doc/index.rst b/doc/index.rst index 3f0d2d44..21d6fa60 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -133,13 +133,13 @@ Example usage:: False >>> is_literal(get_origin(typing.Literal[42])) True - >>> is_literal(get_origin(typing_extensions.Final[42])) + >>> is_literal(get_origin(typing_extensions.Final[int])) False Python version support ---------------------- -``typing_extensions`` currently supports Python versions 3.8 and higher. In the future, +``typing_extensions`` currently supports Python versions 3.9 and higher. In the future, support for older Python versions will be dropped some time after that version reaches end of life. @@ -178,7 +178,7 @@ Special typing primitives See :py:data:`typing.Concatenate` and :pep:`612`. In ``typing`` since 3.10. The backport does not support certain operations involving ``...`` as - a parameter; see :issue:`48` and :issue:`110` for details. + a parameter; see :issue:`48` and :pr:`481` for details. .. data:: Final @@ -255,7 +255,7 @@ Special typing primitives .. data:: NoDefault - See :py:class:`typing.NoDefault`. In ``typing`` since 3.13.0. + See :py:data:`typing.NoDefault`. In ``typing`` since 3.13. .. versionadded:: 4.12.0 @@ -341,7 +341,9 @@ Special typing primitives .. data:: ReadOnly - See :pep:`705`. Indicates that a :class:`TypedDict` item may not be modified. + See :py:data:`typing.ReadOnly` and :pep:`705`. In ``typing`` since 3.13. + + Indicates that a :class:`TypedDict` item may not be modified. .. versionadded:: 4.9.0 @@ -367,13 +369,21 @@ Special typing primitives .. versionadded:: 4.6.0 +.. data:: TypeForm + + See :pep:`747`. A special form representing the value of a type expression. + + .. versionadded:: 4.13.0 + .. data:: TypeGuard See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. .. data:: TypeIs - See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing. + See :py:data:`typing.TypeIs` and :pep:`742`. In ``typing`` since 3.13. + + Similar to :data:`TypeGuard`, but allows more type narrowing. .. versionadded:: 4.10.0 @@ -649,6 +659,18 @@ Protocols .. versionadded:: 4.6.0 +.. class:: Reader + + See :py:class:`io.Reader`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + +.. class:: Writer + + See :py:class:`io.Writer`. Added to the standard library in Python 3.14. + + .. versionadded:: 4.14.0 + Decorators ~~~~~~~~~~ @@ -747,6 +769,56 @@ Functions .. versionadded:: 4.2.0 +.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=None) + + Evaluate an :py:class:`typing.ForwardRef` as a :py:term:`type hint`. + + This is similar to calling :py:meth:`annotationlib.ForwardRef.evaluate`, + but unlike that method, :func:`!evaluate_forward_ref` also: + + * Recursively evaluates forward references nested within the type hint. + However, the amount of recursion is limited in Python 3.8 and 3.10. + * 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`. + * Supports the :attr:`Format.FORWARDREF` and + :attr:`Format.STRING` formats. + + *forward_ref* must be an instance of :py:class:`typing.ForwardRef`. + *owner*, if given, should be the object that holds the annotations that + the forward reference derived from, such as a module, class object, or function. + It is used to infer the namespaces to use for looking up names. + *globals* and *locals* can also be explicitly given to provide + the global and local namespaces. + *type_params* is a tuple of :py:ref:`type parameters ` that + are in scope when evaluating the forward reference. + This parameter must be provided (though it may be an empty tuple) if *owner* + is not given and the forward reference does not already have an owner set. + *format* specifies the format of the annotation and is a member of + the :class:`Format` enum, defaulting to :attr:`Format.VALUE`. + + .. versionadded:: 4.13.0 + +.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE) + + See :py:func:`inspect.get_annotations`. In the standard library since Python 3.10. + + ``typing_extensions`` adds the keyword argument ``format``, as specified + by :pep:`649`. The supported formats are listed in the :class:`Format` enum. + The default format, :attr:`Format.VALUE`, behaves the same across all versions. + For the other two formats, ``typing_extensions`` provides a rough approximation + of the :pep:`649` behavior on versions of Python that do not support it. + + The purpose of this backport is to allow users who would like to use + :attr:`Format.FORWARDREF` or :attr:`Format.STRING` semantics once + :pep:`649` is implemented, but who also + want to support earlier Python versions, to simply write:: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + .. versionadded:: 4.13.0 + .. function:: get_args(tp) See :py:func:`typing.get_args`. In ``typing`` since 3.8. @@ -786,6 +858,8 @@ Functions .. function:: get_protocol_members(tp) + See :py:func:`typing.get_protocol_members`. In ``typing`` since 3.13. + Return the set of members defined in a :class:`Protocol`. This works with protocols defined using either :class:`typing.Protocol` or :class:`typing_extensions.Protocol`. @@ -821,6 +895,8 @@ Functions .. function:: is_protocol(tp) + See :py:func:`typing.is_protocol`. In ``typing`` since 3.13. + Determine if a type is a :class:`Protocol`. This works with protocols defined using either :py:class:`typing.Protocol` or :class:`typing_extensions.Protocol`. @@ -857,6 +933,55 @@ Functions .. versionadded:: 4.1.0 +Enums +~~~~~ + +.. class:: Format + + The formats for evaluating annotations introduced by :pep:`649`. + Members of this enum can be passed as the *format* argument + to :func:`get_annotations`. + + The final place of this enum in the standard library has not yet + been determined (see :pep:`649` and :pep:`749`), but the names + and integer values are stable and will continue to work. + + .. attribute:: VALUE + + Equal to 1. The default value. The function will return the conventional Python values + for the annotations. This format is identical to the return value for + the function under earlier versions of Python. + + .. attribute:: VALUE_WITH_FAKE_GLOBALS + + Equal to 2. Special value used to signal that an annotate function is being + evaluated in a special environment with fake globals. When passed this + value, annotate functions should either return the same value as for + the :attr:`Format.VALUE` format, or raise :exc:`NotImplementedError` + to signal that they do not support execution in this environment. + This format is only used internally and should not be passed to + the functions in this module. + + .. attribute:: FORWARDREF + + Equal to 3. When :pep:`649` is implemented, this format will attempt to return the + conventional Python values for the annotations. However, if it encounters + an undefined name, it dynamically creates a proxy object (a ForwardRef) + that substitutes for that value in the expression. + + ``typing_extensions`` emulates this value on versions of Python which do + not support :pep:`649` by returning the same value as for ``VALUE`` semantics. + + .. attribute:: STRING + + Equal to 4. When :pep:`649` is implemented, this format will produce an annotation + dictionary where the values have been replaced by strings containing + an approximation of the original source code for the annotation expressions. + + ``typing_extensions`` emulates this by evaluating the annotations using + ``VALUE`` semantics and then stringifying the results. + + .. versionadded:: 4.13.0 Annotation metadata ~~~~~~~~~~~~~~~~~~~ @@ -902,6 +1027,34 @@ Capsule objects .. versionadded:: 4.12.0 +Sentinel objects +~~~~~~~~~~~~~~~~ + +.. class:: Sentinel(name, repr=None) + + A type used to define sentinel values. The *name* argument should be the + name of the variable to which the return value shall be assigned. + + If *repr* is provided, it will be used for the :meth:`~object.__repr__` + of the sentinel object. If not provided, ``""`` will be used. + + Example:: + + >>> from typing_extensions import Sentinel, assert_type + >>> MISSING = Sentinel('MISSING') + >>> def func(arg: int | MISSING = MISSING) -> None: + ... if arg is MISSING: + ... assert_type(arg, MISSING) + ... else: + ... assert_type(arg, int) + ... + >>> func(MISSING) + + .. versionadded:: 4.14.0 + + See :pep:`661` + + Pure aliases ~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index c9762f5f..1140ef78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,17 @@ # Build system requirements. [build-system] -requires = ["flit_core >=3.4,<4"] +requires = ["flit_core >=3.11,<4"] build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.12.0" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.9+" readme = "README.md" -requires-python = ">=3.8" -license = { file = "LICENSE" } +requires-python = ">=3.9" +license = "PSF-2.0" +license-files = ["LICENSE"] keywords = [ "annotations", "backport", @@ -30,11 +31,9 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: Python Software Foundation License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -60,3 +59,57 @@ email = "levkivskyi@gmail.com" [tool.flit.sdist] include = ["CHANGELOG.md", "README.md", "tox.ini", "*/*test*.py"] exclude = [] + +[tool.ruff] +line-length = 90 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "B", + "C4", + "E", + "F", + "I", + "ISC001", + "PGH004", + "RUF", + "SIM201", + "SIM202", + "UP", + "W", +] + +ignore = [ + # Ignore various "modernization" rules that tell you off for importing/using + # deprecated things from the typing module, etc. + "UP006", + "UP007", + "UP013", + "UP014", + "UP019", + "UP035", + "UP038", + # Not relevant here + "RUF012", + "RUF022", + "RUF023", + # Ruff doesn't understand the globals() assignment; we test __all__ + # directly in test_all_names_in___all__. + "F822", +] + +[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/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 080c0f7c..c23e94b7 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,39 +1,107 @@ -import sys import abc -import gc -import io -import contextlib +import asyncio import collections -from collections import defaultdict import collections.abc +import contextlib import copy -from functools import lru_cache +import functools +import gc import importlib import inspect +import io +import itertools 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 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, ReadOnly -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, TypeIs, no_type_check, Dict -from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol -from typing_extensions import Doc, NoDefault, List, Union, AnyStr, Iterable, Generic, Optional, Set, Tuple, Callable from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated +from typing_extensions import ( + _FORWARD_REF_HAS_CLASS, + Annotated, + Any, + AnyStr, + AsyncContextManager, + AsyncIterator, + Awaitable, + Buffer, + Callable, + ClassVar, + Concatenate, + Dict, + Doc, + Final, + Format, + Generic, + IntVar, + Iterable, + Iterator, + List, + Literal, + LiteralString, + NamedTuple, + Never, + NewType, + NoDefault, + NoExtraItems, + NoReturn, + NotRequired, + Optional, + ParamSpec, + ParamSpecArgs, + ParamSpecKwargs, + Protocol, + ReadOnly, + Required, + Self, + Sentinel, + Set, + Tuple, + Type, + TypeAlias, + TypeAliasType, + TypedDict, + TypeForm, + TypeGuard, + TypeIs, + TypeVar, + TypeVarTuple, + Union, + Unpack, + assert_never, + assert_type, + clear_overloads, + dataclass_transform, + deprecated, + evaluate_forward_ref, + final, + get_annotations, + 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") @@ -42,18 +110,26 @@ # Flags used to mark tests that only apply after a specific # version of the typing module. -TYPING_3_9_0 = sys.version_info[:3] >= (3, 9, 0) TYPING_3_10_0 = sys.version_info[:3] >= (3, 10, 0) # 3.11 makes runtime type checks (_type_check) more lenient. TYPING_3_11_0 = sys.version_info[:3] >= (3, 11, 0) # 3.12 changes the representation of Unpack[] (PEP 692) +# and adds PEP 695 to CPython's grammar TYPING_3_12_0 = sys.version_info[:3] >= (3, 12, 0) +# @deprecated works differently in Python 3.12 +TYPING_3_12_ONLY = (3, 12) <= sys.version_info < (3, 13) + # 3.13 drops support for the keyword argument syntax of TypedDict TYPING_3_13_0 = sys.version_info[:3] >= (3, 13, 0) +# 3.13.0.rc1 fixes a problem with @deprecated +TYPING_3_13_0_RC = sys.version_info[:4] >= (3, 13, 0, "candidate") + +TYPING_3_14_0 = sys.version_info[:3] >= (3, 14, 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 @@ -64,10 +140,14 @@ ) ANN_MODULE_SOURCE = '''\ +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: @@ -77,8 +157,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 @@ -170,22 +252,235 @@ def g_bad_ann(): ''' +STOCK_ANNOTATIONS = """ +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass +""" + +STRINGIZED_ANNOTATIONS = """ +from __future__ import annotations + +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass + +class MyClassWithLocalAnnotations: + mytype = int + x: mytype +""" + +STRINGIZED_ANNOTATIONS_2 = """ +from __future__ import annotations + + +def foo(a, b, c): pass +""" + +if TYPING_3_12_0: + STRINGIZED_ANNOTATIONS_PEP_695 = textwrap.dedent( + """ + from __future__ import annotations + from typing import Callable, Unpack + + + class A[T, *Ts, **P]: + x: T + y: tuple[*Ts] + z: Callable[P, str] + + + class B[T, *Ts, **P]: + T = int + Ts = str + P = bytes + x: T + y: Ts + z: P + + + Eggs = int + Spam = str + + + class C[Eggs, **Spam]: + x: Eggs + y: Spam + + + def generic_function[T, *Ts, **P]( + x: T, *y: Unpack[Ts], z: P.args, zz: P.kwargs + ) -> None: ... + + + def generic_function_2[Eggs, **Spam](x: Eggs, y: Spam): pass + + + class D: + Foo = int + Bar = str + + def generic_method[Foo, **Bar]( + self, x: Foo, y: Bar + ) -> None: ... + + def generic_method_2[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + + # Eggs is `int` in globals, a TypeVar in type_params, and `str` in locals: + class E[Eggs]: + Eggs = str + x: Eggs + + + + def nested(): + from types import SimpleNamespace + from typing_extensions import get_annotations + + Eggs = bytes + Spam = memoryview + + + class F[Eggs, **Spam]: + x: Eggs + y: Spam + + def generic_method[Eggs, **Spam](self, x: Eggs, y: Spam): pass + + + def generic_function[Eggs, **Spam](x: Eggs, y: Spam): pass + + + # Eggs is `int` in globals, `bytes` in the function scope, + # a TypeVar in the type_params, and `str` in locals: + class G[Eggs]: + Eggs = str + x: Eggs + + + return SimpleNamespace( + F=F, + F_annotations=get_annotations(F, eval_str=True), + F_meth_annotations=get_annotations(F.generic_method, eval_str=True), + G_annotations=get_annotations(G, eval_str=True), + generic_func=generic_function, + generic_func_annotations=get_annotations(generic_function, eval_str=True) + ) + """ + ) +else: + STRINGIZED_ANNOTATIONS_PEP_695 = None + + 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) +class EqualToForwardRef: + """Helper to ease use of annotationlib.ForwardRef in tests. + + This checks only attributes that can be set using the constructor. + + """ + + def __init__( + self, + arg, + *, + module=None, + owner=None, + is_class=False, + ): + self.__forward_arg__ = arg + self.__forward_is_class__ = is_class + self.__forward_module__ = module + self.__owner__ = owner + + def __eq__(self, other): + if not isinstance(other, (EqualToForwardRef, typing.ForwardRef)): + return NotImplemented + if sys.version_info >= (3, 14) and self.__owner__ != other.__owner__: + return False + return ( + self.__forward_arg__ == other.__forward_arg__ + and self.__forward_module__ == other.__forward_module__ + and self.__forward_is_class__ == other.__forward_is_class__ + ) + + def __repr__(self): + extra = [] + if self.__forward_module__ is not None: + extra.append(f", module={self.__forward_module__!r}") + if self.__forward_is_class__: + extra.append(", is_class=True") + if sys.version_info >= (3, 14) and self.__owner__ is not None: + extra.append(f", owner={self.__owner__!r}") + return f"EqualToForwardRef({self.__forward_arg__!r}{''.join(extra)})" + + class Employee: pass @@ -220,7 +515,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): @@ -322,7 +617,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) @@ -454,6 +748,25 @@ class Child(Base, Mixin): instance = Child(42) self.assertEqual(instance.a, 42) + def test_do_not_shadow_user_arguments(self): + new_called = False + new_called_cls = None + + @deprecated("MyMeta will go away soon") + class MyMeta(type): + def __new__(mcs, name, bases, attrs, cls=None): + nonlocal new_called, new_called_cls + new_called = True + new_called_cls = cls + return super().__new__(mcs, name, bases, attrs) + + with self.assertWarnsRegex(DeprecationWarning, "MyMeta will go away soon"): + class Foo(metaclass=MyMeta, cls='haha'): + pass + + self.assertTrue(new_called) + self.assertEqual(new_called_cls, 'haha') + def test_existing_init_subclass(self): @deprecated("C will go away soon") class C: @@ -610,6 +923,39 @@ def d(): pass isinstance(cell.cell_contents, deprecated) for cell in d.__closure__ )) +@deprecated("depr") +def func(): + pass + +@deprecated("depr") +async def coro(): + pass + +class Cls: + @deprecated("depr") + def func(self): + pass + + @deprecated("depr") + async def coro(self): + pass + +class DeprecatedCoroTests(BaseTestCase): + def test_asyncio_iscoroutinefunction(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + self.assertFalse(asyncio.coroutines.iscoroutinefunction(func)) + self.assertFalse(asyncio.coroutines.iscoroutinefunction(Cls.func)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(coro)) + self.assertTrue(asyncio.coroutines.iscoroutinefunction(Cls.coro)) + + @skipUnless(TYPING_3_12_ONLY or TYPING_3_13_0_RC, "inspect.iscoroutinefunction works differently on Python < 3.12") + def test_inspect_iscoroutinefunction(self): + self.assertFalse(inspect.iscoroutinefunction(func)) + self.assertFalse(inspect.iscoroutinefunction(Cls.func)) + self.assertTrue(inspect.iscoroutinefunction(coro)) + self.assertTrue(inspect.iscoroutinefunction(Cls.coro)) + class AnyTests(BaseTestCase): def test_can_subclass(self): @@ -685,7 +1031,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): @@ -726,7 +1072,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): @@ -760,18 +1106,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): @@ -805,18 +1151,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): @@ -836,15 +1182,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): @@ -867,7 +1213,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] @@ -1191,7 +1537,6 @@ async def __aexit__(self, etype, eval, tb): return None - class A: y: float class B(A): @@ -1312,7 +1657,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), {}) @@ -1321,7 +1669,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}) @@ -1336,7 +1687,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), {}) @@ -1361,6 +1712,95 @@ def test_final_forward_ref(self): self.assertNotEqual(gth(Loop, globals())['attr'], Final[int]) self.assertNotEqual(gth(Loop, globals())['attr'], Final) + def test_annotation_and_optional_default(self): + annotation = Annotated[Union[int, None], "data"] + NoneAlias = None + StrAlias = str + T_default = TypeVar("T_default", default=None) + Ts = TypeVarTuple("Ts") + + cases = { + # annotation: expected_type_hints + Annotated[None, "none"] : Annotated[None, "none"], + annotation : annotation, + Optional[int] : Optional[int], + Optional[List[str]] : Optional[List[str]], + Optional[annotation] : Optional[annotation], + Union[str, None, str] : Optional[str], + Unpack[Tuple[int, None]]: Unpack[Tuple[int, None]], + # Note: A starred *Ts will use typing.Unpack in 3.11+ see Issue #485 + Unpack[Ts] : Unpack[Ts], + } + # contains a ForwardRef, TypeVar(~prefix) or no expression + do_not_stringify_cases = { + () : {}, # Special-cased below to create an unannotated parameter + int : int, + "int" : int, + None : type(None), + "NoneAlias" : type(None), + List["str"] : List[str], + Union[str, "str"] : str, + Union[str, None, "str"] : Optional[str], + Union[str, "NoneAlias", "StrAlias"]: Optional[str], + Union[str, "Union[None, StrAlias]"]: Optional[str], + Union["annotation", T_default] : Union[annotation, T_default], + Annotated["annotation", "nested"] : Annotated[Union[int, None], "data", "nested"], + } + if TYPING_3_10_0: # cannot construct UnionTypes before 3.10 + do_not_stringify_cases["str | NoneAlias | StrAlias"] = str | None + cases[str | None] = Optional[str] + cases.update(do_not_stringify_cases) + for (annot, expected), none_default, as_str, wrap_optional in itertools.product( + cases.items(), (False, True), (False, True), (False, True) + ): + # Special case: + skip_reason = None + annot_unchanged = annot + if sys.version_info[:2] == (3, 10) and annot == "str | NoneAlias | StrAlias" and none_default: + # In 3.10 converts Optional[str | None] to Optional[str] which has a different repr + skip_reason = "UnionType not preserved in 3.10" + if wrap_optional: + if annot_unchanged == (): + continue + annot = Optional[annot] + expected = {"x": Optional[expected]} + else: + expected = {"x": expected} if annot_unchanged != () else {} + if as_str: + if annot_unchanged in do_not_stringify_cases or annot_unchanged == (): + continue + annot = str(annot) + with self.subTest( + annotation=annot, + as_str=as_str, + wrap_optional=wrap_optional, + none_default=none_default, + expected_type_hints=expected, + ): + # Create function to check + if annot_unchanged == (): + if none_default: + def func(x=None): pass + else: + def func(x): pass + elif none_default: + def func(x: annot = None): pass + else: + def func(x: annot): pass + type_hints = get_type_hints(func, globals(), locals(), include_extras=True) + # Equality + self.assertEqual(type_hints, expected) + # Hash + for k in type_hints.keys(): + self.assertEqual(hash(type_hints[k]), hash(expected[k])) + # Test if UnionTypes are preserved + self.assertIs(type(type_hints[k]), type(expected[k])) + # Repr + with self.subTest("Check str and repr"): + if skip_reason == "UnionType not preserved in 3.10": + self.skipTest(skip_reason) + self.assertEqual(repr(type_hints), repr(expected)) + class GetUtilitiesTestCase(TestCase): def test_get_origin(self): @@ -1382,8 +1822,7 @@ class C(Generic[T]): pass self.assertIs(get_origin(List), list) self.assertIs(get_origin(Tuple), tuple) self.assertIs(get_origin(Callable), collections.abc.Callable) - if sys.version_info >= (3, 9): - self.assertIs(get_origin(list[int]), list) + self.assertIs(get_origin(list[int]), list) self.assertIs(get_origin(list), None) self.assertIs(get_origin(P.args), P) self.assertIs(get_origin(P.kwargs), P) @@ -1420,28 +1859,28 @@ class C(Generic[T]): pass self.assertEqual(get_args(List), ()) self.assertEqual(get_args(Tuple), ()) self.assertEqual(get_args(Callable), ()) - if sys.version_info >= (3, 9): - self.assertEqual(get_args(list[int]), (int,)) + self.assertEqual(get_args(list[int]), (int,)) self.assertEqual(get_args(list), ()) - if sys.version_info >= (3, 9): - # Support Python versions with and without the fix for - # https://bugs.python.org/issue42195 - # The first variant is for 3.9.2+, the second for 3.9.0 and 1 - self.assertIn(get_args(collections.abc.Callable[[int], str]), - (([int], str), ([[int]], str))) - self.assertIn(get_args(collections.abc.Callable[[], str]), - (([], str), ([[]], str))) - self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str)) + # Support Python versions with and without the fix for + # https://bugs.python.org/issue42195 + # The first variant is for 3.9.2+, the second for 3.9.0 and 1 + self.assertIn(get_args(collections.abc.Callable[[int], str]), + (([int], str), ([[int]], str))) + self.assertIn(get_args(collections.abc.Callable[[], str]), + (([], str), ([[]], str))) + self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str)) P = ParamSpec('P') - # In 3.9 and lower we use typing_extensions's hacky implementation + # In 3.9 we use typing_extensions's hacky implementation # of ParamSpec, which gets incorrectly wrapped in a list self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)]) - self.assertEqual(get_args(Callable[Concatenate[int, P], int]), - (Concatenate[int, P], int)) self.assertEqual(get_args(Required[int]), (int,)) self.assertEqual(get_args(NotRequired[int]), (int,)) self.assertEqual(get_args(Unpack[Ts]), (Ts,)) self.assertEqual(get_args(Unpack), ()) + self.assertEqual(get_args(Callable[Concatenate[int, P], int]), + (Concatenate[int, P], int)) + self.assertEqual(get_args(Callable[Concatenate[int, ...], int]), + (Concatenate[int, ...], int)) class CollectionsAbcTests(BaseTestCase): @@ -1737,7 +2176,7 @@ class D: ... self.assertIsSubclass(D, A) self.assertIsSubclass(D, B) - class M(): ... + class M: ... collections.abc.Generator.register(M) self.assertIsSubclass(M, typing_extensions.Generator) @@ -2034,10 +2473,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 @@ -2350,7 +2789,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) @@ -2978,7 +3417,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) @@ -2994,7 +3433,7 @@ def meth(self): pass # noqa: B027 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( @@ -3196,11 +3635,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') @@ -3264,7 +3703,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 @@ -3409,7 +3848,7 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ... MemoizedFunc[[int, str, str]] if sys.version_info >= (3, 10): - # These unfortunately don't pass on <=3.9, + # These unfortunately don't pass on 3.9, # due to typing._type_check on older Python versions X = MemoizedFunc[[int, str, str], T, T2] self.assertEqual(X.__parameters__, (T, T2)) @@ -3419,6 +3858,10 @@ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: ... self.assertEqual(Y.__parameters__, ()) self.assertEqual(Y.__args__, ((int, str, str), bytes, memoryview)) + # Regression test; fixing #126 might cause an error here + with self.assertRaisesRegex(TypeError, "not a generic class"): + Y[int] + def test_protocol_generic_over_typevartuple(self): Ts = TypeVarTuple("Ts") T = TypeVar("T") @@ -3704,6 +4147,32 @@ def foo(self): pass self.assertIsSubclass(Bar, Functor) +class SpecificProtocolTests(BaseTestCase): + def test_reader_runtime_checkable(self): + class MyReader: + def read(self, n: int) -> bytes: + return b"" + + class WrongReader: + def readx(self, n: int) -> bytes: + return b"" + + self.assertIsInstance(MyReader(), typing_extensions.Reader) + self.assertNotIsInstance(WrongReader(), typing_extensions.Reader) + + def test_writer_runtime_checkable(self): + class MyWriter: + def write(self, b: bytes) -> int: + return 0 + + class WrongWriter: + def writex(self, b: bytes) -> int: + return 0 + + self.assertIsInstance(MyWriter(), typing_extensions.Writer) + self.assertNotIsInstance(WrongWriter(), typing_extensions.Writer) + + class Point2DGeneric(Generic[T], TypedDict): a: T b: T @@ -3735,9 +4204,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): - with self.assertWarns(DeprecationWarning): - 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): @@ -3760,18 +4228,25 @@ def test_basics_keywords_syntax(self): def test_typeddict_special_keyword_names(self): with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", cls=type, self=object, typename=str, _typename=int, - fields=list, _fields=dict) + fields=list, _fields=dict, + closed=bool, extra_items=bool) self.assertEqual(TD.__name__, 'TD') self.assertEqual(TD.__annotations__, {'cls': type, 'self': object, 'typename': str, - '_typename': int, 'fields': list, '_fields': dict}) + '_typename': int, 'fields': list, '_fields': dict, + 'closed': bool, 'extra_items': bool}) + self.assertIsNone(TD.__closed__) + self.assertIs(TD.__extra_items__, NoExtraItems) a = TD(cls=str, self=42, typename='foo', _typename=53, - fields=[('bar', tuple)], _fields={'baz', set}) + fields=[('bar', tuple)], _fields={'baz', set}, + closed=None, extra_items="tea pot") self.assertEqual(a['cls'], str) self.assertEqual(a['self'], 42) self.assertEqual(a['typename'], 'foo') self.assertEqual(a['_typename'], 53) self.assertEqual(a['fields'], [('bar', tuple)]) self.assertEqual(a['_fields'], {'baz', set}) + self.assertIsNone(a['closed']) + self.assertEqual(a['extra_items'], "tea pot") def test_typeddict_create_errors(self): with self.assertRaises(TypeError): @@ -3857,6 +4332,37 @@ def test_total(self): self.assertEqual(Options.__required_keys__, frozenset()) self.assertEqual(Options.__optional_keys__, {'log_level', 'log_path'}) + def test_total_inherits_non_total(self): + class TD1(TypedDict, total=False): + a: int + + self.assertIs(TD1.__total__, False) + + class TD2(TD1): + b: str + + self.assertIs(TD2.__total__, True) + + def test_total_with_assigned_value(self): + class TD(TypedDict): + __total__ = "some_value" + + self.assertIs(TD.__total__, True) + + class TD2(TypedDict, total=True): + __total__ = "some_value" + + self.assertIs(TD2.__total__, True) + + class TD3(TypedDict, total=False): + __total__ = "some value" + + self.assertIs(TD3.__total__, False) + + TD4 = TypedDict('TD4', {'__total__': "some_value"}) # noqa: F821 + self.assertIs(TD4.__total__, True) + + def test_optional_keys(self): class Point2Dor3D(Point2D, total=False): z: int @@ -4003,24 +4509,6 @@ 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), @@ -4037,6 +4525,53 @@ class ChildClosed(Unclosed, Closed): class Wrong(*bases): pass + def test_closed_values(self): + class Implicit(TypedDict): ... + class ExplicitTrue(TypedDict, closed=True): ... + class ExplicitFalse(TypedDict, closed=False): ... + + self.assertIsNone(Implicit.__closed__) + self.assertIs(ExplicitTrue.__closed__, True) + self.assertIs(ExplicitFalse.__closed__, False) + + + @skipIf(TYPING_3_14_0, "only supported on older versions") + def test_closed_typeddict_compat(self): + class Closed(TypedDict, closed=True): + __extra_items__: None + + class Unclosed(TypedDict, closed=False): + ... + + class ChildUnclosed(Closed, Unclosed): + ... + + self.assertIsNone(ChildUnclosed.__closed__) + self.assertEqual(ChildUnclosed.__extra_items__, NoExtraItems) + + class ChildClosed(Unclosed, Closed): + ... + + self.assertIsNone(ChildClosed.__closed__) + self.assertEqual(ChildClosed.__extra_items__, NoExtraItems) + + def test_extra_items_class_arg(self): + class TD(TypedDict, extra_items=int): + a: str + + self.assertIs(TD.__extra_items__, int) + self.assertEqual(TD.__annotations__, {'a': str}) + self.assertEqual(TD.__required_keys__, frozenset({'a'})) + self.assertEqual(TD.__optional_keys__, frozenset()) + + class NoExtra(TypedDict): + a: str + + self.assertIs(NoExtra.__extra_items__, NoExtraItems) + self.assertEqual(NoExtra.__annotations__, {'a': str}) + self.assertEqual(NoExtra.__required_keys__, frozenset({'a'})) + self.assertEqual(NoExtra.__optional_keys__, frozenset()) + def test_is_typeddict(self): self.assertIs(is_typeddict(Point2D), True) self.assertIs(is_typeddict(Point2Dor3D), True) @@ -4084,7 +4619,7 @@ class PointDict3D(PointDict2D, total=False): assert is_typeddict(PointDict2D) is True assert is_typeddict(PointDict3D) is True - @skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9") + @skipUnless(HAS_FORWARD_MODULE, "ForwardRef.__forward_module__ was added in 3.9.7") def test_get_type_hints_cross_module_subclass(self): self.assertNotIn("_DoNotImport", globals()) self.assertEqual( @@ -4178,7 +4713,6 @@ class C(B[int]): with self.assertRaises(TypeError): C[str] - class Point3D(Point2DGeneric[T], Generic[T, KT]): c: KT @@ -4228,11 +4762,9 @@ class WithImplicitAny(B): with self.assertRaises(TypeError): WithImplicitAny[str] - @skipUnless(TYPING_3_9_0, "Was changed in 3.9") def test_non_generic_subscript(self): # For backward compatibility, subscription works # on arbitrary TypedDict types. - # (But we don't attempt to backport this misfeature onto 3.8.) class TD(TypedDict): a: T A = TD[int] @@ -4336,13 +4868,13 @@ class Child1(Base1): self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) class Base2(TypedDict): - a: ReadOnly[int] + a: int class Child2(Base2): - b: str + b: ReadOnly[str] - self.assertEqual(Child1.__readonly_keys__, frozenset({'a'})) - self.assertEqual(Child1.__mutable_keys__, frozenset({'b'})) + self.assertEqual(Child2.__readonly_keys__, frozenset({'b'})) + self.assertEqual(Child2.__mutable_keys__, frozenset({'a'})) def test_make_mutable_key_readonly(self): class Base(TypedDict): @@ -4393,7 +4925,8 @@ class AllTheThings(TypedDict): }, ) - def test_extra_keys_non_readonly(self): + @skipIf(TYPING_3_14_0, "Old syntax only supported on <3.14") + def test_extra_keys_non_readonly_legacy(self): class Base(TypedDict, closed=True): __extra_items__: str @@ -4405,7 +4938,8 @@ class Child(Base): self.assertEqual(Child.__readonly_keys__, frozenset({})) self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) - def test_extra_keys_readonly(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_keys_readonly_legacy(self): class Base(TypedDict, closed=True): __extra_items__: ReadOnly[str] @@ -4417,7 +4951,21 @@ class Child(Base): self.assertEqual(Child.__readonly_keys__, frozenset({})) self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) - def test_extra_key_required(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_keys_readonly_explicit_closed_legacy(self): + class Base(TypedDict, closed=True): + __extra_items__: ReadOnly[str] + + class Child(Base, closed=True): + 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'})) + + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_extra_key_required_legacy(self): with self.assertRaisesRegex( TypeError, "Special key __extra_items__ does not support Required" @@ -4430,7 +4978,7 @@ def test_extra_key_required(self): ): TypedDict("A", {"__extra_items__": NotRequired[int]}, closed=True) - def test_regular_extra_items(self): + def test_regular_extra_items_legacy(self): class ExtraReadOnly(TypedDict): __extra_items__: ReadOnly[str] @@ -4438,8 +4986,8 @@ class ExtraReadOnly(TypedDict): 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__) + self.assertIs(ExtraReadOnly.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraReadOnly.__closed__) class ExtraRequired(TypedDict): __extra_items__: Required[str] @@ -4448,8 +4996,8 @@ class ExtraRequired(TypedDict): 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__) + self.assertIs(ExtraRequired.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraRequired.__closed__) class ExtraNotRequired(TypedDict): __extra_items__: NotRequired[str] @@ -4458,10 +5006,11 @@ class ExtraNotRequired(TypedDict): 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__) + self.assertIs(ExtraNotRequired.__extra_items__, NoExtraItems) + self.assertIsNone(ExtraNotRequired.__closed__) - def test_closed_inheritance(self): + @skipIf(TYPING_3_14_0, "Only supported on <3.14") + def test_closed_inheritance_legacy(self): class Base(TypedDict, closed=True): __extra_items__: ReadOnly[Union[str, None]] @@ -4471,49 +5020,97 @@ class Base(TypedDict, closed=True): self.assertEqual(Base.__mutable_keys__, frozenset({})) self.assertEqual(Base.__annotations__, {}) self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) - self.assertTrue(Base.__closed__) + self.assertIs(Base.__closed__, True) - class Child(Base): + class Child(Base, closed=True): a: int __extra_items__: int - self.assertEqual(Child.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(Child.__required_keys__, frozenset({'a'})) 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__) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": int}) + self.assertIs(Child.__extra_items__, int) + self.assertIs(Child.__closed__, True) class GrandChild(Child, closed=True): __extra_items__: str - self.assertEqual(GrandChild.__required_keys__, frozenset({'a', "__extra_items__"})) + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": int}) + self.assertIs(GrandChild.__extra_items__, str) + self.assertIs(GrandChild.__closed__, True) + + def test_closed_inheritance(self): + class Base(TypedDict, extra_items=ReadOnly[Union[str, None]]): + a: int + + self.assertEqual(Base.__required_keys__, frozenset({"a"})) + self.assertEqual(Base.__optional_keys__, frozenset({})) + self.assertEqual(Base.__readonly_keys__, frozenset({})) + self.assertEqual(Base.__mutable_keys__, frozenset({"a"})) + self.assertEqual(Base.__annotations__, {"a": int}) + self.assertEqual(Base.__extra_items__, ReadOnly[Union[str, None]]) + self.assertIsNone(Base.__closed__) + + class Child(Base, extra_items=int): + a: str + + self.assertEqual(Child.__required_keys__, frozenset({'a'})) + self.assertEqual(Child.__optional_keys__, frozenset({})) + self.assertEqual(Child.__readonly_keys__, frozenset({})) + self.assertEqual(Child.__mutable_keys__, frozenset({'a'})) + self.assertEqual(Child.__annotations__, {"a": str}) + self.assertIs(Child.__extra_items__, int) + self.assertIsNone(Child.__closed__) + + class GrandChild(Child, closed=True): + a: float + + self.assertEqual(GrandChild.__required_keys__, frozenset({'a'})) 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__) + self.assertEqual(GrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandChild.__annotations__, {"a": float}) + self.assertIs(GrandChild.__extra_items__, NoExtraItems) + self.assertIs(GrandChild.__closed__, True) + + class GrandGrandChild(GrandChild): + ... + self.assertEqual(GrandGrandChild.__required_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__optional_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__readonly_keys__, frozenset({})) + self.assertEqual(GrandGrandChild.__mutable_keys__, frozenset({'a'})) + self.assertEqual(GrandGrandChild.__annotations__, {"a": float}) + self.assertIs(GrandGrandChild.__extra_items__, NoExtraItems) + self.assertIsNone(GrandGrandChild.__closed__) def test_implicit_extra_items(self): class Base(TypedDict): a: int - self.assertEqual(Base.__extra_items__, None) - self.assertFalse(Base.__closed__) + self.assertIs(Base.__extra_items__, NoExtraItems) + self.assertIsNone(Base.__closed__) class ChildA(Base, closed=True): ... - self.assertEqual(ChildA.__extra_items__, Never) - self.assertTrue(ChildA.__closed__) + self.assertEqual(ChildA.__extra_items__, NoExtraItems) + self.assertIs(ChildA.__closed__, True) + @skipIf(TYPING_3_14_0, "Backwards compatibility only for Python 3.13") + def test_implicit_extra_items_before_3_14(self): + class Base(TypedDict): + a: int class ChildB(Base, closed=True): __extra_items__: None - self.assertEqual(ChildB.__extra_items__, type(None)) - self.assertTrue(ChildB.__closed__) + self.assertIs(ChildB.__extra_items__, type(None)) + self.assertIs(ChildB.__closed__, True) @skipIf( TYPING_3_13_0, @@ -4523,29 +5120,157 @@ class ChildB(Base, closed=True): def test_backwards_compatibility(self): with self.assertWarns(DeprecationWarning): TD = TypedDict("TD", closed=int) - self.assertFalse(TD.__closed__) + self.assertIs(TD.__closed__, None) self.assertEqual(TD.__annotations__, {"closed": int}) + with self.assertWarns(DeprecationWarning): + TD = TypedDict("TD", extra_items=int) + self.assertIs(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__annotations__, {"extra_items": int}) -class AnnotatedTests(BaseTestCase): + def test_cannot_combine_closed_and_extra_items(self): + with self.assertRaisesRegex( + TypeError, + "Cannot combine closed=True and extra_items" + ): + class TD(TypedDict, closed=True, extra_items=range): + x: str - def test_repr(self): - if hasattr(typing, 'Annotated'): - mod_name = 'typing' - else: - mod_name = "typing_extensions" - self.assertEqual( - repr(Annotated[int, 4, 5]), - mod_name + ".Annotated[int, 4, 5]" - ) - self.assertEqual( - repr(Annotated[List[int], 4, 5]), - mod_name + ".Annotated[typing.List[int], 4, 5]" + def test_typed_dict_signature(self): + self.assertListEqual( + list(inspect.signature(TypedDict).parameters), + ['typename', 'fields', 'total', 'closed', 'extra_items', 'kwargs'] ) - def test_flatten(self): - A = Annotated[Annotated[int, 4], 5] - self.assertEqual(A, Annotated[int, 4, 5]) + def test_inline_too_many_arguments(self): + with self.assertRaises(TypeError): + TypedDict[{"a": int}, "extra"] + + def test_inline_not_a_dict(self): + with self.assertRaises(TypeError): + TypedDict["not_a_dict"] + + # a tuple of elements isn't allowed, even if the first element is a dict: + with self.assertRaises(TypeError): + TypedDict[({"key": int},)] + + def test_inline_empty(self): + TD = TypedDict[{}] + self.assertIs(TD.__total__, True) + self.assertIs(TD.__closed__, True) + self.assertEqual(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__required_keys__, set()) + self.assertEqual(TD.__optional_keys__, set()) + self.assertEqual(TD.__readonly_keys__, set()) + self.assertEqual(TD.__mutable_keys__, set()) + + def test_inline(self): + TD = TypedDict[{ + "a": int, + "b": Required[int], + "c": NotRequired[int], + "d": ReadOnly[int], + }] + self.assertIsSubclass(TD, dict) + self.assertIsSubclass(TD, typing.MutableMapping) + self.assertNotIsSubclass(TD, collections.abc.Sequence) + self.assertTrue(is_typeddict(TD)) + self.assertEqual(TD.__name__, "") + self.assertEqual( + TD.__annotations__, + {"a": int, "b": Required[int], "c": NotRequired[int], "d": ReadOnly[int]}, + ) + self.assertEqual(TD.__module__, __name__) + self.assertEqual(TD.__bases__, (dict,)) + self.assertIs(TD.__total__, True) + self.assertIs(TD.__closed__, True) + self.assertEqual(TD.__extra_items__, NoExtraItems) + self.assertEqual(TD.__required_keys__, {"a", "b", "d"}) + self.assertEqual(TD.__optional_keys__, {"c"}) + self.assertEqual(TD.__readonly_keys__, {"d"}) + self.assertEqual(TD.__mutable_keys__, {"a", "b", "c"}) + + inst = TD(a=1, b=2, d=3) + self.assertIs(type(inst), dict) + self.assertEqual(inst["a"], 1) + + def test_annotations(self): + # _type_check is applied + with self.assertRaisesRegex(TypeError, "Plain typing.Optional is not valid as type argument"): + class X(TypedDict): + a: Optional + + # _type_convert is applied + class Y(TypedDict): + a: None + b: "int" + if sys.version_info >= (3, 14): + import annotationlib + + fwdref = EqualToForwardRef('int', module=__name__) + self.assertEqual(Y.__annotations__, {'a': type(None), 'b': fwdref}) + self.assertEqual(Y.__annotate__(annotationlib.Format.FORWARDREF), {'a': type(None), 'b': fwdref}) + else: + self.assertEqual(Y.__annotations__, {'a': type(None), 'b': typing.ForwardRef('int', module=__name__)}) + + @skipUnless(TYPING_3_14_0, "Only supported on 3.14") + def test_delayed_type_check(self): + # _type_check is also applied later + class Z(TypedDict): + a: undefined # noqa: F821 + + with self.assertRaises(NameError): + Z.__annotations__ + + undefined = Final + with self.assertRaisesRegex(TypeError, "Plain typing.Final is not valid as type argument"): + Z.__annotations__ + + undefined = None # noqa: F841 + self.assertEqual(Z.__annotations__, {'a': type(None)}) + + @skipUnless(TYPING_3_14_0, "Only supported on 3.14") + def test_deferred_evaluation(self): + class A(TypedDict): + x: NotRequired[undefined] # noqa: F821 + y: ReadOnly[undefined] # noqa: F821 + z: Required[undefined] # noqa: F821 + + self.assertEqual(A.__required_keys__, frozenset({'y', 'z'})) + self.assertEqual(A.__optional_keys__, frozenset({'x'})) + self.assertEqual(A.__readonly_keys__, frozenset({'y'})) + self.assertEqual(A.__mutable_keys__, frozenset({'x', 'z'})) + + with self.assertRaises(NameError): + A.__annotations__ + + import annotationlib + self.assertEqual( + A.__annotate__(annotationlib.Format.STRING), + {'x': 'NotRequired[undefined]', 'y': 'ReadOnly[undefined]', + 'z': 'Required[undefined]'}, + ) + + +class AnnotatedTests(BaseTestCase): + + def test_repr(self): + if hasattr(typing, 'Annotated'): + mod_name = 'typing' + else: + mod_name = "typing_extensions" + self.assertEqual( + repr(Annotated[int, 4, 5]), + mod_name + ".Annotated[int, 4, 5]" + ) + self.assertEqual( + repr(Annotated[List[int], 4, 5]), + mod_name + ".Annotated[typing.List[int], 4, 5]" + ) + + def test_flatten(self): + A = Annotated[Annotated[int, 4], 5] + self.assertEqual(A, Annotated[int, 4, 5]) self.assertEqual(A.__metadata__, (4, 5)) self.assertEqual(A.__origin__, int) @@ -4617,7 +5342,7 @@ class C: A.x = 5 self.assertEqual(C.x, 5) - @skipIf(sys.version_info[:2] in ((3, 9), (3, 10)), "Waiting for bpo-46491 bugfix.") + @skipIf(sys.version_info[:2] == (3, 10), "Waiting for https://github.com/python/cpython/issues/90649 bugfix.") def test_special_form_containment(self): class C: classvar: Annotated[ClassVar[int], "a decoration"] = 4 @@ -4703,6 +5428,19 @@ 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",)) + + def test_compatibility(self): + # Test that the _AnnotatedAlias compatibility alias works + self.assertTrue(hasattr(typing_extensions, "_AnnotatedAlias")) + self.assertIs(typing_extensions._AnnotatedAlias, typing._AnnotatedAlias) + class GetTypeHintsTests(BaseTestCase): def test_get_type_hints(self): @@ -4826,7 +5564,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): @@ -4849,7 +5587,7 @@ class C(TypeAlias): pass with self.assertRaises(TypeError): - class C(type(TypeAlias)): + class D(type(TypeAlias)): pass def test_repr(self): @@ -4921,21 +5659,20 @@ def test_valid_uses(self): self.assertEqual(C2.__parameters__, (P, T)) # Test collections.abc.Callable too. - if sys.version_info[:2] >= (3, 9): - # Note: no tests for Callable.__parameters__ here - # because types.GenericAlias Callable is hardcoded to search - # for tp_name "TypeVar" in C. This was changed in 3.10. - C3 = collections.abc.Callable[P, int] - self.assertEqual(C3.__args__, (P, int)) - C4 = collections.abc.Callable[P, T] - self.assertEqual(C4.__args__, (P, T)) + # Note: no tests for Callable.__parameters__ here + # because types.GenericAlias Callable is hardcoded to search + # for tp_name "TypeVar" in C. This was changed in 3.10. + C3 = collections.abc.Callable[P, int] + self.assertEqual(C3.__args__, (P, int)) + C4 = collections.abc.Callable[P, T] + self.assertEqual(C4.__args__, (P, T)) # ParamSpec instances should also have args and kwargs attributes. # Note: not in dir(P) because of __class__ hacks self.assertTrue(hasattr(P, 'args')) self.assertTrue(hasattr(P, 'kwargs')) - @skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs bpo-46676.") + @skipIf((3, 10, 0) <= sys.version_info[:3] <= (3, 10, 2), "Needs https://github.com/python/cpython/issues/90834.") def test_args_kwargs(self): P = ParamSpec('P') P_2 = ParamSpec('P_2') @@ -4967,6 +5704,7 @@ class X(Generic[T, P]): class Y(Protocol[T, P]): pass + things = "arguments" if sys.version_info >= (3, 10) else "parameters" for klass in X, Y: with self.subTest(klass=klass.__name__): G1 = klass[int, P_2] @@ -4977,13 +5715,73 @@ class Y(Protocol[T, P]): self.assertEqual(G2.__args__, (int, Concatenate[int, P_2])) self.assertEqual(G2.__parameters__, (P_2,)) + G3 = klass[int, Concatenate[int, ...]] + self.assertEqual(G3.__args__, (int, Concatenate[int, ...])) + self.assertEqual(G3.__parameters__, ()) + + with self.assertRaisesRegex( + TypeError, + f"Too few {things} for {klass}" + ): + klass[int] + # The following are some valid uses cases in PEP 612 that don't work: # These do not work in 3.9, _type_check blocks the list and ellipsis. # G3 = X[int, [int, bool]] # G4 = X[int, ...] # G5 = Z[[int, str, bool]] - # Not working because this is special-cased in 3.10. - # G6 = Z[int, str, bool] + + def test_single_argument_generic(self): + P = ParamSpec("P") + T = TypeVar("T") + P_2 = ParamSpec("P_2") + + class Z(Generic[P]): + pass + + class ProtoZ(Protocol[P]): + pass + + for klass in Z, ProtoZ: + with self.subTest(klass=klass.__name__): + # Note: For 3.10+ __args__ are nested tuples here ((int, ),) instead of (int, ) + G6 = klass[int, str, T] + G6args = G6.__args__[0] if sys.version_info >= (3, 10) else G6.__args__ + self.assertEqual(G6args, (int, str, T)) + self.assertEqual(G6.__parameters__, (T,)) + + # P = [int] + G7 = klass[int] + G7args = G7.__args__[0] if sys.version_info >= (3, 10) else G7.__args__ + self.assertEqual(G7args, (int,)) + self.assertEqual(G7.__parameters__, ()) + + G8 = klass[Concatenate[T, ...]] + self.assertEqual(G8.__args__, (Concatenate[T, ...], )) + self.assertEqual(G8.__parameters__, (T,)) + + G9 = klass[Concatenate[T, P_2]] + self.assertEqual(G9.__args__, (Concatenate[T, P_2], )) + + # This is an invalid form but useful for testing correct subsitution + G10 = klass[int, Concatenate[str, P]] + G10args = G10.__args__[0] if sys.version_info >= (3, 10) else G10.__args__ + self.assertEqual(G10args, (int, Concatenate[str, P], )) + + @skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10") + def test_is_param_expr(self): + P = ParamSpec("P") + P_typing = typing.ParamSpec("P_typing") + self.assertTrue(typing_extensions._is_param_expr(P)) + self.assertTrue(typing_extensions._is_param_expr(P_typing)) + if hasattr(typing, "_is_param_expr"): + self.assertTrue(typing._is_param_expr(P)) + self.assertTrue(typing._is_param_expr(P_typing)) + + def test_single_argument_generic_with_parameter_expressions(self): + P = ParamSpec("P") + T = TypeVar("T") + P_2 = ParamSpec("P_2") class Z(Generic[P]): pass @@ -4991,6 +5789,74 @@ class Z(Generic[P]): class ProtoZ(Protocol[P]): pass + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + for klass in Z, ProtoZ: + with self.subTest(klass=klass.__name__): + G8 = klass[Concatenate[T, ...]] + + H8_1 = G8[int] + self.assertEqual(H8_1.__parameters__, ()) + with self.assertRaisesRegex(TypeError, "not a generic class"): + H8_1[str] + + H8_2 = G8[T][int] + self.assertEqual(H8_2.__parameters__, ()) + with self.assertRaisesRegex(TypeError, "not a generic class"): + H8_2[str] + + G9 = klass[Concatenate[T, P_2]] + self.assertEqual(G9.__parameters__, (T, P_2)) + + with self.assertRaisesRegex(TypeError, + "The last parameter to Concatenate should be a ParamSpec variable or ellipsis." + if sys.version_info < (3, 10) else + # from __typing_subst__ + "Expected a list of types, an ellipsis, ParamSpec, or Concatenate" + ): + G9[int, int] + + with self.assertRaisesRegex(TypeError, f"Too few {things}"): + G9[int] + + with self.subTest("Check list as parameter expression", klass=klass.__name__): + if sys.version_info < (3, 10): + self.skipTest("Cannot pass non-types") + G5 = klass[[int, str, T]] + self.assertEqual(G5.__parameters__, (T,)) + self.assertEqual(G5.__args__, ((int, str, T),)) + + H9 = G9[int, [T]] + self.assertEqual(H9.__parameters__, (T,)) + + # This is an invalid parameter expression but useful for testing correct subsitution + G10 = klass[int, Concatenate[str, P]] + with self.subTest("Check invalid form substitution"): + self.assertEqual(G10.__parameters__, (P, )) + H10 = G10[int] + if (3, 10) <= sys.version_info < (3, 11, 3): + self.skipTest("3.10-3.11.2 does not substitute Concatenate here") + self.assertEqual(H10.__parameters__, ()) + H10args = H10.__args__[0] if sys.version_info >= (3, 10) else H10.__args__ + self.assertEqual(H10args, (int, (str, int))) + + @skipUnless(TYPING_3_10_0, "ParamSpec not present before 3.10") + def test_substitution_with_typing_variants(self): + # verifies substitution and typing._check_generic working with typing variants + P = ParamSpec("P") + typing_P = typing.ParamSpec("typing_P") + typing_Concatenate = typing.Concatenate[int, P] + + class Z(Generic[typing_P]): + pass + + P1 = Z[typing_P] + self.assertEqual(P1.__parameters__, (typing_P,)) + self.assertEqual(P1.__args__, (typing_P,)) + + C1 = Z[typing_Concatenate] + self.assertEqual(C1.__parameters__, (P,)) + self.assertEqual(C1.__args__, (typing_Concatenate,)) + def test_pickle(self): global P, P_co, P_contra, P_default P = ParamSpec('P') @@ -5072,17 +5938,38 @@ class MyClass: ... c = Concatenate[MyClass, P] self.assertNotEqual(c, Concatenate) - def test_valid_uses(self): + # Test Ellipsis Concatenation + d = Concatenate[MyClass, ...] + self.assertNotEqual(d, c) + self.assertNotEqual(d, Concatenate) + + @skipUnless(TYPING_3_10_0, "Concatenate not available in <3.10") + def test_typing_compatibility(self): P = ParamSpec('P') - T = TypeVar('T') + C1 = Concatenate[int, P][typing.Concatenate[int, P]] + self.assertEqual(C1, Concatenate[int, int, P]) + self.assertEqual(get_args(C1), (int, int, P)) - C1 = Callable[Concatenate[int, P], int] - C2 = Callable[Concatenate[int, T, P], T] + C2 = typing.Concatenate[int, P][Concatenate[int, P]] + with self.subTest("typing compatibility with typing_extensions"): + if sys.version_info < (3, 10, 3): + self.skipTest("Unpacking not introduced until 3.10.3") + self.assertEqual(get_args(C2), (int, int, P)) - # 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] + def test_valid_uses(self): + P = ParamSpec('P') + T = TypeVar('T') + for callable_variant in (Callable, collections.abc.Callable): + with self.subTest(callable_variant=callable_variant): + C1 = callable_variant[Concatenate[int, P], int] + C2 = callable_variant[Concatenate[int, T, P], T] + self.assertEqual(C1.__origin__, C2.__origin__) + self.assertNotEqual(C1, C2) + + C3 = callable_variant[Concatenate[int, ...], int] + C4 = callable_variant[Concatenate[int, T, ...], T] + self.assertEqual(C3.__origin__, C4.__origin__) + self.assertNotEqual(C3, C4) def test_invalid_uses(self): P = ParamSpec('P') @@ -5096,25 +5983,54 @@ def test_invalid_uses(self): with self.assertRaisesRegex( TypeError, - 'The last parameter to Concatenate should be a ParamSpec variable', + 'The last parameter to Concatenate should be a ParamSpec variable or ellipsis', ): Concatenate[P, T] - if not TYPING_3_11_0: - with self.assertRaisesRegex( - TypeError, - 'each arg must be a type', - ): - Concatenate[1, P] + # Test with tuple argument + with self.assertRaisesRegex( + TypeError, + "The last parameter to Concatenate should be a ParamSpec variable or ellipsis.", + ): + Concatenate[(P, T)] + + with self.assertRaisesRegex( + TypeError, + 'is not a generic class', + ): + Callable[Concatenate[int, ...], Any][Any] + + # Assure that `_type_check` is called. + P = ParamSpec('P') + with self.assertRaisesRegex( + TypeError, + "each arg must be a type", + ): + Concatenate[(str,), P] + + @skipUnless(TYPING_3_10_0, "Missing backport to 3.9. See issue #48") + def test_alias_subscription_with_ellipsis(self): + P = ParamSpec('P') + X = Callable[Concatenate[int, P], Any] + + C1 = X[...] + self.assertEqual(C1.__parameters__, ()) + self.assertEqual(get_args(C1), (Concatenate[int, ...], Any)) def test_basic_introspection(self): P = ParamSpec('P') C1 = Concatenate[int, P] C2 = Concatenate[int, T, P] + C3 = Concatenate[int, ...] + C4 = Concatenate[int, T, ...] self.assertEqual(C1.__origin__, Concatenate) self.assertEqual(C1.__args__, (int, P)) self.assertEqual(C2.__origin__, Concatenate) self.assertEqual(C2.__args__, (int, T, P)) + self.assertEqual(C3.__origin__, Concatenate) + self.assertEqual(C3.__args__, (int, Ellipsis)) + self.assertEqual(C4.__origin__, Concatenate) + self.assertEqual(C4.__args__, (int, T, Ellipsis)) def test_eq(self): P = ParamSpec('P') @@ -5125,6 +6041,50 @@ def test_eq(self): self.assertEqual(hash(C1), hash(C2)) self.assertNotEqual(C1, C3) + C4 = Concatenate[int, ...] + C5 = Concatenate[int, ...] + C6 = Concatenate[int, T, ...] + self.assertEqual(C4, C5) + self.assertEqual(hash(C4), hash(C5)) + self.assertNotEqual(C4, C6) + + def test_substitution(self): + T = TypeVar('T') + P = ParamSpec('P') + Ts = TypeVarTuple("Ts") + + C1 = Concatenate[str, T, ...] + self.assertEqual(C1[int], Concatenate[str, int, ...]) + + C2 = Concatenate[str, P] + self.assertEqual(C2[...], Concatenate[str, ...]) + self.assertEqual(C2[int], (str, int)) + U1 = Unpack[Tuple[int, str]] + U2 = Unpack[Ts] + self.assertEqual(C2[U1], (str, int, str)) + self.assertEqual(C2[U2], (str, Unpack[Ts])) + self.assertEqual(C2["U2"], (str, EqualToForwardRef("U2"))) + + if (3, 12, 0) <= sys.version_info < (3, 12, 4): + with self.assertRaises(AssertionError): + C2[Unpack[U2]] + else: + with self.assertRaisesRegex(TypeError, "must be used with a tuple type"): + C2[Unpack[U2]] + + C3 = Concatenate[str, T, P] + self.assertEqual(C3[int, [bool]], (str, int, bool)) + + @skipUnless(TYPING_3_10_0, "Concatenate not present before 3.10") + def test_is_param_expr(self): + P = ParamSpec('P') + concat = Concatenate[str, P] + typing_concat = typing.Concatenate[str, P] + self.assertTrue(typing_extensions._is_param_expr(concat)) + self.assertTrue(typing_extensions._is_param_expr(typing_concat)) + if hasattr(typing, "_is_param_expr"): + self.assertTrue(typing._is_param_expr(concat)) + self.assertTrue(typing._is_param_expr(typing_concat)) class TypeGuardTests(BaseTestCase): def test_basics(self): @@ -5152,7 +6112,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): @@ -5196,7 +6156,7 @@ def test_cannot_subclass(self): class C(type(TypeIs)): pass with self.assertRaises(TypeError): - class C(type(TypeIs[int])): + class D(type(TypeIs[int])): pass def test_cannot_init(self): @@ -5214,6 +6174,64 @@ def test_no_isinstance(self): issubclass(int, TypeIs) +class TypeFormTests(BaseTestCase): + def test_basics(self): + TypeForm[int] # OK + self.assertEqual(TypeForm[int], TypeForm[int]) + + def foo(arg) -> TypeForm[int]: ... + self.assertEqual(gth(foo), {'return': TypeForm[int]}) + + def test_repr(self): + if hasattr(typing, 'TypeForm'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(TypeForm), f'{mod_name}.TypeForm') + cv = TypeForm[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[int]') + cv = TypeForm[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[{__name__}.Employee]') + cv = TypeForm[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeForm[typing.Tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(TypeForm)): + pass + with self.assertRaises(TypeError): + class D(type(TypeForm[int])): + pass + + def test_call(self): + objs = [ + 1, + "int", + int, + Tuple[int, str], + ] + for obj in objs: + with self.subTest(obj=obj): + self.assertIs(TypeForm(obj), obj) + + with self.assertRaises(TypeError): + TypeForm() + with self.assertRaises(TypeError): + TypeForm("too", "many") + + def test_cannot_init_type(self): + with self.assertRaises(TypeError): + type(TypeForm)() + with self.assertRaises(TypeError): + type(TypeForm[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeForm[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeForm) + + class LiteralStringTests(BaseTestCase): def test_basics(self): class Foo: @@ -5231,7 +6249,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): @@ -5242,7 +6260,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): @@ -5285,7 +6303,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): @@ -5427,6 +6445,53 @@ class D(Protocol[T1, T2, Unpack[Ts]]): pass with self.assertRaises(TypeError): klass[int] + def test_substitution(self): + Ts = TypeVarTuple("Ts") + unpacked_str = Unpack[Ts][str] # This should not raise an error + self.assertIs(unpacked_str, str) + + @skipUnless(TYPING_3_11_0, "Needs Issue #103 for <3.11") + def test_nested_unpack(self): + Ts = TypeVarTuple("Ts") + Variadic = Tuple[int, Unpack[Ts]] + # Tuple[int, int, Tuple[str, int]] + direct_subscription = Variadic[int, Tuple[str, int]] + # Tuple[int, int, Tuple[*Ts, int]] + TupleAliasTs = Variadic[int, Tuple[Unpack[Ts], int]] + + # Tuple[int, int, Tuple[str, int]] + recursive_unpack = TupleAliasTs[str] + self.assertEqual(direct_subscription, recursive_unpack) + self.assertEqual(get_args(recursive_unpack), (int, int, Tuple[str, int])) + + # Test with Callable + T = TypeVar("T") + # Tuple[int, (*Ts) -> T] + CallableAliasTsT = Variadic[Callable[[Unpack[Ts]], T]] + # Tuple[int, (str, int) -> object] + callable_fully_subscripted = CallableAliasTsT[Unpack[Tuple[str, int]], object] + self.assertEqual(get_args(callable_fully_subscripted), (int, Callable[[str, int], object])) + + @skipUnless(TYPING_3_11_0, "Needs Issue #103 for <3.11") + def test_equivalent_nested_variadics(self): + T = TypeVar("T") + Ts = TypeVarTuple("Ts") + Variadic = Tuple[int, Unpack[Ts]] + TupleAliasTsT = Variadic[Tuple[Unpack[Ts], T]] + nested_tuple_bare = TupleAliasTsT[str, int, object] + + self.assertEqual(get_args(nested_tuple_bare), (int, Tuple[str, int, object])) + # Variants + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int, object]]]) + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str, int]], object]) + self.assertEqual(nested_tuple_bare, TupleAliasTsT[Unpack[Tuple[str]], Unpack[Tuple[int]], object]) + + @skipUnless(TYPING_3_11_0, "Needed for backport") + def test_type_var_inheritance(self): + Ts = TypeVarTuple("Ts") + self.assertFalse(isinstance(Unpack[Ts], TypeVar)) + self.assertFalse(isinstance(Unpack[Ts], typing.TypeVar)) + class TypeVarTupleTests(BaseTestCase): @@ -5544,7 +6609,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 @@ -5737,7 +6802,7 @@ def test_typing_extensions_defers_when_possible(self): if sys.version_info < (3, 10, 1): exclude |= {"Literal"} if sys.version_info < (3, 11): - exclude |= {'final', 'Any', 'NewType', 'overload'} + exclude |= {'final', 'Any', 'NewType', 'overload', 'Concatenate'} if sys.version_info < (3, 12): exclude |= { 'SupportsAbs', 'SupportsBytes', @@ -5750,6 +6815,10 @@ def test_typing_extensions_defers_when_possible(self): 'AsyncGenerator', 'ContextManager', 'AsyncContextManager', 'ParamSpec', 'TypeVar', 'TypeVarTuple', 'get_type_hints', } + if sys.version_info < (3, 14): + exclude |= { + 'TypeAliasType' + } if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: @@ -5758,6 +6827,15 @@ def test_typing_extensions_defers_when_possible(self): getattr(typing_extensions, item), getattr(typing, item)) + def test_alias_names_still_exist(self): + for name in typing_extensions._typing_names: + # If this fails, change _typing_names to conditionally add the name + # depending on the Python version. + self.assertTrue( + hasattr(typing_extensions, name), + f"{name} no longer exists in typing", + ) + def test_typing_extensions_compiles_with_opt(self): file_path = typing_extensions.__file__ try: @@ -5785,17 +6863,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 @@ -5887,11 +6954,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): @@ -5900,7 +6967,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): @@ -5933,7 +7000,6 @@ class Y(Generic[T], NamedTuple): with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] - @skipUnless(TYPING_3_9_0, "tuple.__class_getitem__ was added in 3.9") def test_non_generic_subscript_py39_plus(self): # For backward compatibility, subscription works # on arbitrary NamedTuple types. @@ -5948,19 +7014,6 @@ class Group(NamedTuple): self.assertIs(type(a), Group) self.assertEqual(a, (1, [2])) - @skipIf(TYPING_3_9_0, "Test isn't relevant to 3.9+") - def test_non_generic_subscript_error_message_py38(self): - class Group(NamedTuple): - key: T - group: List[T] - - with self.assertRaisesRegex(TypeError, 'not subscriptable'): - Group[int] - - for attr in ('__args__', '__origin__', '__parameters__'): - with self.subTest(attr=attr): - self.assertFalse(hasattr(Group, attr)) - def test_namedtuple_keyword_usage(self): with self.assertWarnsRegex( DeprecationWarning, @@ -6079,21 +7132,13 @@ def test_copy_and_pickle(self): def test_docstring(self): self.assertIsInstance(NamedTuple.__doc__, str) - @skipUnless(TYPING_3_9_0, "NamedTuple was a class on 3.8 and lower") - def test_same_as_typing_NamedTuple_39_plus(self): + def test_same_as_typing_NamedTuple(self): self.assertEqual( set(dir(NamedTuple)) - {"__text_signature__"}, set(dir(typing.NamedTuple)) ) self.assertIs(type(NamedTuple), type(typing.NamedTuple)) - @skipIf(TYPING_3_9_0, "tests are only relevant to <=3.8") - def test_same_as_typing_NamedTuple_38_minus(self): - self.assertEqual( - self.NestedEmployee.__annotations__, - self.NestedEmployee._field_types - ) - def test_orig_bases(self): T = TypeVar('T') @@ -6156,11 +7201,6 @@ 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), RuntimeError) self.assertIs(type(namedtuple_exception), type(normal_exception)) self.assertEqual(len(namedtuple_exception.args), len(normal_exception.args)) @@ -6319,8 +7359,8 @@ def test_or(self): self.assertEqual(X | "x", Union[X, "x"]) self.assertEqual("x" | X, Union["x", X]) # make sure the order is correct - self.assertEqual(get_args(X | "x"), (X, typing.ForwardRef("x"))) - self.assertEqual(get_args("x" | X), (typing.ForwardRef("x"), X)) + self.assertEqual(get_args(X | "x"), (X, EqualToForwardRef("x"))) + self.assertEqual(get_args("x" | X), (EqualToForwardRef("x"), X)) def test_union_constrained(self): A = TypeVar('A', str, bytes) @@ -6345,7 +7385,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): @@ -6353,18 +7393,15 @@ def test_cannot_instantiate_vars(self): def test_bound_errors(self): with self.assertRaises(TypeError): - TypeVar('X', bound=Union) + TypeVar('X', bound=Optional) with self.assertRaises(TypeError): TypeVar('X', str, float, bound=Employee) with self.assertRaisesRegex(TypeError, r"Bound must be a type\. Got \(1, 2\)\."): TypeVar('X', bound=(1, 2)) - # Technically we could run it on later versions of 3.8, - # but that's not worth the effort. - @skipUnless(TYPING_3_9_0, "Fix was not backported") def test_missing__name__(self): - # See bpo-39942 + # See https://github.com/python/cpython/issues/84123 code = ("import typing\n" "T = typing.TypeVar('T')\n" ) @@ -6392,7 +7429,7 @@ 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') @@ -6414,7 +7451,7 @@ 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__, ...) @@ -6440,7 +7477,7 @@ 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), @@ -6494,7 +7531,7 @@ def test_no_default_after_non_default(self): T = TypeVar('T') with self.assertRaises(TypeError): - Test = Generic[DefaultStrT, T] + Generic[DefaultStrT, T] def test_need_more_params(self): DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) @@ -6508,7 +7545,7 @@ class A(Generic[T, U, DefaultStrT]): ... with self.assertRaises( TypeError, msg="Too few arguments for .+; actual 1, expected at least 2" ): - Test = A[int] + A[int] def test_pickle(self): global U, U_co, U_contra, U_default # pickle wants to reference the class by name @@ -6545,9 +7582,8 @@ def test_allow_default_after_non_default_in_alias(self): 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)) + 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)) @@ -6555,6 +7591,25 @@ def test_allow_default_after_non_default_in_alias(self): 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" @@ -6708,7 +7763,6 @@ class D(B[str], float): pass with self.assertRaisesRegex(TypeError, "Expected an instance of type"): get_original_bases(object()) - @skipUnless(TYPING_3_9_0, "PEP 585 is yet to be") def test_builtin_generics(self): class E(list[T]): pass class F(list[int]): pass @@ -6818,6 +7872,80 @@ def test_attributes(self): self.assertEqual(Variadic.__type_params__, (Ts,)) self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )) + self.assertEqual(CallableP.__name__, "CallableP") + self.assertEqual(CallableP.__value__, Callable[P, Any]) + self.assertEqual(CallableP.__type_params__, (P,)) + self.assertEqual(CallableP.__parameters__, (P,)) + + def test_alias_types_and_substitutions(self): + T = TypeVar('T') + T2 = TypeVar('T2') + T_default = TypeVar("T_default", default=int) + Ts = TypeVarTuple("Ts") + P = ParamSpec('P') + + test_argument_cases = { + # arguments : expected parameters + int : (), + ... : (), + None : (), + T2 : (T2,), + Union[int, List[T2]] : (T2,), + Tuple[int, str] : (), + Tuple[T, T_default, T2] : (T, T_default, T2), + Tuple[Unpack[Ts]] : (Ts,), + Callable[[Unpack[Ts]], T2] : (Ts, T2), + Callable[P, T2] : (P, T2), + Callable[Concatenate[T2, P], T_default] : (T2, P, T_default), + TypeAliasType("NestedAlias", List[T], type_params=(T,))[T2] : (T2,), + Unpack[Ts] : (Ts,), + Unpack[Tuple[int, T2]] : (T2,), + Concatenate[int, P] : (P,), + # Not tested usage of bare TypeVarTuple, would need 3.11+ + # Ts : (Ts,), # invalid case + } + + test_alias_cases = [ + # Simple cases + TypeAliasType("ListT", List[T], type_params=(T,)), + TypeAliasType("UnionT", Union[int, List[T]], type_params=(T,)), + # Value has no parameter but in type_param + TypeAliasType("ValueWithoutT", int, type_params=(T,)), + # Callable + TypeAliasType("CallableP", Callable[P, Any], type_params=(P, )), + TypeAliasType("CallableT", Callable[..., T], type_params=(T, )), + TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )), + # TypeVarTuple + TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,)), + # TypeVar with default + TypeAliasType("TupleT_default", Tuple[T_default, T], type_params=(T, T_default)), + TypeAliasType("CallableT_default", Callable[[T], T_default], type_params=(T, T_default)), + ] + + for alias in test_alias_cases: + with self.subTest(alias=alias, args=[]): + subscripted = alias[[]] + self.assertEqual(get_args(subscripted), ([],)) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=()): + subscripted = alias[()] + self.assertEqual(get_args(subscripted), ()) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=(int, float)): + subscripted = alias[int, float] + self.assertEqual(get_args(subscripted), (int, float)) + self.assertEqual(subscripted.__parameters__, ()) + with self.subTest(alias=alias, args=[int, float]): + subscripted = alias[[int, float]] + self.assertEqual(get_args(subscripted), ([int, float],)) + self.assertEqual(subscripted.__parameters__, ()) + for expected_args, expected_parameters in test_argument_cases.items(): + with self.subTest(alias=alias, args=expected_args): + self.assertEqual(get_args(alias[expected_args]), (expected_args,)) + self.assertEqual(alias[expected_args].__parameters__, expected_parameters) + def test_cannot_set_attributes(self): Simple = TypeAliasType("Simple", int) with self.assertRaisesRegex(AttributeError, "readonly attribute"): @@ -6870,6 +7998,10 @@ def test_or(self): self.assertEqual(Alias | None, Union[Alias, None]) self.assertEqual(Alias | (int | str), Union[Alias, int | str]) self.assertEqual(Alias | list[float], Union[Alias, list[float]]) + + if sys.version_info >= (3, 12): + Alias2 = typing.TypeAliasType("Alias2", str) + self.assertEqual(Alias | Alias2, Union[Alias, Alias2]) else: with self.assertRaises(TypeError): Alias | int @@ -6878,12 +8010,19 @@ def test_or(self): Alias | "Ref" def test_getitem(self): + T = TypeVar('T') ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) subscripted = ListOrSetT[int] self.assertEqual(get_args(subscripted), (int,)) self.assertIs(get_origin(subscripted), ListOrSetT) - with self.assertRaises(TypeError): - subscripted[str] + with self.assertRaisesRegex(TypeError, + "not a generic class" + # types.GenericAlias raises a different error in 3.10 + if sys.version_info[:2] != (3, 10) + else "There are no type variables left in ListOrSetT" + ): + subscripted[int] + still_generic = ListOrSetT[Iterable[T]] self.assertEqual(get_args(still_generic), (Iterable[T],)) @@ -6892,6 +8031,163 @@ def test_getitem(self): self.assertEqual(get_args(fully_subscripted), (Iterable[float],)) self.assertIs(get_origin(fully_subscripted), ListOrSetT) + ValueWithoutTypeVar = TypeAliasType("ValueWithoutTypeVar", int, type_params=(T,)) + still_subscripted = ValueWithoutTypeVar[str] + self.assertEqual(get_args(still_subscripted), (str,)) + + def test_callable_without_concatenate(self): + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + get_args_test_cases = [ + # List of (alias, expected_args) + # () -> Any + (CallableP[()], ()), + (CallableP[[]], ([],)), + # (int) -> Any + (CallableP[int], (int,)), + (CallableP[[int]], ([int],)), + # (int, int) -> Any + (CallableP[int, int], (int, int)), + (CallableP[[int, int]], ([int, int],)), + # (...) -> Any + (CallableP[...], (...,)), + # (int, ...) -> Any + (CallableP[[int, ...]], ([int, ...],)), + ] + + for index, (expression, expected_args) in enumerate(get_args_test_cases): + with self.subTest(index=index, expression=expression): + self.assertEqual(get_args(expression), expected_args) + + self.assertEqual(CallableP[...], CallableP[(...,)]) + # (T) -> Any + CallableT = CallableP[T] + self.assertEqual(get_args(CallableT), (T,)) + self.assertEqual(CallableT.__parameters__, (T,)) + + def test_callable_with_concatenate(self): + P = ParamSpec('P') + P2 = ParamSpec('P2') + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + + callable_concat = CallableP[Concatenate[int, P2]] + self.assertEqual(callable_concat.__parameters__, (P2,)) + concat_usage = callable_concat[str] + with self.subTest("get_args of Concatenate in TypeAliasType"): + if not TYPING_3_10_0: + # args are: ([, ~P2],) + self.skipTest("Nested ParamSpec is not substituted") + self.assertEqual(get_args(concat_usage), ((int, str),)) + with self.subTest("Equality of parameter_expression without []"): + if not TYPING_3_10_0: + self.skipTest("Nested list is invalid type form") + self.assertEqual(concat_usage, callable_concat[[str]]) + + def test_substitution(self): + T = TypeVar('T') + Ts = TypeVarTuple("Ts") + + CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, )) + unpack_callable = CallableTs[Unpack[Tuple[int, T]]] + self.assertEqual(get_args(unpack_callable), (Unpack[Tuple[int, T]],)) + + P = ParamSpec('P') + CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P, T)) + callable_concat = CallableP[Concatenate[int, P], Any] + self.assertEqual(get_args(callable_concat), (Concatenate[int, P], Any)) + + def test_wrong_amount_of_parameters(self): + T = TypeVar('T') + T2 = TypeVar("T2") + P = ParamSpec('P') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + TwoT = TypeAliasType("TwoT", Union[List[T], Set[T2]], type_params=(T, T2)) + CallablePT = TypeAliasType("CallablePT", Callable[P, T], type_params=(P, T)) + + # Not enough parameters + test_cases = [ + # not_enough + (TwoT[int], [(int,), ()]), + (TwoT[T], [(T,), (T,)]), + # callable and not enough + (CallablePT[int], [(int,), ()]), + # too many + (ListOrSetT[int, bool], [(int, bool), ()]), + # callable and too many + (CallablePT[str, float, int], [(str, float, int), ()]), + # Check if TypeVar is still present even if over substituted + (ListOrSetT[int, T], [(int, T), (T,)]), + # With and without list for ParamSpec + (CallablePT[str, float, T], [(str, float, T), (T,)]), + (CallablePT[[str], float, int, T2], [([str], float, int, T2), (T2,)]), + ] + + for index, (alias, [expected_args, expected_params]) in enumerate(test_cases): + with self.subTest(index=index, alias=alias): + self.assertEqual(get_args(alias), expected_args) + self.assertEqual(alias.__parameters__, expected_params) + + # The condition should align with the version of GeneriAlias usage in __getitem__ or be 3.11+ + @skipIf(TYPING_3_10_0, "Most arguments are allowed in 3.11+ or with GenericAlias") + def test_invalid_cases_before_3_10(self): + T = TypeVar('T') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + with self.assertRaises(TypeError): + ListOrSetT[Generic[T]] + with self.assertRaises(TypeError): + ListOrSetT[(Generic[T], )] + + def test_unpack_parameter_collection(self): + Ts = TypeVarTuple("Ts") + + class Foo(Generic[Unpack[Ts]]): + bar: Tuple[Unpack[Ts]] + + FooAlias = TypeAliasType("FooAlias", Foo[Unpack[Ts]], type_params=(Ts,)) + self.assertEqual(FooAlias[Unpack[Tuple[str]]].__parameters__, ()) + self.assertEqual(FooAlias[Unpack[Tuple[T]]].__parameters__, (T,)) + + P = ParamSpec("P") + CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,)) + call_int_T = CallableP[Unpack[Tuple[int, T]]] + self.assertEqual(call_int_T.__parameters__, (T,)) + + def test_alias_attributes(self): + T = TypeVar('T') + T2 = TypeVar('T2') + ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,)) + + subscripted = ListOrSetT[int] + self.assertEqual(subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(subscripted.__name__, "ListOrSetT") + self.assertEqual(subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(subscripted.__type_params__, (T,)) + + still_generic = ListOrSetT[Iterable[T2]] + self.assertEqual(still_generic.__module__, ListOrSetT.__module__) + self.assertEqual(still_generic.__name__, "ListOrSetT") + self.assertEqual(still_generic.__value__, Union[List[T], Set[T]]) + self.assertEqual(still_generic.__type_params__, (T,)) + + fully_subscripted = still_generic[float] + self.assertEqual(fully_subscripted.__module__, ListOrSetT.__module__) + self.assertEqual(fully_subscripted.__name__, "ListOrSetT") + self.assertEqual(fully_subscripted.__value__, Union[List[T], Set[T]]) + self.assertEqual(fully_subscripted.__type_params__, (T,)) + + def test_subscription_without_type_params(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): + Simple[int] + + # A TypeVar in the value does not allow subscription + T = TypeVar('T') + MissingTypeParamsErr = TypeAliasType("MissingTypeParamsErr", List[T]) + self.assertEqual(MissingTypeParamsErr.__type_params__, ()) + self.assertEqual(MissingTypeParamsErr.__parameters__, ()) + with self.assertRaises(TypeError, msg="Only generic type aliases are subscriptable"): + MissingTypeParamsErr[int] + def test_pickle(self): global Alias Alias = TypeAliasType("Alias", int) @@ -6906,6 +8202,80 @@ def test_no_instance_subclassing(self): class MyAlias(TypeAliasType): pass + def test_type_var_compatibility(self): + # Regression test to assure compatibility with typing variants + typingT = typing.TypeVar('typingT') + T1 = TypeAliasType("TypingTypeVar", ..., type_params=(typingT,)) + self.assertEqual(T1.__type_params__, (typingT,)) + + # Test typing_extensions backports + textT = TypeVar('textT') + T2 = TypeAliasType("TypingExtTypeVar", ..., type_params=(textT,)) + self.assertEqual(T2.__type_params__, (textT,)) + + textP = ParamSpec("textP") + T3 = TypeAliasType("TypingExtParamSpec", ..., type_params=(textP,)) + self.assertEqual(T3.__type_params__, (textP,)) + + textTs = TypeVarTuple("textTs") + T4 = TypeAliasType("TypingExtTypeVarTuple", ..., type_params=(textTs,)) + self.assertEqual(T4.__type_params__, (textTs,)) + + @skipUnless(TYPING_3_10_0, "typing.ParamSpec is not available before 3.10") + def test_param_spec_compatibility(self): + # Regression test to assure compatibility with typing variant + typingP = typing.ParamSpec("typingP") + T5 = TypeAliasType("TypingParamSpec", ..., type_params=(typingP,)) + self.assertEqual(T5.__type_params__, (typingP,)) + + @skipUnless(TYPING_3_12_0, "typing.TypeVarTuple is not available before 3.12") + def test_type_var_tuple_compatibility(self): + # Regression test to assure compatibility with typing variant + typingTs = typing.TypeVarTuple("typingTs") + T6 = TypeAliasType("TypingTypeVarTuple", ..., type_params=(typingTs,)) + self.assertEqual(T6.__type_params__, (typingTs,)) + + def test_type_params_possibilities(self): + T = TypeVar('T') + # Test not a tuple + with self.assertRaisesRegex(TypeError, "type_params must be a tuple"): + TypeAliasType("InvalidTypeParams", List[T], type_params=[T]) + + # Test default order and other invalid inputs + T_default = TypeVar('T_default', default=int) + Ts = TypeVarTuple('Ts') + Ts_default = TypeVarTuple('Ts_default', default=Unpack[Tuple[str, int]]) + P = ParamSpec('P') + P_default = ParamSpec('P_default', default=[str, int]) + + # NOTE: PEP 696 states: "TypeVars with defaults cannot immediately follow TypeVarTuples" + # this is currently not enforced for the type statement and is not tested. + # PEP 695: Double usage of the same name is also not enforced and not tested. + valid_cases = [ + (T, P, Ts), + (T, Ts_default), + (P_default, T_default), + (P, T_default, Ts_default), + (T_default, P_default, Ts_default), + ] + invalid_cases = [ + ((T_default, T), f"non-default type parameter '{T!r}' follows default"), + ((P_default, P), f"non-default type parameter '{P!r}' follows default"), + ((Ts_default, T), f"non-default type parameter '{T!r}' follows default"), + # Only type params are accepted + ((1,), "Expected a type param, got 1"), + ((str,), f"Expected a type param, got {str!r}"), + # Unpack is not a TypeVar but isinstance(Unpack[Ts], TypeVar) is True in Python < 3.12 + ((Unpack[Ts],), f"Expected a type param, got {re.escape(repr(Unpack[Ts]))}"), + ] + + for case in valid_cases: + with self.subTest(type_params=case): + TypeAliasType("OkCase", List[T], type_params=case) + for case, msg in invalid_cases: + with self.subTest(type_params=case): + with self.assertRaisesRegex(TypeError, msg): + TypeAliasType("InvalidCase", List[T], type_params=case) class DocTests(BaseTestCase): def test_annotation(self): @@ -6952,5 +8322,819 @@ def test_capsule_type(self): self.assertIsInstance(_datetime.datetime_CAPI, typing_extensions.CapsuleType) +def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a * 3, b * 3) + + return wrapper + + +class TestGetAnnotations(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "inspect_stock_annotations.py").write_text(STOCK_ANNOTATIONS) + Path(tempdir, "inspect_stringized_annotations.py").write_text(STRINGIZED_ANNOTATIONS) + Path(tempdir, "inspect_stringized_annotations_2.py").write_text(STRINGIZED_ANNOTATIONS_2) + cls.inspect_stock_annotations = importlib.import_module("inspect_stock_annotations") + cls.inspect_stringized_annotations = importlib.import_module("inspect_stringized_annotations") + cls.inspect_stringized_annotations_2 = importlib.import_module("inspect_stringized_annotations_2") + sys.path.pop() + + @classmethod + def tearDownClass(cls): + for modname in ( + "inspect_stock_annotations", + "inspect_stringized_annotations", + "inspect_stringized_annotations_2", + ): + delattr(cls, modname) + del sys.modules[modname] + + def test_builtin_type(self): + self.assertEqual(get_annotations(int), {}) + self.assertEqual(get_annotations(object), {}) + + def test_format(self): + def f1(a: int): + pass + + def f2(a: "undefined"): # noqa: F821 + pass + + self.assertEqual( + get_annotations(f1, format=Format.VALUE), {"a": int} + ) + self.assertEqual(get_annotations(f1, format=1), {"a": int}) + + self.assertEqual( + get_annotations(f2, format=Format.FORWARDREF), + {"a": "undefined"}, + ) + # Test that the raw int also works + self.assertEqual( + get_annotations(f2, format=Format.FORWARDREF.value), + {"a": "undefined"}, + ) + + self.assertEqual( + get_annotations(f1, format=Format.STRING), + {"a": "int"}, + ) + self.assertEqual( + get_annotations(f1, format=Format.STRING.value), + {"a": "int"}, + ) + + with self.assertRaises(ValueError): + get_annotations(f1, format=0) + + with self.assertRaises(ValueError): + get_annotations(f1, format=42) + + def test_custom_object_with_annotations(self): + class C: + def __init__(self, x: int = 0, y: str = ""): + self.__annotations__ = {"x": int, "y": str} + + self.assertEqual(get_annotations(C()), {"x": int, "y": str}) + + def test_custom_format_eval_str(self): + def foo(): + pass + + with self.assertRaises(ValueError): + get_annotations( + foo, format=Format.FORWARDREF, eval_str=True + ) + get_annotations( + foo, format=Format.STRING, eval_str=True + ) + + def test_stock_annotations(self): + def foo(a: int, b: str): + pass + + for format in (Format.VALUE, Format.FORWARDREF): + with self.subTest(format=format): + self.assertEqual( + get_annotations(foo, format=format), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(foo, format=Format.STRING), + {"a": "int", "b": "str"}, + ) + + foo.__annotations__ = {"a": "foo", "b": "str"} + for format in Format: + with self.subTest(format=format): + if format is Format.VALUE_WITH_FAKE_GLOBALS: + with self.assertRaisesRegex( + ValueError, + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ): + get_annotations(foo, format=format) + else: + self.assertEqual( + get_annotations(foo, format=format), + {"a": "foo", "b": "str"}, + ) + + self.assertEqual( + get_annotations(foo, eval_str=True, locals=locals()), + {"a": foo, "b": str}, + ) + self.assertEqual( + get_annotations(foo, eval_str=True, globals=locals()), + {"a": foo, "b": str}, + ) + + def test_stock_annotations_in_module(self): + isa = self.inspect_stock_annotations + + for kwargs in [ + {}, + {"eval_str": False}, + {"format": Format.VALUE}, + {"format": Format.FORWARDREF}, + {"format": Format.VALUE, "eval_str": False}, + {"format": Format.FORWARDREF, "eval_str": False}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(inspect, **kwargs), {} + ) # inspect module has no annotations + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + for kwargs in [ + {"eval_str": True}, + {"format": Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": int, "b": str, "c": isa.MyClass}, + ) + self.assertEqual(get_annotations(inspect, **kwargs), {}) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + self.assertEqual( + get_annotations(isa, format=Format.STRING), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + get_annotations(isa.MyClass, format=Format.STRING), + {"a": "int", "b": "str"}, + ) + mycls = "MyClass" if sys.version_info >= (3, 14) else "inspect_stock_annotations.MyClass" + self.assertEqual( + get_annotations(isa.function, format=Format.STRING), + {"a": "int", "b": "str", "return": mycls}, + ) + self.assertEqual( + get_annotations( + isa.function2, format=Format.STRING + ), + {"a": "int", "b": "str", "c": mycls, "return": mycls}, + ) + self.assertEqual( + get_annotations( + isa.function3, format=Format.STRING + ), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(inspect, format=Format.STRING), + {}, + ) + self.assertEqual( + get_annotations( + isa.UnannotatedClass, format=Format.STRING + ), + {}, + ) + self.assertEqual( + get_annotations( + isa.unannotated_function, format=Format.STRING + ), + {}, + ) + + def test_stock_annotations_on_wrapper(self): + isa = self.inspect_stock_annotations + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual( + get_annotations(wrapped), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, format=Format.FORWARDREF), + {"a": int, "b": str, "return": isa.MyClass}, + ) + mycls = "MyClass" if sys.version_info >= (3, 14) else "inspect_stock_annotations.MyClass" + self.assertEqual( + get_annotations(wrapped, format=Format.STRING), + {"a": "int", "b": "str", "return": mycls}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=False), + {"a": int, "b": str, "return": isa.MyClass}, + ) + + def test_stringized_annotations_in_module(self): + isa = self.inspect_stringized_annotations + for kwargs in [ + {}, + {"eval_str": False}, + {"format": Format.VALUE}, + {"format": Format.FORWARDREF}, + {"format": Format.STRING}, + {"format": Format.VALUE, "eval_str": False}, + {"format": Format.FORWARDREF, "eval_str": False}, + {"format": Format.STRING, "eval_str": False}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": "int", "b": "str"} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": "int", "b": "str"}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, + ) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + for kwargs in [ + {"eval_str": True}, + {"format": Format.VALUE, "eval_str": True}, + ]: + with self.subTest(**kwargs): + self.assertEqual( + get_annotations(isa, **kwargs), {"a": int, "b": str} + ) + self.assertEqual( + get_annotations(isa.MyClass, **kwargs), + {"a": int, "b": str}, + ) + self.assertEqual( + get_annotations(isa.function, **kwargs), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function2, **kwargs), + {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(isa.function3, **kwargs), + {"a": "int", "b": "str", "c": "MyClass"}, + ) + self.assertEqual( + get_annotations(isa.UnannotatedClass, **kwargs), {} + ) + self.assertEqual( + get_annotations(isa.unannotated_function, **kwargs), {} + ) + + def test_stringized_annotations_in_empty_module(self): + isa2 = self.inspect_stringized_annotations_2 + self.assertEqual(get_annotations(isa2), {}) + self.assertEqual(get_annotations(isa2, eval_str=True), {}) + self.assertEqual(get_annotations(isa2, eval_str=False), {}) + + def test_stringized_annotations_on_wrapper(self): + isa = self.inspect_stringized_annotations + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual( + get_annotations(wrapped), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=True), + {"a": int, "b": str, "return": isa.MyClass}, + ) + self.assertEqual( + get_annotations(wrapped, eval_str=False), + {"a": "int", "b": "str", "return": "MyClass"}, + ) + + def test_stringized_annotations_on_class(self): + isa = self.inspect_stringized_annotations + # test that local namespace lookups work + self.assertEqual( + get_annotations(isa.MyClassWithLocalAnnotations), + {"x": "mytype"}, + ) + self.assertEqual( + get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), + {"x": int}, + ) + + def test_modify_annotations(self): + def f(x: int): + pass + + self.assertEqual(get_annotations(f), {"x": int}) + self.assertEqual( + get_annotations(f, format=Format.FORWARDREF), + {"x": int}, + ) + + f.__annotations__["x"] = str + self.assertEqual(get_annotations(f), {"x": str}) + + +class TestGetAnnotationsMetaclasses(BaseTestCase): + def test_annotated_meta(self): + class Meta(type): + a: int + + class X(metaclass=Meta): + pass + + class Y(metaclass=Meta): + b: float + + self.assertEqual(get_annotations(Meta), {"a": int}) + self.assertEqual(get_annotations(X), {}) + self.assertEqual(get_annotations(Y), {"b": float}) + + def test_unannotated_meta(self): + class Meta(type): pass + + class X(metaclass=Meta): + a: str + + class Y(X): pass + + self.assertEqual(get_annotations(Meta), {}) + self.assertEqual(get_annotations(Y), {}) + self.assertEqual(get_annotations(X), {"a": str}) + + def test_ordering(self): + # Based on a sample by David Ellis + # https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38 + + def make_classes(): + class Meta(type): + a: int + expected_annotations = {"a": int} + + class A(type, metaclass=Meta): + b: float + expected_annotations = {"b": float} + + class B(metaclass=A): + c: str + expected_annotations = {"c": str} + + class C(B): + expected_annotations = {} + + class D(metaclass=Meta): + expected_annotations = {} + + return Meta, A, B, C, D + + classes = make_classes() + class_count = len(classes) + for order in itertools.permutations(range(class_count), class_count): + names = ", ".join(classes[i].__name__ for i in order) + with self.subTest(names=names): + classes = make_classes() # Regenerate classes + for i in order: + get_annotations(classes[i]) + for c in classes: + with self.subTest(c=c): + self.assertEqual(get_annotations(c), c.expected_annotations) + + +@skipIf(STRINGIZED_ANNOTATIONS_PEP_695 is None, "PEP 695 has yet to be") +class TestGetAnnotationsWithPEP695(BaseTestCase): + @classmethod + def setUpClass(cls): + with tempfile.TemporaryDirectory() as tempdir: + sys.path.append(tempdir) + Path(tempdir, "inspect_stringized_annotations_pep_695.py").write_text(STRINGIZED_ANNOTATIONS_PEP_695) + cls.inspect_stringized_annotations_pep_695 = importlib.import_module( + "inspect_stringized_annotations_pep_695" + ) + sys.path.pop() + + @classmethod + def tearDownClass(cls): + del cls.inspect_stringized_annotations_pep_695 + del sys.modules["inspect_stringized_annotations_pep_695"] + + def test_pep695_generic_class_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + A_annotations = get_annotations(ann_module695.A, eval_str=True) + A_type_params = ann_module695.A.__type_params__ + self.assertIs(A_annotations["x"], A_type_params[0]) + self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]]) + self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) + + def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self): + B_annotations = get_annotations( + self.inspect_stringized_annotations_pep_695.B, eval_str=True + ) + self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes}) + + def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + C_annotations = get_annotations(ann_module695.C, eval_str=True) + self.assertEqual( + set(C_annotations.values()), + set(ann_module695.C.__type_params__) + ) + + def test_pep_695_generic_function_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + generic_func_annotations = get_annotations( + ann_module695.generic_function, eval_str=True + ) + func_t_params = ann_module695.generic_function.__type_params__ + self.assertEqual( + generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"} + ) + self.assertIs(generic_func_annotations["x"], func_t_params[0]) + self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]]) + self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2]) + self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2]) + + def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + get_annotations( + self.inspect_stringized_annotations_pep_695.generic_function_2, + eval_str=True + ).values() + ), + set( + self.inspect_stringized_annotations_pep_695.generic_function_2.__type_params__ + ) + ) + + def test_pep_695_generic_method_with_future_annotations(self): + ann_module695 = self.inspect_stringized_annotations_pep_695 + generic_method_annotations = get_annotations( + ann_module695.D.generic_method, eval_str=True + ) + params = { + param.__name__: param + for param in ann_module695.D.generic_method.__type_params__ + } + self.assertEqual( + generic_method_annotations, + {"x": params["Foo"], "y": params["Bar"], "return": None} + ) + + def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars(self): + self.assertEqual( + set( + get_annotations( + self.inspect_stringized_annotations_pep_695.D.generic_method_2, + eval_str=True + ).values() + ), + set( + self.inspect_stringized_annotations_pep_695.D.generic_method_2.__type_params__ + ) + ) + + def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars(self): + self.assertEqual( + get_annotations( + self.inspect_stringized_annotations_pep_695.E, eval_str=True + ), + {"x": str}, + ) + + def test_pep_695_generics_with_future_annotations_nested_in_function(self): + results = self.inspect_stringized_annotations_pep_695.nested() + + self.assertEqual( + set(results.F_annotations.values()), + set(results.F.__type_params__) + ) + self.assertEqual( + set(results.F_meth_annotations.values()), + set(results.F.generic_method.__type_params__) + ) + self.assertNotEqual( + set(results.F_meth_annotations.values()), + set(results.F.__type_params__) + ) + self.assertEqual( + set(results.F_meth_annotations.values()).intersection(results.F.__type_params__), + set() + ) + + self.assertEqual(results.G_annotations, {"x": str}) + + self.assertEqual( + set(results.generic_func_annotations.values()), + set(results.generic_func.__type_params__) + ) + +class TestEvaluateForwardRefs(BaseTestCase): + def test_global_constant(self): + if sys.version_info[:3] > (3, 10, 0): + self.assertTrue(_FORWARD_REF_HAS_CLASS) + + def test_forward_ref_fallback(self): + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("doesntexist")) + ref = typing.ForwardRef("doesntexist") + self.assertIs(evaluate_forward_ref(ref, format=Format.FORWARDREF), ref) + + class X: + unresolvable = "doesnotexist2" + + evaluated_ref = evaluate_forward_ref( + typing.ForwardRef("X.unresolvable"), + locals={"X": X}, + type_params=None, + format=Format.FORWARDREF, + ) + self.assertEqual(evaluated_ref, EqualToForwardRef("doesnotexist2")) + + def test_evaluate_with_type_params(self): + # Use a T name that is not in globals + self.assertNotIn("Tx", globals()) + if not TYPING_3_12_0: + Tx = TypeVar("Tx") + class Gen(Generic[Tx]): + alias = int + if not hasattr(Gen, "__type_params__"): + Gen.__type_params__ = (Tx,) + self.assertEqual(Gen.__type_params__, (Tx,)) + del Tx + else: + ns = {} + exec(textwrap.dedent(""" + class Gen[Tx]: + alias = int + """), None, ns) + Gen = ns["Gen"] + + # owner=None, type_params=None + # NOTE: The behavior of owner=None might change in the future when ForwardRef.__owner__ is available + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx")) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=()) + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("Tx"), owner=int) + + (Tx,) = Gen.__type_params__ + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=Gen.__type_params__), Tx) + + # For this test its important that Tx is not a global variable, i.e. do not use "T" here + self.assertNotIn("Tx", globals()) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), owner=Gen), Tx) + + # Different type_params take precedence + not_Tx = TypeVar("Tx") # different TypeVar with same name + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx"), type_params=(not_Tx,), owner=Gen), not_Tx) + + # globals can take higher precedence + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, globals={"Tx": str}), str) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Tx", is_class=True), owner=Gen, type_params=(not_Tx,), globals={"Tx": str}), str) + + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), type_params=Gen.__type_params__) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen), int) + # If you pass custom locals, we don't look at the owner's locals + with self.assertRaises(NameError): + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={}) + # But if the name exists in the locals, it works + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("alias"), owner=Gen, locals={"alias": str}), str + ) + + @skipUnless( + HAS_FORWARD_MODULE, "Needs module 'forward' to test forward references" + ) + def test_fwdref_with_module(self): + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("Counter", module="collections")), collections.Counter + ) + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]", module="collections")), + collections.Counter[int], + ) + + with self.assertRaises(NameError): + # If globals are passed explicitly, we don't look at the module dict + evaluate_forward_ref(typing.ForwardRef("Format", module="annotationlib"), globals={}) + + def test_fwdref_to_builtin(self): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + if HAS_FORWARD_MODULE: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int", module="collections")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), owner=str), int) + + # builtins are still searched with explicit globals + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={}), int) + + def test_fwdref_with_globals(self): + # explicit values in globals have precedence + obj = object() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": obj}), obj) + + def test_fwdref_with_owner(self): + self.assertEqual( + evaluate_forward_ref(typing.ForwardRef("Counter[int]"), owner=collections), + collections.Counter[int], + ) + + def test_name_lookup_without_eval(self): + # test the codepath where we look up simple names directly in the + # namespaces without going through eval() + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), int) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": str}), str) + self.assertIs( + evaluate_forward_ref(typing.ForwardRef("int"), locals={"int": float}, globals={"int": str}), + float, + ) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int"), globals={"int": str}), str) + import builtins + + from test import support + with support.swap_attr(builtins, "int", dict): + self.assertIs(evaluate_forward_ref(typing.ForwardRef("int")), dict) + + def test_nested_strings(self): + # This variable must have a different name TypeVar + Tx = TypeVar("Tx") + + class Y(Generic[Tx]): + a = "X" + bT = "Y[T_nonlocal]" + + Z = TypeAliasType("Z", Y[Tx], type_params=(Tx,)) + + evaluated_ref1a = evaluate_forward_ref(typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y, "Tx": Tx}) + self.assertEqual(get_origin(evaluated_ref1a), Y) + self.assertEqual(get_args(evaluated_ref1a), (Y[Tx],)) + + evaluated_ref1b = evaluate_forward_ref( + typing.ForwardRef("Y[Y['Tx']]"), locals={"Y": Y}, type_params=(Tx,) + ) + self.assertEqual(get_origin(evaluated_ref1b), Y) + self.assertEqual(get_args(evaluated_ref1b), (Y[Tx],)) + + with self.subTest("nested string of TypeVar"): + evaluated_ref2 = evaluate_forward_ref(typing.ForwardRef("""Y["Y['Tx']"]"""), locals={"Y": Y, "Tx": Tx}) + self.assertEqual(get_origin(evaluated_ref2), Y) + self.assertEqual(get_args(evaluated_ref2), (Y[Tx],)) + + with self.subTest("nested string of TypeAliasType and alias"): + # NOTE: Using Y here works for 3.10 + evaluated_ref3 = evaluate_forward_ref(typing.ForwardRef("""Y['Z["StrAlias"]']"""), locals={"Y": Y, "Z": Z, "StrAlias": str}) + self.assertEqual(get_origin(evaluated_ref3), Y) + if sys.version_info[:2] == (3, 10): + self.skipTest("Nested string 'StrAlias' is not resolved in 3.10") + self.assertEqual(get_args(evaluated_ref3), (Z[str],)) + + def test_invalid_special_forms(self): + # tests _lax_type_check to raise errors the same way as the typing module. + # Regex capture "< class 'module.name'> and "module.name" + with self.assertRaisesRegex( + TypeError, r"Plain .*Protocol('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Protocol"), globals=vars(typing)) + with self.assertRaisesRegex( + TypeError, r"Plain .*Generic('>)? is not valid as type argument" + ): + evaluate_forward_ref(typing.ForwardRef("Generic"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final"), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar"), globals=vars(typing)) + if _FORWARD_REF_HAS_CLASS: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_class=True), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_class=True), globals=vars(typing)), ClassVar) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.Final is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)) + with self.assertRaisesRegex(TypeError, r"Plain typing(_extensions)?\.ClassVar is not valid as type argument"): + evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)) + else: + self.assertIs(evaluate_forward_ref(typing.ForwardRef("Final", is_argument=False), globals=vars(typing)), Final) + self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar) + + +class TestSentinels(BaseTestCase): + def test_sentinel_no_repr(self): + sentinel_no_repr = Sentinel('sentinel_no_repr') + + self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') + self.assertEqual(repr(sentinel_no_repr), '') + + def test_sentinel_explicit_repr(self): + sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') + + self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') + + @skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9') + def test_sentinel_type_expression_union(self): + sentinel = Sentinel('sentinel') + + def func1(a: int | sentinel = sentinel): pass + def func2(a: sentinel | int = sentinel): pass + + self.assertEqual(func1.__annotations__['a'], Union[int, sentinel]) + self.assertEqual(func2.__annotations__['a'], Union[sentinel, int]) + + def test_sentinel_not_callable(self): + sentinel = Sentinel('sentinel') + with self.assertRaisesRegex( + TypeError, + "'Sentinel' object is not callable" + ): + sentinel() + + def test_sentinel_not_picklable(self): + sentinel = Sentinel('sentinel') + with self.assertRaisesRegex( + TypeError, + "Cannot pickle 'Sentinel' object" + ): + pickle.dumps(sentinel) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 57e59a8b..d4e92a4c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,15 +1,22 @@ import abc +import builtins import collections import collections.abc import contextlib +import enum import functools import inspect +import io +import keyword import operator import sys import types as _types import typing import warnings +if sys.version_info >= (3, 14): + import annotationlib + __all__ = [ # Super-special typing primitives. 'Any', @@ -53,6 +60,8 @@ 'SupportsIndex', 'SupportsInt', 'SupportsRound', + 'Reader', + 'Writer', # One-off things. 'Annotated', @@ -62,8 +71,11 @@ 'dataclass_transform', 'deprecated', 'Doc', + 'evaluate_forward_ref', 'get_overloads', 'final', + 'Format', + 'get_annotations', 'get_args', 'get_origin', 'get_original_bases', @@ -77,12 +89,14 @@ 'overload', 'override', 'Protocol', + 'Sentinel', 'reveal_type', 'runtime', 'runtime_checkable', 'Text', 'TypeAlias', 'TypeAliasType', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -91,6 +105,8 @@ 'ReadOnly', 'Required', 'NotRequired', + 'NoDefault', + 'NoExtraItems', # Pure aliases, have always been in typing 'AbstractSet', @@ -117,7 +133,6 @@ 'MutableMapping', 'MutableSequence', 'MutableSet', - 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -138,6 +153,9 @@ GenericMeta = type _PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") +# 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. @@ -155,12 +173,9 @@ def _should_collect_from_parameters(t): return isinstance( t, (typing._GenericAlias, _types.GenericAlias, _types.UnionType) ) -elif sys.version_info >= (3, 9): - def _should_collect_from_parameters(t): - return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) else: def _should_collect_from_parameters(t): - return isinstance(t, typing._GenericAlias) and not t._special + return isinstance(t, (typing._GenericAlias, _types.GenericAlias)) NoReturn = typing.NoReturn @@ -418,33 +433,19 @@ def clear_overloads(): if sys.version_info >= (3, 13, 0, "beta"): - from typing import ContextManager, AsyncContextManager, Generator, AsyncGenerator + 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): + class _SpecialGenericAlias(typing._SpecialGenericAlias, _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) + 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: @@ -574,7 +575,7 @@ class _ProtocolMeta(type(typing.Protocol)): # but is necessary for several reasons... # # NOTE: DO NOT call super() in any methods in this class - # That would call the methods on typing._ProtocolMeta on Python 3.8-3.11 + # That would call the methods on typing._ProtocolMeta on Python <=3.11 # and those are slow def __new__(mcls, name, bases, namespace, **kwargs): if name == "Protocol" and len(bases) < 2: @@ -739,8 +740,8 @@ def close(self): ... not their type signatures! """ if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False): - raise TypeError('@runtime_checkable can be only applied to protocol classes,' - ' got %r' % cls) + 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, @@ -775,7 +776,7 @@ def close(self): ... runtime = runtime_checkable -# Our version of runtime-checkable protocols is faster on Python 3.8-3.11 +# Our version of runtime-checkable protocols is faster on Python <=3.11 if sys.version_info >= (3, 12): SupportsInt = typing.SupportsInt SupportsFloat = typing.SupportsFloat @@ -852,19 +853,96 @@ def __round__(self, ndigits: int = 0) -> T_co: pass -def _ensure_subclassable(mro_entries): - def inner(func): - if sys.implementation.name == "pypy" and sys.version_info < (3, 9): - cls_dict = { - "__call__": staticmethod(func), - "__mro_entries__": staticmethod(mro_entries) - } - t = type(func.__name__, (), cls_dict) - return functools.update_wrapper(t(), func) - else: - func.__mro_entries__ = mro_entries - return func - return inner +if hasattr(io, "Reader") and hasattr(io, "Writer"): + Reader = io.Reader + Writer = io.Writer +else: + @runtime_checkable + class Reader(Protocol[T_co]): + """Protocol for simple I/O reader instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def read(self, size: int = ..., /) -> T_co: + """Read data from the input stream and return it. + + If *size* is specified, at most *size* items (bytes/characters) will be + read. + """ + + @runtime_checkable + class Writer(Protocol[T_contra]): + """Protocol for simple I/O writer instances. + + This protocol only supports blocking I/O. + """ + + __slots__ = () + + @abc.abstractmethod + def write(self, data: T_contra, /) -> int: + """Write *data* to the output stream and return the number of items written.""" # noqa: E501 + + +_NEEDS_SINGLETONMETA = ( + not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") +) + +if _NEEDS_SINGLETONMETA: + class SingletonMeta(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}" + ) + + +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType(metaclass=SingletonMeta): + """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 + +if hasattr(typing, "NoExtraItems"): + NoExtraItems = typing.NoExtraItems +else: + class NoExtraItemsType(metaclass=SingletonMeta): + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoExtraItems" + + def __reduce__(self): + return "NoExtraItems" + + NoExtraItems = NoExtraItemsType() + del NoExtraItemsType + +if _NEEDS_SINGLETONMETA: + del SingletonMeta # Update this to something like >=3.13.0b1 if and when @@ -872,8 +950,6 @@ def inner(func): _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" # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 # The standard library TypedDict below Python 3.11 does not store runtime @@ -913,7 +989,9 @@ def _get_typeddict_qualifiers(annotation_type): break class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, *, total=True, closed=False): + + def __new__(cls, name, bases, ns, *, total=True, closed=None, + extra_items=NoExtraItems): """Create new typed dict class object. This method is called when TypedDict is subclassed, @@ -925,6 +1003,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): if type(base) is not _TypedDictMeta and base is not typing.Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, typing.Generic) for b in bases): generic_base = (typing.Generic,) @@ -942,15 +1022,31 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): tp_dict.__orig_bases__ = bases annotations = {} - own_annotations = ns.get('__annotations__', {}) + own_annotate = None + if "__annotations__" in ns: + own_annotations = ns["__annotations__"] + elif sys.version_info >= (3, 14): + if hasattr(annotationlib, "get_annotate_from_class_namespace"): + own_annotate = annotationlib.get_annotate_from_class_namespace(ns) + else: + # 3.14.0a7 and earlier + own_annotate = ns.get("__annotate__") + if own_annotate is not None: + own_annotations = annotationlib.call_annotate_function( + own_annotate, Format.FORWARDREF, owner=tp_dict + ) + else: + own_annotations = {} + else: + own_annotations = {} msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" if _TAKES_MODULE: - own_annotations = { + own_checked_annotations = { n: typing._type_check(tp, msg, module=tp_dict.__module__) for n, tp in own_annotations.items() } else: - own_annotations = { + own_checked_annotations = { n: typing._type_check(tp, msg) for n, tp in own_annotations.items() } @@ -958,24 +1054,24 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): optional_keys = set() readonly_keys = set() mutable_keys = set() - extra_items_type = None + extra_items_type = extra_items for base in bases: base_dict = base.__dict__ - annotations.update(base_dict.get('__annotations__', {})) + if sys.version_info <= (3, 14): + 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__") + + # This was specified in an earlier version of PEP 728. Support + # is retained for backwards compatibility, but only for Python + # 3.13 and lower. + if (closed and sys.version_info < (3, 14) + and "__extra_items__" in own_checked_annotations): + annotation_type = own_checked_annotations.pop("__extra_items__") qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: raise TypeError( @@ -989,8 +1085,8 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): ) extra_items_type = annotation_type - annotations.update(own_annotations) - for annotation_key, annotation_type in own_annotations.items(): + annotations.update(own_checked_annotations) + for annotation_key, annotation_type in own_checked_annotations.items(): qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -1008,13 +1104,43 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): mutable_keys.add(annotation_key) readonly_keys.discard(annotation_key) - tp_dict.__annotations__ = annotations + if sys.version_info >= (3, 14): + def __annotate__(format): + annos = {} + for base in bases: + if base is Generic: + continue + base_annotate = base.__annotate__ + if base_annotate is None: + continue + base_annos = annotationlib.call_annotate_function( + base.__annotate__, format, owner=base) + annos.update(base_annos) + if own_annotate is not None: + own = annotationlib.call_annotate_function( + own_annotate, format, owner=tp_dict) + if format != Format.STRING: + own = { + n: typing._type_check(tp, msg, module=tp_dict.__module__) + for n, tp in own.items() + } + elif format == Format.STRING: + own = annotationlib.annotations_to_string(own_annotations) + elif format in (Format.FORWARDREF, Format.VALUE): + own = own_checked_annotations + else: + raise NotImplementedError(format) + annos.update(own) + return annos + + tp_dict.__annotate__ = __annotate__ + else: + 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.__total__ = total tp_dict.__closed__ = closed tp_dict.__extra_items__ = extra_items_type return tp_dict @@ -1029,8 +1155,94 @@ def __subclasscheck__(cls, other): _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) - @_ensure_subclassable(lambda bases: (_TypedDict,)) - def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs): + def _create_typeddict( + typename, + fields, + /, + *, + typing_is_inline, + total, + closed, + extra_items, + **kwargs, + ): + if fields is _marker or fields is None: + if fields is _marker: + deprecated_thing = ( + "Failing to pass a value for the 'fields' parameter" + ) + else: + deprecated_thing = "Passing `None` as the 'fields' parameter" + + example = f"`{typename} = TypedDict({typename!r}, {{}})`" + deprecation_msg = ( + f"{deprecated_thing} is deprecated and will be disallowed in " + "Python 3.15. To create a TypedDict class with 0 fields " + "using the functional syntax, pass an empty dictionary, e.g. " + ) + example + "." + warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: + kwargs["closed"] = closed + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems + 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 " + "understood by third-party type checkers.", + DeprecationWarning, + stacklevel=2, + ) + + ns = {'__annotations__': dict(fields)} + module = _caller(depth=5 if typing_is_inline else 3) + if module is not None: + # Setting correct module is necessary to make typed dict classes + # pickleable. + ns['__module__'] = module + + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) + td.__orig_bases__ = (TypedDict,) + return td + + class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True): + def __call__( + self, + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): + return _create_typeddict( + typename, + fields, + typing_is_inline=False, + total=total, + closed=closed, + extra_items=extra_items, + **kwargs, + ) + + def __mro_entries__(self, bases): + return (_TypedDict,) + + @_TypedDictSpecialForm + def TypedDict(self, args): """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 @@ -1077,51 +1289,22 @@ class Point2D(TypedDict): See PEP 655 for more details on Required and NotRequired. """ - if fields is _marker or fields is None: - if fields is _marker: - deprecated_thing = "Failing to pass a value for the 'fields' parameter" - else: - deprecated_thing = "Passing `None` as the 'fields' parameter" - - example = f"`{typename} = TypedDict({typename!r}, {{}})`" - deprecation_msg = ( - f"{deprecated_thing} is deprecated and will be disallowed in " - "Python 3.15. To create a TypedDict class with 0 fields " - "using the functional syntax, pass an empty dictionary, e.g. " - ) + example + "." - warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) - 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 " - "understood by third-party type checkers.", - DeprecationWarning, - stacklevel=2, + # This runs when creating inline TypedDicts: + if not isinstance(args, dict): + raise TypeError( + "TypedDict[...] should be used with a single dict argument" ) - ns = {'__annotations__': dict(fields)} - module = _caller() - if module is not None: - # Setting correct module is necessary to make typed dict classes pickleable. - ns['__module__'] = module - - td = _TypedDictMeta(typename, (), ns, total=total, closed=closed) - td.__orig_bases__ = (TypedDict,) - return td + return _create_typeddict( + "", + args, + typing_is_inline=True, + total=True, + closed=True, + extra_items=NoExtraItems, + ) - if hasattr(typing, "_TypedDictMeta"): - _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) - else: - _TYPEDDICT_TYPES = (_TypedDictMeta,) + _TYPEDDICT_TYPES = (typing._TypedDictMeta, _TypedDictMeta) def is_typeddict(tp): """Check if an annotation is a TypedDict class @@ -1134,9 +1317,6 @@ class Film(TypedDict): is_typeddict(Film) # => True is_typeddict(Union[list, str]) # => False """ - # On 3.8, this would otherwise return True - if hasattr(typing, "TypedDict") and tp is typing.TypedDict: - return False return isinstance(tp, _TYPEDDICT_TYPES) @@ -1166,7 +1346,7 @@ def greet(name: str) -> None: # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" - if isinstance(t, _AnnotatedAlias): + if isinstance(t, typing._AnnotatedAlias): return _strip_extras(t.__origin__) if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): return _strip_extras(t.__args__[0]) @@ -1220,141 +1400,86 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): - If two dict arguments are passed, they specify globals and locals, respectively. """ - if hasattr(typing, "Annotated"): # 3.9+ - hint = typing.get_type_hints( - obj, globalns=globalns, localns=localns, include_extras=True - ) - else: # 3.8 - hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + hint = typing.get_type_hints( + obj, globalns=globalns, localns=localns, include_extras=True + ) + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) if include_extras: return hint return {k: _strip_extras(t) for k, t in hint.items()} + _NoneType = type(None) -# Python 3.9+ has PEP 593 (Annotated) -if hasattr(typing, 'Annotated'): - Annotated = typing.Annotated - # Not exported and not a public API, but needed for get_origin() and get_args() - # to work. - _AnnotatedAlias = typing._AnnotatedAlias -# 3.8 -else: - class _AnnotatedAlias(typing._GenericAlias, _root=True): - """Runtime representation of an annotated type. - - At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' - with extra annotations. The alias behaves like a normal typing alias, - instantiating is the same as instantiating the underlying type, binding - it to types is also the same. - """ - def __init__(self, origin, metadata): - if isinstance(origin, _AnnotatedAlias): - metadata = origin.__metadata__ + metadata - origin = origin.__origin__ - super().__init__(origin, origin) - self.__metadata__ = metadata - - def copy_with(self, params): - assert len(params) == 1 - new_type = params[0] - return _AnnotatedAlias(new_type, self.__metadata__) - - def __repr__(self): - return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, " - f"{', '.join(repr(a) for a in self.__metadata__)}]") - - def __reduce__(self): - return operator.getitem, ( - Annotated, (self.__origin__,) + self.__metadata__ - ) - - def __eq__(self, other): - if not isinstance(other, _AnnotatedAlias): - return NotImplemented - if self.__origin__ != other.__origin__: - return False - return self.__metadata__ == other.__metadata__ - - def __hash__(self): - return hash((self.__origin__, self.__metadata__)) - - class Annotated: - """Add context specific metadata to a type. - - Example: Annotated[int, runtime_check.Unsigned] indicates to the - hypothetical runtime_check module that this type is an unsigned int. - Every other consumer of this type can ignore this metadata and treat - this type as int. - - The first argument to Annotated must be a valid type (and will be in - the __origin__ field), the remaining arguments are kept as a tuple in - the __extra__ field. - - Details: - - - It's an error to call `Annotated` with less than two arguments. - - Nested Annotated are flattened:: - - Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] - - - Instantiating an annotated type is equivalent to instantiating the - underlying type:: - - Annotated[C, Ann1](5) == C(5) - - - Annotated can be used as a generic type alias:: - - Optimized = Annotated[T, runtime.Optimize()] - Optimized[int] == Annotated[int, runtime.Optimize()] - - OptimizedList = Annotated[List[T], runtime.Optimize()] - OptimizedList[int] == Annotated[List[int], runtime.Optimize()] - """ - - __slots__ = () - - def __new__(cls, *args, **kwargs): - raise TypeError("Type Annotated cannot be instantiated.") - - @typing._tp_cache - def __class_getitem__(cls, params): - if not isinstance(params, tuple) or len(params) < 2: - raise TypeError("Annotated[...] should be used " - "with at least two arguments (a type and an " - "annotation).") - allowed_special_forms = (ClassVar, Final) - if get_origin(params[0]) in allowed_special_forms: - origin = params[0] - else: - msg = "Annotated[t, ...]: t must be a type." - origin = typing._type_check(params[0], msg) - metadata = tuple(params[1:]) - return _AnnotatedAlias(origin, metadata) + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + if not isinstance(t, typing._UnionGenericAlias): + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True - def __init_subclass__(cls, *args, **kwargs): - raise TypeError( - f"Cannot subclass {cls.__module__}.Annotated" - ) + # < 3.11 + def _clean_optional(obj, hints, globalns=None, localns=None): + # reverts injected Union[..., None] cases from typing.get_type_hints + # when a None default value is used. + # see https://github.com/python/typing_extensions/issues/310 + if not hints or isinstance(obj, type): + return + defaults = typing._get_defaults(obj) # avoid accessing __annotations___ + if not defaults: + return + original_hints = obj.__annotations__ + for name, value in hints.items(): + # Not a Union[..., None] or replacement conditions not fullfilled + if (not _could_be_inserted_optional(value) + or name not in defaults + or defaults[name] is not None + ): + continue + original_value = original_hints[name] + # value=NoneType should have caused a skip above but check for safety + if original_value is None: + original_value = _NoneType + # Forward reference + if isinstance(original_value, str): + if globalns is None: + if isinstance(obj, _types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + # Compare if values differ. Note that even if equal + # value might be cached by typing._tp_cache contrary to original_evaluated + if original_evaluated != value or ( + # 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias + hasattr(_types, "UnionType") + and isinstance(original_evaluated, _types.UnionType) + and not isinstance(value, _types.UnionType) + ): + hints[name] = original_evaluated -# Python 3.8 has get_origin() and get_args() but those implementations aren't -# Annotated-aware, so we can't use those. Python 3.9's versions don't support +# Python 3.9 has get_origin() and get_args() but those implementations don't support # ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do. if sys.version_info[:2] >= (3, 10): get_origin = typing.get_origin get_args = typing.get_args -# 3.8-3.9 +# 3.9 else: - try: - # 3.9+ - from typing import _BaseGenericAlias - except ImportError: - _BaseGenericAlias = typing._GenericAlias - try: - # 3.9+ - from typing import GenericAlias as _typing_GenericAlias - except ImportError: - _typing_GenericAlias = typing._GenericAlias - def get_origin(tp): """Get the unsubscripted version of a type. @@ -1370,9 +1495,9 @@ def get_origin(tp): get_origin(List[Tuple[T, T]][int]) == list get_origin(P.args) is P """ - if isinstance(tp, _AnnotatedAlias): + if isinstance(tp, typing._AnnotatedAlias): return Annotated - if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias, _BaseGenericAlias, + if isinstance(tp, (typing._BaseGenericAlias, _types.GenericAlias, ParamSpecArgs, ParamSpecKwargs)): return tp.__origin__ if tp is typing.Generic: @@ -1390,11 +1515,9 @@ def get_args(tp): get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) get_args(Callable[[], T][int]) == ([], int) """ - if isinstance(tp, _AnnotatedAlias): - return (tp.__origin__,) + tp.__metadata__ - if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)): - if getattr(tp, "_special", False): - return () + if isinstance(tp, typing._AnnotatedAlias): + return (tp.__origin__, *tp.__metadata__) + if isinstance(tp, (typing._GenericAlias, _types.GenericAlias)): res = tp.__args__ if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: res = (list(res[:-1]), res[-1]) @@ -1406,7 +1529,7 @@ def get_args(tp): if hasattr(typing, 'TypeAlias'): TypeAlias = typing.TypeAlias # 3.9 -elif sys.version_info[:2] >= (3, 9): +else: @_ExtensionsSpecialForm def TypeAlias(self, parameters): """Special marker indicating that an assignment should @@ -1420,49 +1543,6 @@ def TypeAlias(self, parameters): It's invalid when used anywhere except as in the example above. """ raise TypeError(f"{self} is not subscriptable") -# 3.8 -else: - TypeAlias = _ExtensionsSpecialForm( - 'TypeAlias', - doc="""Special marker indicating that an assignment should - be recognized as a proper type alias definition by type - checkers. - - For example:: - - Predicate: TypeAlias = Callable[..., bool] - - It's invalid when used anywhere except as in the example - above.""" - ) - - -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): @@ -1536,7 +1616,7 @@ def __init_subclass__(cls) -> None: if hasattr(typing, 'ParamSpecArgs'): ParamSpecArgs = typing.ParamSpecArgs ParamSpecKwargs = typing.ParamSpecKwargs -# 3.8-3.9 +# 3.9 else: class _Immutable: """Mixin to indicate that object should not be copied.""" @@ -1647,7 +1727,7 @@ def _paramspec_prepare_subst(alias, args): def __init_subclass__(cls) -> None: raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type") -# 3.8-3.9 +# 3.9 else: # Inherits from list as a workaround for Callable checks in Python < 3.9.2. @@ -1752,17 +1832,31 @@ def __call__(self, *args, **kwargs): pass -# 3.8-3.9 +# 3.9 if not hasattr(typing, 'Concatenate'): # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + + # 3.9.0-1 + if not hasattr(typing, '_type_convert'): + def _type_convert(arg, module=None, *, allow_special_forms=False): + """For converting None to type(None), and strings to ForwardRef.""" + if arg is None: + return type(None) + if isinstance(arg, str): + if sys.version_info <= (3, 9, 6): + return ForwardRef(arg) + if sys.version_info <= (3, 9, 7): + return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, is_class=allow_special_forms) + return arg + else: + _type_convert = typing._type_convert + class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. __class__ = typing._GenericAlias - # Flag in 3.8. - _special = False - def __init__(self, origin, args): super().__init__(args) self.__origin__ = origin @@ -1786,28 +1880,171 @@ def __parameters__(self): tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) + # 3.9 used by __getitem__ below + def copy_with(self, params): + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return self.__class__(self.__origin__, params) + + # 3.9; accessed during GenericAlias.__getitem__ when substituting + def __getitem__(self, args): + if self.__origin__ in (Generic, Protocol): + # Can't subscript Generic[...] or Protocol[...]. + raise TypeError(f"Cannot subscript already-subscripted {self}") + if not self.__parameters__: + raise TypeError(f"{self} is not a generic class") + + if not isinstance(args, tuple): + args = (args,) + args = _unpack_args(*(_type_convert(p) for p in args)) + params = self.__parameters__ + for param in params: + prepare = getattr(param, "__typing_prepare_subst__", None) + if prepare is not None: + args = prepare(self, args) + # 3.9 & typing.ParamSpec + elif isinstance(param, ParamSpec): + i = params.index(param) + if ( + i == len(args) + and getattr(param, '__default__', NoDefault) is not NoDefault + ): + args = [*args, param.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {self}") + # Special case for Z[[int, str, bool]] == Z[int, str, bool] + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + elif ( + isinstance(args[i], list) + # 3.9 + # This class inherits from list do not convert + and not isinstance(args[i], _ConcatenateGenericAlias) + ): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + + alen = len(args) + plen = len(params) + if alen != plen: + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}" + ) + + subst = dict(zip(self.__parameters__, args)) + # determine new args + new_args = [] + for arg in self.__args__: + if isinstance(arg, type): + new_args.append(arg) + continue + if isinstance(arg, TypeVar): + arg = subst[arg] + if ( + (isinstance(arg, typing._GenericAlias) and _is_unpack(arg)) + or ( + hasattr(_types, "GenericAlias") + and isinstance(arg, _types.GenericAlias) + and getattr(arg, "__unpacked__", False) + ) + ): + raise TypeError(f"{arg} is not valid as type argument") + + elif isinstance(arg, + typing._GenericAlias + if not hasattr(_types, "GenericAlias") else + (typing._GenericAlias, _types.GenericAlias) + ): + subparams = arg.__parameters__ + if subparams: + subargs = tuple(subst[x] for x in subparams) + arg = arg[subargs] + new_args.append(arg) + return self.copy_with(tuple(new_args)) -# 3.8-3.9 +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias + + # 3.10 + if sys.version_info < (3, 11): + + class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True): + # needed for checks in collections.abc.Callable to accept this class + __module__ = "typing" + + def copy_with(self, params): + if isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + if isinstance(params[-1], typing._ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return super(typing._ConcatenateGenericAlias, self).copy_with(params) + + def __getitem__(self, args): + value = super().__getitem__(args) + if isinstance(value, tuple) and any(_is_unpack(t) for t in value): + return tuple(_unpack_args(*(n for n in value))) + return value + + +# 3.9.2 +class _EllipsisDummy: ... + + +# <=3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Hack: Arguments must be types, replace it with one. + parameters = (*parameters[:-1], _EllipsisDummy) + if sys.version_info >= (3, 10, 3): + concatenate = _ConcatenateGenericAlias(origin, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummy: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummy) + return concatenate + + +# <=3.10 @typing._tp_cache def _concatenate_getitem(self, parameters): if parameters == (): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - if not isinstance(parameters[-1], ParamSpec): + if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): raise TypeError("The last parameter to Concatenate should be a " - "ParamSpec variable.") + "ParamSpec variable or ellipsis.") msg = "Concatenate[arg, ...]: each arg must be a type." - parameters = tuple(typing._type_check(p, msg) for p in parameters) - return _ConcatenateGenericAlias(self, parameters) + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _create_concatenate_alias(self, parameters) -# 3.10+ -if hasattr(typing, 'Concatenate'): +# 3.11+; Concatenate does not accept ellipsis in 3.10 +if sys.version_info >= (3, 11): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa: F811 -# 3.9 -elif sys.version_info[:2] >= (3, 9): +# <=3.10 +else: @_ExtensionsSpecialForm def Concatenate(self, parameters): """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a @@ -1821,30 +2058,13 @@ def Concatenate(self, parameters): See PEP 612 for detailed information. """ return _concatenate_getitem(self, parameters) -# 3.8 -else: - class _ConcatenateForm(_ExtensionsSpecialForm, _root=True): - def __getitem__(self, parameters): - return _concatenate_getitem(self, parameters) - - Concatenate = _ConcatenateForm( - 'Concatenate', - doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a - higher order function which adds, removes or transforms parameters of a - callable. - - For example:: - Callable[Concatenate[int, P], int] - - See PEP 612 for detailed information. - """) # 3.10+ if hasattr(typing, 'TypeGuard'): TypeGuard = typing.TypeGuard # 3.9 -elif sys.version_info[:2] >= (3, 9): +else: @_ExtensionsSpecialForm def TypeGuard(self, parameters): """Special typing form used to annotate the return type of a user-defined @@ -1891,64 +2111,13 @@ def is_str(val: Union[str, float]): """ item = typing._type_check(parameters, f'{self} accepts only a single type.') return typing._GenericAlias(self, (item,)) -# 3.8 -else: - class _TypeGuardForm(_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,)) - - TypeGuard = _TypeGuardForm( - 'TypeGuard', - doc="""Special typing form used to annotate the return type of a user-defined - type guard function. ``TypeGuard`` only accepts a single type argument. - At runtime, functions marked this way should return a boolean. - - ``TypeGuard`` 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 ``TypeGuard[...]`` as its - return type to alert static type checkers to this intention. - - Using ``-> TypeGuard`` 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 type inside ``TypeGuard``. - - For example:: - - def is_str(val: Union[str, float]): - # "isinstance" type guard - if isinstance(val, str): - # Type of ``val`` is narrowed to ``str`` - ... - else: - # Else, type of ``val`` is narrowed to ``float``. - ... - - Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower - form of ``TypeA`` (it can even be a wider form) and this may lead to - type-unsafe results. The main reason is to allow for things like - narrowing ``List[object]`` to ``List[str]`` even though the latter is not - a subtype of the former, since ``List`` is invariant. The responsibility of - writing type-safe type guards is left to the user. - ``TypeGuard`` also works with type variables. For more information, see - PEP 647 (User-Defined Type Guards). - """) # 3.13+ if hasattr(typing, 'TypeIs'): TypeIs = typing.TypeIs -# 3.9 -elif sys.version_info[:2] >= (3, 9): +# <=3.12 +else: @_ExtensionsSpecialForm def TypeIs(self, parameters): """Special typing form used to annotate the return type of a user-defined @@ -1970,7 +2139,7 @@ def TypeIs(self, parameters): 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 + is the intersection of the type inside ``TypeIs`` and the argument's previously known type. For example:: @@ -1989,52 +2158,40 @@ def f(val: Union[int, Awaitable[int]]) -> int: """ 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. +# 3.14+? +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm +# <=3.13 +else: + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj - Using ``-> TypeIs`` tells the static type checker that for a given - function: + @_TypeFormForm + def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. - 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. + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. - For example:: + Usage: - def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: - return hasattr(val, '__await__') + def cast[T](typ: TypeForm[T], value: Any) -> T: ... - def f(val: Union[int, Awaitable[int]]) -> int: - if is_awaitable(val): - assert_type(val, Awaitable[int]) - else: - assert_type(val, int) + reveal_type(cast(int, "x")) # int - ``TypeIs`` also works with type variables. For more information, see - PEP 742 (Narrowing types with TypeIs). - """) + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) # Vendored from cpython typing._SpecialFrom @@ -2158,7 +2315,7 @@ def int_or_str(arg: int | str) -> None: if hasattr(typing, 'Required'): # 3.11+ Required = typing.Required NotRequired = typing.NotRequired -elif sys.version_info[:2] >= (3, 9): # 3.9-3.10 +else: # <=3.10 @_ExtensionsSpecialForm def Required(self, parameters): """A special typing construct to mark a key of a total=False TypedDict @@ -2196,49 +2353,10 @@ class Movie(TypedDict): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') return typing._GenericAlias(self, (item,)) -else: # 3.8 - class _RequiredForm(_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,)) - - Required = _RequiredForm( - 'Required', - doc="""A special typing construct to mark a key of a total=False TypedDict - as required. For example: - - class Movie(TypedDict, total=False): - title: Required[str] - year: int - - m = Movie( - title='The Matrix', # typechecker error if key is omitted - year=1999, - ) - - There is no runtime checking that a required key is actually provided - when instantiating a related TypedDict. - """) - NotRequired = _RequiredForm( - 'NotRequired', - doc="""A special typing construct to mark a key of a TypedDict as - potentially missing. For example: - - class Movie(TypedDict): - title: str - year: NotRequired[int] - - m = Movie( - title='The Matrix', # typechecker error if key is omitted - year=1999, - ) - """) - if hasattr(typing, 'ReadOnly'): ReadOnly = typing.ReadOnly -elif sys.version_info[:2] >= (3, 9): # 3.9-3.12 +else: # <=3.12 @_ExtensionsSpecialForm def ReadOnly(self, parameters): """A special typing construct to mark an item of a TypedDict as read-only. @@ -2258,30 +2376,6 @@ def mutate_movie(m: Movie) -> None: 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. @@ -2331,14 +2425,16 @@ def foo(**kwargs: Unpack[Movie]): ... def _is_unpack(obj): return get_origin(obj) is Unpack -elif sys.version_info[:2] >= (3, 9): # 3.9+ +else: # <=3.11 class _UnpackSpecialForm(_ExtensionsSpecialForm, _root=True): def __init__(self, getitem): super().__init__(getitem) self.__doc__ = _UNPACK_DOC class _UnpackAlias(typing._GenericAlias, _root=True): - __class__ = typing.TypeVar + if sys.version_info < (3, 11): + # needed for compatibility with Generic[Unpack[Ts]] + __class__ = typing.TypeVar @property def __typing_unpacked_tuple_args__(self): @@ -2351,6 +2447,17 @@ def __typing_unpacked_tuple_args__(self): return arg.__args__ return None + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + @_UnpackSpecialForm def Unpack(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2359,20 +2466,16 @@ def Unpack(self, parameters): def _is_unpack(obj): return isinstance(obj, _UnpackAlias) -else: # 3.8 - class _UnpackAlias(typing._GenericAlias, _root=True): - __class__ = typing.TypeVar - - class _UnpackForm(_ExtensionsSpecialForm, _root=True): - def __getitem__(self, parameters): - item = typing._type_check(parameters, - f'{self._name} accepts only a single type.') - return _UnpackAlias(self, (item,)) - Unpack = _UnpackForm('Unpack', doc=_UNPACK_DOC) - - def _is_unpack(obj): - return isinstance(obj, _UnpackAlias) +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 if _PEP_696_IMPLEMENTED: @@ -2380,16 +2483,6 @@ def _is_unpack(obj): 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): """Type variable tuple.""" @@ -2720,7 +2813,8 @@ def method(self) -> None: return arg -if hasattr(warnings, "deprecated"): +# Python 3.13.3+ contains a fix for the wrapped __new__ +if sys.version_info >= (3, 13, 3): deprecated = warnings.deprecated else: _T = typing.TypeVar("_T") @@ -2800,7 +2894,7 @@ def __call__(self, arg: _T, /) -> _T: original_new = arg.__new__ @functools.wraps(original_new) - def __new__(cls, *args, **kwargs): + def __new__(cls, /, *args, **kwargs): if cls is arg: warnings.warn(msg, category=category, stacklevel=stacklevel + 1) if original_new is not object.__new__: @@ -2839,13 +2933,21 @@ def __init_subclass__(*args, **kwargs): __init_subclass__.__deprecated__ = msg return arg elif callable(arg): + import asyncio.coroutines import functools + import inspect @functools.wraps(arg) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) return arg(*args, **kwargs) + if asyncio.coroutines.iscoroutinefunction(arg): + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + wrapper._is_coroutine = asyncio.coroutines._is_coroutine + arg.__deprecated__ = wrapper.__deprecated__ = msg return wrapper else: @@ -2854,6 +2956,24 @@ def wrapper(*args, **kwargs): f"a class or callable, not {arg!r}" ) +if sys.version_info < (3, 10): + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, (tuple, list, ParamSpec, _ConcatenateGenericAlias) + ) +else: + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, + ( + tuple, + list, + ParamSpec, + _ConcatenateGenericAlias, + typing._ConcatenateGenericAlias, + ), + ) + # We have to do some monkey patching to deal with the dual nature of # Unpack/TypeVarTuple: @@ -2868,6 +2988,17 @@ def _check_generic(cls, parameters, elen=_marker): This gives a nice error message in case of count mismatch. """ + # If substituting a single ParamSpec with multiple arguments + # we do not check the count + if (inspect.isclass(cls) and issubclass(cls, typing.Generic) + and len(cls.__parameters__) == 1 + and isinstance(cls.__parameters__[0], ParamSpec) + and parameters + and not _is_param_expr(parameters[0]) + ): + # Generic modifies parameters variable, but here we cannot do this + return + if not elen: raise TypeError(f"{cls} is not a generic class") if elen is _marker: @@ -2948,13 +3079,20 @@ def _check_generic(cls, parameters, elen): def _has_generic_or_protocol_as_origin() -> bool: try: frame = sys._getframe(2) - # not all platforms have sys._getframe() - except AttributeError: + # - 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: - return frame.f_locals.get("origin") in { - typing.Generic, Protocol, typing.Protocol - } + # 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)} @@ -2994,7 +3132,10 @@ def _collect_type_vars(types, typevar_types=None): for t in types: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True - elif isinstance(t, typevar_types) and t not in tvars: + elif ( + isinstance(t, typevar_types) and not isinstance(t, _UnpackAlias) + and t not in tvars + ): if enforce_default_ordering: has_default = getattr(t, '__default__', NoDefault) is not NoDefault if has_default: @@ -3009,6 +3150,13 @@ def _collect_type_vars(types, typevar_types=None): tvars.append(t) if _should_collect_from_parameters(t): tvars.extend([t for t in t.__parameters__ if t not in tvars]) + elif isinstance(t, tuple): + # Collect nested type_vars + # tuple wrapped by _prepare_paramspec_params(cls, params) + for x in t: + for collected in _collect_type_vars([x]): + if collected not in tvars: + tvars.append(collected) return tuple(tvars) typing._collect_type_vars = _collect_type_vars @@ -3087,10 +3235,6 @@ def _make_nmtuple(name, types, module, defaults=()): nm_tpl = collections.namedtuple(name, fields, defaults=defaults, module=module) nm_tpl.__annotations__ = nm_tpl.__new__.__annotations__ = annotations - # The `_field_types` attribute was removed in 3.9; - # in earlier versions, it is the same as the `__annotations__` attribute - if sys.version_info < (3, 9): - nm_tpl._field_types = annotations return nm_tpl _prohibited_namedtuple_fields = typing._prohibited @@ -3104,7 +3248,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: @@ -3166,7 +3316,6 @@ def _namedtuple_mro_entries(bases): assert NamedTuple in bases return (_NamedTuple,) - @_ensure_subclassable(_namedtuple_mro_entries) def NamedTuple(typename, fields=_marker, /, **kwargs): """Typed version of namedtuple. @@ -3232,11 +3381,13 @@ class Employee(NamedTuple): nt.__orig_bases__ = (NamedTuple,) return nt + NamedTuple.__mro_entries__ = _namedtuple_mro_entries + 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 @@ -3360,17 +3511,57 @@ def __ror__(self, other): return typing.Union[other, self] -if hasattr(typing, "TypeAliasType"): +if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType +# <=3.13 else: - def _is_unionable(obj): - """Corresponds to is_unionable() in unionobject.c in CPython.""" - return obj is None or isinstance(obj, ( - type, - _types.GenericAlias, - _types.UnionType, - TypeAliasType, - )) + if sys.version_info >= (3, 12): + # 3.12-3.13 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + typing.TypeAliasType, + TypeAliasType, + )) + else: + # <=3.11 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + + if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): + def __getattr__(self, attr): + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) + class TypeAliasType: """Create named, parameterized type aliases. @@ -3403,11 +3594,29 @@ class TypeAliasType: def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") self.__value__ = value self.__type_params__ = type_params + default_value_encountered = False parameters = [] for type_param in type_params: + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # <=3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) + or _is_unpack(type_param) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f"non-default type parameter '{type_param!r}'" + " follows default type parameter") + if has_default: + default_value_encountered = True if isinstance(type_param, TypeVarTuple): parameters.extend(type_param) else: @@ -3444,16 +3653,49 @@ def _raise_attribute_error(self, name: str) -> Never: def __repr__(self) -> str: return self.__name__ + if sys.version_info < (3, 11): + def _check_single_param(self, param, recursion=0): + # Allow [], [int], [int, str], [int, ...], [int, T] + if param is ...: + return ... + if param is None: + return None + # Note in <= 3.9 _ConcatenateGenericAlias inherits from list + if isinstance(param, list) and recursion == 0: + return [self._check_single_param(arg, recursion+1) + for arg in param] + return typing._type_check( + param, f'Subscripting {self.__name__} requires a type.' + ) + + def _check_parameters(self, parameters): + if sys.version_info < (3, 11): + return tuple( + self._check_single_param(item) + for item in parameters + ) + return tuple(typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ) + def __getitem__(self, parameters): + if not self.__type_params__: + raise TypeError("Only generic type aliases are subscriptable") if not isinstance(parameters, tuple): parameters = (parameters,) - parameters = [ - typing._type_check( - item, f'Subscripting {self.__name__} requires a type.' - ) - for item in parameters - ] - return typing._GenericAlias(self, tuple(parameters)) + # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, parameters) + type_vars = _collect_type_vars(parameters) + parameters = self._check_parameters(parameters) + alias = _TypeAliasGenericAlias(self, parameters) + # alias.__parameters__ is not complete if Concatenate is present + # as it is converted to a list from which no parameters are extracted. + if alias.__parameters__ != type_vars: + alias.__parameters__ = type_vars + return alias def __reduce__(self): return self.__name__ @@ -3580,43 +3822,478 @@ def __eq__(self, other: object) -> bool: __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 -# in a future version of Python -AbstractSet = typing.AbstractSet -AnyStr = typing.AnyStr -BinaryIO = typing.BinaryIO -Callable = typing.Callable -Collection = typing.Collection -Container = typing.Container -Dict = typing.Dict -ForwardRef = typing.ForwardRef -FrozenSet = typing.FrozenSet +if sys.version_info >= (3,14): + from annotationlib import Format, get_annotations +else: + class Format(enum.IntEnum): + VALUE = 1 + VALUE_WITH_FAKE_GLOBALS = 2 + FORWARDREF = 3 + STRING = 4 + + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, + format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This is a backport of `inspect.get_annotations`, which has been + in the standard library since Python 3.10. See the standard library + documentation for more: + + https://docs.python.org/3/library/inspect.html#inspect.get_annotations + + This backport adds the *format* argument introduced by PEP 649. The + three formats supported are: + * VALUE: the annotations are returned as-is. This is the default and + it is compatible with the behavior on previous Python versions. + * FORWARDREF: return annotations as-is if possible, but replace any + undefined names with ForwardRef objects. The implementation proposed by + PEP 649 relies on language changes that cannot be backported; the + typing-extensions implementation simply returns the same result as VALUE. + * STRING: return annotations as strings, in a format close to the original + source. Again, this behavior cannot be replicated directly in a backport. + As an approximation, typing-extensions retrieves the annotations under + VALUE semantics and then stringifies them. + + The purpose of this backport is to allow users who would like to use + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also + want to support earlier Python versions, to simply write: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + """ + format = Format(format) + if format is Format.VALUE_WITH_FAKE_GLOBALS: + raise ValueError( + "The VALUE_WITH_FAKE_GLOBALS format is for internal use only" + ) + + if eval_str and format is not Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, _types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, _types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = obj.__dict__ + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + elif hasattr(obj, '__annotations__'): + ann = obj.__annotations__ + obj_globals = obj_locals = unwrap = None + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + if format is Format.STRING: + return { + key: value if isinstance(value, str) else typing._type_repr(value) + for key, value in ann.items() + } + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) + if type_params is not None: + globals = dict(globals) + locals = dict(locals) + for param in type_params: + param_name = param.__name__ + if ( + _FORWARD_REF_HAS_CLASS and not forward_ref.__forward_is_class__ + ) or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def _lax_type_check( + value, msg, is_argument=True, *, module=None, allow_special_forms=False + ): + """ + A lax Python 3.11+ like version of typing._type_check + """ + if hasattr(typing, "_type_convert"): + if ( + sys.version_info >= (3, 10, 3) + or (3, 9, 10) < sys.version_info[:3] < (3, 10) + ): + # allow_special_forms introduced later cpython/#30926 (bpo-46539) + type_ = typing._type_convert( + value, + module=module, + allow_special_forms=allow_special_forms, + ) + # module was added with bpo-41249 before is_class (bpo-46539) + elif "__forward_module__" in typing.ForwardRef.__slots__: + type_ = typing._type_convert(value, module=module) + else: + type_ = typing._type_convert(value) + else: + if value is None: + return type(None) + if isinstance(value, str): + return ForwardRef(value) + type_ = value + invalid_generic_forms = (Generic, Protocol) + if not allow_special_forms: + invalid_generic_forms += (ClassVar,) + if is_argument: + invalid_generic_forms += (Final,) + if ( + isinstance(type_, typing._GenericAlias) + and get_origin(type_) in invalid_generic_forms + ): + raise TypeError(f"{type_} is not valid as type argument") from None + if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias): + return type_ + if allow_special_forms and type_ in (ClassVar, Final): + return type_ + if ( + isinstance(type_, (_SpecialForm, typing._SpecialForm)) + or type_ in (Generic, Protocol) + ): + raise TypeError(f"Plain {type_} is not valid as type argument") from None + if type(type_) is tuple: # lax version with tuple instead of callable + raise TypeError(f"{msg} Got {type_!r:.100}.") + return type_ + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=None, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + msg = "Forward references must evaluate to types." + if not _FORWARD_REF_HAS_CLASS: + allow_special_forms = not forward_ref.__forward_is_argument__ + else: + allow_special_forms = forward_ref.__forward_is_class__ + type_ = _lax_type_check( + value, + msg, + is_argument=forward_ref.__forward_is_argument__, + allow_special_forms=allow_special_forms, + ) + + # Recursively evaluate the type + if isinstance(type_, ForwardRef): + if getattr(type_, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + type_, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 12, 5): + return typing._eval_type( + type_, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + if sys.version_info < (3, 14): + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + format=format, + owner=owner, + ) + + +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") + + 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") + + +# Aliases for items that are in typing in all supported versions. +# We use hasattr() checks so this library will continue to import on +# future versions of Python that may remove these names. +_typing_names = [ + "AbstractSet", + "AnyStr", + "BinaryIO", + "Callable", + "Collection", + "Container", + "Dict", + "FrozenSet", + "Hashable", + "IO", + "ItemsView", + "Iterable", + "Iterator", + "KeysView", + "List", + "Mapping", + "MappingView", + "Match", + "MutableMapping", + "MutableSequence", + "MutableSet", + "Optional", + "Pattern", + "Reversible", + "Sequence", + "Set", + "Sized", + "TextIO", + "Tuple", + "Union", + "ValuesView", + "cast", + "no_type_check", + "no_type_check_decorator", + # This is private, but it was defined by typing_extensions for a long time + # and some users rely on it. + "_AnnotatedAlias", +] +globals().update( + {name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)} +) +# These are defined unconditionally because they are used in +# typing-extensions itself. Generic = typing.Generic -Hashable = typing.Hashable -IO = typing.IO -ItemsView = typing.ItemsView -Iterable = typing.Iterable -Iterator = typing.Iterator -KeysView = typing.KeysView -List = typing.List -Mapping = typing.Mapping -MappingView = typing.MappingView -Match = typing.Match -MutableMapping = typing.MutableMapping -MutableSequence = typing.MutableSequence -MutableSet = typing.MutableSet -Optional = typing.Optional -Pattern = typing.Pattern -Reversible = typing.Reversible -Sequence = typing.Sequence -Set = typing.Set -Sized = typing.Sized -TextIO = typing.TextIO -Tuple = typing.Tuple -Union = typing.Union -ValuesView = typing.ValuesView -cast = typing.cast -no_type_check = typing.no_type_check -no_type_check_decorator = typing.no_type_check_decorator +ForwardRef = typing.ForwardRef +Annotated = typing.Annotated diff --git a/test-requirements.txt b/test-requirements.txt index 675b2c5d..4b0fc81e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1 @@ -flake8 -flake8-bugbear +ruff==0.9.6