Skip to content

GH-103721: Allow defining GenericAlias-like things #103722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8606,5 +8606,70 @@ def test_is_not_instance_of_iterable(self):
self.assertNotIsInstance(type_to_test, collections.abc.Iterable)


class TestGenericAliasLike(BaseTestCase):
def test(self):
T = TypeVar('T')

class GenericType(Generic[T]):
def __class_getitem__(cls, args):
# The goal is that the thing returned from here can
# act like a GenericAlias but also implement custom behavior
# In particular the original feature request involved tracking
# TypeVar substitutions at runtime for Pydantic
if not isinstance(args, tuple):
args = (args,)
parameters = tuple([a for a in args if isinstance(a, TypeVar)])
args = tuple([a for a in args if a not in parameters])
return GenericAliasLike(args, parameters, cls)


class GenericAliasLike:
def __init__(self, args, parameters, origin):
self.__args__ = args
self.__parameters__ = parameters
self.__origin__ = origin

def __getitem__(slf, args):
# Here would go logic to do the tracking of type var substitution
# For the purposes of our tests this hardcoded version does just fine
self.assertEqual(args, (int,))
slf.__args__ = args
slf.__parameters__ = ()
return slf

def __eq__(self, other):
# implemented just for easy comparison in the tests
# below
if not isinstance(other, GenericAliasLike):
return False
return (
self.__args__ == other.__args__
and self.__origin__ == other.__origin__
and self.__parameters__ == other.__parameters__
)

# __call__ needs to be implemented for this to be considered a type by typing.py
def __call__(self):
# In a real implementation this is where we would call our __origin__
# and forward the current state of type var substitution
pass

self.assertEqual(GenericType[T].__parameters__, (T,))
self.assertEqual(get_args(GenericType[T]), ())
self.assertIs(get_origin(GenericType[T]), GenericType)

self.assertEqual(GenericType[int].__parameters__, ())
self.assertEqual(get_args(GenericType[int]), (int,))
self.assertIs(get_origin(GenericType[int]), GenericType)

self.assertEqual(List[GenericType[T]].__parameters__, (T,))
self.assertEqual(get_args(List[GenericType[T]]), (GenericType[T],))
self.assertIs(get_origin(List[GenericType[T]]), list)

self.assertEqual(List[GenericType[T]][int].__parameters__, ())
self.assertEqual(get_args(List[GenericType[T]][int]), (GenericType[int],))
self.assertIs(get_origin(List[GenericType[T]]), list)


if __name__ == '__main__':
main()
20 changes: 17 additions & 3 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,14 @@ def __dir__(self):
# e.g., Dict[T, int].__args__ == (T, int).


def _is_special_typing_construct(t) -> bool:
return (
hasattr(t, "__args__")
and hasattr(t, "__origin__")
and hasattr(t, "__parameters__")
)


class _GenericAlias(_BaseGenericAlias, _root=True):
# The type of parameterized generics.
#
Expand Down Expand Up @@ -2469,8 +2477,11 @@ def get_origin(tp):
"""
if isinstance(tp, _AnnotatedAlias):
return Annotated
if isinstance(tp, (_BaseGenericAlias, GenericAlias,
ParamSpecArgs, ParamSpecKwargs)):
if (
isinstance(tp, (_BaseGenericAlias, GenericAlias,
ParamSpecArgs, ParamSpecKwargs))
or _is_special_typing_construct(tp)
):
return tp.__origin__
if tp is Generic:
return Generic
Expand All @@ -2492,7 +2503,10 @@ def get_args(tp):
"""
if isinstance(tp, _AnnotatedAlias):
return (tp.__origin__,) + tp.__metadata__
if isinstance(tp, (_GenericAlias, GenericAlias)):
if (
isinstance(tp, (_GenericAlias, GenericAlias))
or _is_special_typing_construct(tp)
):
res = tp.__args__
if _should_unflatten_callable_args(tp, res):
res = (list(res[:-1]), res[-1])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Allow defining ``GenericAlias``-like things.
This lets libraries that want to do custom runtime handling of generic
parameters and their substitutions integrate with the rest of typing, for
example with :func:`typing.get_args`, :func:`typing.get_origin`, and as
parameters to other generics like :class:`list`, :class:`dict` and so on.