Skip to main content

Generate .pyi stub files with full **kwargs / *args MRO backtracing

Project description

stubpy

Generate .pyi stub files for Python modules with full **kwargs / *args MRO backtracing, type-alias preservation, generic support, overload stubs, and cross-file import resolution.

PyPI Python License: MIT

Features

  • **kwargs backtracing — walks the entire MRO to expand **kwargs into concrete, named parameters at every inheritance level.
  • cls() detection@classmethod methods that forward **kwargs into cls(...) are resolved against cls.__init__, not MRO siblings.
  • Typed *args preserved — explicitly annotated *args (e.g. *elements: Element) always survive the resolution chain.
  • Positional-only / separatordef f(a, b, /, c) stubs correctly emit the PEP 570 / separator. Parent positional-only params absorbed by **kwargs are promoted to POSITIONAL_OR_KEYWORD to keep the child stub valid.
  • TypeVar / TypeAlias / NewType / ParamSpec / TypeVarTuple — declarations are re-emitted verbatim from the AST pre-pass, preserving bounds and constraints.
  • Generic base classesclass Stack(Generic[T]): is preserved correctly using __orig_bases__ (PEP 560); __bases__ erases the subscript.
  • @overload stubs — each @overload variant gets its own stub; the concrete implementation is suppressed per PEP 484.
  • Type-alias preservationtypes.Length stays types.Length rather than expanding to str | float | int. Works inside Optional[...], tuple[...], list[...], and Union[..., None] too.
  • Cross-file imports — base classes and annotation types from other local modules are re-emitted in the .pyi header automatically.
  • AST pre-pass — a read-only AST harvest runs before (or instead of) module execution, recovering alias names that Python's typing.Union would otherwise flatten.
  • Execution modesRUNTIME (default), AST_ONLY (no module execution), and AUTO (try runtime, fall back to AST-only on error).
  • Structured diagnostics — every pipeline stage records INFO, WARNING, and ERROR entries in a DiagnosticCollector rather than swallowing exceptions silently.
  • Unified symbol table — classes, functions, variables, type aliases, and overload groups are all represented as typed StubSymbol entries in a SymbolTable.
  • Dynamic typing importsfrom typing import ... header is built by scanning typing.__all__ with word-boundary matching, not a static list.
  • Zero runtime dependencies — stdlib only.

Environment setup

Requires Python 3.10+. The steps below use the Windows Python Launcher (py).
On macOS / Linux replace py -3.10 with python3.10.

# 1. Clone the repository
git clone https://github.com/wzjoriv/stubpy.git
cd stubpy

# 2. Create a virtual environment
py -3.11 -m venv .venv

# 3. Activate the environment
.venv\Scripts\activate          # Windows CMD / PowerShell
# source .venv/bin/activate     # macOS / Linux

# 4. Install in editable mode with development dependencies
pip install -e ".[dev]"

# 5. Verify — run the full test suite
pytest

How it works

stubpy runs an eight-stage pipeline, each stage in its own module:

generate_stub(filepath)
    │
    ├─ 1. loader      load_module()              load source as a live module
    │        └─ (skipped in AST_ONLY; warning+fallback in AUTO)
    ├─ 2. ast_pass    ast_harvest()              read-only AST pre-pass
    ├─ 3. imports     scan_import_statements()   parse AST → {name: import_stmt}
    ├─ 4. aliases     build_alias_registry()     discover type-alias sub-modules
    ├─ 5. symbols     build_symbol_table()       merge AST + runtime → SymbolTable
    ├─ 6. emitter     for each symbol (source order):
    │       ├─ AliasSymbol    → generate_alias_stub()
    │       ├─ ClassSymbol    → generate_class_stub()
    │       │       └─ for each method:
    │       │           resolver  resolve_params()        ← MRO backtracing
    │       │           emitter   generate_method_stub()  ← raw AST annotations
    │       ├─ OverloadGroup → generate_overload_group_stub()
    │       ├─ FunctionSymbol → generate_function_stub()
    │       └─ VariableSymbol → generate_variable_stub()
    ├─ 7. imports     collect_typing_imports()   assemble header
    │                 collect_special_imports()
    │                 collect_cross_imports()
    └─ 8. write       .pyi file written to disk

resolve_params uses three strategies in order:

  1. No variadics — if the method has neither *args nor **kwargs, return its own parameters unchanged.
  2. cls() detection — if a @classmethod body contains cls(..., **kwargs), the **kwargs is resolved against cls.__init__ via AST analysis. Parameters hardcoded in the call are excluded.
  3. MRO walk — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved. Parent POSITIONAL_ONLY params absorbed by **kwargs are promoted to POSITIONAL_OR_KEYWORD.

StubContext carries all mutable state for one run. A fresh instance is created per generate_stub() call, making the generator fully re-entrant.


CLI

stubpy path/to/module.py                    # writes module.pyi alongside source
stubpy path/to/module.py -o out/module.pyi  # custom output path
stubpy path/to/module.py --print            # also print to stdout
stubpy path/to/module.py --include-private  # include _private symbols
stubpy path/to/module.py --verbose          # print all diagnostics to stderr
stubpy path/to/module.py --strict           # exit 1 if any ERROR diagnostic

Python API

from stubpy import generate_stub

content = generate_stub("path/to/module.py")
content = generate_stub("path/to/module.py", "out/module.pyi")

Custom configuration

from stubpy import generate_stub
from stubpy import StubContext, StubConfig, ExecutionMode

# AST-only mode — no module execution
ctx = StubContext(config=StubConfig(execution_mode=ExecutionMode.AST_ONLY))
content = generate_stub("path/to/module.py", ctx=ctx)

# Strict mode — exit 1 on any ERROR diagnostic
ctx = StubContext(config=StubConfig(strict=True, verbose=True))
content = generate_stub("path/to/module.py", ctx=ctx)
if ctx.diagnostics.has_errors():
    raise SystemExit(1)

Extended public API

# Diagnostics
from stubpy import DiagnosticCollector, DiagnosticLevel, DiagnosticStage, Diagnostic

# AST pre-pass
from stubpy import ast_harvest, ASTSymbols

# Symbol table
from stubpy import (
    SymbolTable, SymbolKind,
    ClassSymbol, FunctionSymbol, VariableSymbol, AliasSymbol, OverloadGroup,
    build_symbol_table,
)

# Emitters (usable for custom stub generation)
from stubpy import (
    generate_class_stub,
    generate_function_stub,
    generate_variable_stub,
    generate_alias_stub,
    generate_overload_group_stub,
)

Documentation (optional)

pip install -e ".[docs]"
cd docs && make html
# → open docs/_build/html/index.html in a browser

Installation (end users)

pip install stubpy
# or
uv add stubpy

Example

# shapes.py
from typing import TypeVar, Generic, overload

T = TypeVar("T")

class Shape:
    def __init__(self, color: str = "black", opacity: float = 1.0) -> None: ...

class Circle(Shape):
    def __init__(self, radius: float, **kwargs) -> None:
        super().__init__(**kwargs)

    @classmethod
    def unit(cls, **kwargs) -> "Circle":
        return cls(radius=1.0, **kwargs)

class Box(Generic[T]):
    def put(self, item: T) -> None: ...
    def get(self) -> T: ...

@overload
def parse(x: int) -> int: ...
@overload
def parse(x: str) -> str: ...
def parse(x):
    return x
stubpy shapes.py --print
# shapes.pyi  (generated)
from __future__ import annotations
from typing import Generic, TypeVar, overload

T = TypeVar('T')

class Shape:
    def __init__(self, color: str = 'black', opacity: float = 1.0) -> None: ...

class Circle(Shape):
    def __init__(
        self,
        radius: float,
        color: str = 'black',
        opacity: float = 1.0,
    ) -> None: ...
    @classmethod
    def unit(cls, color: str = 'black', opacity: float = 1.0) -> Circle: ...

class Box(Generic[T]):
    def put(self, item: T) -> None: ...
    def get(self) -> T: ...

@overload
def parse(x: int) -> int: ...

@overload
def parse(x: str) -> str: ...

Project layout

stubpy/
├── stubpy/             ← package (stdlib only, no runtime deps)
│   ├── context.py      StubContext, AliasEntry, StubConfig, ExecutionMode
│   ├── diagnostics.py  DiagnosticCollector, Diagnostic
│   ├── ast_pass.py     ast_harvest, ASTSymbols
│   ├── symbols.py      SymbolTable, StubSymbol hierarchy
│   ├── loader.py       load_module
│   ├── aliases.py      build_alias_registry
│   ├── imports.py      scan / collect imports
│   ├── annotations.py  dispatch-table annotation_to_str
│   ├── resolver.py     resolve_params (3 strategies + pos-only normalisation)
│   ├── emitter.py      generate_class / method / function / alias / overload stubs
│   └── generator.py    generate_stub orchestrator (8-stage pipeline)
├── demo/               demo package used for integration tests
├── tests/              pytest suite (670+ tests)
├── docs/               Sphinx + Furo documentation
├── LICENSE
└── pyproject.toml

Contributing

git clone https://github.com/wzjoriv/stubpy.git
cd stubpy
py -3.11 -m venv .venv
.venv\Scripts\activate
pip install -e ".[dev]"
pytest

License

MIT © 2026 Josue N Rivera

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stubpy-0.4.0.tar.gz (90.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stubpy-0.4.0-py3-none-any.whl (51.8 kB view details)

Uploaded Python 3

File details

Details for the file stubpy-0.4.0.tar.gz.

File metadata

  • Download URL: stubpy-0.4.0.tar.gz
  • Upload date:
  • Size: 90.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for stubpy-0.4.0.tar.gz
Algorithm Hash digest
SHA256 b171b20f067dd7bcf92b10c990061e1e3e59f569387c916e49e767c922e680bd
MD5 3de9075f9303d7a583df9ac7d1b2a9ad
BLAKE2b-256 31c9d610958a831ac5ccaef678716fbbfed4a2ab98100a480dd761c55a2ca290

See more details on using hashes here.

File details

Details for the file stubpy-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: stubpy-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 51.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for stubpy-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8a8ea8b6f36fe098217076c760795b900ae384e623404262537fb6e024fffc2b
MD5 48ef08b5651a9d295d1d762b6cc10772
BLAKE2b-256 dbd94c555d2caf60bf039e496a744141c7c904d15210bc03d611b1152ed63e74

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page