From d93860a591be61b09af3ded371a5901ca71e94f7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 2 Jun 2025 19:06:37 -0700 Subject: [PATCH 1/2] allow TypedDict as a type argument --- CHANGELOG.md | 6 +++++ src/test_typing_extensions.py | 6 +++++ src/typing_extensions.py | 45 ++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e833be..5d949cc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Fix usage of `typing_extensions.TypedDict` nested inside other types + (e.g., `typing.Type[typing_extensions.TypedDict]`). This is not allowed by the + type system but worked on older versions, so we maintain support. + # Release 4.14.0 (June 2, 2025) Changes since 4.14.0rc1: diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7fb748bb..6bc3de5a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4202,6 +4202,12 @@ def test_basics_functional_syntax(self): self.assertEqual(Emp.__annotations__, {'name': str, 'id': int}) self.assertEqual(Emp.__total__, True) + def test_allowed_as_type_argument(self): + # https://github.com/python/typing_extensions/issues/613 + obj = typing.Type[typing_extensions.TypedDict] + self.assertIs(typing_extensions.get_origin(obj), type) + self.assertEqual(typing_extensions.get_args(obj), (typing_extensions.TypedDict,)) + @skipIf(sys.version_info < (3, 13), "Change in behavior in 3.13") def test_keywords_syntax_raises_on_3_13(self): with self.assertRaises(TypeError), self.assertWarns(DeprecationWarning): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5d5a5c7f..d274c12f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -1223,7 +1223,50 @@ def _create_typeddict( td.__orig_bases__ = (TypedDict,) return td - class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True): + # Cannot inherit from typing._SpecialForm, because then typing._type_check + # would reject TypedDict as a type, and we need that to work for compatibility. + if hasattr(typing, "_NotIterable"): + _special_form_bases = (typing._Final, typing._NotIterable) + else: + _special_form_bases = (typing._Final,) + + class _TypedDictSpecialForm(*_special_form_bases, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ + + def __getattr__(self, item): + if item in {'__name__', '__qualname__'}: + return self._name + + raise AttributeError(item) + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __reduce__(self): + return self._name + + if sys.version_info >= (3, 10): + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance()") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass()") + + @typing._tp_cache + def __getitem__(self, parameters): + return self._getitem(self, parameters) + def __call__( self, typename, From af262d011d496f5700de9be55ab877fd6ed2066a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 3 Jun 2025 07:41:22 -0700 Subject: [PATCH 2/2] better solution --- src/typing_extensions.py | 133 +++++++++++++++------------------------ 1 file changed, 49 insertions(+), 84 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d274c12f..b97acf80 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -221,7 +221,55 @@ def __new__(cls, *args, **kwargs): ClassVar = typing.ClassVar +# Vendored from cpython typing._SpecialFrom +# Having a separate class means that instances will not be rejected by +# typing._type_check. +class _SpecialForm(typing._Final, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ + + def __getattr__(self, item): + if item in {'__name__', '__qualname__'}: + return self._name + + raise AttributeError(item) + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass {self!r}") + + def __repr__(self): + return f'typing_extensions.{self._name}' + + def __reduce__(self): + return self._name + + def __call__(self, *args, **kwds): + raise TypeError(f"Cannot instantiate {self!r}") + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance()") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass()") + + @typing._tp_cache + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + +# Note that inheriting from this class means that the object will be +# rejected by typing._type_check, so do not use it if the special form +# is arguably valid as a type by itself. class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): def __repr__(self): return 'typing_extensions.' + self._name @@ -1223,50 +1271,9 @@ def _create_typeddict( td.__orig_bases__ = (TypedDict,) return td - # Cannot inherit from typing._SpecialForm, because then typing._type_check - # would reject TypedDict as a type, and we need that to work for compatibility. - if hasattr(typing, "_NotIterable"): - _special_form_bases = (typing._Final, typing._NotIterable) - else: - _special_form_bases = (typing._Final,) - - class _TypedDictSpecialForm(*_special_form_bases, _root=True): + class _TypedDictSpecialForm(_SpecialForm, _root=True): __slots__ = ('_name', '__doc__', '_getitem') - def __init__(self, getitem): - self._getitem = getitem - self._name = getitem.__name__ - self.__doc__ = getitem.__doc__ - - def __getattr__(self, item): - if item in {'__name__', '__qualname__'}: - return self._name - - raise AttributeError(item) - - def __repr__(self): - return 'typing_extensions.' + self._name - - def __reduce__(self): - return self._name - - if sys.version_info >= (3, 10): - def __or__(self, other): - return typing.Union[self, other] - - def __ror__(self, other): - return typing.Union[other, self] - - def __instancecheck__(self, obj): - raise TypeError(f"{self} cannot be used with isinstance()") - - def __subclasscheck__(self, cls): - raise TypeError(f"{self} cannot be used with issubclass()") - - @typing._tp_cache - def __getitem__(self, parameters): - return self._getitem(self, parameters) - def __call__( self, typename, @@ -2244,48 +2251,6 @@ def cast[T](typ: TypeForm[T], value: Any) -> T: ... return typing._GenericAlias(self, (item,)) -# Vendored from cpython typing._SpecialFrom -class _SpecialForm(typing._Final, _root=True): - __slots__ = ('_name', '__doc__', '_getitem') - - def __init__(self, getitem): - self._getitem = getitem - self._name = getitem.__name__ - self.__doc__ = getitem.__doc__ - - def __getattr__(self, item): - if item in {'__name__', '__qualname__'}: - return self._name - - raise AttributeError(item) - - def __mro_entries__(self, bases): - raise TypeError(f"Cannot subclass {self!r}") - - def __repr__(self): - return f'typing_extensions.{self._name}' - - def __reduce__(self): - return self._name - - def __call__(self, *args, **kwds): - raise TypeError(f"Cannot instantiate {self!r}") - - def __or__(self, other): - return typing.Union[self, other] - - def __ror__(self, other): - return typing.Union[other, self] - - def __instancecheck__(self, obj): - raise TypeError(f"{self} cannot be used with isinstance()") - - def __subclasscheck__(self, cls): - raise TypeError(f"{self} cannot be used with issubclass()") - - @typing._tp_cache - def __getitem__(self, parameters): - return self._getitem(self, parameters) if hasattr(typing, "LiteralString"): # 3.11+