From 53eca659f379d0e99fd6a35a5d233feef6acbff1 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Tue, 15 Sep 2026 20:57:43 -0400 Subject: [PATCH] fix: convert positional-only parameters on methods, not just functions `_process_classdef` passed `cls_ctx=name.name`, but `Class.name` already returns the parso `Name` leaf, whose string lives on `.value`. Every class body therefore raised `AttributeError: 'Name' object has no attribute 'name'`, and since `bpc_utils` catches per node the process still exited 0 -- so the markers survived into the output and callers wrapping poseur in `subprocess.check_call` saw success. `_process_suite_node` declares `cls_ctx: Optional[str]`, stores it as `Optional[str]`, and hands it to `mangle()`, so `.value` is what was meant. Added `test_classdef`, which reproduces the crash without this change. `tests/sample.py` has no `class` at all, which is why the golden-file suite never caught it. Fixes #20 --- poseur.py | 2 +- tests/test.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/poseur.py b/poseur.py index d1bbda7..a08d0b7 100644 --- a/poseur.py +++ b/poseur.py @@ -741,7 +741,7 @@ def _process_classdef(self, node: parso.python.tree.Class) -> None: # PythonNode(suite, [...]) / PythonNode(simple_stmt, [...]) suite = node.children[-1] - self._process_suite_node(suite, cls_ctx=name.name) + self._process_suite_node(suite, cls_ctx=name.value) def _process_if_stmt(self, node: parso.python.tree.IfStmt) -> None: """Process if statement (:token:`if_stmt`). diff --git a/tests/test.py b/tests/test.py index 6337b54..5c20e81 100644 --- a/tests/test.py +++ b/tests/test.py @@ -209,6 +209,22 @@ def test_funcdef(self): POSEUR_LINESEP.join(_decorator) % dict(decorator='_poseur_decorator', indentation='\t'.expandtabs(4))).lstrip() self._check_convert(src, dst) + def test_classdef(self): + # no poseur + src = 'class Cls:\n def func(self): pass' + dst = 'class Cls:\n def func(self): pass' + self._check_convert(src, dst) + + # poseur on a method -- this used to raise AttributeError from + # ``_process_classdef`` and leave the source unconverted, and because + # ``bpc_utils`` swallows the exception per node the process still exited 0 + self.assertNotIn(', /', convert('class Cls:\n def func(self, a, /, b): pass')) + + src = 'class Cls:\n def func(self, a, /, b): pass' + dst = "%s\n\n\nclass Cls:\n @_poseur_decorator('self', 'a')\n def func(self, a, b): pass" % ( + POSEUR_LINESEP.join(_decorator) % dict(decorator='_poseur_decorator', indentation='\t'.expandtabs(4))).lstrip() + self._check_convert(src, dst) + if __name__ == '__main__': unittest.main()