lazysafe 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 (32) hide show
  1. lazysafe-0.1.0/.github/workflows/ci.yml +29 -0
  2. lazysafe-0.1.0/.github/workflows/release.yml +31 -0
  3. lazysafe-0.1.0/.gitignore +31 -0
  4. lazysafe-0.1.0/CHANGELOG.md +27 -0
  5. lazysafe-0.1.0/LICENSE +21 -0
  6. lazysafe-0.1.0/PKG-INFO +104 -0
  7. lazysafe-0.1.0/README.md +80 -0
  8. lazysafe-0.1.0/conftest.py +6 -0
  9. lazysafe-0.1.0/pyproject.toml +70 -0
  10. lazysafe-0.1.0/src/lazysafe/__init__.py +5 -0
  11. lazysafe-0.1.0/src/lazysafe/__main__.py +5 -0
  12. lazysafe-0.1.0/src/lazysafe/_version.py +3 -0
  13. lazysafe-0.1.0/src/lazysafe/cli.py +128 -0
  14. lazysafe-0.1.0/src/lazysafe/config.py +51 -0
  15. lazysafe-0.1.0/src/lazysafe/discovery.py +158 -0
  16. lazysafe-0.1.0/src/lazysafe/model.py +66 -0
  17. lazysafe-0.1.0/src/lazysafe/report.py +99 -0
  18. lazysafe-0.1.0/src/lazysafe/static/__init__.py +5 -0
  19. lazysafe-0.1.0/src/lazysafe/static/classify.py +34 -0
  20. lazysafe-0.1.0/src/lazysafe/static/rules.py +286 -0
  21. lazysafe-0.1.0/tests/fixtures/clean_pkg/__init__.py +16 -0
  22. lazysafe-0.1.0/tests/fixtures/clean_pkg/pure.py +4 -0
  23. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se01_call.py +6 -0
  24. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se02_assign.py +5 -0
  25. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se03_sys.py +5 -0
  26. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se04_register.py +12 -0
  27. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se05_io.py +6 -0
  28. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se07_importlib.py +5 -0
  29. lazysafe-0.1.0/tests/fixtures/sideeffect_pkg/se08_tryexcept.py +8 -0
  30. lazysafe-0.1.0/tests/test_analysis.py +86 -0
  31. lazysafe-0.1.0/tests/test_cli.py +17 -0
  32. lazysafe-0.1.0/tests/test_config.py +35 -0
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ continue-on-error: ${{ matrix.experimental == true }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ include:
16
+ - { os: ubuntu-latest, python: "3.11" }
17
+ - { os: ubuntu-latest, python: "3.12" }
18
+ - { os: ubuntu-latest, python: "3.13" }
19
+ - { os: ubuntu-latest, python: "3.14" }
20
+ - { os: macos-latest, python: "3.14" }
21
+ - { os: windows-latest, python: "3.14" }
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v5
25
+ with:
26
+ python-version: ${{ matrix.python }}
27
+ - run: uv sync
28
+ - run: uv run pytest -q
29
+ - run: uv run ruff check .
@@ -0,0 +1,31 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - run: uv build
14
+ - run: uv run twine check dist/*
15
+ - uses: actions/upload-artifact@v4
16
+ with:
17
+ name: dist
18
+ path: dist/
19
+
20
+ publish-pypi:
21
+ needs: build
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write
26
+ steps:
27
+ - uses: actions/download-artifact@v4
28
+ with:
29
+ name: dist
30
+ path: dist
31
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,31 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+ .eggs/
10
+
11
+ .venv/
12
+ venv/
13
+ ENV/
14
+
15
+ .idea/
16
+ .vscode/
17
+ *.swp
18
+ *.swo
19
+ *~
20
+
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+ .mypy_cache/
25
+ .ruff_cache/
26
+
27
+ .lazysafe/
28
+
29
+ .DS_Store
30
+ Thumbs.db
31
+ uv.lock
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
+
7
+ ## [0.1.0] - 2026-09-06
8
+
9
+ ### added
10
+
11
+ - `lazysafe analyze` static analysis engine with SE01-SE08 side-effect rules
12
+ - classification decision tree (SAFE / RISKY / UNSAFE / UNKNOWN)
13
+ - JSON report output (`--save`, `--json` flags)
14
+ - `lazysafe version` command with capability matrix
15
+ - configuration via `lazysafe.toml` with unknown-key rejection
16
+ - fixture corpus for side-effect rule testing
17
+
18
+ ### side-effect rules
19
+
20
+ - SE01: module-level call expressions
21
+ - SE02: foreign attribute assignment (monkeypatching)
22
+ - SE03: sys.path / sys.modules / os.environ mutation
23
+ - SE04: atexit / signal / logging registration
24
+ - SE05: module-level file I/O
25
+ - SE06: framework registration decorators
26
+ - SE07: importlib.import_module with computed names
27
+ - SE08: try/except ImportError with fallback patches
lazysafe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rahul Sharma
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.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.3
2
+ Name: lazysafe
3
+ Version: 0.1.0
4
+ Summary: safely adopt python 3.15 lazy imports: analyze, probe, apply, verify, measure
5
+ Project-URL: Homepage, https://github.com/rahulxs/lazysafe
6
+ Project-URL: Repository, https://github.com/rahulxs/lazysafe
7
+ Project-URL: Issues, https://github.com/rahulxs/lazysafe/issues
8
+ Project-URL: Changelog, https://github.com/rahulxs/lazysafe/blob/main/CHANGELOG.md
9
+ Author-email: Rahul Sharma <rahulxsh@gmail.com>
10
+ License: MIT
11
+ Keywords: cpython315,import-time,lazy-imports,pep810,performance,python,side-effects,startup,verification
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+
25
+ # lazysafe
26
+
27
+ safely adopt python 3.15 lazy imports.
28
+
29
+ ## why it exists
30
+
31
+ python 3.15 ships [PEP 810](https://peps.python.org/pep-0810/) explicit lazy
32
+ imports. `lazy import json` defers loading until first use. reported wins: ~2.9x
33
+ faster startup for import-heavy applications.
34
+
35
+ the catch: laziness changes *when* import-time side effects run. some libraries
36
+ depend on them. under lazy mode, programs break in confusing ways.
37
+
38
+ lazysafe answers that question empirically.
39
+
40
+ ## install
41
+
42
+ ```
43
+ pip install lazysafe
44
+ ```
45
+
46
+ ## quickstart
47
+
48
+ ```bash
49
+ # see which imports are safe to make lazy
50
+ lazysafe analyze src/
51
+
52
+ # show all modules including safe ones
53
+ lazysafe analyze src/ --all
54
+
55
+ # save a JSON report
56
+ lazysafe analyze src/ --save report.json
57
+ ```
58
+
59
+ ## what it does
60
+
61
+ lazysafe scans your Python source files and classifies every import statement
62
+ by its side-effect risk:
63
+
64
+ | finding | meaning | action |
65
+ |---------|---------|--------|
66
+ | SAFE | no side effects detected | safe to make lazy |
67
+ | RISKY | might have side effects | investigate before lazy |
68
+ | UNSAFE | side effect detected | keep eager |
69
+ | UNKNOWN | can't determine statically | needs probing (v0.3.0) |
70
+
71
+ ### side-effect rules
72
+
73
+ | code | what it detects | confidence |
74
+ |------|-----------------|------------|
75
+ | SE01 | module-level function calls | 0.6 |
76
+ | SE02 | foreign attribute assignment (monkeypatching) | 0.9 |
77
+ | SE03 | sys.path / sys.modules / os.environ mutation | 0.95 |
78
+ | SE04 | atexit / signal / logging registration | 0.85-0.9 |
79
+ | SE05 | module-level file I/O | 0.8 |
80
+ | SE06 | framework registration decorators (2+) | 0.5 |
81
+ | SE07 | importlib.import_module with computed names | 0.9 |
82
+ | SE08 | try/except ImportError with fallback patches | 0.7 |
83
+
84
+ ## what it does not do
85
+
86
+ - runtime lazy-loading for older pythons (no backport of PEP 810)
87
+ - patching third-party packages' code
88
+ - function-level profiling (use [Tachyon](https://docs.python.org/3.15/whatsnew/3.15.html#tachyon) in 3.15)
89
+ - general linting/formatting (use [ruff](https://docs.astral.sh/ruff/))
90
+
91
+ ## requirements
92
+
93
+ - python >= 3.11 (host running lazysafe)
94
+ - target programs using lazy import semantics require python >= 3.15
95
+ - linux, macos, windows
96
+
97
+ ## links
98
+
99
+ - [changelog](https://github.com/rahulxs/lazysafe/blob/main/CHANGELOG.md)
100
+ - [issue tracker](https://github.com/rahulxs/lazysafe/issues)
101
+
102
+ ## license
103
+
104
+ MIT
@@ -0,0 +1,80 @@
1
+ # lazysafe
2
+
3
+ safely adopt python 3.15 lazy imports.
4
+
5
+ ## why it exists
6
+
7
+ python 3.15 ships [PEP 810](https://peps.python.org/pep-0810/) explicit lazy
8
+ imports. `lazy import json` defers loading until first use. reported wins: ~2.9x
9
+ faster startup for import-heavy applications.
10
+
11
+ the catch: laziness changes *when* import-time side effects run. some libraries
12
+ depend on them. under lazy mode, programs break in confusing ways.
13
+
14
+ lazysafe answers that question empirically.
15
+
16
+ ## install
17
+
18
+ ```
19
+ pip install lazysafe
20
+ ```
21
+
22
+ ## quickstart
23
+
24
+ ```bash
25
+ # see which imports are safe to make lazy
26
+ lazysafe analyze src/
27
+
28
+ # show all modules including safe ones
29
+ lazysafe analyze src/ --all
30
+
31
+ # save a JSON report
32
+ lazysafe analyze src/ --save report.json
33
+ ```
34
+
35
+ ## what it does
36
+
37
+ lazysafe scans your Python source files and classifies every import statement
38
+ by its side-effect risk:
39
+
40
+ | finding | meaning | action |
41
+ |---------|---------|--------|
42
+ | SAFE | no side effects detected | safe to make lazy |
43
+ | RISKY | might have side effects | investigate before lazy |
44
+ | UNSAFE | side effect detected | keep eager |
45
+ | UNKNOWN | can't determine statically | needs probing (v0.3.0) |
46
+
47
+ ### side-effect rules
48
+
49
+ | code | what it detects | confidence |
50
+ |------|-----------------|------------|
51
+ | SE01 | module-level function calls | 0.6 |
52
+ | SE02 | foreign attribute assignment (monkeypatching) | 0.9 |
53
+ | SE03 | sys.path / sys.modules / os.environ mutation | 0.95 |
54
+ | SE04 | atexit / signal / logging registration | 0.85-0.9 |
55
+ | SE05 | module-level file I/O | 0.8 |
56
+ | SE06 | framework registration decorators (2+) | 0.5 |
57
+ | SE07 | importlib.import_module with computed names | 0.9 |
58
+ | SE08 | try/except ImportError with fallback patches | 0.7 |
59
+
60
+ ## what it does not do
61
+
62
+ - runtime lazy-loading for older pythons (no backport of PEP 810)
63
+ - patching third-party packages' code
64
+ - function-level profiling (use [Tachyon](https://docs.python.org/3.15/whatsnew/3.15.html#tachyon) in 3.15)
65
+ - general linting/formatting (use [ruff](https://docs.astral.sh/ruff/))
66
+
67
+ ## requirements
68
+
69
+ - python >= 3.11 (host running lazysafe)
70
+ - target programs using lazy import semantics require python >= 3.15
71
+ - linux, macos, windows
72
+
73
+ ## links
74
+
75
+ - [changelog](https://github.com/rahulxs/lazysafe/blob/main/CHANGELOG.md)
76
+ - [issue tracker](https://github.com/rahulxs/lazysafe/issues)
77
+
78
+ ## license
79
+
80
+ MIT
@@ -0,0 +1,6 @@
1
+ """pytest configuration -- adds src/ to sys.path for imports."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
@@ -0,0 +1,70 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lazysafe"
7
+ dynamic = ["version"]
8
+ description = "safely adopt python 3.15 lazy imports: analyze, probe, apply, verify, measure"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11"
12
+ authors = [{ name = "Rahul Sharma", email = "rahulxsh@gmail.com" }]
13
+ keywords = [
14
+ "python", "lazy-imports", "pep810", "startup", "performance",
15
+ "import-time", "side-effects", "verification", "cpython315",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Programming Language :: Python :: 3.15",
27
+ "Topic :: Software Development :: Quality Assurance",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.scripts]
32
+ lazysafe = "lazysafe.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/rahulxs/lazysafe"
36
+ Repository = "https://github.com/rahulxs/lazysafe"
37
+ Issues = "https://github.com/rahulxs/lazysafe/issues"
38
+ Changelog = "https://github.com/rahulxs/lazysafe/blob/main/CHANGELOG.md"
39
+
40
+ [tool.hatch.version]
41
+ path = "src/lazysafe/_version.py"
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ core-metadata-version = "2.3"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/lazysafe"]
48
+ core-metadata-version = "2.3"
49
+
50
+ [tool.ruff]
51
+ target-version = "py311"
52
+ line-length = 100
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF", "C901"]
56
+
57
+ [tool.ruff.lint.mccabe]
58
+ max-complexity = 10
59
+
60
+ [tool.pytest.ini_options]
61
+ testpaths = ["tests"]
62
+ addopts = "-q --tb=short"
63
+
64
+ [dependency-groups]
65
+ dev = [
66
+ "pytest>=8",
67
+ "ruff>=0.5",
68
+ "build>=1.2",
69
+ "twine>=6",
70
+ ]
@@ -0,0 +1,5 @@
1
+ """lazysafe -- safely adopt Python 3.15 lazy imports."""
2
+
3
+ from lazysafe._version import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,5 @@
1
+ """allow running as: python -m lazysafe"""
2
+
3
+ from lazysafe.cli import main
4
+
5
+ main()
@@ -0,0 +1,3 @@
1
+ """single source of truth for the version."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,128 @@
1
+ """CLI entry point -- argparse wiring only."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from lazysafe._version import __version__
10
+ from lazysafe.config import load_config
11
+ from lazysafe.discovery import scan_directory
12
+ from lazysafe.report import write_analysis_report
13
+ from lazysafe.static import run_static
14
+ from lazysafe.static.classify import classify_all
15
+
16
+
17
+ def _cmd_analyze(args: argparse.Namespace) -> None:
18
+ """run static analysis on target directories."""
19
+ config = load_config(Path.cwd())
20
+ targets = args.targets or config.targets
21
+
22
+ start = time.monotonic()
23
+ model = scan_directory(targets, Path.cwd())
24
+ duration_ms = (time.monotonic() - start) * 1000
25
+
26
+ for node in model.modules:
27
+ if node.file:
28
+ try:
29
+ source = Path(node.file).read_text(encoding="utf-8")
30
+ node.findings = run_static(node, source)
31
+ except (OSError, UnicodeDecodeError):
32
+ pass
33
+
34
+ classify_all(model)
35
+
36
+ report = write_analysis_report(
37
+ model, targets, duration_ms, output=Path(args.save) if args.save else None
38
+ )
39
+
40
+ _print_analysis_table(report, args)
41
+
42
+
43
+ def _print_analysis_table(report: dict, args: argparse.Namespace) -> None:
44
+ """print analysis results as a table."""
45
+ stats = report["stats"]
46
+ print(f"\nscanned {stats['files_scanned']} files, {stats['top_level_imports']} imports")
47
+ print(
48
+ f"classified: {stats['by_class']['safe']} safe, "
49
+ f"{stats['by_class']['risky']} risky, "
50
+ f"{stats['by_class']['unsafe']} unsafe, "
51
+ f"{stats['by_class']['unknown']} unknown"
52
+ )
53
+
54
+ if args.json:
55
+ print(json.dumps(report, indent=2))
56
+ return
57
+
58
+ has_findings = any(mod["reasons"] for mod in report["modules"])
59
+ if not has_findings and not args.all:
60
+ print("\n all imports look safe. use --all to see every module.\n")
61
+ return
62
+
63
+ for mod in report["modules"]:
64
+ cls = mod["class"]
65
+ if cls == "safe" and not args.all:
66
+ continue
67
+
68
+ symbol = {"safe": "+", "risky": "?", "unsafe": "!", "unknown": "?"}.get(
69
+ cls, "?"
70
+ )
71
+ imports = mod["imports_top_level"]
72
+ import_str = f" ({len(imports)} imports)" if imports else ""
73
+ print(f" {symbol} {mod['module']} ({mod['origin']}) [{cls}]{import_str}")
74
+ for reason in mod["reasons"]:
75
+ print(f" {reason['rule']}@{reason['lineno']}: {reason['evidence']}")
76
+
77
+ print()
78
+
79
+
80
+ def _cmd_version(_args: argparse.Namespace) -> None:
81
+ """show version and capability matrix."""
82
+ print(f"lazysafe {__version__} \xb7 python {sys.version.split()[0]}")
83
+ print()
84
+ print("capabilities:")
85
+ print(" analyze yes")
86
+ print(" measure no (v0.2.0)")
87
+ print(" probe no (v0.3.0)")
88
+ print(" apply no (v0.4.0)")
89
+ print(" verify no (v0.5.0)")
90
+
91
+
92
+ def main(argv: list[str] | None = None) -> None:
93
+ """main entry point."""
94
+ parser = argparse.ArgumentParser(
95
+ prog="lazysafe",
96
+ description="safely adopt python 3.15 lazy imports.",
97
+ )
98
+ parser.add_argument(
99
+ "--version", action="version", version=f"%(prog)s {__version__}"
100
+ )
101
+ sub = parser.add_subparsers(dest="command")
102
+
103
+ p_analyze = sub.add_parser("analyze", help="analyze imports for side effects")
104
+ p_analyze.add_argument("targets", nargs="*", help="directories to scan")
105
+ p_analyze.add_argument("--save", metavar="FILE", help="save JSON report")
106
+ p_analyze.add_argument("--json", action="store_true", help="output as JSON")
107
+ p_analyze.add_argument(
108
+ "--all", action="store_true", help="show safe modules too"
109
+ )
110
+
111
+ sub.add_parser("version", help="show capability matrix")
112
+
113
+ args = parser.parse_args(argv)
114
+
115
+ if args.command is None:
116
+ parser.print_help()
117
+ sys.exit(0)
118
+
119
+ commands = {
120
+ "analyze": _cmd_analyze,
121
+ "version": _cmd_version,
122
+ }
123
+
124
+ if args.command in commands:
125
+ commands[args.command](args)
126
+ else:
127
+ print(f"command '{args.command}' not yet implemented.")
128
+ sys.exit(1)
@@ -0,0 +1,51 @@
1
+ """configuration loading and merging."""
2
+
3
+ import tomllib
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class Config:
10
+ """lazysafe configuration."""
11
+
12
+ targets: list[str] = field(default_factory=lambda: ["src"])
13
+ budget_ms: float | None = None
14
+ measure_runs: int = 15
15
+ measure_warmup: int = 2
16
+ probe_timeout: float = 30.0
17
+ apply_mode: str = "dunder"
18
+ allowlist: list[str] = field(default_factory=list)
19
+
20
+
21
+ _KNOWN_KEYS = {
22
+ "targets",
23
+ "budget_ms",
24
+ "measure_runs",
25
+ "measure_warmup",
26
+ "probe_timeout",
27
+ "apply_mode",
28
+ "allowlist",
29
+ }
30
+
31
+
32
+ def load_config(project_root: Path) -> Config:
33
+ """load lazysafe.toml from project root, falling back to defaults."""
34
+ config = Config()
35
+ config_path = project_root / "lazysafe.toml"
36
+
37
+ if not config_path.exists():
38
+ return config
39
+
40
+ with open(config_path, "rb") as f:
41
+ raw = tomllib.load(f)
42
+
43
+ unknown = set(raw.keys()) - _KNOWN_KEYS
44
+ if unknown:
45
+ raise ValueError(f"unknown config key: {unknown.pop()!r}")
46
+
47
+ for key in _KNOWN_KEYS:
48
+ if key in raw:
49
+ setattr(config, key, raw[key])
50
+
51
+ return config