Skip to content

Conversation

@IDrokin117
Copy link
Contributor

@IDrokin117 IDrokin117 commented Jul 18, 2025

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:

  1. Python doc super
  2. Python doc classes
  3. pep-3135

As I understand it, Python will inject at runtime into local scope a __class__ variable if it detects references to super or __class__. This allows calling super() and passing appropriate parameters. However, the compiler doesn't do the same for builtins.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:

  1. super or __class__ names have been loaded within function scope
  2. __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.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 18, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@IDrokin117 IDrokin117 marked this pull request as draft July 20, 2025 13:13
@IDrokin117 IDrokin117 marked this pull request as ready for review July 21, 2025 15:01
@IDrokin117
Copy link
Contributor Author

@ntBre Could you please review a PR?

@ntBre ntBre self-requested a review July 24, 2025 13:04
@ntBre ntBre added bug Something isn't working fixes Related to suggested fixes for violations labels Jul 24, 2025
@ntBre
Copy link
Contributor

ntBre commented Jul 24, 2025

Will do! Sorry, I'm a bit behind on my review queue, but this has been on the list.

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Comment on lines 216 to 223
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@IDrokin117 IDrokin117 force-pushed the fix/ruff-19357 branch 3 times, most recently from db72ff9 to f1e50af Compare August 9, 2025 20:07
@IDrokin117 IDrokin117 requested a review from ntBre August 9, 2025 20:08
@ntBre
Copy link
Contributor

ntBre commented Aug 13, 2025

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 __class__ cell it might help here too.

ntBre added a commit that referenced this pull request Aug 22, 2025
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 <Alex.Waygood@Gmail.com>
ntBre added a commit that referenced this pull request Aug 22, 2025
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 <Alex.Waygood@Gmail.com>
@ntBre
Copy link
Contributor

ntBre commented Aug 26, 2025

We added a new ScopeKind::DunderClassCell (and BindingKind::DunderClassCell) in #20048. I don't believe that will resolve the issue addressed by this PR, but would you mind seeing if it makes anything easier? I was hoping it might help to avoid the new Visitor, but I think we'll still need some rule-specific changes in any case.

Thanks again for your work here :)

@IDrokin117
Copy link
Contributor Author

@ntBre Good news! Here are my thoughts after reviewing the new feature.
It seems the __class__ binding is being created in any case now, regardless of whether __class__ and super names are present. Am I right? This is going to be simple and good for some rules, but not for this case. Since the __class__ binding pretends to always exist within a method, it doesn't provide any new information. I mean, if a __class__ cell is available, then builtins.super() has to work. But that's not true since __class__ is not always available. I see two options to resolve the issue:

  1. Left the extra visitor to ensure determination of whether __class__ is really injected at runtime based on specific rules.
  2. Simplify the rule and apply it only if super() is encountered, ignoring builtins.super().

Since I've already implemented tailored __class__ determination, I prefer the first option. What do you think?

@ntBre
Copy link
Contributor

ntBre commented Aug 29, 2025

Thanks for looking into it and rebasing the PR!

It seems the __class__ binding is being created in any case now, regardless of whether __class__ and super names are present. Am I right?

Yes that's right, we didn't try to detect if __class__ or super was used, so it makes sense that it may not help here.

  1. Left the extra visitor to ensure determination of whether __class__ is really injected at runtime based on specific rules.
  2. Simplify the rule and apply it only if super() is encountered, ignoring builtins.super().

Since I've already implemented tailored __class__ determination, I prefer the first option. What do you think?

Makes sense to me, I'll give this another review soon!

Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Just a couple of minor simplification suggestions and one corner case from the docs.

IDrokin117 and others added 4 commits September 9, 2025 19:32
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
@IDrokin117 IDrokin117 requested a review from ntBre September 9, 2025 18:09
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ntBre ntBre changed the title [pyupgrade] Apply UP008 only when the __class__ cell exists (UP008) [pyupgrade] Apply UP008 only when the __class__ cell exists (UP008) Sep 9, 2025
@ntBre ntBre changed the title [pyupgrade] Apply UP008 only when the __class__ cell exists (UP008) [pyupgrade] Apply UP008 only when the __class__ cell exists Sep 9, 2025
@ntBre ntBre merged commit 54df73c into astral-sh:main Sep 9, 2025
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fixes Related to suggested fixes for violations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UP008 should only apply when the __class__ cell exists

3 participants