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, 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.
  • Type-alias preservationtypes.Length stays types.Length rather than expanding to str | float | int.
  • Cross-file imports — base classes and annotation types from other local modules are re-emitted in the .pyi header automatically.
  • 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 with Python 3.11
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 is a pipeline of six focused stages, each in its own module:

generate_stub(filepath)
    │
    ├─ 1. loader      load_module()             load source as a live module
    ├─ 2. imports     scan_import_statements()  parse AST → {name: import_stmt}
    ├─ 3. aliases     build_alias_registry()    discover type-alias sub-modules
    ├─ 4. generator   collect_classes()         gather classes in source order
    │       └─ for each class:
    │           emitter   generate_class_stub()
    │               └─ for each method:
    │                   resolver  resolve_params()     ← MRO backtracing
    │                   emitter   generate_method_stub()
    └─ 5. generator   assemble header + body    → write .pyi

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.

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


Documentation (optional)

# Install documentation dependencies
pip install -e ".[docs]"

# Build the HTML site
cd docs && make html
# → open docs/_build/html/index.html in a browser

# Live-reloading dev server (auto-rebuilds on file changes)
make livehtml

Installation (end users)

pip install stubpy
# or
uv add stubpy

Usage

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

Python API

from stubpy import generate_stub

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

Example

# shapes.py
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)
stubpy shapes.py --print
# shapes.pyi  (generated)
from __future__ import annotations

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: ...

**kwargs in Circle.__init__ resolves to color and opacity from Shape.__init__. Circle.unit detects cls(radius=1.0, **kwargs) via AST — radius is hardcoded so it's excluded; the remaining Shape params appear.


How it works

stubpy is a pipeline of six focused stages, each in its own module:

generate_stub(filepath)
    │
    ├─ 1. loader      load_module()             load source as a live module
    ├─ 2. imports     scan_import_statements()  parse AST → {name: import_stmt}
    ├─ 3. aliases     build_alias_registry()    discover type-alias sub-modules
    ├─ 4. generator   collect_classes()         gather classes in source order
    │       └─ for each class:
    │           emitter   generate_class_stub()
    │               └─ for each method:
    │                   resolver  resolve_params()     ← MRO backtracing
    │                   emitter   generate_method_stub()
    └─ 5. generator   assemble header + body    → write .pyi

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.

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


Documentation

Full documentation at wzjoriv.github.io/stubpy including:


Project layout

stubpy/
├── stubpy/            ← package (stdlib only, no runtime deps)
│   ├── context.py     StubContext, AliasEntry
│   ├── 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)
│   ├── emitter.py     generate_class / method stub
│   └── generator.py   generate_stub orchestrator
├── demo/              demo package used for integration tests
├── tests/             pytest suite (~235 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 © 2024 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.1.1.tar.gz (38.2 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.1.1-py3-none-any.whl (26.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for stubpy-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c24bcea658e5da86dd7b9a74a44c71d3572153c427d0ea760a6fe4da73cef546
MD5 1ef0d00029a6deff8c3fc5dd930d7dc0
BLAKE2b-256 525d8f808f4d208fabbaebf6b19c75c14b8570c72fe601d37e98fa9835ec3a60

See more details on using hashes here.

File details

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

File metadata

  • Download URL: stubpy-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 26.7 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.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b52ff6c8122d194160b500390f5834768cc76f4f3ad183ba0e9ad5bc18517099
MD5 8233acb94e04e844979dd7bed7abe732
BLAKE2b-256 cd71d2555ff7d91d4c5a46924ba7b7fc5fdde96cde9f95df1b915b291428a5ff

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