From 57aae6249dd3c068a7068fd11e7d67379a544acf Mon Sep 17 00:00:00 2001 From: Maxim Martynov Date: Wed, 24 May 2023 13:42:58 +0300 Subject: [PATCH 1/4] Fix isinstance check for Generic classes on Python 3.7 (#188) --- CHANGELOG.md | 6 ++++++ src/test_typing_extensions.py | 23 +++++++++++++++++++++++ src/typing_extensions.py | 5 +++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b163a93..853c211c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# Unreleased + +- Fix regression in version 4.6.1 where comparing a generic class against a + runtime-checkable protocol using `isinstance()` would cause `AttributeError` + to be raised if using Python 3.7 + # Release 4.6.1 (May 23, 2023) - Change deprecated `@runtime` to formal API `@runtime_checkable` in the error diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 9c605fa4..736b46b4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -2246,6 +2246,28 @@ class Foo: ... del f.x self.assertNotIsInstance(f, HasX) + def test_protocols_isinstance_generic_classes(self): + T = TypeVar("T") + + class Foo(Generic[T]): + x: T + + def __init__(self, x): + self.x = x + + class Bar(Foo[int]): + ... + + @runtime_checkable + class HasX(Protocol): + x: int + + foo = Foo(1) + self.assertIsInstance(foo, HasX) + + bar = Bar(2) + self.assertIsInstance(bar, HasX) + def test_protocols_support_register(self): @runtime_checkable class P(Protocol): @@ -4330,6 +4352,7 @@ class Y(Generic[T], NamedTuple): a = A(3) self.assertIs(type(a), G) + self.assertIsInstance(a, G) self.assertEqual(a.x, 3) things = "arguments" if sys.version_info >= (3, 11) else "parameters" diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f13859f0..a3f45daa 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -9,7 +9,6 @@ import typing import warnings - __all__ = [ # Super-special typing primitives. 'Any', @@ -659,7 +658,9 @@ def _proto_hook(cls, other): isinstance(annotations, collections.abc.Mapping) and attr in annotations and issubclass(other, (typing.Generic, _ProtocolMeta)) - and other._is_protocol + # All subclasses of Generic have an _is_proto attribute on 3.8+ + # But not on 3.7 + and getattr(other, "_is_protocol", False) ): break else: From e7fe63f0673938e79078577cb749707aad3a2dea Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 24 May 2023 23:33:46 -0700 Subject: [PATCH 2/4] Add guidance on robust runtime use (#189) --- doc/index.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/index.rst b/doc/index.rst index b38e6477..e790a2fd 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -42,6 +42,26 @@ version from which features were backported; for example, ``typing_extensions`` 3.10.0.0 was meant to reflect ``typing`` as of Python 3.10.0. During this period, no changelog was maintained. +Runtime use of types +~~~~~~~~~~~~~~~~~~~~ + +We aim for complete backwards compatibility in terms of the names we export: +code like ``from typing_extensions import X`` that works on one +typing-extensions release will continue to work on the next. +It is more difficult to maintain compatibility for users that introspect +types at runtime, as almost any detail can potentially break compatibility. +Users who introspect types should follow these guidelines to minimize +the risk of compatibility issues: + +- Always check for both the :mod:`typing` and ``typing_extensions`` versions + of objects, even if they are currently the same on some Python version. + Future ``typing_extensions`` releases may re-export a separate version of + the object to backport some new feature or bugfix. +- Use public APIs like :func:`get_origin` and :func:`get_original_bases` to + access internal information about types, instead of accessing private + attributes directly. If some information is not available through a public + attribute, consider opening an issue in CPython to add such an API. + Python version support ---------------------- From 2912585d53130092a868f8109ce2739abf0a7f95 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 05:46:22 -0700 Subject: [PATCH 3/4] Fix @deprecated on classes with only __new__ (#193) --- CHANGELOG.md | 6 ++++-- src/test_typing_extensions.py | 21 +++++++++++++++++++++ src/typing_extensions.py | 6 +++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 853c211c..428fcfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ # Unreleased -- Fix regression in version 4.6.1 where comparing a generic class against a +- Fix use of `@deprecated` on classes with `__new__` but no `__init__`. + Patch by Jelle Zijlstra. +- Fix regression in version 4.6.1 where comparing a generic class against a runtime-checkable protocol using `isinstance()` would cause `AttributeError` - to be raised if using Python 3.7 + to be raised if using Python 3.7. # Release 4.6.1 (May 23, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 736b46b4..fd2a91c3 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -357,6 +357,7 @@ class A: with self.assertRaises(TypeError): A(42) + def test_class_with_init(self): @deprecated("HasInit will go away soon") class HasInit: def __init__(self, x): @@ -366,6 +367,7 @@ def __init__(self, x): instance = HasInit(42) self.assertEqual(instance.x, 42) + def test_class_with_new(self): has_new_called = False @deprecated("HasNew will go away soon") @@ -382,6 +384,8 @@ def __init__(self, x) -> None: instance = HasNew(42) self.assertEqual(instance.x, 42) self.assertTrue(has_new_called) + + def test_class_with_inherited_new(self): new_base_called = False class NewBase: @@ -402,6 +406,23 @@ class HasInheritedNew(NewBase): self.assertEqual(instance.x, 42) self.assertTrue(new_base_called) + def test_class_with_new_but_no_init(self): + new_called = False + + @deprecated("HasNewNoInit will go away soon") + class HasNewNoInit: + def __new__(cls, x): + nonlocal new_called + new_called = True + obj = super().__new__(cls) + obj.x = x + return obj + + with self.assertWarnsRegex(DeprecationWarning, "HasNewNoInit will go away soon"): + instance = HasNewNoInit(42) + self.assertEqual(instance.x, 42) + self.assertTrue(new_called) + def test_function(self): @deprecated("b will go away soon") def b(): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a3f45daa..9aa84d7e 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2497,11 +2497,11 @@ def decorator(__arg: _T) -> _T: @functools.wraps(original_new) def __new__(cls, *args, **kwargs): warnings.warn(__msg, category=category, stacklevel=stacklevel + 1) - # Mirrors a similar check in object.__new__. - if not has_init and (args or kwargs): - raise TypeError(f"{cls.__name__}() takes no arguments") if original_new is not object.__new__: return original_new(cls, *args, **kwargs) + # Mirrors a similar check in object.__new__. + elif not has_init and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") else: return original_new(cls) From e84f909f6a9e8e03ef444a3a3c7e506e8e3b19e4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 25 May 2023 06:13:04 -0700 Subject: [PATCH 4/4] Prepare release 4.6.2 (#195) --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428fcfd4..a6165d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Unreleased +# Release 4.6.2 (May 25, 2023) - Fix use of `@deprecated` on classes with `__new__` but no `__init__`. Patch by Jelle Zijlstra. diff --git a/pyproject.toml b/pyproject.toml index fd1aeb60..74ec5ed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" # Project metadata [project] name = "typing_extensions" -version = "4.6.1" +version = "4.6.2" description = "Backported and Experimental Type Hints for Python 3.7+" readme = "README.md" requires-python = ">=3.7"