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..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,7 +1271,9 @@ def _create_typeddict( td.__orig_bases__ = (TypedDict,) return td - class _TypedDictSpecialForm(_ExtensionsSpecialForm, _root=True): + class _TypedDictSpecialForm(_SpecialForm, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + def __call__( self, typename, @@ -2201,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+