pep810 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.
- pep810-0.1.0/LICENSE +14 -0
- pep810-0.1.0/PKG-INFO +270 -0
- pep810-0.1.0/README.md +248 -0
- pep810-0.1.0/pyproject.toml +38 -0
- pep810-0.1.0/setup.cfg +4 -0
- pep810-0.1.0/src/pep810/__init__.py +86 -0
- pep810-0.1.0/src/pep810/__main__.py +6 -0
- pep810-0.1.0/src/pep810/_syntax.py +192 -0
- pep810-0.1.0/src/pep810/analyzer.py +560 -0
- pep810-0.1.0/src/pep810/api.py +171 -0
- pep810-0.1.0/src/pep810/bench.py +319 -0
- pep810-0.1.0/src/pep810/cli.py +320 -0
- pep810-0.1.0/src/pep810/codemod.py +161 -0
- pep810-0.1.0/src/pep810/effects.py +627 -0
- pep810-0.1.0/src/pep810/filters.py +168 -0
- pep810-0.1.0/src/pep810/importtime.py +156 -0
- pep810-0.1.0/src/pep810/knowledge.py +347 -0
- pep810-0.1.0/src/pep810/project.py +155 -0
- pep810-0.1.0/src/pep810/report.py +330 -0
- pep810-0.1.0/src/pep810/resolver.py +252 -0
- pep810-0.1.0/src/pep810/verdict.py +415 -0
- pep810-0.1.0/src/pep810.egg-info/PKG-INFO +270 -0
- pep810-0.1.0/src/pep810.egg-info/SOURCES.txt +35 -0
- pep810-0.1.0/src/pep810.egg-info/dependency_links.txt +1 -0
- pep810-0.1.0/src/pep810.egg-info/entry_points.txt +2 -0
- pep810-0.1.0/src/pep810.egg-info/requires.txt +8 -0
- pep810-0.1.0/src/pep810.egg-info/top_level.txt +1 -0
- pep810-0.1.0/tests/test_analyzer.py +123 -0
- pep810-0.1.0/tests/test_cli.py +113 -0
- pep810-0.1.0/tests/test_codemod.py +99 -0
- pep810-0.1.0/tests/test_effects.py +124 -0
- pep810-0.1.0/tests/test_filters.py +34 -0
- pep810-0.1.0/tests/test_importtime.py +36 -0
- pep810-0.1.0/tests/test_project.py +45 -0
- pep810-0.1.0/tests/test_resolver.py +33 -0
- pep810-0.1.0/tests/test_syntax.py +72 -0
- pep810-0.1.0/tests/test_verdict.py +132 -0
pep810-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Woojin Kim
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
pep810-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pep810
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find, apply and measure PEP 810 lazy imports
|
|
5
|
+
License-Expression: 0BSD
|
|
6
|
+
Project-URL: Homepage, https://github.com/rladnwls122/pep810
|
|
7
|
+
Project-URL: Source, https://github.com/rladnwls122/pep810
|
|
8
|
+
Project-URL: Issues, https://github.com/rladnwls122/pep810/issues
|
|
9
|
+
Keywords: pep810,lazy-imports,startup,codemod,static-analysis
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Provides-Extra: toml
|
|
18
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "toml"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# pep810
|
|
24
|
+
|
|
25
|
+
Find, apply and measure [PEP 810](https://peps.python.org/pep-0810/) lazy imports.
|
|
26
|
+
|
|
27
|
+
Python 3.15 adds a `lazy` soft keyword that defers a module's loading until its
|
|
28
|
+
name is first used. The PEP cites 50–70% off startup and 30–40% off memory on
|
|
29
|
+
real workloads. What it does not give you is a way to tell *which* of your
|
|
30
|
+
imports are safe to defer — and getting that wrong is silent:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import readline # nothing in this file ever mentions `readline`
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Make that lazy and the import never happens at all, because nothing ever touches
|
|
37
|
+
the name. Interactive line editing quietly stops working. No error, no warning.
|
|
38
|
+
|
|
39
|
+
PEP 810 anticipates the gap in its own text:
|
|
40
|
+
|
|
41
|
+
> Static analysis tools could detect modules with side effects and automatically
|
|
42
|
+
> configure filters.
|
|
43
|
+
|
|
44
|
+
`pep810` is that tool, plus the two things you need on either side of it: a
|
|
45
|
+
codemod that applies the answer, and a benchmark that checks the answer was
|
|
46
|
+
worth applying.
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
$ pep810 analyze src/
|
|
50
|
+
src/myapp/core.py
|
|
51
|
+
4 safe import subprocess
|
|
52
|
+
S500: no import-time side effects found; safe to defer
|
|
53
|
+
8 risky from . import plugins
|
|
54
|
+
W301: importing myapp.plugins calls atexit.register()
|
|
55
|
+
|
|
56
|
+
src/myapp/__init__.py
|
|
57
|
+
3 unsafe import readline
|
|
58
|
+
E201: `readline` is never used in this module, so the import exists for its
|
|
59
|
+
side effect -- deferring it would cancel the effect, not delay it
|
|
60
|
+
|
|
61
|
+
3 files, 12 import statements
|
|
62
|
+
safe 6
|
|
63
|
+
risky 1
|
|
64
|
+
unsafe 2
|
|
65
|
+
low-benefit 3
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```console
|
|
71
|
+
$ pip install pep810
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
No dependencies. Runs on Python 3.9+ — you do not need 3.15 to analyse or rewrite
|
|
75
|
+
code for 3.15, only to benchmark it.
|
|
76
|
+
|
|
77
|
+
## The five commands
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ pep810 analyze src/ # what could be lazy, and what must not be
|
|
81
|
+
$ pep810 hotspots src/ -e "import myapp" # what is actually expensive
|
|
82
|
+
$ pep810 apply src/ --write # rewrite the safe ones
|
|
83
|
+
$ pep810 bench -e "import myapp" # prove it worked
|
|
84
|
+
$ pep810 filter src/ -o sitecustomize.py # or skip the codemod entirely
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Plus `pep810 check` for CI, which fails the build when an import that could be
|
|
88
|
+
lazy still is not.
|
|
89
|
+
|
|
90
|
+
### analyze
|
|
91
|
+
|
|
92
|
+
Every import statement gets one of seven decisions:
|
|
93
|
+
|
|
94
|
+
| decision | meaning |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `safe` | Deferring changes nothing observable. Convert it. |
|
|
97
|
+
| `risky` | Legal, but importing the target has side effects. Read the reason. |
|
|
98
|
+
| `unsafe` | Deferring would change behaviour. Do not convert. |
|
|
99
|
+
| `low-benefit` | Legal and safe, but the name is used at module level anyway. |
|
|
100
|
+
| `ineligible` | PEP 810 does not allow `lazy` here. |
|
|
101
|
+
| `skipped` | Inside `if TYPE_CHECKING:` or a `__main__` guard; never runs. |
|
|
102
|
+
| `already-lazy` | Nothing to do. |
|
|
103
|
+
|
|
104
|
+
Reasons carry stable codes you can silence like lint rules — `E` blocks the
|
|
105
|
+
rewrite, `W` marks it risky, `I` is informational:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
$ pep810 analyze src/ --ignore W311 --ignore I403
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
or in `pyproject.toml`:
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
[tool.pep810]
|
|
115
|
+
ignore = ["W311"]
|
|
116
|
+
exclude = ["vendor"]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`--format json` and `--format markdown` are there for editors and PR comments.
|
|
120
|
+
|
|
121
|
+
### apply
|
|
122
|
+
|
|
123
|
+
The rewrite inserts five characters — `lazy ` — before the `import` or `from`
|
|
124
|
+
keyword, and touches nothing else:
|
|
125
|
+
|
|
126
|
+
```diff
|
|
127
|
+
-import json
|
|
128
|
+
-import subprocess # for the shell-out
|
|
129
|
+
-from xml.etree import (
|
|
130
|
+
+lazy import json
|
|
131
|
+
+lazy import subprocess # for the shell-out
|
|
132
|
+
+lazy from xml.etree import (
|
|
133
|
+
ElementTree,
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Comments, quote styles and parenthesised import lists survive, because the
|
|
138
|
+
codemod edits bytes rather than round-tripping through an AST. Running it twice
|
|
139
|
+
is a no-op. Every rewritten file is compiled before it is written; a file that
|
|
140
|
+
would not compile is left alone and reported.
|
|
141
|
+
|
|
142
|
+
Without `--write` you get the diff. With `--style lazy-modules` you get PEP 810's
|
|
143
|
+
`__lazy_modules__` declaration instead, which is inert before 3.15 — one release
|
|
144
|
+
that is fast on new Pythons and unchanged on old ones.
|
|
145
|
+
|
|
146
|
+
### filter
|
|
147
|
+
|
|
148
|
+
The other half of PEP 810. Instead of editing sources, run the whole application
|
|
149
|
+
with `-X lazy_imports=all` and let a generated deny-list force back to eager
|
|
150
|
+
exactly the modules the analysis found unsafe:
|
|
151
|
+
|
|
152
|
+
```console
|
|
153
|
+
$ pep810 filter src/ -o sitecustomize.py --lazy-only myapp
|
|
154
|
+
$ python -X lazy_imports=all -m myapp
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The generated module installs `sys.set_lazy_imports_filter` and documents why
|
|
158
|
+
each module is on the list. Note that PEP 810 consults the filter when an import
|
|
159
|
+
statement *runs*, so it has to be installed before your entry point — from
|
|
160
|
+
`sitecustomize` or a wrapper, not from the top of `main.py`.
|
|
161
|
+
|
|
162
|
+
### hotspots and bench
|
|
163
|
+
|
|
164
|
+
`hotspots` ranks measured import cost against the analysis, so you know what to
|
|
165
|
+
do first:
|
|
166
|
+
|
|
167
|
+
```console
|
|
168
|
+
$ pep810 hotspots src/ -e "import myapp"
|
|
169
|
+
cost verdict module
|
|
170
|
+
41.2 ms safe pandas
|
|
171
|
+
18.7 ms risky myapp.plugins
|
|
172
|
+
5.4 ms low-benefit myapp.config
|
|
173
|
+
|
|
174
|
+
312.8 ms of import cost; 214.1 ms sits behind imports the analysis found safe to defer
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`bench` measures the real thing. It uses the interpreter's own switch rather than
|
|
178
|
+
two copies of your tree — `-X lazy_imports=none` forces every `lazy import` back
|
|
179
|
+
to eager — so both sides run the same files on the same interpreter and differ in
|
|
180
|
+
exactly one variable:
|
|
181
|
+
|
|
182
|
+
```console
|
|
183
|
+
$ pep810 bench -e "import myapp" --runs 15
|
|
184
|
+
eager (-X lazy_imports=none) 412.6 ms process 331.0 ms import (range 401.2-433.8) 1284 modules 84.2 MiB
|
|
185
|
+
lazy (as written) 168.3 ms process 92.4 ms import (range 161.9-179.4) 412 modules 51.7 MiB
|
|
186
|
+
|
|
187
|
+
process -59.2% import -72.1% memory -38.6% modules -872
|
|
188
|
+
61 imports still unreified at exit
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Startup benchmarks are noisy, so the report carries the spread and says so
|
|
192
|
+
outright when the two sample ranges overlap. Benchmarking needs a Python 3.15+
|
|
193
|
+
interpreter; point `--python` at one if it is not the interpreter running the
|
|
194
|
+
tool. `pep810` refuses to report a comparison it cannot actually make.
|
|
195
|
+
|
|
196
|
+
## How the analysis decides
|
|
197
|
+
|
|
198
|
+
Four questions, in this order.
|
|
199
|
+
|
|
200
|
+
**1. Is `lazy` legal here?** PEP 810 restricts it to module scope. Inside a
|
|
201
|
+
function, a class body, or a `try`/`except`/`finally` block it is a
|
|
202
|
+
`SyntaxError`, as are `lazy from x import *` and `lazy from __future__ import`.
|
|
203
|
+
|
|
204
|
+
**2. Would deferring change behaviour?** The important case is an import whose
|
|
205
|
+
bound name is never used: it exists for its side effect, and deferring it does
|
|
206
|
+
not delay the effect, it cancels it. A name that is deleted or rebound at module
|
|
207
|
+
level is treated the same way. A package `__init__` with no `__all__` is the
|
|
208
|
+
exception — there, an unused name is an implicit re-export.
|
|
209
|
+
|
|
210
|
+
**3. What does importing the target do?** The target module's top level is
|
|
211
|
+
scanned for registration, monkeypatching, I/O, global configuration, threads and
|
|
212
|
+
process exit. Because deferring an import defers everything it pulls in, the walk
|
|
213
|
+
is transitive, and each finding carries the chain that produced it:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
W301: importing myapp.api -> myapp.db reaches myapp.registry, which calls
|
|
217
|
+
atexit.register() (myapp/registry.py:41)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**4. Is it worth it?** A lazy import reifies the moment its name is touched, so
|
|
221
|
+
an import used at module level saves nothing. The win is names used only in
|
|
222
|
+
function bodies — and, since [PEP 649](https://peps.python.org/pep-0649/),
|
|
223
|
+
annotations.
|
|
224
|
+
|
|
225
|
+
Everything is a heuristic, and the tool says which ones it is confident about.
|
|
226
|
+
`--confidence low` widens what counts as risky; `--confidence high` narrows it.
|
|
227
|
+
|
|
228
|
+
## Working on 3.15 source from an older interpreter
|
|
229
|
+
|
|
230
|
+
`lazy import json` is a `SyntaxError` on every Python before 3.15, which would
|
|
231
|
+
otherwise stop the tool from running on the interpreter most projects are
|
|
232
|
+
migrating *from*. `pep810` reads and writes PEP 810 source on 3.9+ by finding
|
|
233
|
+
the soft keywords with `tokenize` — which knows about strings and comments, so
|
|
234
|
+
`lazy = 1` and `"lazy import x"` in a docstring are not mistaken for keywords —
|
|
235
|
+
stripping them for parsing, and mapping the AST's offsets back onto the original
|
|
236
|
+
bytes.
|
|
237
|
+
|
|
238
|
+
## Library use
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from pathlib import Path
|
|
242
|
+
from pep810 import analyze_paths, Decision, Policy
|
|
243
|
+
|
|
244
|
+
result = analyze_paths([Path("src")], Policy(include_low_benefit=True))
|
|
245
|
+
for file, verdict in result.verdicts(Decision.SAFE):
|
|
246
|
+
print(f"{file.path}:{verdict.site.lineno} {verdict.site.source_line.strip()}")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The layers underneath are independently useful: `pep810.analyzer` for import
|
|
250
|
+
sites and usage, `pep810.effects` for import-time side effects,
|
|
251
|
+
`pep810.importtime` for parsing `-X importtime`, `pep810.codemod` for the
|
|
252
|
+
minimal-diff rewrite.
|
|
253
|
+
|
|
254
|
+
## Limitations
|
|
255
|
+
|
|
256
|
+
- Import-time side effects are undecidable in general. The knowledge base covers
|
|
257
|
+
the calls that show up at the top level of real code; anything unrecognised is
|
|
258
|
+
reported at low confidence rather than guessed at.
|
|
259
|
+
- Compiled extension modules cannot be analysed. They are reported as opaque.
|
|
260
|
+
- Non-path importers — zipimport, frozen modules, packages that extend
|
|
261
|
+
`__path__` at runtime — resolve as unknown rather than as safe.
|
|
262
|
+
- Star-imported names are invisible to the usage analysis, so a module using
|
|
263
|
+
`from x import *` may show false `unsafe` verdicts. Those imports are
|
|
264
|
+
ineligible anyway.
|
|
265
|
+
- `--include-risky` exists, but the name is the recommendation.
|
|
266
|
+
|
|
267
|
+
## Licence
|
|
268
|
+
|
|
269
|
+
[0BSD](LICENSE) -- public-domain-equivalent. Use it for anything, with no
|
|
270
|
+
attribution required.
|
pep810-0.1.0/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# pep810
|
|
2
|
+
|
|
3
|
+
Find, apply and measure [PEP 810](https://peps.python.org/pep-0810/) lazy imports.
|
|
4
|
+
|
|
5
|
+
Python 3.15 adds a `lazy` soft keyword that defers a module's loading until its
|
|
6
|
+
name is first used. The PEP cites 50–70% off startup and 30–40% off memory on
|
|
7
|
+
real workloads. What it does not give you is a way to tell *which* of your
|
|
8
|
+
imports are safe to defer — and getting that wrong is silent:
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import readline # nothing in this file ever mentions `readline`
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Make that lazy and the import never happens at all, because nothing ever touches
|
|
15
|
+
the name. Interactive line editing quietly stops working. No error, no warning.
|
|
16
|
+
|
|
17
|
+
PEP 810 anticipates the gap in its own text:
|
|
18
|
+
|
|
19
|
+
> Static analysis tools could detect modules with side effects and automatically
|
|
20
|
+
> configure filters.
|
|
21
|
+
|
|
22
|
+
`pep810` is that tool, plus the two things you need on either side of it: a
|
|
23
|
+
codemod that applies the answer, and a benchmark that checks the answer was
|
|
24
|
+
worth applying.
|
|
25
|
+
|
|
26
|
+
```console
|
|
27
|
+
$ pep810 analyze src/
|
|
28
|
+
src/myapp/core.py
|
|
29
|
+
4 safe import subprocess
|
|
30
|
+
S500: no import-time side effects found; safe to defer
|
|
31
|
+
8 risky from . import plugins
|
|
32
|
+
W301: importing myapp.plugins calls atexit.register()
|
|
33
|
+
|
|
34
|
+
src/myapp/__init__.py
|
|
35
|
+
3 unsafe import readline
|
|
36
|
+
E201: `readline` is never used in this module, so the import exists for its
|
|
37
|
+
side effect -- deferring it would cancel the effect, not delay it
|
|
38
|
+
|
|
39
|
+
3 files, 12 import statements
|
|
40
|
+
safe 6
|
|
41
|
+
risky 1
|
|
42
|
+
unsafe 2
|
|
43
|
+
low-benefit 3
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
$ pip install pep810
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
No dependencies. Runs on Python 3.9+ — you do not need 3.15 to analyse or rewrite
|
|
53
|
+
code for 3.15, only to benchmark it.
|
|
54
|
+
|
|
55
|
+
## The five commands
|
|
56
|
+
|
|
57
|
+
```console
|
|
58
|
+
$ pep810 analyze src/ # what could be lazy, and what must not be
|
|
59
|
+
$ pep810 hotspots src/ -e "import myapp" # what is actually expensive
|
|
60
|
+
$ pep810 apply src/ --write # rewrite the safe ones
|
|
61
|
+
$ pep810 bench -e "import myapp" # prove it worked
|
|
62
|
+
$ pep810 filter src/ -o sitecustomize.py # or skip the codemod entirely
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Plus `pep810 check` for CI, which fails the build when an import that could be
|
|
66
|
+
lazy still is not.
|
|
67
|
+
|
|
68
|
+
### analyze
|
|
69
|
+
|
|
70
|
+
Every import statement gets one of seven decisions:
|
|
71
|
+
|
|
72
|
+
| decision | meaning |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `safe` | Deferring changes nothing observable. Convert it. |
|
|
75
|
+
| `risky` | Legal, but importing the target has side effects. Read the reason. |
|
|
76
|
+
| `unsafe` | Deferring would change behaviour. Do not convert. |
|
|
77
|
+
| `low-benefit` | Legal and safe, but the name is used at module level anyway. |
|
|
78
|
+
| `ineligible` | PEP 810 does not allow `lazy` here. |
|
|
79
|
+
| `skipped` | Inside `if TYPE_CHECKING:` or a `__main__` guard; never runs. |
|
|
80
|
+
| `already-lazy` | Nothing to do. |
|
|
81
|
+
|
|
82
|
+
Reasons carry stable codes you can silence like lint rules — `E` blocks the
|
|
83
|
+
rewrite, `W` marks it risky, `I` is informational:
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
$ pep810 analyze src/ --ignore W311 --ignore I403
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
or in `pyproject.toml`:
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[tool.pep810]
|
|
93
|
+
ignore = ["W311"]
|
|
94
|
+
exclude = ["vendor"]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`--format json` and `--format markdown` are there for editors and PR comments.
|
|
98
|
+
|
|
99
|
+
### apply
|
|
100
|
+
|
|
101
|
+
The rewrite inserts five characters — `lazy ` — before the `import` or `from`
|
|
102
|
+
keyword, and touches nothing else:
|
|
103
|
+
|
|
104
|
+
```diff
|
|
105
|
+
-import json
|
|
106
|
+
-import subprocess # for the shell-out
|
|
107
|
+
-from xml.etree import (
|
|
108
|
+
+lazy import json
|
|
109
|
+
+lazy import subprocess # for the shell-out
|
|
110
|
+
+lazy from xml.etree import (
|
|
111
|
+
ElementTree,
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Comments, quote styles and parenthesised import lists survive, because the
|
|
116
|
+
codemod edits bytes rather than round-tripping through an AST. Running it twice
|
|
117
|
+
is a no-op. Every rewritten file is compiled before it is written; a file that
|
|
118
|
+
would not compile is left alone and reported.
|
|
119
|
+
|
|
120
|
+
Without `--write` you get the diff. With `--style lazy-modules` you get PEP 810's
|
|
121
|
+
`__lazy_modules__` declaration instead, which is inert before 3.15 — one release
|
|
122
|
+
that is fast on new Pythons and unchanged on old ones.
|
|
123
|
+
|
|
124
|
+
### filter
|
|
125
|
+
|
|
126
|
+
The other half of PEP 810. Instead of editing sources, run the whole application
|
|
127
|
+
with `-X lazy_imports=all` and let a generated deny-list force back to eager
|
|
128
|
+
exactly the modules the analysis found unsafe:
|
|
129
|
+
|
|
130
|
+
```console
|
|
131
|
+
$ pep810 filter src/ -o sitecustomize.py --lazy-only myapp
|
|
132
|
+
$ python -X lazy_imports=all -m myapp
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The generated module installs `sys.set_lazy_imports_filter` and documents why
|
|
136
|
+
each module is on the list. Note that PEP 810 consults the filter when an import
|
|
137
|
+
statement *runs*, so it has to be installed before your entry point — from
|
|
138
|
+
`sitecustomize` or a wrapper, not from the top of `main.py`.
|
|
139
|
+
|
|
140
|
+
### hotspots and bench
|
|
141
|
+
|
|
142
|
+
`hotspots` ranks measured import cost against the analysis, so you know what to
|
|
143
|
+
do first:
|
|
144
|
+
|
|
145
|
+
```console
|
|
146
|
+
$ pep810 hotspots src/ -e "import myapp"
|
|
147
|
+
cost verdict module
|
|
148
|
+
41.2 ms safe pandas
|
|
149
|
+
18.7 ms risky myapp.plugins
|
|
150
|
+
5.4 ms low-benefit myapp.config
|
|
151
|
+
|
|
152
|
+
312.8 ms of import cost; 214.1 ms sits behind imports the analysis found safe to defer
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`bench` measures the real thing. It uses the interpreter's own switch rather than
|
|
156
|
+
two copies of your tree — `-X lazy_imports=none` forces every `lazy import` back
|
|
157
|
+
to eager — so both sides run the same files on the same interpreter and differ in
|
|
158
|
+
exactly one variable:
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ pep810 bench -e "import myapp" --runs 15
|
|
162
|
+
eager (-X lazy_imports=none) 412.6 ms process 331.0 ms import (range 401.2-433.8) 1284 modules 84.2 MiB
|
|
163
|
+
lazy (as written) 168.3 ms process 92.4 ms import (range 161.9-179.4) 412 modules 51.7 MiB
|
|
164
|
+
|
|
165
|
+
process -59.2% import -72.1% memory -38.6% modules -872
|
|
166
|
+
61 imports still unreified at exit
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Startup benchmarks are noisy, so the report carries the spread and says so
|
|
170
|
+
outright when the two sample ranges overlap. Benchmarking needs a Python 3.15+
|
|
171
|
+
interpreter; point `--python` at one if it is not the interpreter running the
|
|
172
|
+
tool. `pep810` refuses to report a comparison it cannot actually make.
|
|
173
|
+
|
|
174
|
+
## How the analysis decides
|
|
175
|
+
|
|
176
|
+
Four questions, in this order.
|
|
177
|
+
|
|
178
|
+
**1. Is `lazy` legal here?** PEP 810 restricts it to module scope. Inside a
|
|
179
|
+
function, a class body, or a `try`/`except`/`finally` block it is a
|
|
180
|
+
`SyntaxError`, as are `lazy from x import *` and `lazy from __future__ import`.
|
|
181
|
+
|
|
182
|
+
**2. Would deferring change behaviour?** The important case is an import whose
|
|
183
|
+
bound name is never used: it exists for its side effect, and deferring it does
|
|
184
|
+
not delay the effect, it cancels it. A name that is deleted or rebound at module
|
|
185
|
+
level is treated the same way. A package `__init__` with no `__all__` is the
|
|
186
|
+
exception — there, an unused name is an implicit re-export.
|
|
187
|
+
|
|
188
|
+
**3. What does importing the target do?** The target module's top level is
|
|
189
|
+
scanned for registration, monkeypatching, I/O, global configuration, threads and
|
|
190
|
+
process exit. Because deferring an import defers everything it pulls in, the walk
|
|
191
|
+
is transitive, and each finding carries the chain that produced it:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
W301: importing myapp.api -> myapp.db reaches myapp.registry, which calls
|
|
195
|
+
atexit.register() (myapp/registry.py:41)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**4. Is it worth it?** A lazy import reifies the moment its name is touched, so
|
|
199
|
+
an import used at module level saves nothing. The win is names used only in
|
|
200
|
+
function bodies — and, since [PEP 649](https://peps.python.org/pep-0649/),
|
|
201
|
+
annotations.
|
|
202
|
+
|
|
203
|
+
Everything is a heuristic, and the tool says which ones it is confident about.
|
|
204
|
+
`--confidence low` widens what counts as risky; `--confidence high` narrows it.
|
|
205
|
+
|
|
206
|
+
## Working on 3.15 source from an older interpreter
|
|
207
|
+
|
|
208
|
+
`lazy import json` is a `SyntaxError` on every Python before 3.15, which would
|
|
209
|
+
otherwise stop the tool from running on the interpreter most projects are
|
|
210
|
+
migrating *from*. `pep810` reads and writes PEP 810 source on 3.9+ by finding
|
|
211
|
+
the soft keywords with `tokenize` — which knows about strings and comments, so
|
|
212
|
+
`lazy = 1` and `"lazy import x"` in a docstring are not mistaken for keywords —
|
|
213
|
+
stripping them for parsing, and mapping the AST's offsets back onto the original
|
|
214
|
+
bytes.
|
|
215
|
+
|
|
216
|
+
## Library use
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from pathlib import Path
|
|
220
|
+
from pep810 import analyze_paths, Decision, Policy
|
|
221
|
+
|
|
222
|
+
result = analyze_paths([Path("src")], Policy(include_low_benefit=True))
|
|
223
|
+
for file, verdict in result.verdicts(Decision.SAFE):
|
|
224
|
+
print(f"{file.path}:{verdict.site.lineno} {verdict.site.source_line.strip()}")
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The layers underneath are independently useful: `pep810.analyzer` for import
|
|
228
|
+
sites and usage, `pep810.effects` for import-time side effects,
|
|
229
|
+
`pep810.importtime` for parsing `-X importtime`, `pep810.codemod` for the
|
|
230
|
+
minimal-diff rewrite.
|
|
231
|
+
|
|
232
|
+
## Limitations
|
|
233
|
+
|
|
234
|
+
- Import-time side effects are undecidable in general. The knowledge base covers
|
|
235
|
+
the calls that show up at the top level of real code; anything unrecognised is
|
|
236
|
+
reported at low confidence rather than guessed at.
|
|
237
|
+
- Compiled extension modules cannot be analysed. They are reported as opaque.
|
|
238
|
+
- Non-path importers — zipimport, frozen modules, packages that extend
|
|
239
|
+
`__path__` at runtime — resolve as unknown rather than as safe.
|
|
240
|
+
- Star-imported names are invisible to the usage analysis, so a module using
|
|
241
|
+
`from x import *` may show false `unsafe` verdicts. Those imports are
|
|
242
|
+
ineligible anyway.
|
|
243
|
+
- `--include-risky` exists, but the name is the recommendation.
|
|
244
|
+
|
|
245
|
+
## Licence
|
|
246
|
+
|
|
247
|
+
[0BSD](LICENSE) -- public-domain-equivalent. Use it for anything, with no
|
|
248
|
+
attribution required.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pep810"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Find, apply and measure PEP 810 lazy imports"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "0BSD"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["pep810", "lazy-imports", "startup", "codemod", "static-analysis"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/rladnwls122/pep810"
|
|
24
|
+
Source = "https://github.com/rladnwls122/pep810"
|
|
25
|
+
Issues = "https://github.com/rladnwls122/pep810/issues"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
toml = ["tomli>=2.0; python_version<'3.11'"]
|
|
29
|
+
dev = ["pytest>=7"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
pep810 = "pep810.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
pep810-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Find, apply and measure PEP 810 lazy imports.
|
|
2
|
+
|
|
3
|
+
PEP 810 adds a ``lazy`` soft keyword to Python 3.15 that defers a module's
|
|
4
|
+
loading until its name is first used. The savings are real -- the PEP cites
|
|
5
|
+
50-70% off startup and 30-40% off memory on real workloads -- but the keyword
|
|
6
|
+
alone does not tell you which of your imports are safe to defer, and getting it
|
|
7
|
+
wrong is silent: an import kept only for its side effect simply stops happening.
|
|
8
|
+
|
|
9
|
+
This package answers that question statically and then acts on the answer.
|
|
10
|
+
|
|
11
|
+
>>> from pathlib import Path
|
|
12
|
+
>>> from pep810 import analyze_paths
|
|
13
|
+
>>> result = analyze_paths([Path("src")]) # doctest: +SKIP
|
|
14
|
+
>>> result.counts[Decision.SAFE] # doctest: +SKIP
|
|
15
|
+
42
|
|
16
|
+
|
|
17
|
+
The pipeline is four steps, each usable on its own:
|
|
18
|
+
|
|
19
|
+
:mod:`pep810.analyzer`
|
|
20
|
+
Where each import sits and how its names are used in that file.
|
|
21
|
+
:mod:`pep810.effects`
|
|
22
|
+
What importing the target module actually does, transitively.
|
|
23
|
+
:mod:`pep810.verdict`
|
|
24
|
+
The decision, with reason codes you can suppress like lint rules.
|
|
25
|
+
:mod:`pep810.codemod` / :mod:`pep810.filters`
|
|
26
|
+
Rewrite the source, or generate the runtime filter PEP 810 invites instead.
|
|
27
|
+
|
|
28
|
+
:mod:`pep810.bench` closes the loop by measuring startup with lazy imports
|
|
29
|
+
forced off and then on, so the claim can be checked rather than assumed.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
from .analyzer import FileAnalysis, ImportContext, ImportSite, analyze_file, analyze_source
|
|
37
|
+
from .api import AnalysisResult, FileResult, analyze_paths, build_filter_plan
|
|
38
|
+
from .bench import Comparison, Measurement, run_benchmark, supports_lazy_imports
|
|
39
|
+
from .codemod import FileEdit, render_lazy_modules, rewrite
|
|
40
|
+
from .effects import Effect, EffectAnalyzer, ModuleEffects, scan_source
|
|
41
|
+
from .filters import FilterPlan, render_filter_module
|
|
42
|
+
from .importtime import ImportTimeTree, measure_importtime, parse_importtime
|
|
43
|
+
from .knowledge import Confidence, EffectKind
|
|
44
|
+
from .project import Project, discover
|
|
45
|
+
from .resolver import ModuleKind, ModuleResolver
|
|
46
|
+
from .verdict import Decision, Policy, Reason, Verdict, judge
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"__version__",
|
|
50
|
+
"AnalysisResult",
|
|
51
|
+
"Comparison",
|
|
52
|
+
"Confidence",
|
|
53
|
+
"Decision",
|
|
54
|
+
"Effect",
|
|
55
|
+
"EffectAnalyzer",
|
|
56
|
+
"EffectKind",
|
|
57
|
+
"FileAnalysis",
|
|
58
|
+
"FileEdit",
|
|
59
|
+
"FileResult",
|
|
60
|
+
"FilterPlan",
|
|
61
|
+
"ImportContext",
|
|
62
|
+
"ImportSite",
|
|
63
|
+
"ImportTimeTree",
|
|
64
|
+
"Measurement",
|
|
65
|
+
"ModuleEffects",
|
|
66
|
+
"ModuleKind",
|
|
67
|
+
"ModuleResolver",
|
|
68
|
+
"Policy",
|
|
69
|
+
"Project",
|
|
70
|
+
"Reason",
|
|
71
|
+
"Verdict",
|
|
72
|
+
"analyze_file",
|
|
73
|
+
"analyze_paths",
|
|
74
|
+
"analyze_source",
|
|
75
|
+
"build_filter_plan",
|
|
76
|
+
"discover",
|
|
77
|
+
"judge",
|
|
78
|
+
"measure_importtime",
|
|
79
|
+
"parse_importtime",
|
|
80
|
+
"render_filter_module",
|
|
81
|
+
"render_lazy_modules",
|
|
82
|
+
"rewrite",
|
|
83
|
+
"run_benchmark",
|
|
84
|
+
"scan_source",
|
|
85
|
+
"supports_lazy_imports",
|
|
86
|
+
]
|