Skip to content

Mark all arguments as positional-only except those with default values #19

@Remco646

Description

@Remco646

Currently, in the PySide6 stub files (.pyi), all function parameters are marked as normal positional-or-keyword arguments. In practice, however, non-default arguments cannot be passed by keyword at all – doing so always raises runtime errors like "no matching signature" or "not enough arguments".

This behavior is consistent with PYSIDE-1964, where it is explained that C++ does not support keyword arguments, and only default parameters can be used that way. The .pyi stubs are therefore misleading for both type checkers and developers.

A proposed solution would be to update all stubs so that parameters without default values are explicitly marked as positional-only using the Python 3.8+ / syntax. I have written a LibCST-based script that can automatically refactor the stubs accordingly, while preserving formatting and comments.

This would make the stubs consistent with actual PySide6 behavior and prevent confusing errors when users try to call functions with keyword arguments that aren’t supported.

import sys
import libcst as cst
from pathlib import Path


class AddPosOnlyTransformer(cst.CSTTransformer):

    def leave_FunctionDef(self, original: cst.FunctionDef, updated: cst.FunctionDef) -> cst.FunctionDef:
        p: cst.Parameters = updated.params

        if hasattr(p, "posonly_params") and len(p.posonly_params) > 0:
            return updated

        if not p.params:
            return updated

        last_required_idx = -1
        for i, prm in enumerate(p.params):
            if isinstance(prm, cst.Param) and prm.default is None:
                last_required_idx = i

        if last_required_idx == -1:
            return updated

        leading_required = list(p.params[: last_required_idx + 1])
        remaining_params = list(p.params[last_required_idx + 1 :])

        new_params = p.with_changes(
            posonly_params=list(p.posonly_params) + leading_required,
            params=remaining_params,
        )

        return updated.with_changes(params=new_params)


def process_file(infile: str, outfile: str | None = None) -> None:
    with open(infile, encoding="utf8") as f:
        src = f.read()

    mod = cst.parse_module(src)
    new_mod = mod.visit(AddPosOnlyTransformer())
    new_code = new_mod.code

    if outfile is None:
        outfile = infile
    with open(outfile, "w", encoding="utf8") as f:
        f.write(new_code)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python make_positional.py input.pyi [output.pyi]")
        sys.exit(1)

    infile = Path(sys.argv[1])
    outfile = Path(sys.argv[2]) if len(sys.argv) > 2 else infile

    process_file(infile, outfile)

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