[pyupgrade] Apply UP008 only when the __class__ cell exists#19424
[pyupgrade] Apply UP008 only when the __class__ cell exists#19424ntBre merged 5 commits intoastral-sh:mainfrom
pyupgrade] Apply UP008 only when the __class__ cell exists#19424Conversation
|
01d1724 to
a00d546
Compare
|
@ntBre Could you please review a PR? |
|
Will do! Sorry, I'm a bit behind on my review queue, but this has been on the list. |
ntBre
left a comment
There was a problem hiding this comment.
Thanks, this looks great! Sorry again for the delay.
I just had a few small suggestions.
Thanks for the docs links too. I skimmed over a few of those, and I think you've captured everything I understood from them and from the issue too :)
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Show resolved
Hide resolved
| let has_load_super = finder | ||
| .names | ||
| .iter() | ||
| .any(|expr| expr.id.as_str() == "super" && expr.ctx.is_load()); | ||
| let has_load_dunder_class = finder | ||
| .names | ||
| .iter() | ||
| .any(|expr| expr.id.as_str() == "__class__" && expr.ctx.is_load()); |
There was a problem hiding this comment.
Because these are both any, I think we can just check these conditions as the visitor visits and avoid needing to collect a Vec of expressions. We may even be able to terminate the visitor early in that case.
There was a problem hiding this comment.
My intention was to make it simple and not necessarily optimized.
I have reworked it, and now it is implemented according to your recommendation, but I think it is more complex to read. I would appreciate any simplification advice.
BTW, it was intentional to pass conditions due to initialization rather than hardcode them in visit_expr. In my opinion, it provides more flexibility for future changes.
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Show resolved
Hide resolved
db72ff9 to
f1e50af
Compare
|
I just came back to this notification right after reviewing #19783. I somehow hadn't connected these two before, but now I'm wondering if they have the same root cause. I think if we're able to work out a neat, general solution for binding the |
Summary -- This PR aims to resolve (or help to resolve) #18442 and #19357 by encoding the CPython semantics around the `__class__` cell in our semantic model. Namely, > __class__ is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__ or super. from the Python [docs](https://docs.python.org/3/reference/datamodel.html#creating-the-class-object). As noted in the code comment by @AlexWaygood, we don't fully model this behavior, opting always to create the `__class__` cell binding in a new `ScopeKind::ClassCell` around each method definition, without checking if any method in the class body actually refers to `__class__` or `super`. As such, this PR may only help with #18442 and not #19357. Test Plan -- Existing tests, plus (TODO) tests from #19783 and #19424, which tackled the issues above on a per-rule basis. Co-authored-by: Alex Waygood <[email protected]>
Summary -- This PR aims to resolve (or help to resolve) #18442 and #19357 by encoding the CPython semantics around the `__class__` cell in our semantic model. Namely, > __class__ is an implicit closure reference created by the compiler if any methods in a class body refer to either __class__ or super. from the Python [docs](https://docs.python.org/3/reference/datamodel.html#creating-the-class-object). As noted in the code comment by @AlexWaygood, we don't fully model this behavior, opting always to create the `__class__` cell binding in a new `ScopeKind::ClassCell` around each method definition, without checking if any method in the class body actually refers to `__class__` or `super`. As such, this PR may only help with #18442 and not #19357. Test Plan -- Existing tests, plus (TODO) tests from #19783 and #19424, which tackled the issues above on a per-rule basis. Co-authored-by: Alex Waygood <[email protected]>
|
We added a new Thanks again for your work here :) |
|
@ntBre Good news! Here are my thoughts after reviewing the new feature.
Since I've already implemented tailored |
f1e50af to
db109c8
Compare
|
Thanks for looking into it and rebasing the PR!
Yes that's right, we didn't try to detect if
Makes sense to me, I'll give this another review soon! |
ntBre
left a comment
There was a problem hiding this comment.
Thanks! Just a couple of minor simplification suggestions and one corner case from the docs.
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Outdated
Show resolved
Hide resolved
crates/ruff_linter/src/rules/pyupgrade/rules/super_call_with_parameters.rs
Show resolved
Hide resolved
Skip UP008 diagnostic for `builtins.super(Class, self)` calls when neither `__class__` nor `super` are referenced locally, preventing incorrect fixes. Issue: astral-sh#19357
`has_local_dunder_class_var_ref` checks if `super` or `__class__` expr.ctx is load and `__class__` var in scope didn't override. Issue: astral-sh#19357
renamed FunctionScopeNameFinder to AnyExpressionFinder moved AnyExpressionFinder to super_call_with_parameters.rs updated test cases and snapshots removed duplicated parents Now AnyExpressionFinder finds any expression by given conditions
db109c8 to
27ab45c
Compare
27ab45c to
c394853
Compare
pyupgrade] Apply UP008 only when the __class__ cell exists (UP008)pyupgrade] Apply UP008 only when the __class__ cell exists (UP008)
pyupgrade] Apply UP008 only when the __class__ cell exists (UP008)pyupgrade] Apply UP008 only when the __class__ cell exists
Summary
Resolves #19357
Skip UP008 diagnostic for
builtins.super(P, self)calls when__class__is not referenced locally, preventing incorrect fixes.Note: I haven't found concrete information about which cases
__class__will be loaded into the scope. Let me know if anyone has references, it would be useful to enhance the implementation. I did a lot of tests to determine when__class__is loaded. Considered sources:As I understand it, Python will inject at runtime into local scope a
__class__variable if it detects references tosuperor__class__. This allows callingsuper()and passing appropriate parameters. However, the compiler doesn't do the same forbuiltins.super, so we need to somehow introduce__class__into the local scope.I figured out
__class__will be in scope with valid value when two conditions are met:superor__class__names have been loaded within function scope__class__is not overridden.I think my solution isn't elegant, so I would be appreciate a detailed review.
Test Plan
Added 19 test cases, updated snapshots.