From 61c4f521e12ef37beffec8d656ef066b131e10bd Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sun, 30 Jan 2022 16:22:17 +0000 Subject: [PATCH 1/4] Improve documentation for typing._GenericAlias --- Lib/typing.py | 139 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 107 insertions(+), 32 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index dac9c6c4f87cfe..09a641ec7fb351 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -999,16 +999,37 @@ def __dir__(self): class _GenericAlias(_BaseGenericAlias, _root=True): - def __init__(self, origin, params, *, inst=True, name=None, + """The type of parameterized generics. + + That is, for example, `type(List[int])` is `_GenericAlias`. + + Objects which are instances of this class include: + * Parameterized container types, e.g. `Tuple[int]`, `List[int]` + * Note that native container types, e.g. `tuple`, `list`, use + `types.GenericAlias` instead. + * Parameterized classes: + T = TypeVar('T') + class C(Generic[T]): pass + # C[int] is a _GenericAlias + * `Callable` aliases, generic `Callable` aliases, and + parameterized `Callable` aliases: + T = TypeVar('T') + # _CallableGenericAlias inherits from _GenericAlias. + A = Callable[[], None] # _CallableGenericAlias + B = Callable[[T], None] # _CallableGenericAlias + C = B[int] # _CallableGenericAlias + """ + + def __init__(self, origin, args, *, inst=True, name=None, _typevar_types=TypeVar, _paramspec_tvars=False): super().__init__(origin, inst=inst, name=name) - if not isinstance(params, tuple): - params = (params,) + if not isinstance(args, tuple): + args = (args,) self.__args__ = tuple(... if a is _TypingEllipsis else () if a is _TypingEmpty else - a for a in params) - self.__parameters__ = _collect_type_vars(params, typevar_types=_typevar_types) + a for a in args) + self.__parameters__ = _collect_type_vars(args, typevar_types=_typevar_types) self._typevar_types = _typevar_types self._paramspec_tvars = _paramspec_tvars if not name: @@ -1030,44 +1051,98 @@ def __ror__(self, left): return Union[left, self] @_tp_cache - def __getitem__(self, params): + def __getitem__(self, args): + """Parameterizes an already-parameterized object. + + For example, we arrive here doing something like: + T1 = TypeVar('T1') + T2 = TypeVar('T2') + T3 = TypeVar('T3') + class A(Generic[T1]): pass + B = A[T2] # B is a _GenericAlias + C = B[T2] # Invokes _GenericAlias.__getitem__ + + We also arrive here when parameterizing a generic `Callable` alias: + T = TypeVar('T') + C = Callable[[T], None] + C[int] # Invokes _GenericAlias.__getitem__ + """ if self.__origin__ in (Generic, Protocol): # Can't subscript Generic[...] or Protocol[...]. raise TypeError(f"Cannot subscript already-subscripted {self}") - if not isinstance(params, tuple): - params = (params,) - params = tuple(_type_convert(p) for p in params) + + # Preprocess `args`. + if not isinstance(args, tuple): + args = (args,) + args = tuple(_type_convert(p) for p in args) if (self._paramspec_tvars and any(isinstance(t, ParamSpec) for t in self.__parameters__)): - params = _prepare_paramspec_params(self, params) + args = _prepare_paramspec_params(self, args) else: - _check_generic(self, params, len(self.__parameters__)) + _check_generic(self, args, len(self.__parameters__)) + + new_args = self._determine_new_args(args) + r = self.copy_with(new_args) + return r + + def _determine_new_args(self, args): + """Determines new __args__ for __getitem__. + + For example, suppose we had: + T1 = TypeVar('T1') + T2 = TypeVar('T2') + class A(Generic[T1, T2]): pass + T3 = TypeVar('T3') + B = A[int, T3] + C = B[str] + `B.__args__` is `(int, T3)`, so `C.__args__` should be `(int, str)`. + Unfortunately, this is harder than it looks, because if `T3` is + anything more exotic than a plain `TypeVar`, we need to consider + edge cases. + """ + + # In the example above, this would be {T3: str} + new_arg_by_param = dict(zip(self.__parameters__, args)) - subst = dict(zip(self.__parameters__, params)) new_args = [] - for arg in self.__args__: - if isinstance(arg, self._typevar_types): - if isinstance(arg, ParamSpec): - arg = subst[arg] - if not _is_param_expr(arg): - raise TypeError(f"Expected a list of types, an ellipsis, " - f"ParamSpec, or Concatenate. Got {arg}") + for old_arg in self.__args__: + + if isinstance(old_arg, ParamSpec): + new_arg = new_arg_by_param[old_arg] + if not _is_param_expr(new_arg): + raise TypeError(f"Expected a list of types, an ellipsis, " + f"ParamSpec, or Concatenate. Got {new_arg}") + elif isinstance(old_arg, self._typevar_types): + new_arg = new_arg_by_param[old_arg] + elif isinstance(old_arg, (_GenericAlias, GenericAlias, types.UnionType)): + subparams = old_arg.__parameters__ + if not subparams: + new_arg = old_arg else: - arg = subst[arg] - elif isinstance(arg, (_GenericAlias, GenericAlias, types.UnionType)): - subparams = arg.__parameters__ - if subparams: - subargs = tuple(subst[x] for x in subparams) - arg = arg[subargs] - # Required to flatten out the args for CallableGenericAlias - if self.__origin__ == collections.abc.Callable and isinstance(arg, tuple): - new_args.extend(arg) + subargs = tuple(new_arg_by_param[x] for x in subparams) + new_arg = old_arg[subargs] + else: + new_arg = old_arg + + if self.__origin__ == collections.abc.Callable and isinstance(new_arg, tuple): + # Consider the following `Callable`. + # C = Callable[[int], str] + # Here, `C.__args__` should be (int, str) - NOT ([int], str). + # That means that if we had something like... + # P = ParamSpec('P') + # T = TypeVar('T') + # C = Callable[P, T] + # D = C[[int, str], float] + # ...we need to be careful; `new_args` should end up as + # `(int, str, float)` rather than `([int, str], float)`. + new_args.extend(new_arg) else: - new_args.append(arg) - return self.copy_with(tuple(new_args)) + new_args.append(new_arg) - def copy_with(self, params): - return self.__class__(self.__origin__, params, name=self._name, inst=self._inst) + return tuple(new_args) + + def copy_with(self, args): + return self.__class__(self.__origin__, args, name=self._name, inst=self._inst) def __repr__(self): if self._name: From fe01a2746a10c104f856574e7b3dabef745ea1cf Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Sun, 30 Jan 2022 20:06:17 +0000 Subject: [PATCH 2/4] Address comments from Jelle and Fidget-Spinner --- Lib/typing.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 09a641ec7fb351..cd5cf0ae047903 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1018,6 +1018,11 @@ class C(Generic[T]): pass A = Callable[[], None] # _CallableGenericAlias B = Callable[[T], None] # _CallableGenericAlias C = B[int] # _CallableGenericAlias + * Parameterized `Final`, `ClassVar` and `TypeGuard`: + # All _GenericAlias + Final[int] + ClassVar[float] + TypeVar[bool] """ def __init__(self, origin, args, *, inst=True, name=None, @@ -1060,7 +1065,7 @@ def __getitem__(self, args): T3 = TypeVar('T3') class A(Generic[T1]): pass B = A[T2] # B is a _GenericAlias - C = B[T2] # Invokes _GenericAlias.__getitem__ + C = B[T3] # Invokes _GenericAlias.__getitem__ We also arrive here when parameterizing a generic `Callable` alias: T = TypeVar('T') From 620cfe43bd52f1510146d56ed8644ca7f426e3f2 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Tue, 1 Feb 2022 15:21:22 +0000 Subject: [PATCH 3/4] Convert _GenericAlias docstring to a comment --- Lib/typing.py | 49 ++++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index cd5cf0ae047903..72f30079a5970d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -999,31 +999,30 @@ def __dir__(self): class _GenericAlias(_BaseGenericAlias, _root=True): - """The type of parameterized generics. - - That is, for example, `type(List[int])` is `_GenericAlias`. - - Objects which are instances of this class include: - * Parameterized container types, e.g. `Tuple[int]`, `List[int]` - * Note that native container types, e.g. `tuple`, `list`, use - `types.GenericAlias` instead. - * Parameterized classes: - T = TypeVar('T') - class C(Generic[T]): pass - # C[int] is a _GenericAlias - * `Callable` aliases, generic `Callable` aliases, and - parameterized `Callable` aliases: - T = TypeVar('T') - # _CallableGenericAlias inherits from _GenericAlias. - A = Callable[[], None] # _CallableGenericAlias - B = Callable[[T], None] # _CallableGenericAlias - C = B[int] # _CallableGenericAlias - * Parameterized `Final`, `ClassVar` and `TypeGuard`: - # All _GenericAlias - Final[int] - ClassVar[float] - TypeVar[bool] - """ + # The type of parameterized generics. + # + # That is, for example, `type(List[int])` is `_GenericAlias`. + # + # Objects which are instances of this class include: + # * Parameterized container types, e.g. `Tuple[int]`, `List[int]`. + # * Note that native container types, e.g. `tuple`, `list`, use + # `types.GenericAlias` instead. + # * Parameterized classes: + # T = TypeVar('T') + # class C(Generic[T]): pass + # # C[int] is a _GenericAlias + # * `Callable` aliases, generic `Callable` aliases, and + # parameterized `Callable` aliases: + # T = TypeVar('T') + # # _CallableGenericAlias inherits from _GenericAlias. + # A = Callable[[], None] # _CallableGenericAlias + # B = Callable[[T], None] # _CallableGenericAlias + # C = B[int] # _CallableGenericAlias + # * Parameterized `Final`, `ClassVar` and `TypeGuard`: + # # All _GenericAlias + # Final[int] + # ClassVar[float] + # TypeVar[bool] def __init__(self, origin, args, *, inst=True, name=None, _typevar_types=TypeVar, From dd334cda002abe815eb787de038114e49ede5403 Mon Sep 17 00:00:00 2001 From: Matthew Rahtz Date: Tue, 1 Feb 2022 15:24:44 +0000 Subject: [PATCH 4/4] Convert other docstrings on private methods to comments too --- Lib/typing.py | 57 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 72f30079a5970d..bb7793187a45b0 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1056,21 +1056,21 @@ def __ror__(self, left): @_tp_cache def __getitem__(self, args): - """Parameterizes an already-parameterized object. - - For example, we arrive here doing something like: - T1 = TypeVar('T1') - T2 = TypeVar('T2') - T3 = TypeVar('T3') - class A(Generic[T1]): pass - B = A[T2] # B is a _GenericAlias - C = B[T3] # Invokes _GenericAlias.__getitem__ - - We also arrive here when parameterizing a generic `Callable` alias: - T = TypeVar('T') - C = Callable[[T], None] - C[int] # Invokes _GenericAlias.__getitem__ - """ + # Parameterizes an already-parameterized object. + # + # For example, we arrive here doing something like: + # T1 = TypeVar('T1') + # T2 = TypeVar('T2') + # T3 = TypeVar('T3') + # class A(Generic[T1]): pass + # B = A[T2] # B is a _GenericAlias + # C = B[T3] # Invokes _GenericAlias.__getitem__ + # + # We also arrive here when parameterizing a generic `Callable` alias: + # T = TypeVar('T') + # C = Callable[[T], None] + # C[int] # Invokes _GenericAlias.__getitem__ + if self.__origin__ in (Generic, Protocol): # Can't subscript Generic[...] or Protocol[...]. raise TypeError(f"Cannot subscript already-subscripted {self}") @@ -1090,20 +1090,19 @@ class A(Generic[T1]): pass return r def _determine_new_args(self, args): - """Determines new __args__ for __getitem__. - - For example, suppose we had: - T1 = TypeVar('T1') - T2 = TypeVar('T2') - class A(Generic[T1, T2]): pass - T3 = TypeVar('T3') - B = A[int, T3] - C = B[str] - `B.__args__` is `(int, T3)`, so `C.__args__` should be `(int, str)`. - Unfortunately, this is harder than it looks, because if `T3` is - anything more exotic than a plain `TypeVar`, we need to consider - edge cases. - """ + # Determines new __args__ for __getitem__. + # + # For example, suppose we had: + # T1 = TypeVar('T1') + # T2 = TypeVar('T2') + # class A(Generic[T1, T2]): pass + # T3 = TypeVar('T3') + # B = A[int, T3] + # C = B[str] + # `B.__args__` is `(int, T3)`, so `C.__args__` should be `(int, str)`. + # Unfortunately, this is harder than it looks, because if `T3` is + # anything more exotic than a plain `TypeVar`, we need to consider + # edge cases. # In the example above, this would be {T3: str} new_arg_by_param = dict(zip(self.__parameters__, args))