-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
bpo-44801: Check arguments in substitution of ParamSpec in Callable #27585
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -581,17 +581,33 @@ def test_paramspec(self): | |
Callable = self.Callable | ||
fullname = f"{Callable.__module__}.Callable" | ||
P = ParamSpec('P') | ||
P2 = ParamSpec('P2') | ||
C1 = Callable[P, T] | ||
# substitution | ||
self.assertEqual(C1[int, str], Callable[[int], str]) | ||
self.assertEqual(C1[[int], str], Callable[[int], str]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm I feel a little strange about this. I feel that the new form is correct, but I also feel the old form should work. There's not much ambiguity in using the old form. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PEP explicitly claim that only substituting ParamSpec with the parameters expression should be accepted. It will also simplify the future code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah ok I see it now:
Thanks for the reminder. |
||
self.assertEqual(C1[[int, str], str], Callable[[int, str], str]) | ||
self.assertEqual(C1[[], str], Callable[[], str]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good test! 👍 |
||
self.assertEqual(C1[..., str], Callable[..., str]) | ||
self.assertEqual(C1[P2, str], Callable[P2, str]) | ||
self.assertEqual(C1[Concatenate[int, P2], str], | ||
Callable[Concatenate[int, P2], str]) | ||
self.assertEqual(repr(C1), f"{fullname}[~P, ~T]") | ||
self.assertEqual(repr(C1[int, str]), f"{fullname}[[int], str]") | ||
self.assertEqual(repr(C1[[int, str], str]), f"{fullname}[[int, str], str]") | ||
with self.assertRaises(TypeError): | ||
C1[int, str] | ||
|
||
C2 = Callable[P, int] | ||
self.assertEqual(C2[[int]], Callable[[int], int]) | ||
self.assertEqual(C2[[int, str]], Callable[[int, str], int]) | ||
self.assertEqual(C2[[]], Callable[[], int]) | ||
self.assertEqual(C2[...], Callable[..., int]) | ||
self.assertEqual(C2[P2], Callable[P2, int]) | ||
self.assertEqual(C2[Concatenate[int, P2]], | ||
Callable[Concatenate[int, P2], int]) | ||
# special case in PEP 612 where | ||
# X[int, str, float] == X[[int, str, float]] | ||
self.assertEqual(C2[int, str, float], C2[[int, str, float]]) | ||
self.assertEqual(C2[int], Callable[[int], int]) | ||
self.assertEqual(C2[int, str], Callable[[int, str], int]) | ||
self.assertEqual(repr(C2), f"{fullname}[~P, int]") | ||
self.assertEqual(repr(C2[int, str]), f"{fullname}[[int, str], int]") | ||
|
||
|
@@ -4656,6 +4672,29 @@ class Z(Generic[P]): | |
self.assertEqual(G5.__parameters__, G6.__parameters__) | ||
self.assertEqual(G5, G6) | ||
|
||
G7 = Z[int] | ||
self.assertEqual(G7.__args__, ((int,),)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a critique of your PR, but a minor regret of mine: Looking at G5, G6, G7, I realized their I don't remember what made me code it like that back then, but it could be me not wanting to make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does not work for user generics because they can have more than one ParamSpec. For example: class X(Generic[P1, P2]):
f: Callable[P1, int]
g: Callable[P2, str]
G1 = X[[int, str], [bytes]]
G2 = X[int, [str, bytes]]
assert G1.__args__ == ((int, str), (bytes,))
assert G2.__args__ == ((int,) (str, bytes)) If flatten arguments, I'll add a special test for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, thanks for pointing that out! I forgot there was ambiguity with multiple ParamSpec. Hopefully some future PEP makes this clearer. |
||
self.assertEqual(G7.__parameters__, ()) | ||
|
||
with self.assertRaisesRegex(TypeError, "many arguments for"): | ||
Z[[int, str], bool] | ||
with self.assertRaisesRegex(TypeError, "many arguments for"): | ||
Z[P_2, bool] | ||
|
||
def test_multiple_paramspecs_in_user_generics(self): | ||
P = ParamSpec("P") | ||
P2 = ParamSpec("P2") | ||
|
||
class X(Generic[P, P2]): | ||
f: Callable[P, int] | ||
g: Callable[P2, str] | ||
|
||
G1 = X[[int, str], [bytes]] | ||
G2 = X[[int], [str, bytes]] | ||
self.assertNotEqual(G1, G2) | ||
self.assertEqual(G1.__args__, ((int, str), (bytes,))) | ||
self.assertEqual(G2.__args__, ((int,), (str, bytes))) | ||
|
||
def test_no_paramspec_in__parameters__(self): | ||
# ParamSpec should not be found in __parameters__ | ||
# of generics. Usages outside Callable, Concatenate | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Ensure that the :class:`~typing.ParamSpec` variable in Callable | ||
can only be substituted with a parameters expression (a list of types, | ||
an ellipsis, ParamSpec or Concatenate). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking for something like
_is_paramspec
would be clearer IMO, but we don't have that function and it's kind of a waste to make a function just for this one-time use.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code will gone after moving the logic in
ParamSpec.__getitem__
in #27511.I am planning first to fix most bugs and to cover more cases by tests, and only then apply #27511, so we may not need to backport it.