From 5b8322c5c8164376945bcc91b726eb6f63746d0a Mon Sep 17 00:00:00 2001 From: eternalrights <3147268827@qq.com> Date: Sun, 17 May 2026 19:13:45 +0800 Subject: [PATCH 1/5] Fix Code.getargs() treating co_flags bitmask values as counts CO_VARARGS (4) and CO_VARKEYWORDS (8) are bitmask flags, but they were used directly as increment values for the argument count via `&`. This caused co_varnames[:argcount] to slice beyond the actual arguments when the function body contained enough local variables. Fix by wrapping the bitmask results in bool(), so they contribute at most 1 to the argcount. Closes #14492 --- changelog/14492.bugfix.rst | 1 + src/_pytest/_code/code.py | 4 ++-- testing/code/test_code.py | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog/14492.bugfix.rst diff --git a/changelog/14492.bugfix.rst b/changelog/14492.bugfix.rst new file mode 100644 index 00000000000..deb5ce8fa7e --- /dev/null +++ b/changelog/14492.bugfix.rst @@ -0,0 +1 @@ +Fixed ``Code.getargs()`` incorrectly including local variable names in the returned argument tuple for functions with ``*args`` and/or ``**kwargs``. The method was using ``co_flags`` bitmask values (``4`` and ``8``) directly as counts instead of converting them to ``1`` via ``bool()``. \ No newline at end of file diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4cf99a77340..c0f4061875c 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -121,8 +121,8 @@ def getargs(self, var: bool = False) -> tuple[str, ...]: raw = self.raw argcount = raw.co_argcount if var: - argcount += raw.co_flags & CO_VARARGS - argcount += raw.co_flags & CO_VARKEYWORDS + argcount += bool(raw.co_flags & CO_VARARGS) + argcount += bool(raw.co_flags & CO_VARKEYWORDS) return raw.co_varnames[:argcount] diff --git a/testing/code/test_code.py b/testing/code/test_code.py index e10139dee50..18122ba7d40 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -115,6 +115,13 @@ def f4(x, *y, **z): c4 = Code.from_function(f4) assert c4.getargs(var=True) == ("x", "y", "z") + def f5(x, *y, **z): + a1 = a2 = a3 = a4 = a5 = a6 = 1 + raise NotImplementedError() + + c5 = Code.from_function(f5) + assert c5.getargs(var=True) == ("x", "y", "z") + def test_frame_getargs() -> None: def f1(x) -> FrameType: From 3f5ad64724ef7adc2e0ae118dea96fbc1340362b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 11:50:18 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog/14492.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/14492.bugfix.rst b/changelog/14492.bugfix.rst index deb5ce8fa7e..301958d6400 100644 --- a/changelog/14492.bugfix.rst +++ b/changelog/14492.bugfix.rst @@ -1 +1 @@ -Fixed ``Code.getargs()`` incorrectly including local variable names in the returned argument tuple for functions with ``*args`` and/or ``**kwargs``. The method was using ``co_flags`` bitmask values (``4`` and ``8``) directly as counts instead of converting them to ``1`` via ``bool()``. \ No newline at end of file +Fixed ``Code.getargs()`` incorrectly including local variable names in the returned argument tuple for functions with ``*args`` and/or ``**kwargs``. The method was using ``co_flags`` bitmask values (``4`` and ``8``) directly as counts instead of converting them to ``1`` via ``bool()``. From 17b1a0d99b32713384e54ce213f15abd525880ce Mon Sep 17 00:00:00 2001 From: EternalRights <162705204+EternalRights@users.noreply.github.com> Date: Sun, 17 May 2026 22:35:01 +0800 Subject: [PATCH 3/5] account for co_kwonlyargcount in getargs when var=True --- src/_pytest/_code/code.py | 1 + testing/code/test_code.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index c0f4061875c..4fcce0427ef 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -121,6 +121,7 @@ def getargs(self, var: bool = False) -> tuple[str, ...]: raw = self.raw argcount = raw.co_argcount if var: + argcount += raw.co_kwonlyargcount argcount += bool(raw.co_flags & CO_VARARGS) argcount += bool(raw.co_flags & CO_VARKEYWORDS) return raw.co_varnames[:argcount] diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 18122ba7d40..64680973138 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -122,6 +122,13 @@ def f5(x, *y, **z): c5 = Code.from_function(f5) assert c5.getargs(var=True) == ("x", "y", "z") + def f6(x, *y, kw=1, **z): + a1 = a2 = a3 = a4 = a5 = a6 = 1 + raise NotImplementedError() + + c6 = Code.from_function(f6) + assert c6.getargs(var=True) == ("x", "y", "kw", "z") + def test_frame_getargs() -> None: def f1(x) -> FrameType: From 816a164623fcdf719f3728a9b66c2af99bd91b81 Mon Sep 17 00:00:00 2001 From: eternalrights <3147268827@qq.com> Date: Mon, 18 May 2026 11:26:17 +0800 Subject: [PATCH 4/5] fix test expectation order for kwonly args and suppress ruff F841 co_varnames layout is: positional args, kwonly args, *args, **kwargs, locals. The f6 test expected the wrong order. Also suppress ruff F841 false positives on locals deliberately placed to test co_varnames slicing. --- testing/code/test_code.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 64680973138..b43b8fc0a17 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -116,18 +116,18 @@ def f4(x, *y, **z): assert c4.getargs(var=True) == ("x", "y", "z") def f5(x, *y, **z): - a1 = a2 = a3 = a4 = a5 = a6 = 1 + a1 = a2 = a3 = a4 = a5 = a6 = 1 # noqa: F841 raise NotImplementedError() c5 = Code.from_function(f5) assert c5.getargs(var=True) == ("x", "y", "z") def f6(x, *y, kw=1, **z): - a1 = a2 = a3 = a4 = a5 = a6 = 1 + a1 = a2 = a3 = a4 = a5 = a6 = 1 # noqa: F841 raise NotImplementedError() c6 = Code.from_function(f6) - assert c6.getargs(var=True) == ("x", "y", "kw", "z") + assert c6.getargs(var=True) == ("x", "kw", "y", "z") def test_frame_getargs() -> None: From a2a34fb7f77a791f61baa632e3e09d7bd35487b5 Mon Sep 17 00:00:00 2001 From: eternalrights <3147268827@qq.com> Date: Mon, 18 May 2026 15:13:50 +0800 Subject: [PATCH 5/5] call f5/f6 directly to cover function body lines for codecov Codecov reports coverage for testing/ files since the project config includes them. The function bodies of f5/f6 (local var assignments) were never executed because the test only used Code.from_function(), never called the functions. Replace raise NotImplementedError() with actual calls to cover all diff lines. --- testing/code/test_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/code/test_code.py b/testing/code/test_code.py index b43b8fc0a17..6947320a9ce 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -117,16 +117,16 @@ def f4(x, *y, **z): def f5(x, *y, **z): a1 = a2 = a3 = a4 = a5 = a6 = 1 # noqa: F841 - raise NotImplementedError() c5 = Code.from_function(f5) + f5(1, 2, 3, z=4) # cover function body assert c5.getargs(var=True) == ("x", "y", "z") def f6(x, *y, kw=1, **z): a1 = a2 = a3 = a4 = a5 = a6 = 1 # noqa: F841 - raise NotImplementedError() c6 = Code.from_function(f6) + f6(1, 2, kw=3, z=4) # cover function body assert c6.getargs(var=True) == ("x", "kw", "y", "z")