From 2d742164d717a39f40aca46aeb4a3b5188c5bda3 Mon Sep 17 00:00:00 2001 From: arthur-tacca Date: Fri, 1 Mar 2024 22:53:24 +0000 Subject: [PATCH 01/11] Add module name to doc source (to allow intersphinx usage) (#346) --- doc/conf.py | 17 ++++++++++++++++- doc/index.rst | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 7984bc22..40d3c6b7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -5,6 +5,8 @@ import os.path import sys +from sphinx.writers.html5 import HTML5Translator +from docutils.nodes import Element sys.path.insert(0, os.path.abspath('.')) @@ -26,9 +28,22 @@ intersphinx_mapping = {'py': ('https://docs.python.org/3.12', None)} +add_module_names = False # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'alabaster' -html_static_path = ['_static'] + + +class MyTranslator(HTML5Translator): + """Adds a link target to name without `typing_extensions.` prefix.""" + def visit_desc_signature(self, node: Element) -> None: + desc_name = node.get("fullname") + if desc_name: + self.body.append(f'') + super().visit_desc_signature(node) + + +def setup(app): + app.set_translator('html', MyTranslator) diff --git a/doc/index.rst b/doc/index.rst index 4bd8c702..63082ddd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,3 +1,4 @@ +.. module:: typing_extensions Welcome to typing_extensions's documentation! ============================================= From c3dc681a298fae6f2aa3e937e20a32a446ecb58c Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 6 Mar 2024 16:35:04 +0300 Subject: [PATCH 02/11] Make sure that `ReadOnly` is removed when using `get_type_hints(include_extra=False)` (#349) --- CHANGELOG.md | 4 ++++ doc/index.rst | 5 +++++ src/test_typing_extensions.py | 14 ++++++++++++++ src/typing_extensions.py | 6 +++--- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fc328d..a545e25b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Release 4.11.0 (WIP) + +- When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. + # Release 4.10.0 (February 24, 2024) This feature release adds support for PEP 728 (TypedDict with extra diff --git a/doc/index.rst b/doc/index.rst index 63082ddd..bdf94c75 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -760,6 +760,11 @@ Functions Interaction with :data:`Required` and :data:`NotRequired`. + .. versionchanged:: 4.11.0 + + When ``include_extra=False``, ``get_type_hints()`` now strips + :data:`ReadOnly` from the annotation. + .. function:: is_protocol(tp) Determine if a type is a :class:`Protocol`. This works with protocols diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 79c1b881..d48880ff 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4199,6 +4199,20 @@ class AllTheThings(TypedDict): self.assertEqual(AllTheThings.__readonly_keys__, frozenset({'a', 'b', 'c'})) self.assertEqual(AllTheThings.__mutable_keys__, frozenset({'d'})) + self.assertEqual( + get_type_hints(AllTheThings, include_extras=False), + {'a': int, 'b': int, 'c': int, 'd': int}, + ) + self.assertEqual( + get_type_hints(AllTheThings, include_extras=True), + { + 'a': Annotated[Required[ReadOnly[int]], 'why not'], + 'b': Required[Annotated[ReadOnly[int], 'why not']], + 'c': ReadOnly[NotRequired[Annotated[int, 'why not']]], + 'd': NotRequired[Annotated[int, 'why not']], + }, + ) + def test_extra_keys_non_readonly(self): class Base(TypedDict, closed=True): __extra_items__: str diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f3132ea4..4499c616 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1122,15 +1122,15 @@ def greet(name: str) -> None: return val -if hasattr(typing, "Required"): # 3.11+ +if hasattr(typing, "ReadOnly"): # 3.13+ get_type_hints = typing.get_type_hints -else: # <=3.10 +else: # <=3.13 # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" if isinstance(t, _AnnotatedAlias): return _strip_extras(t.__origin__) - if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired): + if hasattr(t, "__origin__") and t.__origin__ in (Required, NotRequired, ReadOnly): return _strip_extras(t.__args__[0]) if isinstance(t, typing._GenericAlias): stripped_args = tuple(_strip_extras(a) for a in t.__args__) From 3304a5f0045fc81ccc10c9c9fd238d378d020d94 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Thu, 7 Mar 2024 06:07:57 -0800 Subject: [PATCH 03/11] Stabilise third party tests (#348) Use uv to test with the state of PyPI as of the commit we are testing --- .github/workflows/third_party.yml | 51 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index 92ce3676..cee1fe21 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -103,12 +103,16 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typing_inspect test dependencies - run: pip install -r typing_inspect/test-requirements.txt + run: | + cd typing_inspect + uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typing_inspect tests run: | cd typing_inspect @@ -147,12 +151,16 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install pyanalyze test requirements - run: pip install ./pyanalyze[tests] + run: | + cd pyanalyze + uv pip install --system 'pyanalyze[tests] @ .' --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run pyanalyze tests run: | cd pyanalyze @@ -191,12 +199,16 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install typeguard test requirements - run: pip install -e ./typeguard[test] + run: | + cd typeguard + uv pip install --system "typeguard[test] @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typeguard tests run: | cd typeguard @@ -234,6 +246,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Configure git for typed-argument-parser tests # typed-argument parser does this in their CI, # and the tests fail unless we do this @@ -242,12 +256,13 @@ jobs: git config --global user.name "Your Name" - name: Install typed-argument-parser test requirements run: | - pip install -e ./typed-argument-parser - pip install pytest + cd typed-argument-parser + uv pip install --system "typed-argument-parser @ ." --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system pytest --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run typed-argument-parser tests run: | cd typed-argument-parser @@ -286,15 +301,17 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true + - name: Install uv + run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Install mypy test requirements run: | cd mypy - pip install -r test-requirements.txt - pip install -e . + uv pip install --system -r test-requirements.txt --exclude-newer $(git show -s --date=format:'%Y-%m-%dT%H:%M:%SZ' --format=%cd HEAD) + uv pip install --system -e . - name: Install typing_extensions latest - run: pip install ./typing-extensions-latest + run: uv pip install --system "typing-extensions @ ./typing-extensions-latest" - name: List all installed dependencies - run: pip freeze --all + run: uv pip freeze - name: Run stubtest & mypyc tests run: | cd mypy From 4fdc09ddb54be26580f68e26443a422c6024364c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 7 Mar 2024 15:57:21 +0000 Subject: [PATCH 04/11] Third-party tests: don't run pydantic tests on pypy (#351) they keep segfaulting and it's nothing to do with us --- .github/workflows/third_party.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/third_party.yml b/.github/workflows/third_party.yml index cee1fe21..a0feeefc 100644 --- a/.github/workflows/third_party.yml +++ b/.github/workflows/third_party.yml @@ -41,7 +41,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9", "pypy3.10"] + # PyPy is deliberately omitted here, + # since pydantic's tests intermittently segfault on PyPy, + # and it's nothing to do with typing_extensions + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] runs-on: ubuntu-latest timeout-minutes: 60 steps: From 9d1689ede041302d85f41292bf25a9d13bf16a7b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 13:38:44 -0700 Subject: [PATCH 05/11] Fix indentation in TypedDict docs (#352) --- doc/index.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index bdf94c75..f9097a41 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -396,37 +396,37 @@ Special typing primitives .. versionadded:: 4.9.0 - The experimental ``closed`` keyword argument and the special key - ``__extra_items__`` proposed in :pep:`728` are supported. + The experimental ``closed`` keyword argument and the special key + ``__extra_items__`` proposed in :pep:`728` are supported. - When ``closed`` is unspecified or ``closed=False`` is given, - ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a - special key that does not show up in ``__readonly_keys__``, - ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or - ``__annotations__``. + When ``closed`` is unspecified or ``closed=False`` is given, + ``__extra_items__`` behaves like a regular key. Otherwise, this becomes a + special key that does not show up in ``__readonly_keys__``, + ``__mutable_keys__``, ``__required_keys__``, ``__optional_keys``, or + ``__annotations__``. - For runtime introspection, two attributes can be looked at: + For runtime introspection, two attributes can be looked at: - .. attribute:: __closed__ + .. attribute:: __closed__ - A boolean flag indicating whether the current ``TypedDict`` is - considered closed. This is not inherited by the ``TypedDict``'s - subclasses. + A boolean flag indicating whether the current ``TypedDict`` is + considered closed. This is not inherited by the ``TypedDict``'s + subclasses. - .. versionadded:: 4.10.0 + .. versionadded:: 4.10.0 - .. attribute:: __extra_items__ + .. attribute:: __extra_items__ - The type annotation of the extra items allowed on the ``TypedDict``. - This attribute defaults to ``None`` on a TypedDict that has itself and - all its bases non-closed. This default is different from ``type(None)`` - that represents ``__extra_items__: None`` defined on a closed - ``TypedDict``. + The type annotation of the extra items allowed on the ``TypedDict``. + This attribute defaults to ``None`` on a TypedDict that has itself and + all its bases non-closed. This default is different from ``type(None)`` + that represents ``__extra_items__: None`` defined on a closed + ``TypedDict``. - If ``__extra_items__`` is not defined or inherited on a closed - ``TypedDict``, this defaults to ``Never``. + If ``__extra_items__`` is not defined or inherited on a closed + ``TypedDict``, this defaults to ``Never``. - .. versionadded:: 4.10.0 + .. versionadded:: 4.10.0 .. versionchanged:: 4.3.0 From d409ec98e3889462e59c85a4b34f9f83ce40bf2c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 18:44:13 -0700 Subject: [PATCH 06/11] Run CPython test suite in our CI (#353) --- .github/workflows/ci.yml | 8 ++++++++ CHANGELOG.md | 4 +++- src/test_typing_extensions.py | 4 ++-- src/typing_extensions.py | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1dc21e06..1174ce36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,6 +74,14 @@ jobs: cd src python -m unittest test_typing_extensions.py + - name: Test CPython typing test suite + run: | + # Run the typing test suite from CPython with typing_extensions installed, + # because we monkeypatch typing under some circumstances. + python -c 'import typing_extensions; import test.__main__' test_typing -v + # Test suite fails on PyPy even without typing_extensions + if: !startsWith(matrix.python-version, 'pypy') + linting: name: Lint diff --git a/CHANGELOG.md b/CHANGELOG.md index a545e25b..ce9d3f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ -# Release 4.11.0 (WIP) +# Unreleased +- Fix minor discrepancy between error messages produced by `typing` + and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra. - When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. # Release 4.10.0 (February 24, 2024) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index d48880ff..8940145e 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -3262,7 +3262,7 @@ def __call__(self, *args: Unpack[Ts]) -> T: ... self.assertEqual(MemoizedFunc.__parameters__, (Ts, T, T2)) self.assertTrue(MemoizedFunc._is_protocol) - things = "arguments" if sys.version_info >= (3, 11) else "parameters" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" # A bug was fixed in 3.11.1 # (https://github.com/python/cpython/commit/74920aa27d0c57443dd7f704d6272cca9c507ab3) @@ -5711,7 +5711,7 @@ class Y(Generic[T], NamedTuple): self.assertIsInstance(a, G) self.assertEqual(a.x, 3) - things = "arguments" if sys.version_info >= (3, 11) else "parameters" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4499c616..94218334 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -164,7 +164,8 @@ def _check_generic(cls, parameters, elen=_marker): num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): return - raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};" + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things} for {cls};" f" actual {alen}, expected {elen}") From d34c389d3d1f8cce006dfd1200e203551c16418c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 10 Mar 2024 20:53:23 -0700 Subject: [PATCH 07/11] Try to fix GH actions syntax (#355) --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1174ce36..e9d69774 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,12 +75,13 @@ jobs: python -m unittest test_typing_extensions.py - name: Test CPython typing test suite + # Test suite fails on PyPy even without typing_extensions + if: ${{ !startsWith(matrix.python-version, 'pypy') }} run: | + cd src # Run the typing test suite from CPython with typing_extensions installed, # because we monkeypatch typing under some circumstances. python -c 'import typing_extensions; import test.__main__' test_typing -v - # Test suite fails on PyPy even without typing_extensions - if: !startsWith(matrix.python-version, 'pypy') linting: name: Lint From 8170fc7744ca1c2ca4911ce22095c907f7f58f8b Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Tue, 12 Mar 2024 22:36:21 +0000 Subject: [PATCH 08/11] Fix runtime behaviour of PEP 696 (#293) Co-authored-by: Jelle Zijlstra Co-authored-by: James Hilton-Balfe Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- CHANGELOG.md | 2 + src/test_typing_extensions.py | 22 +++- src/typing_extensions.py | 187 ++++++++++++++++++++++++++-------- 3 files changed, 166 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9d3f0f..4ac948a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Fix the runtime behavior of type parameters with defaults (PEP 696). + Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` and `typing_extensions` on Python 3.10. Patch by Jelle Zijlstra. - When `include_extra=False`, `get_type_hints()` now strips `ReadOnly` from the annotation. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8940145e..03d3afda 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5712,7 +5712,6 @@ class Y(Generic[T], NamedTuple): self.assertEqual(a.x, 3) things = "arguments" if sys.version_info >= (3, 10) else "parameters" - with self.assertRaisesRegex(TypeError, f'Too many {things}'): G[int, str] @@ -6215,6 +6214,27 @@ def test_typevartuple(self): class A(Generic[Unpack[Ts]]): ... Alias = Optional[Unpack[Ts]] + def test_erroneous_generic(self): + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) + T = TypeVar('T') + + with self.assertRaises(TypeError): + Test = Generic[DefaultStrT, T] + + def test_need_more_params(self): + DefaultStrT = typing_extensions.TypeVar('DefaultStrT', default=str) + T = typing_extensions.TypeVar('T') + U = typing_extensions.TypeVar('U') + + class A(Generic[T, U, DefaultStrT]): ... + A[int, bool] + A[int, bool, str] + + with self.assertRaises( + TypeError, msg="Too few arguments for .+; actual 1, expected at least 2" + ): + Test = A[int] + def test_pickle(self): global U, U_co, U_contra, U_default # pickle wants to reference the class by name U = typing_extensions.TypeVar('U') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 94218334..09fcfd87 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -147,28 +147,6 @@ def __repr__(self): _marker = _Sentinel() -def _check_generic(cls, parameters, elen=_marker): - """Check correct count for parameters of a generic cls (internal helper). - This gives a nice error message in case of count mismatch. - """ - if not elen: - raise TypeError(f"{cls} is not a generic class") - if elen is _marker: - if not hasattr(cls, "__parameters__") or not cls.__parameters__: - raise TypeError(f"{cls} is not a generic class") - elen = len(cls.__parameters__) - alen = len(parameters) - if alen != elen: - if hasattr(cls, "__parameters__"): - parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] - num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) - if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): - return - things = "arguments" if sys.version_info >= (3, 10) else "parameters" - raise TypeError(f"Too {'many' if alen > elen else 'few'} {things} for {cls};" - f" actual {alen}, expected {elen}") - - if sys.version_info >= (3, 10): def _should_collect_from_parameters(t): return isinstance( @@ -182,27 +160,6 @@ def _should_collect_from_parameters(t): return isinstance(t, typing._GenericAlias) and not t._special -def _collect_type_vars(types, typevar_types=None): - """Collect all type variable contained in types in order of - first appearance (lexicographic order). For example:: - - _collect_type_vars((T, List[S, T])) == (T, S) - """ - if typevar_types is None: - typevar_types = typing.TypeVar - tvars = [] - for t in types: - if ( - isinstance(t, typevar_types) and - t not in tvars and - not _is_unpack(t) - ): - tvars.append(t) - if _should_collect_from_parameters(t): - tvars.extend([t for t in t.__parameters__ if t not in tvars]) - return tuple(tvars) - - NoReturn = typing.NoReturn # Some unconstrained type variables. These are used by the container types. @@ -2690,9 +2647,151 @@ def wrapper(*args, **kwargs): # counting generic parameters, so that when we subscript a generic, # the runtime doesn't try to substitute the Unpack with the subscripted type. if not hasattr(typing, "TypeVarTuple"): + def _check_generic(cls, parameters, elen=_marker): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + if elen is _marker: + if not hasattr(cls, "__parameters__") or not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + elen = len(cls.__parameters__) + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + num_tv_tuples = sum(isinstance(p, TypeVarTuple) for p in parameters) + if (num_tv_tuples > 0) and (alen >= elen - num_tv_tuples): + return + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return + + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + things = "arguments" if sys.version_info >= (3, 10) else "parameters" + raise TypeError(f"Too {'many' if alen > elen else 'few'} {things}" + f" for {cls}; actual {alen}, expected {expect_val}") +else: + # Python 3.11+ + + def _check_generic(cls, parameters, elen): + """Check correct count for parameters of a generic cls (internal helper). + + This gives a nice error message in case of count mismatch. + """ + if not elen: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + if alen != elen: + expect_val = elen + if hasattr(cls, "__parameters__"): + parameters = [p for p in cls.__parameters__ if not _is_unpack(p)] + + # deal with TypeVarLike defaults + # required TypeVarLikes cannot appear after a defaulted one. + if alen < elen: + # since we validate TypeVarLike default in _collect_type_vars + # or _collect_parameters we can safely check parameters[alen] + if getattr(parameters[alen], '__default__', None) is not None: + return + + num_default_tv = sum(getattr(p, '__default__', None) + is not None for p in parameters) + + elen -= num_default_tv + + expect_val = f"at least {elen}" + + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments" + f" for {cls}; actual {alen}, expected {expect_val}") + +typing._check_generic = _check_generic + +# Python 3.11+ _collect_type_vars was renamed to _collect_parameters +if hasattr(typing, '_collect_type_vars'): + def _collect_type_vars(types, typevar_types=None): + """Collect all type variable contained in types in order of + first appearance (lexicographic order). For example:: + + _collect_type_vars((T, List[S, T])) == (T, S) + """ + if typevar_types is None: + typevar_types = typing.TypeVar + tvars = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in types: + if ( + isinstance(t, typevar_types) and + t not in tvars and + not _is_unpack(t) + ): + if getattr(t, '__default__', None) is not None: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + tvars.append(t) + if _should_collect_from_parameters(t): + tvars.extend([t for t in t.__parameters__ if t not in tvars]) + return tuple(tvars) + typing._collect_type_vars = _collect_type_vars - typing._check_generic = _check_generic +else: + def _collect_parameters(args): + """Collect all type variables and parameter specifications in args + in order of first appearance (lexicographic order). + + For example:: + + assert _collect_parameters((T, Callable[P, T])) == (T, P) + """ + parameters = [] + # required TypeVarLike cannot appear after TypeVarLike with default + default_encountered = False + for t in args: + if isinstance(t, type): + # We don't want __parameters__ descriptor of a bare Python class. + pass + elif isinstance(t, tuple): + # `t` might be a tuple, when `ParamSpec` is substituted with + # `[T, int]`, or `[int, *Ts]`, etc. + for x in t: + for collected in _collect_parameters([x]): + if collected not in parameters: + parameters.append(collected) + elif hasattr(t, '__typing_subst__'): + if t not in parameters: + if getattr(t, '__default__', None) is not None: + default_encountered = True + elif default_encountered: + raise TypeError(f'Type parameter {t!r} without a default' + ' follows type parameter with a default') + + parameters.append(t) + else: + for x in getattr(t, '__parameters__', ()): + if x not in parameters: + parameters.append(x) + + return tuple(parameters) + typing._collect_parameters = _collect_parameters # Backport typing.NamedTuple as it exists in Python 3.13. # In 3.11, the ability to define generic `NamedTuple`s was supported. From 10648b6149e3b98cfb7d842684859318f01e940d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 14 Mar 2024 00:17:44 -0700 Subject: [PATCH 09/11] Fix tests on 3.13.0a5 (#358) --- CHANGELOG.md | 1 + src/test_typing_extensions.py | 2 +- src/typing_extensions.py | 9 +++++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac948a0..02f221d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Fix tests on 3.13.0a5. Patch by Jelle Zijlstra. - Fix the runtime behavior of type parameters with defaults (PEP 696). Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 03d3afda..27488550 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5531,7 +5531,7 @@ def test_typing_extensions_defers_when_possible(self): } if sys.version_info < (3, 13): exclude |= {'NamedTuple', 'Protocol', 'runtime_checkable'} - if not hasattr(typing, 'ReadOnly'): + if not typing_extensions._PEP_728_IMPLEMENTED: exclude |= {'TypedDict', 'is_typeddict'} for item in typing_extensions.__all__: if item not in exclude and hasattr(typing, item): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 09fcfd87..9ccd519c 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -792,7 +792,11 @@ def inner(func): return inner -if hasattr(typing, "ReadOnly"): +# Update this to something like >=3.13.0b1 if and when +# PEP 728 is implemented in CPython +_PEP_728_IMPLEMENTED = False + +if _PEP_728_IMPLEMENTED: # The standard library TypedDict in Python 3.8 does not store runtime information # about which (if any) keys are optional. See https://bugs.python.org/issue38834 # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" @@ -803,7 +807,8 @@ def inner(func): # Aaaand on 3.12 we add __orig_bases__ to TypedDict # to enable better runtime introspection. # On 3.13 we deprecate some odd ways of creating TypedDicts. - # PEP 705 proposes adding the ReadOnly[] qualifier. + # Also on 3.13, PEP 705 adds the ReadOnly[] qualifier. + # PEP 728 (still pending) makes more changes. TypedDict = typing.TypedDict _TypedDictMeta = typing._TypedDictMeta is_typeddict = typing.is_typeddict From 94bec447d6f7b9d3625ef0e688a0b0f9e487e951 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 24 Mar 2024 08:02:37 -0600 Subject: [PATCH 10/11] Prepare release 4.11.0rc1 (#362) --- CHANGELOG.md | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f221d7..52bf2ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -# Unreleased +# Release 4.11.0rc1 (March 24, 2024) -- Fix tests on 3.13.0a5. Patch by Jelle Zijlstra. +- Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra. - Fix the runtime behavior of type parameters with defaults (PEP 696). Patch by Nadir Chowdhury. - Fix minor discrepancy between error messages produced by `typing` diff --git a/pyproject.toml b/pyproject.toml index e0ef3432..2ecfbd3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.10.0" +version = "4.11.0rc1" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8" From d4d929d44bd984350e2d17726362295f588eaace Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 5 Apr 2024 08:33:24 -0400 Subject: [PATCH 11/11] Prepare release 4.11.0 (#363) --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52bf2ab3..4cf71773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Release 4.11.0 (April 5, 2024) + +This feature release provides improvements to various recently +added features, most importantly type parameter defaults (PEP 696). + +There are no changes since 4.11.0rc1. + # Release 4.11.0rc1 (March 24, 2024) - Fix tests on Python 3.13.0a5. Patch by Jelle Zijlstra. diff --git a/pyproject.toml b/pyproject.toml index 2ecfbd3a..4b1a7601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.11.0rc1" +version = "4.11.0" description = "Backported and Experimental Type Hints for Python 3.8+" readme = "README.md" requires-python = ">=3.8"