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.
Files changed (37) hide show
  1. pep810-0.1.0/LICENSE +14 -0
  2. pep810-0.1.0/PKG-INFO +270 -0
  3. pep810-0.1.0/README.md +248 -0
  4. pep810-0.1.0/pyproject.toml +38 -0
  5. pep810-0.1.0/setup.cfg +4 -0
  6. pep810-0.1.0/src/pep810/__init__.py +86 -0
  7. pep810-0.1.0/src/pep810/__main__.py +6 -0
  8. pep810-0.1.0/src/pep810/_syntax.py +192 -0
  9. pep810-0.1.0/src/pep810/analyzer.py +560 -0
  10. pep810-0.1.0/src/pep810/api.py +171 -0
  11. pep810-0.1.0/src/pep810/bench.py +319 -0
  12. pep810-0.1.0/src/pep810/cli.py +320 -0
  13. pep810-0.1.0/src/pep810/codemod.py +161 -0
  14. pep810-0.1.0/src/pep810/effects.py +627 -0
  15. pep810-0.1.0/src/pep810/filters.py +168 -0
  16. pep810-0.1.0/src/pep810/importtime.py +156 -0
  17. pep810-0.1.0/src/pep810/knowledge.py +347 -0
  18. pep810-0.1.0/src/pep810/project.py +155 -0
  19. pep810-0.1.0/src/pep810/report.py +330 -0
  20. pep810-0.1.0/src/pep810/resolver.py +252 -0
  21. pep810-0.1.0/src/pep810/verdict.py +415 -0
  22. pep810-0.1.0/src/pep810.egg-info/PKG-INFO +270 -0
  23. pep810-0.1.0/src/pep810.egg-info/SOURCES.txt +35 -0
  24. pep810-0.1.0/src/pep810.egg-info/dependency_links.txt +1 -0
  25. pep810-0.1.0/src/pep810.egg-info/entry_points.txt +2 -0
  26. pep810-0.1.0/src/pep810.egg-info/requires.txt +8 -0
  27. pep810-0.1.0/src/pep810.egg-info/top_level.txt +1 -0
  28. pep810-0.1.0/tests/test_analyzer.py +123 -0
  29. pep810-0.1.0/tests/test_cli.py +113 -0
  30. pep810-0.1.0/tests/test_codemod.py +99 -0
  31. pep810-0.1.0/tests/test_effects.py +124 -0
  32. pep810-0.1.0/tests/test_filters.py +34 -0
  33. pep810-0.1.0/tests/test_importtime.py +36 -0
  34. pep810-0.1.0/tests/test_project.py +45 -0
  35. pep810-0.1.0/tests/test_resolver.py +33 -0
  36. pep810-0.1.0/tests/test_syntax.py +72 -0
  37. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -0,0 +1,6 @@
1
+ """Entry point for ``python -m pep810``."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())