Skip to content

Commit d21cf1b

Browse files
stubtest: don't crash when a classmethod's first parameter isn't named cls (#21941)
fixes #16583 the first parameter of a classmethod can be called anything . networkx really does write `def construct(EdgeComponentAuxGraph, G)` . stubtest checked that name against a list of four and raised `StubtestFailure` when it did not match : ``` StubtestFailure: unexpected class parameter name 'EdgeComponentAuxGraph' in pkg.EdgeComponentAuxGraph.construct ``` that is not a normal stubtest error , it stops the whole run , so one oddly named parameter takes out stub checking for a whole distribution . that is what happens on python/typeshed#13587 . the name is never used for anything . the line right under it drops the argument by position , so the check was not guarding the code below it . it looks at the argument kind now instead . while testing i found a second way onto the same line . a stub written `def f(*args)` under `@classmethod` also crashed . there is nothing bound to drop in that case , and `inspect.signature` of the runtime classmethod keeps the star argument too , so both sides already match and it returns unchanged . dropping it would have made a false mismatch . two cases added to `test_static_class_method` , one for the odd name and one for `*args` . both fail on master with the crash above . `teststubtest.py` and `teststubgen.py` give 440 passed , 1 skipped , 2 xfailed . self check on the two files is clean , and so are black and ruff . small heads up , #21863 adds its own cases to `test_static_class_method` starting on the same line , so whichever goes in second will need a rebase . it is only the test file , that one changes `verify_var` and this one changes `_resolve_funcitem_from_decorator` , so they do not touch the same code .
1 parent 45b9b1d commit d21cf1b

2 files changed

Lines changed: 33 additions & 6 deletions

File tree

‎mypy/stubtest.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,14 +1576,14 @@ def apply_decorator_to_funcitem(
15761576
):
15771577
return func
15781578
if decorator.fullname == "builtins.classmethod":
1579-
if func.arguments[0].variable.name not in ("_cls", "cls", "mcs", "metacls"):
1580-
raise StubtestFailure(
1581-
f"unexpected class parameter name {func.arguments[0].variable.name!r} "
1582-
f"in {dec.fullname}"
1583-
)
1579+
if not func.arguments or func.arguments[0].kind not in (nodes.ARG_POS, nodes.ARG_OPT):
1580+
# Nothing to drop, e.g. `def f(*args)`. inspect.signature of the runtime
1581+
# classmethod keeps the star argument too, so the two already line up.
1582+
return func
15841583
# FuncItem is written so that copy.copy() actually works, even when compiled
15851584
ret = copy.copy(func)
1586-
# Remove the cls argument, since it's not present in inspect.signature of classmethods
1585+
# Remove the cls argument, since it's not present in inspect.signature of classmethods.
1586+
# It can be given any name, so don't look at the name to decide.
15871587
ret.arguments = ret.arguments[1:]
15881588
return ret
15891589
# Just give up on any other decorators. After excluding properties, we don't run into

‎mypy/test/teststubtest.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,33 @@ def __new__(cls, *args, **kwargs): pass
687687
""",
688688
error=None,
689689
)
690+
# The first parameter of a classmethod can be called anything, see #16583
691+
yield Case(
692+
stub="""
693+
class GoodOddClsName:
694+
@classmethod
695+
def f(GoodOddClsName, number: int) -> None: ...
696+
""",
697+
runtime="""
698+
class GoodOddClsName:
699+
@classmethod
700+
def f(GoodOddClsName, number): pass
701+
""",
702+
error=None,
703+
)
704+
yield Case(
705+
stub="""
706+
class GoodStarArgsCls:
707+
@classmethod
708+
def f(*args: int) -> None: ...
709+
""",
710+
runtime="""
711+
class GoodStarArgsCls:
712+
@classmethod
713+
def f(*args): pass
714+
""",
715+
error=None,
716+
)
690717

691718
@collect_cases
692719
def test_arg_mismatch(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)