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

# 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 (~162 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.0.tar.gz (36.9 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.0-py3-none-any.whl (26.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: stubpy-0.1.0.tar.gz
  • Upload date:
  • Size: 36.9 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.0.tar.gz
Algorithm Hash digest
SHA256 833c2ad591b029ec1d927d97813b322df932265627a5b5c9603a0df39ff8fca0
MD5 8a3e9731e9a23f314ffff5bdfe098730
BLAKE2b-256 3338e918c84df301f4e2a56bff5dba0de556dd8d7149d68a9c8f1064d1adbecb

See more details on using hashes here.

File details

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

File metadata

  • Download URL: stubpy-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 26.5 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 09d07def66a89e97fdd9548e9a0dc570c84d93b3f8cf531051a5729b0fdd99fb
MD5 9b7f4d6d973b8e122c721e292f1018d2
BLAKE2b-256 8afc5eed5f62d1b4219b36e94def03a8402b05130192ec0d83bc668935956d06

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