Skip to content
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

Support ParamSpec for TypeAliasType #449

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
b6bc323
Support ParamSpec for TypeAliasType
Daraan Aug 21, 2024
79985f3
Removed trailing whitespaces
Daraan Aug 21, 2024
02025d4
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 9, 2024
3199b7b
raise TypeError for invalid type_params like typing.TypeAliasType
Daraan Sep 18, 2024
f7d79d9
Keep dunder attributes like typing.TypeAliasType
Daraan Sep 18, 2024
c4c0e68
Added test to catch invalid cases, fix wrong intendation
Daraan Sep 18, 2024
d6af983
removed pyright pragma
Daraan Sep 18, 2024
0a5039b
fix whitespaces
Daraan Sep 18, 2024
e532429
Avoid incompatible global types
Daraan Sep 19, 2024
9911ac7
Add __name__ for TypeAliasType for <=3.10
Daraan Sep 19, 2024
e1b3095
Added missing support for Unpack to TypeAliasType variant
Daraan Sep 19, 2024
408ae2e
Added more invalid test cases
Daraan Sep 19, 2024
621085f
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 19, 2024
b3e6b7a
Unpack invalid Concatenate correctly
Daraan Sep 19, 2024
c3d98c6
Removed parameter checking from TypeAliasType._check_parameter
Daraan Sep 23, 2024
8255667
extended and reworked tests
Daraan Sep 23, 2024
255de76
Clean duplicated tests
Daraan Sep 23, 2024
3037923
Removed duplicated or valid cases from invalid
Daraan Sep 23, 2024
b8ae82e
Slightly more refined tests covering more cases
Daraan Sep 23, 2024
8d2ec0a
Removed cases that is mentioned elsewhere
Daraan Sep 23, 2024
7029d51
Revert change of global Protocol variables
Daraan Sep 24, 2024
02fd0ba
Raise TypeError on parameterless alias
Daraan Sep 24, 2024
5c0938c
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Sep 24, 2024
b6fefb0
Correct subscription handling when type_params are empty
Daraan Sep 24, 2024
af0a133
Restructured test cases
Daraan Sep 24, 2024
f2aa35c
Added general and comprehensive test
Daraan Sep 24, 2024
6b1bafb
Subtests to test parameter amount
Daraan Sep 24, 2024
7c1fea7
unify __getitem__ again
Daraan Sep 24, 2024
efa1214
covered all cases for TypeErrors during subscription
Daraan Sep 25, 2024
66eebb1
Removed code addressing #468
Daraan Sep 25, 2024
df10751
Use _types.GenericAlias for TypeAliasType in 3.10+
Daraan Sep 25, 2024
889e9ae
Assure that args and parameter tests pass
Daraan Sep 25, 2024
b6b5a14
Remove code to fix dunder attributes -> other PR
Daraan Sep 26, 2024
9562635
small reordering
Daraan Sep 26, 2024
0033813
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Sep 26, 2024
014109c
Remove invalid case
Daraan Sep 26, 2024
0b3ce7d
Updated to latest changes from main
Daraan Sep 26, 2024
0ae4c63
revert mistakes of wrong merge
Daraan Sep 26, 2024
144c7b8
Merge branch 'main' into TypeAliasType-extension
AlexWaygood Sep 26, 2024
e613294
No need to skip tests anymore
Daraan Sep 26, 2024
e44fdcc
Merge remote-tracking branch 'origin/TypeAliasType-extension' into Ty…
Daraan Sep 26, 2024
6bc1f57
removed tests related to: https://github.com/python/cpython/issues/12…
Daraan Sep 30, 2024
e8bfa30
Removed tests related to #474
Daraan Sep 30, 2024
617656d
Removed invalid tests
Daraan Sep 30, 2024
41a87b8
minor comment update
Daraan Sep 30, 2024
a8c4bda
Merge branch 'main' into TypeAliasType-extension
Daraan Sep 30, 2024
249b869
More refined skip reason
Daraan Sep 30, 2024
e71902e
Remove type check for tuples; handled by #477
Daraan Oct 1, 2024
b8799ce
updated changelog
Daraan Oct 1, 2024
5bc1360
3.8, 3.9 use collected parameters more explicitly
Daraan Oct 1, 2024
3ae9e35
Correct error message for 3.10
Daraan Oct 2, 2024
9878cc0
Merge branch 'main' into TypeAliasType-extension
Daraan Oct 11, 2024
3863c8b
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 21, 2024
8c86619
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
251d312
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
b3aa598
Generator not necessary anymore
Daraan Oct 22, 2024
07ba7b7
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 22, 2024
28d1e84
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 25, 2024
a365355
Merge and type correction
Daraan Oct 25, 2024
fb55b88
TODO: handle None
Daraan Oct 25, 2024
f2d6890
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 25, 2024
054f083
Merge remote-tracking branch 'upstream/main' into TypeAliasType-exten…
Daraan Oct 28, 2024
eb840ef
Handle None case correctly, also check list
Daraan Oct 28, 2024
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
223 changes: 217 additions & 6 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3572,13 +3572,13 @@ class P(Protocol):
self.assertEqual(Alias, Alias2)

def test_protocols_pickleable(self):
global P, CP # pickle wants to reference the class by name
global GlobalProto, CP # pickle wants to reference the class by name
T = TypeVar('T')

@runtime_checkable
class P(Protocol[T]):
class GlobalProto(Protocol[T]):
x = 1
class CP(P[int]):
class CP(GlobalProto[int]):
pass

c = CP()
Expand All @@ -3591,7 +3591,7 @@ class CP(P[int]):
self.assertEqual(x.bar, 'abc')
self.assertEqual(x.x, 1)
self.assertEqual(x.__dict__, {'foo': 42, 'bar': 'abc'})
s = pickle.dumps(P)
s = pickle.dumps(GlobalProto)
D = pickle.loads(s)
Daraan marked this conversation as resolved.
Show resolved Hide resolved
class E:
x = 1
Expand Down Expand Up @@ -7165,13 +7165,58 @@ def test_attributes(self):
self.assertEqual(ListOrSetT.__type_params__, (T,))
self.assertEqual(ListOrSetT.__parameters__, (T,))

subscripted = ListOrSetT[int]
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
self.assertEqual(subscripted.__name__, "ListOrSetT")
self.assertEqual(subscripted.__value__, Union[List[T], Set[T]],)
self.assertEqual(subscripted.__type_params__, (T, ))
self.assertEqual(subscripted.__parameters__, ())

T2 = TypeVar("T2")
subscriptedT = ListOrSetT[T2]
self.assertEqual(subscriptedT.__name__, "ListOrSetT")
self.assertEqual(subscriptedT.__value__, Union[List[T], Set[T]],)
self.assertEqual(subscriptedT.__type_params__, (T, ))
self.assertEqual(subscriptedT.__parameters__, (T2, ))

Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))
self.assertEqual(Variadic.__name__, "Variadic")
self.assertEqual(Variadic.__value__, Tuple[int, Unpack[Ts]])
self.assertEqual(Variadic.__type_params__, (Ts,))
self.assertEqual(Variadic.__parameters__, tuple(iter(Ts)))

# Test bare
subscripted_tuple = Variadic[int, float]
self.assertEqual(subscripted_tuple.__name__, "Variadic")
self.assertEqual(subscripted_tuple.__value__, Tuple[int, Unpack[Ts]])
self.assertEqual(subscripted_tuple.__type_params__, (Ts,))
self.assertEqual(subscripted_tuple.__parameters__, ())

# Test with Unpack
subscripted_tupleT = Variadic[Unpack[Tuple[int, T]]]
self.assertEqual(subscripted_tupleT.__name__, "Variadic")
self.assertEqual(subscripted_tupleT.__parameters__, (T, ))

# Test with Unpack and TypeVarTuple
subscripted_Ts = Variadic[Unpack[Ts]]
self.assertEqual(subscripted_Ts.__parameters__, (Ts, ))

# Use with Callable
# Use with Callable+Concatenate
subscripted_callable_concat = Variadic[Callable[Concatenate[Literal["s"], P], T]]
self.assertEqual(subscripted_callable_concat.__parameters__, (P, T))

subcriped_callable_tvt = Variadic[Callable[[Unpack[Ts]], T]]
self.assertEqual(subcriped_callable_tvt.__parameters__, (Ts, T))

# Use with Callable+Unpack
CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, ))
self.assertEqual(CallableTs.__type_params__, (Ts,))
self.assertEqual(CallableTs.__parameters__, (*Ts,))

unpack_callable = CallableTs[Unpack[Tuple[int, T]]]
self.assertEqual(unpack_callable.__parameters__, (T,))

def test_cannot_set_attributes(self):
Simple = TypeAliasType("Simple", int)
with self.assertRaisesRegex(AttributeError, "readonly attribute"):
Expand Down Expand Up @@ -7232,12 +7277,13 @@ def test_or(self):
Alias | "Ref"

def test_getitem(self):
T = TypeVar("T")
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
subscripted = ListOrSetT[int]
self.assertEqual(get_args(subscripted), (int,))
self.assertIs(get_origin(subscripted), ListOrSetT)
with self.assertRaises(TypeError):
subscripted[str]
with self.assertRaises(TypeError, msg="not a generic class"):
Daraan marked this conversation as resolved.
Show resolved Hide resolved
subscripted[int]

still_generic = ListOrSetT[Iterable[T]]
self.assertEqual(get_args(still_generic), (Iterable[T],))
Expand All @@ -7246,6 +7292,171 @@ def test_getitem(self):
self.assertEqual(get_args(fully_subscripted), (Iterable[float],))
self.assertIs(get_origin(fully_subscripted), ListOrSetT)

# Test ParamSpec and Ellipsis
P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, Any], type_params=(P,))
# () -> Any
callable_no_arg = CallableP[[]]
self.assertEqual(get_args(callable_no_arg), ([],))
# (int) -> Any
callable_arg = CallableP[int]
self.assertEqual(get_args(callable_arg), (int,))

callable_arg_list = CallableP[[int]]
self.assertEqual(get_args(callable_arg_list), ([int],))

# (int, int) -> Any
callable_arg2 = CallableP[int, int]
self.assertEqual(get_args(callable_arg2), (int, int,))

callable_arg2_list = CallableP[[int, int]]
self.assertEqual(get_args(callable_arg2_list), ([int, int],))
# (...) -> Any
callable_ellipsis = CallableP[...]
self.assertEqual(get_args(callable_ellipsis), (...,))

callable_ellipsis2 = CallableP[(...,)]
self.assertEqual(callable_ellipsis, callable_ellipsis2)
# (int, ...) -> Any
callable_arg_more = CallableP[[int, ...]]
self.assertEqual(get_args(callable_arg_more), ([int, ...],))
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
# (T) -> Any
callable_generic_raw = CallableP[T]
self.assertEqual(get_args(callable_generic_raw), (T,))
self.assertEqual(callable_generic_raw.__parameters__, (T,))

# Usage with Concatenate
callable_concat = CallableP[Concatenate[int, P]]
self.assertEqual(callable_concat.__parameters__, (P,))
if TYPING_3_11_0:
concat_usage = callable_concat[str]
self.assertEqual(get_args(concat_usage), ((int, str),))
self.assertEqual(concat_usage, callable_concat[[str]])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this block be removed? This is more about GenericAlias usage here and how Concatenate/Unpack is handled.

I think the self.assertEqual(concat_usage, callable_concat[[str]]) test is interesting, but I am not sure about the specifications if this should be valid and stay here.
See also the test_invalid_cases tests where this is redone with a TypeVar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since using types.GenericAlias these tests became much clearer. Given that Concatenate might need some special treatmeant in TypeAliasType.__getitem__ (currently done by isinstance(param, list)) and this is the only test where this is looked into with a bit more detail, I think it should probaly stay.

elif TYPING_3_10_0:
with self.assertRaises(TypeError, msg="Parameters to generic types must be types"):
callable_concat[str]
concat_usage = callable_concat[[str]]
self.assertEqual(get_args(concat_usage), (int, [str]))
else:
with self.assertRaises(TypeError, msg="Parameters to generic types must be types"):
callable_concat[[str]]
concat_usage = callable_concat[str]
self.assertEqual(get_args(concat_usage), (int, str))

# More complex cases
Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))
mixed_subscripedPT = Variadic[Callable[Concatenate[int, P], T]]
self.assertEqual(get_args(mixed_subscripedPT), (Callable[Concatenate[int, P], T],))


@skipUnless(TYPING_3_11_0, "__args__ behaves differently")
Daraan marked this conversation as resolved.
Show resolved Hide resolved
def test_311_substitution(self):
# To pass these tests alias.__args__ in TypeAliasType.__getitem__ needs adjustment
# Unpack and Concatenate are unpacked in versions before
T = TypeVar("T")
Ts = TypeVarTuple("Ts")

CallableTs = TypeAliasType("CallableTs", Callable[[Unpack[Ts]], Any], type_params=(Ts, ))
unpack_callable = CallableTs[Unpack[Tuple[int, T]]]
self.assertEqual(get_args(unpack_callable), (Unpack[Tuple[int, T]],))

P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P, T))
callable_concat = CallableP[Concatenate[int, P], Any]
self.assertEqual(get_args(callable_concat), (Concatenate[int, P], Any))

@skipUnless(TYPING_3_12_0, "__args__ behaves differently")
def test_312_substitution(self):
# To pass these tests alias.__args__ in TypeAliasType.__getitem__ needs to be adjustment
# Would raise: TypeError: Substitution of bare TypeVarTuple is not supported
T = TypeVar("T")
Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))
Daraan marked this conversation as resolved.
Show resolved Hide resolved

subcriped_callable_tvt = Variadic[Callable[[Unpack[Ts]], T]]
variadic_tvt_callableA = subcriped_callable_tvt[str, object]
variadic_tvt_callableA2 = subcriped_callable_tvt[Unpack[Tuple[str]], object]
self.assertEqual(variadic_tvt_callableA, variadic_tvt_callableA2)

variadic_tvt_callableB = subcriped_callable_tvt[[str, int], object]
variadic_tvt_callableB2 = subcriped_callable_tvt[Unpack[Tuple[str, int]], object]
variadic_tvt_callableB3 = subcriped_callable_tvt[str, int, object]
self.assertNotEqual(variadic_tvt_callableB, variadic_tvt_callableB2)
self.assertEqual(variadic_tvt_callableB2, variadic_tvt_callableB3)

def test_invalid_cases(self):
Daraan marked this conversation as resolved.
Show resolved Hide resolved
# NOTE: If these cases fail the specificiation might have changed
# some of the cases could be seen as valid but are currently not

# More parameters
T = TypeVar("T")
T2 = TypeVar("T2")
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))

too_many = ListOrSetT[int, bool]
self.assertEqual(get_args(too_many), (int, bool))
self.assertEqual(too_many.__parameters__, ())

# Not enough parameters
ListOrSet2T = TypeAliasType("ListOrSet2T", Union[List[T], Set[T2]], type_params=(T, T2))
not_enough = ListOrSet2T[int]
self.assertEqual(get_args(not_enough), (int,))
self.assertEqual(not_enough.__parameters__, ())

not_enough2 = ListOrSet2T[T]
self.assertEqual(get_args(not_enough2), (T,))
self.assertEqual(not_enough2.__parameters__, (T,))
# ParamSpec
P = ParamSpec('P')
CallableP = TypeAliasType("CallableP", Callable[P, T], type_params=(P,))

callable_not_enough = CallableP[int]
self.assertEqual(callable_not_enough.__parameters__, ())
self.assertEqual(get_args(callable_not_enough), (int, ))

callable_too_many = CallableP[str, float, T2, int]
self.assertEqual(callable_too_many.__parameters__, (T2, ))
self.assertEqual(get_args(callable_too_many), (str, float, T2, int, ))

# Cases that result in parameterless variable

# Callable
Daraan marked this conversation as resolved.
Show resolved Hide resolved
CallableT = CallableP[[T]]
self.assertEqual(get_args(CallableT), ([T],))
self.assertEqual(CallableT.__parameters__, ())
with self.assertRaises(TypeError, msg="is not a generic class"):
CallableT[str]

ImplicitConcatP = CallableP[[int, P]]
self.assertEqual(get_args(ImplicitConcatP), ([int, P],))
self.assertEqual(ImplicitConcatP.__parameters__, ())
with self.assertRaises(TypeError, msg="is not a generic class"):
ImplicitConcatP[str]

# TypeVarTuple
Ts = TypeVarTuple("Ts")
Variadic = TypeAliasType("Variadic", Tuple[int, Unpack[Ts]], type_params=(Ts,))

# No Tuple, but list
invalid_tupleT = Variadic[[int, T]]
self.assertEqual(invalid_tupleT.__parameters__, ())
self.assertEqual(get_args(invalid_tupleT), ([int, T],))

with self.assertRaises(TypeError, msg="is not a generic class"):
invalid_tupleT[str]


@skipIf(TYPING_3_11_0, "Most cases are allowed in 3.11+")
def test_invalid_cases_before_3_11(self):
T = TypeVar("T")
ListOrSetT = TypeAliasType("ListOrSetT", Union[List[T], Set[T]], type_params=(T,))
with self.assertRaises(TypeError):
ListOrSetT[Generic[T]]
with self.assertRaises(TypeError):
ListOrSetT[(Generic[T], )]


def test_pickle(self):
global Alias
Alias = TypeAliasType("Alias", int)
Expand Down
63 changes: 53 additions & 10 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3487,6 +3487,8 @@ def __init__(self, name: str, value, *, type_params=()):
self.__type_params__ = type_params

parameters = []
if not isinstance(type_params, tuple):
raise TypeError("type_params must be a tuple")
for type_param in type_params:
if isinstance(type_param, TypeVarTuple):
parameters.extend(type_param)
Expand Down Expand Up @@ -3524,16 +3526,57 @@ def _raise_attribute_error(self, name: str) -> Never:
def __repr__(self) -> str:
return self.__name__

def __getitem__(self, parameters):
if not isinstance(parameters, tuple):
parameters = (parameters,)
parameters = [
typing._type_check(
item, f'Subscripting {self.__name__} requires a type.'
)
for item in parameters
]
return typing._GenericAlias(self, tuple(parameters))
if sys.version_info >= (3, 11):
def __getitem__(self, parameters):
if not isinstance(parameters, tuple):
parameters = (parameters,)
parameters = [
typing._type_check(
item, f'Subscripting {self.__name__} requires a type.'
)
for item in parameters
]
Daraan marked this conversation as resolved.
Show resolved Hide resolved
alias = typing._GenericAlias(self, tuple(parameters))
Daraan marked this conversation as resolved.
Show resolved Hide resolved
alias.__value__ = self.__value__
alias.__type_params__ = self.__type_params__
return alias
else:
def _check_parameter(self, item, recursion=0):
# Allow [], [int], [int, str], [int, ...], [int, T]
if isinstance(item, (_UnpackAlias, _ConcatenateGenericAlias)):
# Unpack
yield from [checked
for arg in item.__args__
for checked in self._check_parameter(arg, recursion+1)]
elif item is ...:
yield ...
elif isinstance(item, list) and recursion == 0:
yield [checked
for arg in item
for checked in self._check_parameter(arg, recursion+1)]
else:
yield typing._type_check(
item, f'Subscripting {self.__name__} requires a type.'
)

def __getitem__(self, parameters):
if not isinstance(parameters, tuple):
parameters = (parameters,)
parameters = [
checked
for item in parameters
for checked in self._check_parameter(item)
Daraan marked this conversation as resolved.
Show resolved Hide resolved
]
if sys.version_info[:2] == (3, 10):
alias = typing._GenericAlias(self, tuple(parameters),
_typevar_types=(TypeVar, ParamSpec)
Daraan marked this conversation as resolved.
Show resolved Hide resolved
)
else:
alias = typing._GenericAlias(self, tuple(parameters))
alias.__name__ = self.__name__
alias.__value__ = self.__value__
alias.__type_params__ = self.__type_params__
return alias

def __reduce__(self):
return self.__name__
Expand Down
Loading