Skip to content

Positional-only parameters on methods are silently not converted: AttributeError in _process_classdef, exit code 0 #20

Description

@JarryShaw

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions