[red-knot] Improve handling of inherited class attributes#16160
[red-knot] Improve handling of inherited class attributes#16160AlexWaygood merged 5 commits intomainfrom
Conversation
6d715e2 to
3289df1
Compare
…ibutes on classes inheriting from dynamic types
3289df1 to
77bd0ca
Compare
| /// Transform the symbol into a [`LookupResult`], | ||
| /// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol | ||
| /// and the `Err` variant represents a symbol that is either definitely or possibly unbound. | ||
| pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { | ||
| match self { | ||
| Symbol::Type(ty, Boundness::Bound) => Ok(ty), | ||
| Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)), | ||
| Symbol::Unbound => Err(LookupError::Unbound), | ||
| } | ||
| } |
There was a problem hiding this comment.
In my work attempting to do a wholesale refactor of our Outcome enums, I looked for a while at whether it might be possible to use Results for all of them. I'm no longer convinced that doing so would necessarily be a good idea, but it's still the case that a lot of the time we want to treat these Outcomes "like Results".
There was a problem hiding this comment.
Hehe... I think using Result's is good. The main awkward thing about them is composition AND that we can't add helper methods to them (unless we use an extension trait). But I think the "somewhat more verbose" is actually a good thing because it so far has been easy to do the wrong thing and hard to do the right thing. It should be the other way round ;)
MichaReiser
left a comment
There was a problem hiding this comment.
That overall makes sense to me and goes into the direction I'm heading in as well. I'd love to get a second review.
| // TODO: if `rhs_reflected` is possibly unbound, we should union the two possible | ||
| // CallOutcomes together |
There was a problem hiding this comment.
This should become "easy" with my refactor but requires fixing our float/int/bool parameter assignability first :(
| /// Transform the symbol into a [`LookupResult`], | ||
| /// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol | ||
| /// and the `Err` variant represents a symbol that is either definitely or possibly unbound. | ||
| pub(crate) fn into_lookup_result(self) -> LookupResult<'db> { | ||
| match self { | ||
| Symbol::Type(ty, Boundness::Bound) => Ok(ty), | ||
| Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)), | ||
| Symbol::Unbound => Err(LookupError::Unbound), | ||
| } | ||
| } |
There was a problem hiding this comment.
Hehe... I think using Result's is good. The main awkward thing about them is composition AND that we can't add helper methods to them (unless we use an extension trait). But I think the "somewhat more verbose" is actually a good thing because it so far has been easy to do the wrong thing and hard to do the right thing. It should be the other way round ;)
carljm
left a comment
There was a problem hiding this comment.
I like this, thanks for implementing it!
It does feel a little bit like we have too many ways to represent the same thing now? As in, Symbol and LookupResult seem entirely isomorphic, so why do we have both? Maybe LookupResult should entirely replace Symbol?
| (Symbol::Type(ty, _), Some(dynamic_type)) => Symbol::bound( | ||
| IntersectionBuilder::new(db) | ||
| .add_positive(ty) | ||
| .add_positive(dynamic_type) |
There was a problem hiding this comment.
Compared to the previous code, we are now hardcoding here the idea that all attributes of Any are also Any, and same for other dynamic types; we no longer actually call .member() on them, as the previous code did. I think this is probably fine, but maybe worth noting?
(It does raise some interesting questions, which I don't think should be answered in this PR, about the definition of member for dynamic types. With this PR, and the way we construct the MRO when inheriting Any as (A, Any, object) given class A(Any): pass, for attributes defined on object we will give e.g. Literal[object.__repr__] & Any as the type of A.__repr__. But really Any itself could be considered to implicitly inherit object (because any Python object must), so perhaps the type of x: Any; x.__repr__ should also be Literal[object.__repr__] & Any, not just Any?)
There was a problem hiding this comment.
Good points!... but I think if I synthesise your two points here, the conclusion I come to is that it would actually be incorrect to calling Type::Any.member() here? If we were to call a method on Type::Any here, I think we'd want the equivalent of Type::Any::own_class_member() (to avoid traversing up Any's "MRO" and hitting object). But we obviously don't have an own_class_member() method for Any -- and there doesn't seem much point in adding one, since it would always just return Any!
(It does raise some interesting questions, which I don't think should be answered in this PR, about the definition of
memberfor dynamic types. With this PR, and the way we construct the MRO when inheritingAnyas(A, Any, object)givenclass A(Any): pass, for attributes defined onobjectwe will give e.g.Literal[object.__repr__] & Anyas the type ofA.__repr__. But reallyAnyitself could be considered to implicitly inheritobject(because any Python object must), so perhaps the type ofx: Any; x.__repr__should also beLiteral[object.__repr__] & Any, not justAny?)
Yeah, I fully agree with all this, and it's consistent with the observations made in e.g. https://github.com/astral-sh/ruff/issues/15381. I guess partially fixing these issues (as I do in this PR) leaves things in a slightly inconsistent state... but I think you're right that scope creep here probably isn't a great idea 😄
Co-authored-by: Carl Meyer <[email protected]>
yep, that's exactly correct!
It's a great question. In my work looking at ways to refactor our I still think it would be possible to use ruff/crates/red_knot_python_semantic/src/symbol.rs Lines 99 to 105 in df45a9d There isn't a consistent way in which we want to subdivide the three variants; it depends quite a lot on context. So in some cases it makes more sense to think about it as a It still might make sense overall to use |
Summary
This PR fixes a number of issues to do with inherited class attributes.
On
main, if you run red-knot on this file, you get these results:There are the following issues here:
Foo3.__repr__is not incorrect, but it could be more precise. All classes -- even non-fully-static classes that haveAnyin their MRO -- inherit fromobject, andobjecthas a__repr__method. We therefore know that whatever the type ofFoo3.__repr__is, it must be a consistent subtype of the type ofobject.__repr__;Literal[object.__repr__] & Anyis therefore a better type forFoo3.__repr__thanAny.C.x.Cis not a fully static type (it hasAnyin its MRO), but it inherits from a fully static typeAthat has anxattribute. We therefore know that whatever type theAnyin its MRO materialises to, the type ofC.xmust be a consistent subtype of the type ofA.x;Literal[1] & Anyis therefore a better type here than simplyAny.This PR fixes these issues by reworking
types::Class::class_member. The PR is best reviewed one commit at a time:Symbolthat moves theSymbolcloser to our otherOutcomeenums. This refactoring makes commit 2 much easier.Test Plan
New mdtests added, which all fail on
main.