[pyupgrade] Properly trigger super change in nested class (UP008)#22677
[pyupgrade] Properly trigger super change in nested class (UP008)#22677charliermarsh merged 5 commits intoastral-sh:mainfrom
pyupgrade] Properly trigger super change in nested class (UP008)#22677Conversation
pyupgrade] properly trigger UP008 in nested classpyupgrade] properly trigger in nested class (UP008)
|
|
This bug seems easy to introduce into the codebase. I did a quick search and found another example. PLC1802 won’t trigger if the list is an attribute. https://play.ruff.rs/f9f8f76b-c468-4f4f-a3b8-0bca5850daf1 class Fruits:
fruits: list[str]
def __init__(self, fruits: list[str]):
self.fruits = fruits
fruits_class = Fruits(["orange", "apple"])
fruits_list = ["a", "b"]
if len(fruits_class.fruits):
...
if len(fruits_list):
...I opened an issue and I can't think on how to prevent this class of bug. |
|
|
||
| class Outer(Base): | ||
| def __init__(self, foo): | ||
| super(self).__init__(foo) # Should not trigger UP008 |
There was a problem hiding this comment.
I'm not sure what you intended here with the 1-argument super(self) call, but it actually raises an error at runtime:
Python 3.14.2 (tags/v3.14.2:df79316, Dec 5 2025, 17:18:21) [MSC v.1944 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> class Base:
... def __init__(self, foo):
... self.foo = foo
...
>>> class Outer(Base):
... def __init__(self, foo):
... super(self).__init__(foo) # Should not trigger UP008
...
>>> o = Outer(5)
Traceback (most recent call last):
File "<python-input-2>", line 1, in <module>
o = Outer(5)
File "<python-input-1>", line 3, in __init__
super(self).__init__(foo) # Should not trigger UP008
~~~~~^^^^^^
TypeError: super() argument 1 must be a type, not OuterPerhaps you wanted to use either a 2-argument or 0-argument call instead? Also, if the super call doesn't have two arguments, the lint will return early, so I don't know if you wanted to verify that there is no panic in that case, for example?
There was a problem hiding this comment.
It was supposed to be with 0-argument, thanks for pointing it.
|
@leandrobbraga (#22677 (comment)):
This doesn't work either - I suppose the indentation isn't what you intended? $ cat test.py
class Base:
def __init__(self, foo):
self.foo = foo
class Outer(Base):
def __init__(self, foo):
super().__init__(foo) # Should not trigger UP008
class Inner(Base):
def __init__(self, foo):
super().__init__(foo) # Should not trigger UP008
class InnerInner(Base):
def __init__(self, foo):
super(Outer.Inner.InnerInner, self).__init__(foo) # Should trigger UP008
o = Outer(5)
i = Outer.Inner(5)
ii = Outer.InnerInner(5)
$ python3 --version
Python 3.14.2
$ python3 test.py
Traceback (most recent call last):
File "/home/pp/test.py", line 21, in <module>
ii = Outer.InnerInner(5)
File "/home/pp/test.py", line 16, in __init__
super(Outer.Inner.InnerInner, self).__init__(foo) # Should trigger UP008
^^^^^^^^^^^^^^^^^^^^^^
AttributeError: type object 'Inner' has no attribute 'InnerInner' |
|
This was a copy-paste indentation error, for some reason I lost the indentation when I copy-pasted, but it was tested properly. Try linting this code: class Base:
def __init__(self, foo):
self.foo = foo
class Outer(Base):
def __init__(self, foo):
super().__init__(foo) # Should not trigger UP008
class Inner(Base):
def __init__(self, foo):
super(Outer.Inner, self).__init__(foo) # Should trigger UP008
class InnerInner(Base):
def __init__(self, foo):
super(Outer.Inner.InnerInner, self).__init__(foo) # Should trigger UP008
oii = Outer.Inner.InnerInner(1)Output: > echo 'class Base:
def __init__(self, foo):
self.foo = foo
class Outer(Base):
def __init__(self, foo):
super().__init__(foo) # Should not trigger UP008
class Inner(Base):
def __init__(self, foo):
super(Outer.Inner, self).__init__(foo) # Should trigger UP008
class InnerInner(Base):
def __init__(self, foo):
super(Outer.Inner.InnerInner, self).__init__(foo) # Should trigger UP008
oii = Outer.Inner.InnerInner(1)' | cargo run -p ruff -- check --select UP008 -
UP008 Use `super()` instead of `super(__class__, self)`
--> -:12:18
|
10 | class Inner(Base):
11 | def __init__(self, foo):
12 | super(Outer.Inner, self).__init__(foo) # Should trigger UP008
| ^^^^^^^^^^^^^^^^^^^
13 |
14 | class InnerInner(Base):
|
help: Remove `super()` parameters
UP008 Use `super()` instead of `super(__class__, self)`
--> -:16:22
|
14 | class InnerInner(Base):
15 | def __init__(self, foo):
16 | super(Outer.Inner.InnerInner, self).__init__(foo) # Should trigger UP008 |
| let second_arg_id = match second_arg { | ||
| Expr::Name(ast::ExprName { id, .. }) => id, | ||
| Expr::Attribute(ast::ExprAttribute { attr, .. }) => &attr.id, | ||
| _ => return, | ||
| }; |
There was a problem hiding this comment.
What's the point of accepting an attribute in the second argument? This second_arg_id is required to be equal to the name of the "parent argument" (parent_arg), which is typically self:
I think it doesn't make sense to accept wild stuff like super(..., a.b.c.self).
There was a problem hiding this comment.
That's correct, I removed the match on Expr::Attribute for the second parameter.
Remove the `Expr::Attribute` matching for the second parameter in `super`. The UP008 does not care when it's an Attribute, the only cases are: - super(MyClass, self) - super(OuterClass.InnerClass, self) Which is always an `Expr::Name`
0cf6617 to
a9b58e1
Compare
| // Simple case: super(MyClass, self) | ||
| Expr::Name(ast::ExprName { id, .. }) => id, | ||
| // Nested class case: super(OuterClass.InnerClass, self) | ||
| Expr::Attribute(ast::ExprAttribute { attr, .. }) => &attr.id, |
There was a problem hiding this comment.
This assumes that whenever super(A.B, self) appears inside a class named B, it must be the super(__class__, self) case and an upgrade can be performed. Unfortunately, this is not necessarily true. Consider this:
class Base:
def __init__(self, foo):
print(f"Base.__init__({foo}) called")
self.foo = foo
class Outer:
class Inner(Base):
def __init__(self, foo):
print(f"Outer.Inner.__init__({foo}) called")
super().__init__(foo)
class Inner(Outer.Inner):
def __init__(self, foo):
super(Outer.Inner, self).__init__(foo)
i = Inner(5)Note that the super call in the Inner.__init__ method body is super(Outer.Inner, self), not super(Inner, self). If you run the above code, the output is the following:
root@d2a45e4529f7:~# python3 --version
Python 3.13.5
root@d2a45e4529f7:~# python3 test.py
Base.__init__(5) calledAs you can see, only Base.__init__ was called, not Outer.Inner.__init__.
Nevertheless, your current implementation at a9b58e1 still suggests omitting arguments to this super call:
root@d2a45e4529f7:~/ruff# git status
On branch bugfix/up008-inner-class
Your branch is up to date with 'origin/bugfix/up008-inner-class'.
nothing to commit, working tree clean
root@d2a45e4529f7:~/ruff# git rev-parse HEAD
a9b58e1d3610ef14430569af480d9a5fd0b4d209
root@d2a45e4529f7:~/ruff# cargo run -p ruff -- check --select UP ../test.py --no-cache
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/ruff check --select UP ../test.py --no-cache`
UP008 Use `super()` instead of `super(__class__, self)`
--> /root/test.py:16:14
|
14 | class Inner(Outer.Inner):
15 | def __init__(self, foo):
16 | super(Outer.Inner, self).__init__(foo)
| ^^^^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).In preview, this is even offered as a safe fix:
root@d2a45e4529f7:~/ruff# cargo run -p ruff -- check --preview --select UP ../test.py --no-cache
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/ruff check --preview --select UP ../test.py --no-cache`
UP008 [*] Use `super()` instead of `super(__class__, self)`
--> /root/test.py:16:14
|
14 | class Inner(Outer.Inner):
15 | def __init__(self, foo):
16 | super(Outer.Inner, self).__init__(foo)
| ^^^^^^^^^^^^^^^^^^^
|
help: Remove `super()` parameters
13 |
14 | class Inner(Outer.Inner):
15 | def __init__(self, foo):
- super(Outer.Inner, self).__init__(foo)
16 + super().__init__(foo)
17 |
18 |
19 | i = Inner(5)
Found 1 error.
[*] 1 fixable with the `--fix` option.However, if you apply it, the behavior of the program changes - notice the additional message Outer.Inner.__init__(5) called, which the original program did not print:
root@d2a45e4529f7:~# cat test.py
class Base:
def __init__(self, foo):
print(f"Base.__init__({foo}) called")
self.foo = foo
class Outer:
class Inner(Base):
def __init__(self, foo):
print(f"Outer.Inner.__init__({foo}) called")
super().__init__(foo)
class Inner(Outer.Inner):
def __init__(self, foo):
super().__init__(foo)
i = Inner(5)
root@d2a45e4529f7:~# python3 test.py
Outer.Inner.__init__(5) called
Base.__init__(5) calledI admit that this is perhaps an unusual scenario. Calling super(Outer.Inner, self) in the Inner class looks rather unintentional or at least suspicious, but from Python's point of view, it is a valid program that does not raise any runtime errors.
So I believe that Ruff should verify whether the entire attribute chain (in this case Outer.Inner) matches, not just its last segment. This means that instead of just finding the closest enclosing class:
... and comparing its name with the last segment in the attribute chain:
..., Ruff should also inspect further (nested) enclosing classes and compare the second-to-last segment in the attribute chain, third-to-last segment, etc. If anything doesn't match, UP008 should not be triggered.
There was a problem hiding this comment.
Thank you for your review, I'll try to think about the corner cases and come up with a solution in the next days.
There was a problem hiding this comment.
Thank you for your review, I'll try to think about the corner cases and come up with a solution in the next days.
There was a problem hiding this comment.
See #22597 (comment) - I've just found out that the situation I described above is already covered in this pyupgrade test case:
asottile / pyupgrade / tests/features/super_test.py:32-35
'class Outer:\n' # super arg1 nested in unrelated name
' class C(Base):\n'
' def f(self):\n'
' super(some_module.Outer.C, self).f()\n',Honestly, I think it's worth looking at the entire tests/features/super_test.py file and ideally porting all test cases that are not yet covered by the Ruff test suite. There aren't that many, but Ruff still seems to be missing a few important ones.
amyreese
left a comment
There was a problem hiding this comment.
Needs a rebase and updated snapshots.
pyupgrade] properly trigger in nested class (UP008)pyupgrade] Properly trigger super change in nested class (UP008)
While debugging, I noticed that the function parameters were of type
Expr::Namein the normal case andExpr::Attributewhen the function was in a nested class. Reading the documentation, it seems that being anExpr::Attributemakes sense becauseInneris an attribute ofOuter.I did some thinking and could not identify any additional
Exprtypes I should be pattern-matching here, but I would like some input from you.The change seems to fix the issue without any regression.
Closes #22597