Further 3.12 compatibility fixes (#164) Make our TypeAliasType behave exactly like the 3.12 one
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f7820ec..8d0ed9d 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py
@@ -4745,30 +4745,64 @@ self.assertEqual(Variadic.__type_params__, (Ts,)) self.assertEqual(Variadic.__parameters__, tuple(iter(Ts))) - def test_immutable(self): + def test_cannot_set_attributes(self): Simple = TypeAliasType("Simple", int) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex(AttributeError, "readonly attribute"): Simple.__name__ = "NewName" - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__value__ = str - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__type_params__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__type_params__ = (T,) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__parameters__' of 'typing.TypeAliasType' objects is not writable", + ): Simple.__parameters__ = (T,) - with self.assertRaisesRegex(AttributeError, "Can't set attribute"): + with self.assertRaisesRegex( + AttributeError, + "attribute '__module__' of 'typing.TypeAliasType' objects is not writable", + ): + Simple.__module__ = 42 + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): Simple.some_attribute = "not allowed" - with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): + + def test_cannot_delete_attributes(self): + Simple = TypeAliasType("Simple", int) + with self.assertRaisesRegex(AttributeError, "readonly attribute"): del Simple.__name__ - with self.assertRaisesRegex(AttributeError, "Can't delete attribute"): - del Simple.nonexistent_attribute + with self.assertRaisesRegex( + AttributeError, + "attribute '__value__' of 'typing.TypeAliasType' objects is not writable", + ): + del Simple.__value__ + with self.assertRaisesRegex( + AttributeError, + "'typing.TypeAliasType' object has no attribute 'some_attribute'", + ): + del Simple.some_attribute def test_or(self): Alias = TypeAliasType("Alias", int) if sys.version_info >= (3, 10): - self.assertEqual(Alias | "Ref", Union[Alias, typing.ForwardRef("Ref")]) + self.assertEqual(Alias | int, Union[Alias, int]) + 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]]) else: with self.assertRaises(TypeError): - Alias | "Ref" + Alias | int + # Rejected on all versions + with self.assertRaises(TypeError): + Alias | "Ref" def test_getitem(self): ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ff5aefe..2a635ba 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py
@@ -1061,9 +1061,6 @@ if hasattr(typing, "Required"): get_type_hints = typing.get_type_hints else: - import functools - import types - # replaces _strip_annotations() def _strip_extras(t): """Strips Annotated, Required and NotRequired from a given type.""" @@ -1076,12 +1073,12 @@ if stripped_args == t.__args__: return t return t.copy_with(stripped_args) - if hasattr(types, "GenericAlias") and isinstance(t, types.GenericAlias): + if hasattr(_types, "GenericAlias") and isinstance(t, _types.GenericAlias): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t - return types.GenericAlias(t.__origin__, stripped_args) - if hasattr(types, "UnionType") and isinstance(t, types.UnionType): + return _types.GenericAlias(t.__origin__, stripped_args) + if hasattr(_types, "UnionType") and isinstance(t, _types.UnionType): stripped_args = tuple(_strip_extras(a) for a in t.__args__) if stripped_args == t.__args__: return t @@ -2691,6 +2688,15 @@ if hasattr(typing, "TypeAliasType"): TypeAliasType = typing.TypeAliasType 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, + )) + class TypeAliasType: """Create named, parameterized type aliases. @@ -2740,15 +2746,25 @@ def __setattr__(self, __name: str, __value: object) -> None: if hasattr(self, "__name__"): - raise AttributeError( - f"Can't set attribute {__name!r} on an instance of TypeAliasType" - ) + self._raise_attribute_error(__name) super().__setattr__(__name, __value) - def __delattr__(self, __name: str) -> None: - raise AttributeError( - f"Can't delete attribute {__name!r} on an instance of TypeAliasType" - ) + def __delattr__(self, __name: str) -> Never: + self._raise_attribute_error(__name) + + def _raise_attribute_error(self, name: str) -> Never: + # Match the Python 3.12 error messages exactly + if name == "__name__": + raise AttributeError("readonly attribute") + elif name in {"__value__", "__type_params__", "__parameters__", "__module__"}: + raise AttributeError( + f"attribute '{name}' of 'typing.TypeAliasType' objects " + "is not writable" + ) + else: + raise AttributeError( + f"'typing.TypeAliasType' object has no attribute '{name}'" + ) def __repr__(self) -> str: return self.__name__ @@ -2779,7 +2795,13 @@ if sys.version_info >= (3, 10): def __or__(self, right): + # For forward compatibility with 3.12, reject Unions + # that are not accepted by the built-in Union. + if not _is_unionable(right): + return NotImplemented return typing.Union[self, right] def __ror__(self, left): + if not _is_unionable(left): + return NotImplemented return typing.Union[left, self]