From 7a36b1f85779d3f58b71c6830cd4cb26b53639dc Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 26 May 2023 12:57:57 +0100 Subject: [PATCH 1/5] Add introspection helper functions for third-party libraries --- src/test_typing_extensions.py | 68 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 44 +++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index ac0a2691..59e43aa2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -37,6 +37,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar +from typing_extensions import typing_extensions_reexports_name, get_typing_objects_by_name_of from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -4988,5 +4989,72 @@ class MyAlias(TypeAliasType): pass +class IntrospectionHelperTests(BaseTestCase): + def test_typing_extensions_reexports(self): + for name in typing_extensions.__all__: + with self.subTest(name=name): + self.assertIs(type(typing_extensions_reexports_name(name)), bool) + + self.assertTrue(typing_extensions_reexports_name("ClassVar")) + + with self.assertRaisesRegex(ValueError, "no object called 'foo'"): + typing_extensions_reexports_name("foo") + + @skipIf(TYPING_3_12_0, "We reexport TypeAliasType from typing on 3.12+") + def test_typing_extensions_doesnt_reexport(self): + self.assertFalse(typing_extensions_reexports_name("TypeAliasType")) + + def test_typing_objects_by_name_of(self): + for name in typing_extensions.__all__: + with self.subTest(name=name): + objs = get_typing_objects_by_name_of(name) + self.assertIsInstance(objs, tuple) + self.assertIn(len(objs), (1, 2)) + te_obj = getattr(typing_extensions, name) + if len(objs) == 1: + self.assertIs(te_obj, getattr(typing, name, te_obj)) + else: + self.assertTrue(hasattr(typing, name)) + self.assertIsNot(te_obj, getattr(typing, name)) + + with self.assertRaisesRegex(ValueError, "no object called 'foo'"): + get_typing_objects_by_name_of("foo") + + def test_typing_objects_by_name_of_2(self): + classvar_objs = get_typing_objects_by_name_of("ClassVar") + self.assertEqual(len(classvar_objs), 1) + classvar_obj = classvar_objs[0] + self.assertIs(classvar_obj, typing.ClassVar) + self.assertIs(classvar_obj, typing_extensions.ClassVar) + self.assertEqual(classvar_obj.__module__, "typing") + + @skipIf(TYPING_3_12_0, "We reexport TypeAliasType from typing on 3.12+") + def test_typing_objects_by_name_of_2(self): + name = "TypeAliasType" + # Sanity check; the test won't work correctly if this doesn't hold true: + self.assertFalse(hasattr(typing, name)) + typealiastype_objs = get_typing_objects_by_name_of(name) + self.assertEqual(len(typealiastype_objs), 1) + typealiastype_obj = typealiastype_objs[0] + self.assertIs(typealiastype_obj, typing_extensions.TypeAliasType) + self.assertEqual(typealiastype_obj.__module__, "typing_extensions") + + @skipUnless( + (3, 8) <= sys.version_info < (3, 12), + ( + "Needs a Python version where typing.Protocol " + "and typing_extensions.Protocol are different objects" + ) + ) + def test_typing_objects_by_name_of_3(self): + name = "Protocol" + # Sanity check; the test won't work correctly if this doesn't hold true: + self.assertTrue(hasattr(typing, name)) + protocol_objs = get_typing_objects_by_name_of(name) + self.assertEqual(len(protocol_objs), 2) + modules = {obj.__module__ for obj in protocol_objs} + self.assertEqual(modules, {"typing", "typing_extensions"}) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 9aa84d7e..ead08210 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -85,6 +85,11 @@ 'NoReturn', 'Required', 'NotRequired', + + # Introspection helpers unique to typing_extensions + # These will never be added to typing.py in CPython + 'typing_extensions_reexports_name', + 'get_typing_objects_by_name_of', ] # for backward compatibility @@ -2873,3 +2878,42 @@ def __ror__(self, left): if not _is_unionable(left): return NotImplemented return typing.Union[left, self] + + +############################################################# +# Introspection helpers for third-party libraries +# +# These are not part of the typing-module API, +# and nor will they ever become part of the typing-module API. +# +# They are specific to typing-extensions +############################################################## + + +def _get_name_from_globals(name: str) -> object: + try: + obj = globals()[name] + except KeyError: + raise ValueError( + f"The typing_extensions module has no object called {name!r}!" + ) from None + return obj + + +@functools.lru_cache(maxsize=None) +def typing_extensions_reexports_name(name: str) -> bool: + return _get_name_from_globals(name) is getattr(typing, name, object()) + + +@functools.lru_cache(maxsize=None) +def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: + te_obj = _get_name_from_globals(name) + objs = [te_obj] + if hasattr(typing, name): + typing_obj = getattr(typing, name) + # Some typing objects compare equal to the equivalent typing_extensions object, + # but aren't actually the exact same object, + # so we can't use a set here; a list is better + if typing_obj is not te_obj: + objs.append(typing_obj) + return tuple(objs) From 56da97e9824a47dabd42ab6c5c34b91bb25bf45c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 26 May 2023 13:47:12 +0100 Subject: [PATCH 2/5] Get rid of the useless one --- src/test_typing_extensions.py | 16 +--------------- src/typing_extensions.py | 6 ------ 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 59e43aa2..8f21e57a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -37,7 +37,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar -from typing_extensions import typing_extensions_reexports_name, get_typing_objects_by_name_of +from typing_extensions import get_typing_objects_by_name_of from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -4990,20 +4990,6 @@ class MyAlias(TypeAliasType): class IntrospectionHelperTests(BaseTestCase): - def test_typing_extensions_reexports(self): - for name in typing_extensions.__all__: - with self.subTest(name=name): - self.assertIs(type(typing_extensions_reexports_name(name)), bool) - - self.assertTrue(typing_extensions_reexports_name("ClassVar")) - - with self.assertRaisesRegex(ValueError, "no object called 'foo'"): - typing_extensions_reexports_name("foo") - - @skipIf(TYPING_3_12_0, "We reexport TypeAliasType from typing on 3.12+") - def test_typing_extensions_doesnt_reexport(self): - self.assertFalse(typing_extensions_reexports_name("TypeAliasType")) - def test_typing_objects_by_name_of(self): for name in typing_extensions.__all__: with self.subTest(name=name): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ead08210..0f026145 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -88,7 +88,6 @@ # Introspection helpers unique to typing_extensions # These will never be added to typing.py in CPython - 'typing_extensions_reexports_name', 'get_typing_objects_by_name_of', ] @@ -2900,11 +2899,6 @@ def _get_name_from_globals(name: str) -> object: return obj -@functools.lru_cache(maxsize=None) -def typing_extensions_reexports_name(name: str) -> bool: - return _get_name_from_globals(name) is getattr(typing, name, object()) - - @functools.lru_cache(maxsize=None) def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: te_obj = _get_name_from_globals(name) From 02fac4324813edeb7766f797a4bcdb33ff18c22c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 26 May 2023 13:56:41 +0100 Subject: [PATCH 3/5] Add `is_typing_name` --- src/test_typing_extensions.py | 10 +++++++++- src/typing_extensions.py | 16 ++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 8f21e57a..c11e4d5a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -37,7 +37,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar -from typing_extensions import get_typing_objects_by_name_of +from typing_extensions import get_typing_objects_by_name_of, is_typing_name from _typed_dict_test_helper import Foo, FooGeneric # Flags used to mark tests that only apply after a specific @@ -5041,6 +5041,14 @@ def test_typing_objects_by_name_of_3(self): modules = {obj.__module__ for obj in protocol_objs} self.assertEqual(modules, {"typing", "typing_extensions"}) + def test_is_typing_name(self): + for name in typing_extensions.__all__: + te_obj = getattr(typing_extensions, name) + self.assertTrue(is_typing_name(te_obj, name)) + if hasattr(typing, name): + typing_obj = getattr(typing, name) + self.assertTrue(is_typing_name(typing_obj, name)) + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 0f026145..54b9f45f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -89,6 +89,7 @@ # Introspection helpers unique to typing_extensions # These will never be added to typing.py in CPython 'get_typing_objects_by_name_of', + 'is_typing_name', ] # for backward compatibility @@ -2889,19 +2890,14 @@ def __ror__(self, left): ############################################################## -def _get_name_from_globals(name: str) -> object: +@functools.lru_cache(maxsize=None) +def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: try: - obj = globals()[name] + te_obj = globals()[name] except KeyError: raise ValueError( f"The typing_extensions module has no object called {name!r}!" ) from None - return obj - - -@functools.lru_cache(maxsize=None) -def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: - te_obj = _get_name_from_globals(name) objs = [te_obj] if hasattr(typing, name): typing_obj = getattr(typing, name) @@ -2911,3 +2907,7 @@ def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: if typing_obj is not te_obj: objs.append(typing_obj) return tuple(objs) + + +def is_typing_name(obj: object, name: str) -> bool: + return any(obj is thing for thing in get_typing_objects_by_name_of(name)) From 60b331e3f46f0f510b14e770cd2b0a02aa06ee83 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 26 May 2023 14:23:46 +0100 Subject: [PATCH 4/5] Make it work even if it only exists in typing, not typing_extensions --- src/test_typing_extensions.py | 27 ++++++++++++++++++++++++++- src/typing_extensions.py | 29 +++++++++++++++++------------ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index c11e4d5a..4b303e92 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -5003,9 +5003,20 @@ def test_typing_objects_by_name_of(self): self.assertTrue(hasattr(typing, name)) self.assertIsNot(te_obj, getattr(typing, name)) - with self.assertRaisesRegex(ValueError, "no object called 'foo'"): + with self.assertRaisesRegex( + ValueError, + "Neither typing nor typing_extensions has an object called 'foo'" + ): get_typing_objects_by_name_of("foo") + def test_typing_objects_by_name_not_in_typing_extensions(self): + objs = get_typing_objects_by_name_of("ByteString") + self.assertIsInstance(objs, tuple) + self.assertEqual(len(objs), 1) + bytestring = objs[0] + self.assertIs(bytestring, typing.ByteString) + self.assertEqual(bytestring.__module__, "typing") + def test_typing_objects_by_name_of_2(self): classvar_objs = get_typing_objects_by_name_of("ClassVar") self.assertEqual(len(classvar_objs), 1) @@ -5049,6 +5060,20 @@ def test_is_typing_name(self): typing_obj = getattr(typing, name) self.assertTrue(is_typing_name(typing_obj, name)) + def test_is_typing_name_fails_appropriately(self): + self.assertFalse(is_typing_name(typing_extensions.NoReturn, "ClassVar")) + self.assertFalse(is_typing_name(typing.NoReturn, "ClassVar")) + error_msg = "Neither typing nor typing_extensions has an object called 'foo'" + with self.assertRaisesRegex(ValueError, error_msg): + is_typing_name(typing_extensions.NoReturn, "foo") + with self.assertRaisesRegex(ValueError, error_msg): + is_typing_name(typing_extensions.NoReturn, "foo") + + def test_is_typing_name_not_in_typing_extensions(self): + # Sanity check -- this is a useless test otherwise: + self.assertFalse(hasattr(typing_extensions, "ByteString")) + self.assertTrue(is_typing_name(typing.ByteString, "ByteString")) + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 54b9f45f..8741937a 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2895,18 +2895,23 @@ def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: try: te_obj = globals()[name] except KeyError: - raise ValueError( - f"The typing_extensions module has no object called {name!r}!" - ) from None - objs = [te_obj] - if hasattr(typing, name): - typing_obj = getattr(typing, name) - # Some typing objects compare equal to the equivalent typing_extensions object, - # but aren't actually the exact same object, - # so we can't use a set here; a list is better - if typing_obj is not te_obj: - objs.append(typing_obj) - return tuple(objs) + try: + typing_obj = getattr(typing, name) + except AttributeError: + raise ValueError( + f"Neither typing nor typing_extensions has an object called {name!r}!" + ) from None + else: + return (typing_obj,) + else: + if hasattr(typing, name): + typing_obj = getattr(typing, name) + # Some typing objects compare equal to the equivalent typing_extensions object, + # but aren't actually the exact same object, + # so we can't use a set here + if typing_obj is not te_obj: + return (te_obj, typing_obj) + return (te_obj,) def is_typing_name(obj: object, name: str) -> bool: From be51587cbb2ef9f2ec1ec81d8250990fdc917d27 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 26 May 2023 14:27:46 +0100 Subject: [PATCH 5/5] Fix lint --- src/typing_extensions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 8741937a..a66ca6bd 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2906,9 +2906,6 @@ def get_typing_objects_by_name_of(name: str) -> typing.Tuple[Any, ...]: else: if hasattr(typing, name): typing_obj = getattr(typing, name) - # Some typing objects compare equal to the equivalent typing_extensions object, - # but aren't actually the exact same object, - # so we can't use a set here if typing_obj is not te_obj: return (te_obj, typing_obj) return (te_obj,)