From b0ba44a1f05894e837d9a3da89f7c3944adf4a9d Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Sat, 5 Sep 2026 03:05:13 +0500 Subject: [PATCH] stubtest: stop crashing when a classmethod's first parameter is not called cls the first parameter of a classmethod can be named anything, and networkx really does write def construct(EdgeComponentAuxGraph, G). stubtest checked the name against a list of four and raised StubtestFailure when it did not match, which aborts the whole run rather than reporting an error, so one odd name takes out stub checking for a whole distribution. the name is never used, the argument is dropped positionally on the next line, so the check guarded nothing. it looks at the argument kind instead now. a stub written def f(*args) reached the same line and crashed too. there is nothing bound to drop in that case, and inspect.signature of the runtime classmethod keeps the star argument as well, so both sides already line up and it returns unchanged. --- mypy/stubtest.py | 12 ++++++------ mypy/test/teststubtest.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4a38939a03907..61587469b5a6d 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1574,14 +1574,14 @@ def apply_decorator_to_funcitem( ): return func if decorator.fullname == "builtins.classmethod": - if func.arguments[0].variable.name not in ("_cls", "cls", "mcs", "metacls"): - raise StubtestFailure( - f"unexpected class parameter name {func.arguments[0].variable.name!r} " - f"in {dec.fullname}" - ) + if not func.arguments or func.arguments[0].kind not in (nodes.ARG_POS, nodes.ARG_OPT): + # Nothing to drop, e.g. `def f(*args)`. inspect.signature of the runtime + # classmethod keeps the star argument too, so the two already line up. + return func # FuncItem is written so that copy.copy() actually works, even when compiled ret = copy.copy(func) - # Remove the cls argument, since it's not present in inspect.signature of classmethods + # Remove the cls argument, since it's not present in inspect.signature of classmethods. + # It can be given any name, so don't look at the name to decide. ret.arguments = ret.arguments[1:] return ret # Just give up on any other decorators. After excluding properties, we don't run into diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 2b54a150892b2..24ce579429668 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -682,6 +682,33 @@ def __new__(cls, *args, **kwargs): pass """, error=None, ) + # The first parameter of a classmethod can be called anything, see #16583 + yield Case( + stub=""" + class GoodOddClsName: + @classmethod + def f(GoodOddClsName, number: int) -> None: ... + """, + runtime=""" + class GoodOddClsName: + @classmethod + def f(GoodOddClsName, number): pass + """, + error=None, + ) + yield Case( + stub=""" + class GoodStarArgsCls: + @classmethod + def f(*args: int) -> None: ... + """, + runtime=""" + class GoodStarArgsCls: + @classmethod + def f(*args): pass + """, + error=None, + ) @collect_cases def test_arg_mismatch(self) -> Iterator[Case]: