Summary
poseur crashes on any positional-only parameter declared on a method inside a class, and then exits 0 — so callers that check the exit status (including poseur's own recommended setup.py integration) cannot tell the conversion failed. The positional-only markers survive into the output, which then fails to parse on the very interpreters the conversion exists to support.
Plain module-level functions convert correctly, which is why this has gone unnoticed.
Reproducer
# probe.py
class Thing:
def f(self, a, /) -> None:
self.a = a
$ python -m poseur --no-archive probe.py
Now converting: 'probe.py'
Traceback (most recent call last):
...
File ".../bpc_utils/context.py", line 145, in _process
func(node)
File ".../poseur.py", line 744, in _process_classdef
self._process_suite_node(suite, cls_ctx=name.name)
AttributeError: 'Name' object has no attribute 'name'
$ echo $?
0 # <-- exit status says success
$ cat probe.py # <-- unchanged, marker still there
class Thing:
def f(self, a, /) -> None:
self.a = a
Moving the same function to module level converts fine:
$ printf 'def f(a, b, /, c):\n return a + b + c\n' > ok.py
$ python -m poseur --no-archive ok.py && grep -c ', /' ok.py
0
Reproduced with bpc-poseur 0.4.3.post1 on CPython 3.7.16 with parso 0.6.2, and the same line is present on master today (poseur.py:744).
Root cause
def _process_classdef(self, node: parso.python.tree.Class) -> None:
# <Name: ...>
name = node.name
...
suite = node.children[-1]
self._process_suite_node(suite, cls_ctx=name.name) # <-- here
parso.python.tree.Class.name already returns the Name leaf — the comment on the line above says so. A parso Name exposes .value, not .name, so name.name raises AttributeError.
That the intended value is a plain string is clear from the signature and from what it is used for:
_process_suite_node(self, node, *, cls_ctx: Optional[str] = None) (poseur.py:442)
- it is stored as
self._cls_ctx # type: Optional[str] (poseur.py:438)
- and consumed by
return self.mangle(self._cls_ctx, name) (poseur.py:1076)
So the fix is cls_ctx=name.value.
Why the exit status is 0
bpc_utils' context walker catches the exception per node, so the process finishes normally. Any integration that wraps poseur in subprocess.check_call — which is what the README suggests, and what e.g. PyPCAPKit's setup.py does — therefore sees success and ships unconverted code.
Why the test suite does not catch it
tests/sample.py contains no class statement at all. Every positional-only case in the fixture is a module-level function, a nested function, or a lambda, so the _process_classdef path is never exercised with a positional-only parameter in scope.
Verification of the fix
With cls_ctx=name.name changed to cls_ctx=name.value, all three shapes convert (plain function, method, and a @property/@x.setter pair), and a real-world file — pcapkit/corekit/io.py from PyPCAPKit, which has 12 positional-only parameters spread across class bodies — converts to something CPython 3.7 parses cleanly:
$ python -m walrus --no-archive io.py && python -m poseur --no-archive io.py
$ grep -c ', /)' io.py ; grep -c ':=' io.py
0
0
$ python3.7 -c "import ast; ast.parse(open('io.py').read()); print('parses under 3.7')"
parses under 3.7
Downstream impact
PyPCAPKit declares requires-python = ">=3.6" and relies on bpc-walrus + bpc-poseur in setup.py to convert its 3.8 syntax at install time. Because of this bug the wheel builds successfully on 3.7 and then import pcapkit fails with SyntaxError, so that declared support does not currently work. This is the only blocker we found.
A PR with the one-line fix and a regression test follows.
Summary
poseurcrashes on any positional-only parameter declared on a method inside a class, and then exits 0 — so callers that check the exit status (includingposeur's own recommendedsetup.pyintegration) cannot tell the conversion failed. The positional-only markers survive into the output, which then fails to parse on the very interpreters the conversion exists to support.Plain module-level functions convert correctly, which is why this has gone unnoticed.
Reproducer
Moving the same function to module level converts fine:
Reproduced with
bpc-poseur0.4.3.post1 on CPython 3.7.16 withparso0.6.2, and the same line is present onmastertoday (poseur.py:744).Root cause
parso.python.tree.Class.namealready returns theNameleaf — the comment on the line above says so. A parsoNameexposes.value, not.name, soname.nameraisesAttributeError.That the intended value is a plain string is clear from the signature and from what it is used for:
_process_suite_node(self, node, *, cls_ctx: Optional[str] = None)(poseur.py:442)self._cls_ctx # type: Optional[str](poseur.py:438)return self.mangle(self._cls_ctx, name)(poseur.py:1076)So the fix is
cls_ctx=name.value.Why the exit status is 0
bpc_utils' context walker catches the exception per node, so the process finishes normally. Any integration that wrapsposeurinsubprocess.check_call— which is what the README suggests, and what e.g. PyPCAPKit'ssetup.pydoes — therefore sees success and ships unconverted code.Why the test suite does not catch it
tests/sample.pycontains noclassstatement at all. Every positional-only case in the fixture is a module-level function, a nested function, or a lambda, so the_process_classdefpath is never exercised with a positional-only parameter in scope.Verification of the fix
With
cls_ctx=name.namechanged tocls_ctx=name.value, all three shapes convert (plain function, method, and a@property/@x.setterpair), and a real-world file —pcapkit/corekit/io.pyfrom PyPCAPKit, which has 12 positional-only parameters spread across class bodies — converts to something CPython 3.7 parses cleanly:Downstream impact
PyPCAPKit declares
requires-python = ">=3.6"and relies onbpc-walrus+bpc-poseurinsetup.pyto convert its 3.8 syntax at install time. Because of this bug the wheel builds successfully on 3.7 and thenimport pcapkitfails withSyntaxError, so that declared support does not currently work. This is the only blocker we found.A PR with the one-line fix and a regression test follows.