-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Don't special-case class instances in binary expression inference #15161
base: main
Are you sure you want to change the base?
Conversation
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => { | ||
Some(Type::BooleanLiteral(b1 | b2)) | ||
} |
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 reordered these so that all of the special cases for literals appeared together
(todo @ Type::Todo(_), _, _) | (_, todo @ Type::Todo(_), _) => Some(todo), | ||
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), |
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.
Had to add clauses for our other gradual type forms
(Type::Instance(_), Type::IntLiteral(_), op) => self.infer_binary_expression_type( | ||
left_ty, | ||
KnownClass::Int.to_instance(self.db()), | ||
op, | ||
), | ||
|
||
(Type::IntLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( | ||
KnownClass::Int.to_instance(self.db()), | ||
right_ty, | ||
op, | ||
), | ||
|
||
(Type::Instance(_), Type::Tuple(_), op) => self.infer_binary_expression_type( | ||
left_ty, | ||
KnownClass::Tuple.to_instance(self.db()), | ||
op, | ||
), | ||
|
||
(Type::Tuple(_), Type::Instance(_), op) => self.infer_binary_expression_type( | ||
KnownClass::Tuple.to_instance(self.db()), | ||
right_ty, | ||
op, | ||
), |
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.
These are now all handled by the to_meta_type
calls down below. The new approach is more complete: before we handled when each literal type appeared in an expression with an Instance
, but not with other (different) literal types.
|
||
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared | ||
# formal parameter type. | ||
reveal_type(2 + "f") # revealed: 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.
This test produced a @Todo
type before, since our literal coercions weren't complete
Type::FunctionLiteral(_) | ||
| Type::ModuleLiteral(_) | ||
| Type::ClassLiteral(_) | ||
| Type::SubclassOf(_) | ||
| Type::Instance(_) | ||
| Type::KnownInstance(_) | ||
| Type::Union(_) | ||
| Type::Intersection(_) |
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've expanded this out into a list of all of the coercable types, just like @AlexWaygood requested in #15045 (comment)
CodSpeed Performance ReportMerging #15161 will degrade performances by 4.76%Comparing Summary
Benchmarks breakdown
|
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false), | ||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true), | ||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true), |
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.
The test failures were because we're now noticing the recursiveness of some additional class definitions (#14672)
class Tree(list[Tree | Leaf]): ...
We now see into the BitOr
of two ClassLiterals
, and in most cases, can see via the typeshed that the result is a UnionType
. But in these two fixtures, there's a recursive reference inside that union, which we can't yet handle.
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.
Maybe eliminating the special case and just returning types.UnionType
instead, as commented above, will help these cases?
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, perhaps not; I see now that this comment was made before you added that special case.
class A: ... | ||
class B: ... | ||
|
||
reveal_type(A | B) # revealed: UnionType |
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 comes from the typeshed. We might consider special-casing the BitOr
of two ClassLiteral
s to return a more specific Type::Union
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.
(Note that this is not a type annotation, so none of the logic in Type::in_type_expression
applies)
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 went ahead and added this special case, but retaining the behavior that it's only available in Python ≥3.10
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, I commented on this in my review; I think the added special case is not correct, the type from typeshed is :)
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.
👍 Sounds good! I wasn't sure about which way to go, so I added the special case in a separate commit that would be easier to revert
|
code | total | + violation | - violation | + fix | - fix |
---|---|---|---|---|---|
UP006 | 23 | 0 | 23 | 0 | 0 |
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.
Looks great, thank you! Just one substantive comment.
class A: ... | ||
class B: ... | ||
|
||
reveal_type(A | B) # revealed: Literal[A, B] |
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 don't think Literal[A, B]
is the right type to assign to the value expression A | B
.
In a type expression, A | B
spells the type Type::Union[Type::Instance(A), Type::Instance(B)]
-- that is, the type consisting of all instances of A
(or any subclass) and all instances of B
(or any subclass).
The value expression A if flag else B
would correctly be assigned the type Literal[A, B]
, since its runtime value must either be the class object A
or the class object B
.
But the runtime value of the value expression A | B
is not either of those, so its type can't be Literal[A, B]
. Its runtime value is an instance of types.UnionType
, which can't be treated as if it were a class object at all. So the type we should assign to it is Type::Instance(types.UnionType)
.
reveal_type(A | B) # revealed: Literal[A, B] | |
reveal_type(A | B) # revealed: UnionType |
If we did any special-casing here, the correct special casing would be to create a custom type representation for types.UnionType
so we can record the fact that its __args__
attribute (in this case) has the type tuple[Literal[A], Literal[B]]
. But I don't think this is 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.
Reverted the special case, this is now returning UnionType
again as before
reveal_type(No() & Yes()) # revealed: Unknown | ||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" | ||
reveal_type(No() // Yes()) # revealed: Unknown | ||
``` |
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.
It feels like we are missing the Yes() <op> No()
case here; is there a reason you didn't include that?
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 wasn't sure if that would be needed, since No
is not a subclass of Yes
. So it doesn't matter whether it implements __radd__
, we're always going to use Yes.__add__
, and so that would give the same result as the Yes() <op> Yes()
case.
So I think what I want is another stanza, where Sub
and No
both implemented the reflected methods, to show that we correctly use the Sub
reflections since it's a subclass, but don't pick up the No
reflections since it's not.
reveal_type(f // f) # revealed: Unknown | ||
``` | ||
|
||
## Subclass |
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 feels very closely related to the ## Classes
section above -- maybe group them together instead of having the intervening function literals section?
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.
Done
(Type::ClassLiteral(_), Type::ClassLiteral(_), ast::Operator::BitOr) | ||
if Program::get(self.db()).python_version(self.db()) >= PythonVersion::PY310 => | ||
{ | ||
Some(UnionType::from_elements(self.db(), [left_ty, right_ty])) | ||
} | ||
|
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 is the clause that I think we don't want; I expect typeshed will natively give us the correct type of types.UnionType
?
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.
Removed by the revert mentioned above
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false), | ||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true), | ||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true), |
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.
Maybe eliminating the special case and just returning types.UnionType
instead, as commented above, will help these cases?
It's odd that this PR would cause a regression. The profiles on CodSpeed suggest that we are fetching some ingredient more often. I suspect that the cause is that improving completeness here means we now return a more complex type in lots of cases that would previously have just returned |
* main: Add all PEP-585 names to UP006 rule (#5454) [`flake8-simplify`] More precise inference for dictionaries (`SIM300`) (#15164) `@no_type_check` support (#15122) Visit PEP 764 inline `TypedDict`s' keys as non-type-expressions (#15073) [red-knot] Add diagnostic for invalid unpacking (#15086) [`flake8-use-pathlib`] Catch redundant joins in `PTH201` and avoid syntax errors (#15177) Update Rust crate glob to v0.3.2 (#15185) Update astral-sh/setup-uv action to v5 (#15193) Update dependency mdformat-mkdocs to v4.1.1 (#15192) Update Rust crate serde_with to v3.12.0 (#15191) Update NPM Development dependencies (#15190) Update pre-commit hook rhysd/actionlint to v1.7.5 (#15189) Update Rust crate syn to v2.0.93 (#15188) Update Rust crate serde to v1.0.217 (#15187) Update Rust crate quote to v1.0.38 (#15186) Update Rust crate compact_str to v0.8.1 (#15184) [`flake8-type-checking`] Disable TC006 & TC007 in stub files (#15179) Test explicit shadowing involving `def`s (#15174) Fix typo in `NameImport.qualified_name` docstring (#15170) [airflow]: extend names moved from core to provider (AIR303) (#15159)
This reverts commit a52486f.
Just like in #15045 for unary expressions: In binary expressions, we were only looking for dunder expressions for
Type::Instance
types. We had some special cases for coercing the variousLiteral
types into their correspondingInstance
types before doing the lookup. But we can side-step all of that by using the existingType::to_meta_type
andType::to_instance
methods.Closes #14549