diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f062801..d0ced0b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,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 @@ -44,20 +39,21 @@ jobs: # 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" + - "3.13.0" - "pypy3.8" - "pypy3.9" - "pypy3.10" - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -73,6 +69,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 diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 8424d8fe..b477b930 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 @@ -123,32 +110,14 @@ jobs: 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' - ) - }} + 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,37 @@ jobs: allow-prereleases: true - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh + - name: Check out pyanalyze + run: git clone --depth=1 https://github.com/quora/pyanalyze.git || git clone --depth=1 https://github.com/quora/pyanalyze.git + - name: Checkout typing_extensions + uses: actions/checkout@v4 + with: + path: typing-extensions-latest - name: Install pyanalyze test requirements run: | + set -x cd pyanalyze uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies run: uv pip freeze - - name: Run pyanalyze tests - run: | - cd pyanalyze - pytest pyanalyze/ + # TODO: re-enable + # - name: Run pyanalyze tests + # run: | + # cd pyanalyze + # pytest pyanalyze/ typeguard: name: typeguard tests - if: >- - # if 'schedule' was the trigger, - # don't run it on contributors' forks - ${{ - github.event_name != 'schedule' - || ( - github.repository == 'python/typing_extensions' - && github.event_name == 'schedule' - ) - }} + 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,8 +163,15 @@ 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) - name: Install typing_extensions latest @@ -215,42 +181,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 +214,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 +229,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 +244,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 +267,108 @@ 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"] + # skip 3.13 because msgspec doesn't support 3.13 yet + python-version: ["3.9", "3.10", "3.11", "3.12"] 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 - 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 @@ -376,6 +382,7 @@ jobs: - typed-argument-parser - mypy - cattrs + - sqlalchemy if: >- ${{ @@ -390,6 +397,7 @@ jobs: || 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 90f5b682..c2105ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,79 @@ +# 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 diff --git a/doc/conf.py b/doc/conf.py index 42273604..cbb15a70 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,7 +27,7 @@ templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} +intersphinx_mapping = {'py': ('https://docs.python.org/3', None)} add_module_names = False diff --git a/doc/index.rst b/doc/index.rst index 3f0d2d44..2c1a149c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -133,7 +133,7 @@ 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 @@ -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 @@ -747,6 +757,56 @@ Functions .. versionadded:: 4.2.0 +.. function:: evaluate_forward_ref(forward_ref, *, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE) + + 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. + + .. 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 +846,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 +883,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 +921,45 @@ 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:: FORWARDREF + + Equal to 2. 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 3. 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 ~~~~~~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 3388d553..b2f62fe6 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.2" +version = "4.13.2" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" -license = { file = "LICENSE" } +license = "PSF-2.0" +license-files = ["LICENSE"] keywords = [ "annotations", "backport", @@ -30,7 +31,6 @@ 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", @@ -81,9 +81,21 @@ select = [ "W", ] -# Ignore various "modernization" rules that tell you off for importing/using -# deprecated things from the typing module, etc. -ignore = ["UP006", "UP007", "UP013", "UP014", "UP019", "UP035", "UP038"] +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", +] [tool.ruff.lint.per-file-ignores] "!src/typing_extensions.py" = [ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2f98765b..584b0fa4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -1,12 +1,15 @@ import abc +import asyncio import collections import collections.abc import contextlib import copy +import functools import gc import importlib import inspect import io +import itertools import pickle import re import subprocess @@ -25,6 +28,8 @@ import typing_extensions from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated from typing_extensions import ( + _FORWARD_REF_HAS_CLASS, + _PEP_649_OR_749_IMPLEMENTED, Annotated, Any, AnyStr, @@ -38,6 +43,7 @@ Dict, Doc, Final, + Format, Generic, IntVar, Iterable, @@ -49,6 +55,7 @@ Never, NewType, NoDefault, + NoExtraItems, NoReturn, NotRequired, Optional, @@ -65,6 +72,7 @@ TypeAlias, TypeAliasType, TypedDict, + TypeForm, TypeGuard, TypeIs, TypeVar, @@ -76,7 +84,9 @@ clear_overloads, dataclass_transform, deprecated, + evaluate_forward_ref, final, + get_annotations, get_args, get_origin, get_original_bases, @@ -107,11 +117,20 @@ 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 @@ -234,6 +253,177 @@ 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): @@ -517,6 +707,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: @@ -673,6 +882,37 @@ 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): + 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): @@ -1429,6 +1669,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): @@ -1504,12 +1833,14 @@ class C(Generic[T]): pass # In 3.9 and lower 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): @@ -3487,6 +3818,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") @@ -3827,18 +4162,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): @@ -3924,6 +4266,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 @@ -4070,24 +4443,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), @@ -4104,6 +4459,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) @@ -4402,13 +4804,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): @@ -4459,7 +4861,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 @@ -4471,7 +4874,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] @@ -4483,7 +4887,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" @@ -4496,7 +4914,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] @@ -4504,8 +4922,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] @@ -4514,8 +4932,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] @@ -4524,10 +4942,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]] @@ -4537,49 +4956,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, @@ -4589,9 +5056,22 @@ 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}) + + 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 + class AnnotatedTests(BaseTestCase): @@ -5041,6 +5521,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] @@ -5051,13 +5532,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 @@ -5065,6 +5606,76 @@ 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, )) + if sys.version_info < (3, 9): + self.skipTest("3.8 typing._type_subst does not support this substitution process") + 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') @@ -5146,21 +5757,41 @@ class MyClass: ... c = Concatenate[MyClass, P] self.assertNotEqual(c, Concatenate) + # 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') + C1 = Concatenate[int, P][typing.Concatenate[int, P]] + self.assertEqual(C1, Concatenate[int, int, P]) + self.assertEqual(get_args(C1), (int, int, P)) + + 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)) + 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): + if not TYPING_3_9_0 and callable_variant is collections.abc.Callable: + self.skipTest("Needs PEP 585") - C1 = Callable[Concatenate[int, P], int] - C2 = Callable[Concatenate[int, T, P], T] - self.assertEqual(C1.__origin__, C2.__origin__) - self.assertNotEqual(C1, C2) + 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) - # Test collections.abc.Callable too. - if sys.version_info[:2] >= (3, 9): - C3 = collections.abc.Callable[Concatenate[int, P], int] - C4 = collections.abc.Callable[Concatenate[int, T, P], T] - self.assertEqual(C3.__origin__, C4.__origin__) - self.assertNotEqual(C3, C4) + 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') @@ -5174,25 +5805,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') @@ -5203,6 +5863,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, typing.ForwardRef("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): @@ -5292,6 +5996,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: @@ -5505,6 +6267,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): @@ -5815,7 +6624,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', @@ -5828,6 +6637,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__: @@ -6899,6 +7712,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"): @@ -6951,6 +7838,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 @@ -6959,12 +7850,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],)) @@ -6973,6 +7871,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) @@ -6987,6 +8042,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): @@ -7033,5 +8162,776 @@ 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"}, + ) + self.assertEqual(get_annotations(f2, format=2), {"a": "undefined"}) + + self.assertEqual( + get_annotations(f1, format=Format.STRING), + {"a": "int"}, + ) + self.assertEqual(get_annotations(f1, format=3), {"a": "int"}) + + with self.assertRaises(ValueError): + get_annotations(f1, format=0) + + with self.assertRaises(ValueError): + get_annotations(f1, format=4) + + 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): + 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 _PEP_649_OR_749_IMPLEMENTED 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 _PEP_649_OR_749_IMPLEMENTED 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, typing.ForwardRef("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_value_is_cached(self): + fr = typing.ForwardRef("hello") + with self.assertRaises(NameError): + evaluate_forward_ref(fr) + self.assertIs(evaluate_forward_ref(fr, globals={"hello": str}), str) + self.assertIs(evaluate_forward_ref(fr), str) + + @skipUnless(TYPING_3_9_0, "Needs PEP 585 support") + 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}) + self.assertEqual(get_origin(evaluated_ref2), Y) + if not TYPING_3_9_0: + self.skipTest("Nested string 'Tx' stays ForwardRef in 3.8") + 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] in ((3,8), (3, 10)): + self.skipTest("Nested string 'StrAlias' is not resolved in 3.8 and 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) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index dec429ca..fa89c83e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1,9 +1,12 @@ import abc +import builtins import collections import collections.abc import contextlib +import enum import functools import inspect +import keyword import operator import sys import types as _types @@ -62,8 +65,11 @@ 'dataclass_transform', 'deprecated', 'Doc', + 'evaluate_forward_ref', 'get_overloads', 'final', + 'Format', + 'get_annotations', 'get_args', 'get_origin', 'get_original_bases', @@ -83,6 +89,7 @@ 'Text', 'TypeAlias', 'TypeAliasType', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -91,6 +98,8 @@ 'ReadOnly', 'Required', 'NotRequired', + 'NoDefault', + 'NoExtraItems', # Pure aliases, have always been in typing 'AbstractSet', @@ -117,7 +126,6 @@ 'MutableMapping', 'MutableSequence', 'MutableSet', - 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -138,6 +146,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. @@ -867,6 +878,63 @@ def inner(func): return inner +_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 # PEP 728 is implemented in CPython _PEP_728_IMPLEMENTED = False @@ -913,7 +981,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 +995,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,) @@ -964,7 +1036,7 @@ 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__ @@ -974,13 +1046,12 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): 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: + # 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_annotations): annotation_type = own_annotations.pop("__extra_items__") qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -1019,8 +1090,7 @@ def __new__(cls, name, bases, ns, *, total=True, closed=False): 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 @@ -1036,7 +1106,16 @@ 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 TypedDict( + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -1096,9 +1175,14 @@ class Point2D(TypedDict): "using the functional syntax, pass an empty dictionary, e.g. " ) + example + "." warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) - if closed is not False and closed is not True: + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: kwargs["closed"] = closed - closed = False + 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," @@ -1120,7 +1204,8 @@ class Point2D(TypedDict): # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total, closed=closed) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td @@ -1232,10 +1317,90 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): ) else: # 3.8 hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) + if sys.version_info < (3, 9): + # In 3.8 eval_type does not flatten Optional[ForwardRef] correctly + # This will recreate and and cache Unions. + hint = { + k: (t + if get_origin(t) != Union + else Union[t.__args__]) + for k, t in hint.items() + } if include_extras: return hint return {k: _strip_extras(t) for k, t in hint.items()} + _NoneType = type(None) + + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + # 3.8+ compatible checking before _UnionGenericAlias + if get_origin(t) is not Union: + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True + + # < 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 + if sys.version_info < (3, 9): + original_value = ForwardRef(original_value) + else: + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: + # Union[str, None, "str"] is not reduced to Union[str, None] + original_evaluated = Union[original_evaluated.__args__] + # 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.9+ has PEP 593 (Annotated) if hasattr(typing, 'Annotated'): @@ -1443,34 +1608,6 @@ def TypeAlias(self, parameters): ) -if hasattr(typing, "NoDefault"): - NoDefault = typing.NoDefault -else: - class NoDefaultTypeMeta(type): - def __setattr__(cls, attr, value): - # TypeError is consistent with the behavior of NoneType - raise TypeError( - f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" - ) - - class NoDefaultType(metaclass=NoDefaultTypeMeta): - """The type of the NoDefault singleton.""" - - __slots__ = () - - def __new__(cls): - return globals().get("NoDefault") or object.__new__(cls) - - def __repr__(self): - return "typing_extensions.NoDefault" - - def __reduce__(self): - return "NoDefault" - - NoDefault = NoDefaultType() - del NoDefaultType, NoDefaultTypeMeta - - def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault type_param.__default__ = default @@ -1761,6 +1898,23 @@ def __call__(self, *args, **kwargs): # 3.8-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__. @@ -1792,27 +1946,171 @@ def __parameters__(self): tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) + # 3.8; needed for typing._subst_tvars + # 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.8 - 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.8 - 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:]) -# 3.8-3.9 + 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.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.8-3.9.2 +class _EllipsisDummy: ... + + +# 3.8-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.8-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 -# 3.9 +# 3.9-3.10 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm def Concatenate(self, parameters): @@ -1976,7 +2274,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:: @@ -2024,7 +2322,7 @@ def __getitem__(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:: @@ -2042,6 +2340,69 @@ def f(val: Union[int, Awaitable[int]]) -> int: PEP 742 (Narrowing types with TypeIs). """) +# 3.14+? +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm +# 3.9 +elif sys.version_info[:2] >= (3, 9): + 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 + + @_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. + + 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. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeFormForm(_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,)) + + def __call__(self, obj, /): + return obj + + TypeForm = _TypeFormForm( + 'TypeForm', + doc="""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. + + 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. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True): @@ -2344,7 +2705,9 @@ def __init__(self, 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): @@ -2357,6 +2720,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.') @@ -2369,6 +2743,28 @@ def _is_unpack(obj): class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, typing._GenericAlias): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + 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) + class _UnpackForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, @@ -2381,21 +2777,22 @@ 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: from typing import TypeVarTuple elif hasattr(typing, "TypeVarTuple"): # 3.11+ - def _unpack_args(*args): - newargs = [] - for arg in args: - subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) - if subargs is not None and not (subargs and subargs[-1] is ...): - newargs.extend(subargs) - else: - newargs.append(arg) - return newargs - # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): """Type variable tuple.""" @@ -2726,7 +3123,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") @@ -2806,7 +3204,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__: @@ -2845,13 +3243,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: @@ -2860,6 +3266,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: @@ -2874,6 +3298,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: @@ -3007,7 +3442,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: @@ -3022,6 +3460,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 @@ -3379,17 +3824,62 @@ def __ror__(self, other): return typing.Union[other, self] -if hasattr(typing, "TypeAliasType"): +if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType +# 3.8-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.14 + 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.8-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) + + if sys.version_info < (3, 9): + def __getitem__(self, item): + result = super().__getitem__(item) + result.__class__ = type(self) + return result class TypeAliasType: """Create named, parameterized type aliases. @@ -3422,11 +3912,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.8-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: @@ -3463,16 +3971,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__ @@ -3599,6 +4140,408 @@ def __eq__(self, other: object) -> bool: __all__.append("CapsuleType") +# Using this convoluted approach so that this keeps working +# whether we end up using PEP 649 as written, PEP 749, or +# some other variation: in any case, inspect.get_annotations +# will continue to exist and will gain a `format` parameter. +_PEP_649_OR_749_IMPLEMENTED = ( + hasattr(inspect, 'get_annotations') + and inspect.get_annotations.__kwdefaults__ is not None + and "format" in inspect.get_annotations.__kwdefaults__ +) + + +class Format(enum.IntEnum): + VALUE = 1 + FORWARDREF = 2 + STRING = 3 + + +if _PEP_649_OR_749_IMPLEMENTED: + get_annotations = inspect.get_annotations +else: + 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 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=Format.VALUE, + _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, 9): + return typing._eval_type( + type_, + globals, + locals, + ) + 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, + ) + + # 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 diff --git a/test-requirements.txt b/test-requirements.txt index 7242d3b5..4b0fc81e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1 @@ -ruff==0.4.5 +ruff==0.9.6 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy