-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[red-knot] Correct modeling of dunder calls #16368
Conversation
@@ -259,11 +259,7 @@ class A: | |||
class B: | |||
__add__ = A() | |||
|
|||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type |
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.
We can also get rid of the Unknown
by declaring it as A
(__add__: A = A()
), so I don't think it's worthy a TODO. The test is fine as is, I think. In fact, declaring it using a callable type would "hide" the actual type of this specific callable (A
).
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.
Yeah I think this is less a TODO and more a commentary on why Unknown
is in the revealed type, since it may not be obvious at first.
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.
Nice how this ended up simplifying the implementation. I, obviously, didn't review the semantic changes. I'll leave that to someone else.
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.
excellent!
@@ -82,7 +82,7 @@ class C: | |||
|
|||
c = C() | |||
|
|||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`" | |||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`" |
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.
better might be
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`" | |
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `C.__call__`; expected type `int`" |
(definitely not a blocking comment!)
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.
I wanted to do something like this at first, but then we probably need to construct a String
for callable names instead of relying on str
slices. Happy to make that change though.
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.
I had the same thought on the previous diff that improved error messages, that ultimately probably we don't need to describe "function" vs "bound method" vs "wrapper-descriptor", we can probably replace all of that with fully-qualified (within the module) function names (e.g. C.f.__get__
already clarifies that this is is the __get__
method of the function f
in the class C
, and is more similar to how things look in runtime messages). I think this is worth creating an issue for but isn't necessarily urgent.
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.
e.g.
C.f.__get__
already clarifies that this is is the__get__
method of the functionf
in the classC
Consider the CPython error messages here (and imagine that everything "above the fold" is not visible in the current view):
class C:
def f(self, x: int) -> str:
return str(x)
c = C()
# ------------
# C.f() missing 2 required positional arguments: 'self' and 'x'
C.f()
# C.f() missing 1 required positional argument: 'x'
c.f()
The notation C.f()
which CPython uses does not clarify if we are calling the function f
or the bound method f
. Sure, the self
gives it away, but adding that additional piece of information (function vs bound method) seems like it could be potentially helpful.
and is more similar to how things look in runtime messages
It feels to me like innovation in this area is not necessarily a bad thing, as long as we don't digress too far from established norms. We already do provide much better error messages than CPython. But that does not mean that they can't be better still. That's a general comment and not necessarily related to my changes earlier today. I'm happy to revisit them. I can open a ticket to discuss this.
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.
adding that additional piece of information (function vs bound method) seems like it could be potentially helpful.
Yes, I agree this is helpful.
innovation in this area is not necessarily a bad thing
And I agree with this too!
I still think qualified name display will help messages be less ambiguous overall, and may allow us to remove some of the distinctions between function kinds -- but I definitely don't think we should closely hew to whatever the runtime does, and I think you're right that specifically distinguishing bound methods may be useful.
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Outdated
Show resolved
Hide resolved
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Outdated
Show resolved
Hide resolved
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.
Excellent!
@@ -259,11 +259,7 @@ class A: | |||
class B: | |||
__add__ = A() | |||
|
|||
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type |
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.
Yeah I think this is less a TODO and more a commentary on why Unknown
is in the revealed type, since it may not be obvious at first.
@@ -82,7 +82,7 @@ class C: | |||
|
|||
c = C() | |||
|
|||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`" | |||
# error: 15 [invalid-argument-type] "Object of type `Literal["foo"]` cannot be assigned to parameter 2 (`x`) of bound method `__call__`; expected type `int`" |
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.
I had the same thought on the previous diff that improved error messages, that ultimately probably we don't need to describe "function" vs "bound method" vs "wrapper-descriptor", we can probably replace all of that with fully-qualified (within the module) function names (e.g. C.f.__get__
already clarifies that this is is the __get__
method of the function f
in the class C
, and is more similar to how things look in runtime messages). I think this is worth creating an issue for but isn't necessarily urgent.
crates/red_knot_python_semantic/resources/mdtest/call/dunder.md
Outdated
Show resolved
Hide resolved
b2a24ef
to
001033c
Compare
@@ -78,7 +78,7 @@ error: lint:not-iterable | |||
| | |||
26 | # error: [not-iterable] | |||
27 | for y in Iterable2(): | |||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `Literal[__getitem__] | None`) may not be callable | |||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `<bound method `__getitem__` of `Iterable2`> | None`) may not be callable |
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.
@AlexWaygood Looks like the half-life of these snapshot tests was rather small 😄. Do these new messages look okay to you? I think they are more correct now, but not necessarily more helpful. If we want to keep the current display-output of bound-method callable types, maybe we should consider reducing the verbosity in a different way here? I can look at this in a follow up, if we think it's worth doing?
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.
Oh no, what are you doing to my beautiful not-iterable
diagnostics 😆
but yes, no need for the PR to be blocked on this. Ideally I think we'd make several changes to how we talk about callable types in diagnostic messages:
- I don't think the
Literal[f]
display is working for us really; I think it's going to be pretty confusing for users. I'd vote for changing the display to<function f>
- I still think using the fully qualified name (with the module prepended) would be clearer in nearly all cases, and wouldn't make things much more verbose (e.g.
<function foo.f>
- We may want to add a method to
FunctionType
and the various otherCallableType
s that pretty-prints the callable signature ((str, int) -> bytes
, ordef f(x: str, y: int) -> bytes
, or similar), for uses in diagnostics like this. It's obviously no good referring to the function by (qualified) name if there are two functions in the same scope with the same name, but different signatures.
All told, the function-literal display issues were already the weakest part of the not-iterable
diagnostics prior to this PR, and I think we need to fix it holistically since it affects our diagnostic rendering in general -- it was already on my mind :-)
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.
Alright, thanks!
@@ -52,7 +52,7 @@ error: lint:not-iterable | |||
| | |||
16 | # error: [not-iterable] | |||
17 | for x in Iterable1(): | |||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `Literal[__iter__, __iter__]`) may have an invalid signature (expected `def __iter__(self): ...`) | |||
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `<bound method `__iter__` of `Iterable1`> | <bound method `__iter__` of `Iterable1`>`) may have an invalid signature (expected `def __iter__(self): ...`) |
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.
oh no, I didn't think it could get worse than Literal[__iter__, __iter__]
😆
Summary
Model dunder-calls correctly (and in one single place), by implementing this behavior (using
__getitem__
as an example).See the new
calls/dunder.md
test suite for more information. The new behavior also needs much fewer lines of code (the diff is positive due to new tests).Test Plan
New tests; fix TODOs in existing tests.