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.
Features
**kwargsbacktracing — walks the entire MRO to expand**kwargsinto concrete, named parameters at every inheritance level.cls()detection —@classmethodmethods that forward**kwargsintocls(...)are resolved againstcls.__init__, not MRO siblings.- Typed
*argspreserved — explicitly annotated*args(e.g.*elements: Element) always survive the resolution chain. - Type-alias preservation —
types.Lengthstaystypes.Lengthrather than expanding tostr | float | int. - Cross-file imports — base classes and annotation types from other local modules are re-emitted in the
.pyiheader automatically. - Zero runtime dependencies — stdlib only.
Environment setup
Requires Python 3.10+. The steps below use the Windows Python Launcher (
py).
On macOS / Linux replacepy -3.10withpython3.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:
- No variadics — if the method has neither
*argsnor**kwargs, return its own parameters unchanged. cls()detection — if a@classmethodbody containscls(..., **kwargs), the**kwargsis resolved againstcls.__init__via AST analysis. Parameters hardcoded in the call are excluded.- 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:
- No variadics — if the method has neither
*argsnor**kwargs, return its own parameters unchanged. cls()detection — if a@classmethodbody containscls(..., **kwargs), the**kwargsis resolved againstcls.__init__via AST analysis. Parameters hardcoded in the call are excluded.- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c24bcea658e5da86dd7b9a74a44c71d3572153c427d0ea760a6fe4da73cef546
|
|
| MD5 |
1ef0d00029a6deff8c3fc5dd930d7dc0
|
|
| BLAKE2b-256 |
525d8f808f4d208fabbaebf6b19c75c14b8570c72fe601d37e98fa9835ec3a60
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b52ff6c8122d194160b500390f5834768cc76f4f3ad183ba0e9ad5bc18517099
|
|
| MD5 |
8233acb94e04e844979dd7bed7abe732
|
|
| BLAKE2b-256 |
cd71d2555ff7d91d4c5a46924ba7b7fc5fdde96cde9f95df1b915b291428a5ff
|