stubpy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stubpy-0.1.0/LICENSE +21 -0
- stubpy-0.1.0/PKG-INFO +274 -0
- stubpy-0.1.0/README.md +238 -0
- stubpy-0.1.0/pyproject.toml +74 -0
- stubpy-0.1.0/setup.cfg +4 -0
- stubpy-0.1.0/stubpy/__init__.py +38 -0
- stubpy-0.1.0/stubpy/__main__.py +81 -0
- stubpy-0.1.0/stubpy/aliases.py +120 -0
- stubpy-0.1.0/stubpy/annotations.py +364 -0
- stubpy-0.1.0/stubpy/context.py +139 -0
- stubpy-0.1.0/stubpy/emitter.py +311 -0
- stubpy-0.1.0/stubpy/generator.py +162 -0
- stubpy-0.1.0/stubpy/imports.py +191 -0
- stubpy-0.1.0/stubpy/loader.py +86 -0
- stubpy-0.1.0/stubpy/resolver.py +362 -0
- stubpy-0.1.0/stubpy.egg-info/PKG-INFO +274 -0
- stubpy-0.1.0/stubpy.egg-info/SOURCES.txt +25 -0
- stubpy-0.1.0/stubpy.egg-info/dependency_links.txt +1 -0
- stubpy-0.1.0/stubpy.egg-info/entry_points.txt +2 -0
- stubpy-0.1.0/stubpy.egg-info/requires.txt +10 -0
- stubpy-0.1.0/stubpy.egg-info/top_level.txt +1 -0
- stubpy-0.1.0/tests/test_aliases.py +176 -0
- stubpy-0.1.0/tests/test_annotations.py +248 -0
- stubpy-0.1.0/tests/test_emitter.py +337 -0
- stubpy-0.1.0/tests/test_imports.py +200 -0
- stubpy-0.1.0/tests/test_integration.py +635 -0
- stubpy-0.1.0/tests/test_resolver.py +385 -0
stubpy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Josue N Rivera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
stubpy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stubpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate .pyi stub files with full **kwargs / *args MRO backtracing
|
|
5
|
+
Author-email: Josue N Rivera <josue.n.rivera@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wzjoriv/stubpy
|
|
8
|
+
Project-URL: Repository, https://github.com/wzjoriv/stubpy
|
|
9
|
+
Project-URL: Documentation, https://wzjoriv.github.io/stubpy
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/wzjoriv/stubpy/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/wzjoriv/stubpy/blob/main/docs/changelog.rst
|
|
12
|
+
Keywords: stubs,type hints,pyi,static analysis,mypy,pyright
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: sphinx>=7.0; extra == "docs"
|
|
32
|
+
Requires-Dist: furo>=2024.1.29; extra == "docs"
|
|
33
|
+
Requires-Dist: myst-parser>=2.0; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-autobuild; extra == "docs"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# stubpy
|
|
38
|
+
|
|
39
|
+
Generate `.pyi` stub files for Python modules with full `**kwargs` / `*args` MRO backtracing, type-alias preservation, and cross-file import resolution.
|
|
40
|
+
|
|
41
|
+
[](https://pypi.org/project/stubpy/)
|
|
42
|
+
[](https://pypi.org/project/stubpy/)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **`**kwargs` backtracing** — walks the entire MRO to expand `**kwargs` into concrete, named parameters at every inheritance level.
|
|
48
|
+
- **`cls()` detection** — `@classmethod` methods that forward `**kwargs` into `cls(...)` are resolved against `cls.__init__`, not MRO siblings.
|
|
49
|
+
- **Typed `*args` preserved** — explicitly annotated `*args` (e.g. `*elements: Element`) always survive the resolution chain.
|
|
50
|
+
- **Type-alias preservation** — `types.Length` stays `types.Length` rather than expanding to `str | float | int`.
|
|
51
|
+
- **Cross-file imports** — base classes and annotation types from other local modules are re-emitted in the `.pyi` header automatically.
|
|
52
|
+
- **Zero runtime dependencies** — stdlib only.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Environment setup
|
|
57
|
+
|
|
58
|
+
> Requires **Python 3.10+**. The steps below use the Windows Python Launcher (`py`).
|
|
59
|
+
> On macOS / Linux replace `py -3.10` with `python3.11`.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 1. Clone the repository
|
|
63
|
+
git clone https://github.com/wzjoriv/stubpy.git
|
|
64
|
+
cd stubpy
|
|
65
|
+
|
|
66
|
+
# 2. Create a virtual environment with Python 3.11
|
|
67
|
+
py -3.11 -m venv .venv
|
|
68
|
+
|
|
69
|
+
# 3. Activate the environment
|
|
70
|
+
.venv\Scripts\activate # Windows CMD / PowerShell
|
|
71
|
+
# source .venv/bin/activate # macOS / Linux
|
|
72
|
+
|
|
73
|
+
# 4. Install in editable mode with development dependencies
|
|
74
|
+
pip install -e ".[dev]"
|
|
75
|
+
|
|
76
|
+
# 5. Verify — run the full test suite
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+
stubpy is a pipeline of six focused stages, each in its own module:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
generate_stub(filepath)
|
|
87
|
+
│
|
|
88
|
+
├─ 1. loader load_module() load source as a live module
|
|
89
|
+
├─ 2. imports scan_import_statements() parse AST → {name: import_stmt}
|
|
90
|
+
├─ 3. aliases build_alias_registry() discover type-alias sub-modules
|
|
91
|
+
├─ 4. generator collect_classes() gather classes in source order
|
|
92
|
+
│ └─ for each class:
|
|
93
|
+
│ emitter generate_class_stub()
|
|
94
|
+
│ └─ for each method:
|
|
95
|
+
│ resolver resolve_params() ← MRO backtracing
|
|
96
|
+
│ emitter generate_method_stub()
|
|
97
|
+
└─ 5. generator assemble header + body → write .pyi
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**`resolve_params` uses three strategies in order:**
|
|
101
|
+
|
|
102
|
+
1. **No variadics** — if the method has neither `*args` nor `**kwargs`, return its own parameters unchanged.
|
|
103
|
+
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.
|
|
104
|
+
3. **MRO walk** — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved.
|
|
105
|
+
|
|
106
|
+
**`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.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
## Documentation (optional)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Install documentation dependencies
|
|
113
|
+
pip install -e ".[docs]"
|
|
114
|
+
|
|
115
|
+
# Build the HTML site
|
|
116
|
+
cd docs && make html
|
|
117
|
+
# → open docs/_build/html/index.html in a browser
|
|
118
|
+
|
|
119
|
+
# Live-reloading dev server (auto-rebuilds on file changes)
|
|
120
|
+
make livehtml
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Installation (end users)
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install stubpy
|
|
129
|
+
# or
|
|
130
|
+
uv add stubpy
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Usage
|
|
136
|
+
|
|
137
|
+
### CLI
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
stubpy path/to/module.py # writes module.pyi alongside source
|
|
141
|
+
stubpy path/to/module.py -o out/module.pyi # custom output path
|
|
142
|
+
stubpy path/to/module.py --print # also print to stdout
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Python API
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from stubpy import generate_stub
|
|
149
|
+
|
|
150
|
+
content = generate_stub("path/to/module.py")
|
|
151
|
+
content = generate_stub("path/to/module.py", "out/module.pyi")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Example
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# shapes.py
|
|
160
|
+
class Shape:
|
|
161
|
+
def __init__(self, color: str = "black", opacity: float = 1.0) -> None: ...
|
|
162
|
+
|
|
163
|
+
class Circle(Shape):
|
|
164
|
+
def __init__(self, radius: float, **kwargs) -> None:
|
|
165
|
+
super().__init__(**kwargs)
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def unit(cls, **kwargs) -> "Circle":
|
|
169
|
+
return cls(radius=1.0, **kwargs)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
stubpy shapes.py --print
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# shapes.pyi (generated)
|
|
178
|
+
from __future__ import annotations
|
|
179
|
+
|
|
180
|
+
class Shape:
|
|
181
|
+
def __init__(self, color: str = 'black', opacity: float = 1.0) -> None: ...
|
|
182
|
+
|
|
183
|
+
class Circle(Shape):
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
radius: float,
|
|
187
|
+
color: str = 'black',
|
|
188
|
+
opacity: float = 1.0,
|
|
189
|
+
) -> None: ...
|
|
190
|
+
@classmethod
|
|
191
|
+
def unit(cls, color: str = 'black', opacity: float = 1.0) -> Circle: ...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`**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.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
## How it works
|
|
200
|
+
|
|
201
|
+
stubpy is a pipeline of six focused stages, each in its own module:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
generate_stub(filepath)
|
|
205
|
+
│
|
|
206
|
+
├─ 1. loader load_module() load source as a live module
|
|
207
|
+
├─ 2. imports scan_import_statements() parse AST → {name: import_stmt}
|
|
208
|
+
├─ 3. aliases build_alias_registry() discover type-alias sub-modules
|
|
209
|
+
├─ 4. generator collect_classes() gather classes in source order
|
|
210
|
+
│ └─ for each class:
|
|
211
|
+
│ emitter generate_class_stub()
|
|
212
|
+
│ └─ for each method:
|
|
213
|
+
│ resolver resolve_params() ← MRO backtracing
|
|
214
|
+
│ emitter generate_method_stub()
|
|
215
|
+
└─ 5. generator assemble header + body → write .pyi
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**`resolve_params` uses three strategies in order:**
|
|
219
|
+
|
|
220
|
+
1. **No variadics** — if the method has neither `*args` nor `**kwargs`, return its own parameters unchanged.
|
|
221
|
+
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.
|
|
222
|
+
3. **MRO walk** — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved.
|
|
223
|
+
|
|
224
|
+
**`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.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
## Documentation
|
|
228
|
+
|
|
229
|
+
Full documentation at **[wzjoriv.github.io/stubpy](https://wzjoriv.github.io/stubpy)** including:
|
|
230
|
+
|
|
231
|
+
- [Getting Started](https://wzjoriv.github.io/stubpy/guides/quickstart.html)
|
|
232
|
+
- [How It Works](https://wzjoriv.github.io/stubpy/guides/how_it_works.html)
|
|
233
|
+
- [API Reference](https://wzjoriv.github.io/stubpy/api/index.html)
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Project layout
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
stubpy/
|
|
241
|
+
├── stubpy/ ← package (stdlib only, no runtime deps)
|
|
242
|
+
│ ├── context.py StubContext, AliasEntry
|
|
243
|
+
│ ├── loader.py load_module
|
|
244
|
+
│ ├── aliases.py build_alias_registry
|
|
245
|
+
│ ├── imports.py scan / collect imports
|
|
246
|
+
│ ├── annotations.py dispatch-table annotation_to_str
|
|
247
|
+
│ ├── resolver.py resolve_params (3 strategies)
|
|
248
|
+
│ ├── emitter.py generate_class / method stub
|
|
249
|
+
│ └── generator.py generate_stub orchestrator
|
|
250
|
+
├── demo/ demo package used for integration tests
|
|
251
|
+
├── tests/ pytest suite (~162 tests)
|
|
252
|
+
├── docs/ Sphinx + Furo documentation
|
|
253
|
+
├── LICENSE
|
|
254
|
+
└── pyproject.toml
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Contributing
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
git clone https://github.com/wzjoriv/stubpy.git
|
|
263
|
+
cd stubpy
|
|
264
|
+
py -3.11 -m venv .venv
|
|
265
|
+
.venv\Scripts\activate
|
|
266
|
+
pip install -e ".[dev]"
|
|
267
|
+
pytest
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
[MIT](LICENSE) © 2024 [Josue N Rivera](https://github.com/wzjoriv)
|
stubpy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# stubpy
|
|
2
|
+
|
|
3
|
+
Generate `.pyi` stub files for Python modules with full `**kwargs` / `*args` MRO backtracing, type-alias preservation, and cross-file import resolution.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/stubpy/)
|
|
6
|
+
[](https://pypi.org/project/stubpy/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **`**kwargs` backtracing** — walks the entire MRO to expand `**kwargs` into concrete, named parameters at every inheritance level.
|
|
12
|
+
- **`cls()` detection** — `@classmethod` methods that forward `**kwargs` into `cls(...)` are resolved against `cls.__init__`, not MRO siblings.
|
|
13
|
+
- **Typed `*args` preserved** — explicitly annotated `*args` (e.g. `*elements: Element`) always survive the resolution chain.
|
|
14
|
+
- **Type-alias preservation** — `types.Length` stays `types.Length` rather than expanding to `str | float | int`.
|
|
15
|
+
- **Cross-file imports** — base classes and annotation types from other local modules are re-emitted in the `.pyi` header automatically.
|
|
16
|
+
- **Zero runtime dependencies** — stdlib only.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Environment setup
|
|
21
|
+
|
|
22
|
+
> Requires **Python 3.10+**. The steps below use the Windows Python Launcher (`py`).
|
|
23
|
+
> On macOS / Linux replace `py -3.10` with `python3.11`.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. Clone the repository
|
|
27
|
+
git clone https://github.com/wzjoriv/stubpy.git
|
|
28
|
+
cd stubpy
|
|
29
|
+
|
|
30
|
+
# 2. Create a virtual environment with Python 3.11
|
|
31
|
+
py -3.11 -m venv .venv
|
|
32
|
+
|
|
33
|
+
# 3. Activate the environment
|
|
34
|
+
.venv\Scripts\activate # Windows CMD / PowerShell
|
|
35
|
+
# source .venv/bin/activate # macOS / Linux
|
|
36
|
+
|
|
37
|
+
# 4. Install in editable mode with development dependencies
|
|
38
|
+
pip install -e ".[dev]"
|
|
39
|
+
|
|
40
|
+
# 5. Verify — run the full test suite
|
|
41
|
+
pytest
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
stubpy is a pipeline of six focused stages, each in its own module:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
generate_stub(filepath)
|
|
51
|
+
│
|
|
52
|
+
├─ 1. loader load_module() load source as a live module
|
|
53
|
+
├─ 2. imports scan_import_statements() parse AST → {name: import_stmt}
|
|
54
|
+
├─ 3. aliases build_alias_registry() discover type-alias sub-modules
|
|
55
|
+
├─ 4. generator collect_classes() gather classes in source order
|
|
56
|
+
│ └─ for each class:
|
|
57
|
+
│ emitter generate_class_stub()
|
|
58
|
+
│ └─ for each method:
|
|
59
|
+
│ resolver resolve_params() ← MRO backtracing
|
|
60
|
+
│ emitter generate_method_stub()
|
|
61
|
+
└─ 5. generator assemble header + body → write .pyi
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**`resolve_params` uses three strategies in order:**
|
|
65
|
+
|
|
66
|
+
1. **No variadics** — if the method has neither `*args` nor `**kwargs`, return its own parameters unchanged.
|
|
67
|
+
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.
|
|
68
|
+
3. **MRO walk** — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved.
|
|
69
|
+
|
|
70
|
+
**`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.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
## Documentation (optional)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Install documentation dependencies
|
|
77
|
+
pip install -e ".[docs]"
|
|
78
|
+
|
|
79
|
+
# Build the HTML site
|
|
80
|
+
cd docs && make html
|
|
81
|
+
# → open docs/_build/html/index.html in a browser
|
|
82
|
+
|
|
83
|
+
# Live-reloading dev server (auto-rebuilds on file changes)
|
|
84
|
+
make livehtml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Installation (end users)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install stubpy
|
|
93
|
+
# or
|
|
94
|
+
uv add stubpy
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
### CLI
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
stubpy path/to/module.py # writes module.pyi alongside source
|
|
105
|
+
stubpy path/to/module.py -o out/module.pyi # custom output path
|
|
106
|
+
stubpy path/to/module.py --print # also print to stdout
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Python API
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from stubpy import generate_stub
|
|
113
|
+
|
|
114
|
+
content = generate_stub("path/to/module.py")
|
|
115
|
+
content = generate_stub("path/to/module.py", "out/module.pyi")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Example
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
# shapes.py
|
|
124
|
+
class Shape:
|
|
125
|
+
def __init__(self, color: str = "black", opacity: float = 1.0) -> None: ...
|
|
126
|
+
|
|
127
|
+
class Circle(Shape):
|
|
128
|
+
def __init__(self, radius: float, **kwargs) -> None:
|
|
129
|
+
super().__init__(**kwargs)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def unit(cls, **kwargs) -> "Circle":
|
|
133
|
+
return cls(radius=1.0, **kwargs)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
stubpy shapes.py --print
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# shapes.pyi (generated)
|
|
142
|
+
from __future__ import annotations
|
|
143
|
+
|
|
144
|
+
class Shape:
|
|
145
|
+
def __init__(self, color: str = 'black', opacity: float = 1.0) -> None: ...
|
|
146
|
+
|
|
147
|
+
class Circle(Shape):
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
radius: float,
|
|
151
|
+
color: str = 'black',
|
|
152
|
+
opacity: float = 1.0,
|
|
153
|
+
) -> None: ...
|
|
154
|
+
@classmethod
|
|
155
|
+
def unit(cls, color: str = 'black', opacity: float = 1.0) -> Circle: ...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`**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.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
## How it works
|
|
164
|
+
|
|
165
|
+
stubpy is a pipeline of six focused stages, each in its own module:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
generate_stub(filepath)
|
|
169
|
+
│
|
|
170
|
+
├─ 1. loader load_module() load source as a live module
|
|
171
|
+
├─ 2. imports scan_import_statements() parse AST → {name: import_stmt}
|
|
172
|
+
├─ 3. aliases build_alias_registry() discover type-alias sub-modules
|
|
173
|
+
├─ 4. generator collect_classes() gather classes in source order
|
|
174
|
+
│ └─ for each class:
|
|
175
|
+
│ emitter generate_class_stub()
|
|
176
|
+
│ └─ for each method:
|
|
177
|
+
│ resolver resolve_params() ← MRO backtracing
|
|
178
|
+
│ emitter generate_method_stub()
|
|
179
|
+
└─ 5. generator assemble header + body → write .pyi
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**`resolve_params` uses three strategies in order:**
|
|
183
|
+
|
|
184
|
+
1. **No variadics** — if the method has neither `*args` nor `**kwargs`, return its own parameters unchanged.
|
|
185
|
+
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.
|
|
186
|
+
3. **MRO walk** — walk ancestor classes that define the same method, collecting concrete parameters until all variadics are fully resolved.
|
|
187
|
+
|
|
188
|
+
**`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.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
## Documentation
|
|
192
|
+
|
|
193
|
+
Full documentation at **[wzjoriv.github.io/stubpy](https://wzjoriv.github.io/stubpy)** including:
|
|
194
|
+
|
|
195
|
+
- [Getting Started](https://wzjoriv.github.io/stubpy/guides/quickstart.html)
|
|
196
|
+
- [How It Works](https://wzjoriv.github.io/stubpy/guides/how_it_works.html)
|
|
197
|
+
- [API Reference](https://wzjoriv.github.io/stubpy/api/index.html)
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Project layout
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
stubpy/
|
|
205
|
+
├── stubpy/ ← package (stdlib only, no runtime deps)
|
|
206
|
+
│ ├── context.py StubContext, AliasEntry
|
|
207
|
+
│ ├── loader.py load_module
|
|
208
|
+
│ ├── aliases.py build_alias_registry
|
|
209
|
+
│ ├── imports.py scan / collect imports
|
|
210
|
+
│ ├── annotations.py dispatch-table annotation_to_str
|
|
211
|
+
│ ├── resolver.py resolve_params (3 strategies)
|
|
212
|
+
│ ├── emitter.py generate_class / method stub
|
|
213
|
+
│ └── generator.py generate_stub orchestrator
|
|
214
|
+
├── demo/ demo package used for integration tests
|
|
215
|
+
├── tests/ pytest suite (~162 tests)
|
|
216
|
+
├── docs/ Sphinx + Furo documentation
|
|
217
|
+
├── LICENSE
|
|
218
|
+
└── pyproject.toml
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
git clone https://github.com/wzjoriv/stubpy.git
|
|
227
|
+
cd stubpy
|
|
228
|
+
py -3.11 -m venv .venv
|
|
229
|
+
.venv\Scripts\activate
|
|
230
|
+
pip install -e ".[dev]"
|
|
231
|
+
pytest
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
[MIT](LICENSE) © 2024 [Josue N Rivera](https://github.com/wzjoriv)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "stubpy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generate .pyi stub files with full **kwargs / *args MRO backtracing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Josue N Rivera", email = "josue.n.rivera@outlook.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["stubs", "type hints", "pyi", "static analysis", "mypy", "pyright"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Code Generators",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
# No runtime dependencies — stdlib only.
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/wzjoriv/stubpy"
|
|
34
|
+
Repository = "https://github.com/wzjoriv/stubpy"
|
|
35
|
+
Documentation = "https://wzjoriv.github.io/stubpy"
|
|
36
|
+
"Bug Tracker" = "https://github.com/wzjoriv/stubpy/issues"
|
|
37
|
+
Changelog = "https://github.com/wzjoriv/stubpy/blob/main/docs/changelog.rst"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"pytest-cov",
|
|
43
|
+
]
|
|
44
|
+
docs = [
|
|
45
|
+
"sphinx>=7.0",
|
|
46
|
+
"furo>=2024.1.29",
|
|
47
|
+
"myst-parser>=2.0",
|
|
48
|
+
"sphinx-autobuild",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
stubpy = "stubpy.__main__:main"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.packages.find]
|
|
55
|
+
where = ["."]
|
|
56
|
+
include = ["stubpy*"]
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# pytest
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
testpaths = ["tests"]
|
|
63
|
+
addopts = "-v --tb=short"
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# coverage
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
[tool.coverage.run]
|
|
69
|
+
source = ["stubpy"]
|
|
70
|
+
omit = ["tests/*", "demo/*"]
|
|
71
|
+
|
|
72
|
+
[tool.coverage.report]
|
|
73
|
+
show_missing = true
|
|
74
|
+
skip_covered = false
|
stubpy-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
stubpy
|
|
3
|
+
======
|
|
4
|
+
|
|
5
|
+
Generate ``.pyi`` stub files for Python modules with full ``**kwargs``
|
|
6
|
+
and ``*args`` MRO backtracing.
|
|
7
|
+
|
|
8
|
+
Quickstart::
|
|
9
|
+
|
|
10
|
+
from stubpy import generate_stub
|
|
11
|
+
|
|
12
|
+
# Write stub alongside the source and return its content
|
|
13
|
+
content = generate_stub("path/to/module.py")
|
|
14
|
+
|
|
15
|
+
# Write to a custom path
|
|
16
|
+
content = generate_stub("path/to/module.py", "out/module.pyi")
|
|
17
|
+
|
|
18
|
+
CLI::
|
|
19
|
+
|
|
20
|
+
stubpy path/to/module.py
|
|
21
|
+
stubpy path/to/module.py -o out/module.pyi --print
|
|
22
|
+
|
|
23
|
+
Public API
|
|
24
|
+
----------
|
|
25
|
+
|
|
26
|
+
.. currentmodule:: stubpy
|
|
27
|
+
|
|
28
|
+
.. autosummary::
|
|
29
|
+
|
|
30
|
+
generate_stub
|
|
31
|
+
StubContext
|
|
32
|
+
AliasEntry
|
|
33
|
+
"""
|
|
34
|
+
from .context import AliasEntry, StubContext
|
|
35
|
+
from .generator import generate_stub
|
|
36
|
+
|
|
37
|
+
__all__ = ["generate_stub", "StubContext", "AliasEntry"]
|
|
38
|
+
__version__ = "0.1.0"
|