pythonfaster 1.8.0__py3-none-any.whl

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.
@@ -0,0 +1,92 @@
1
+ """pythonfaster — zero-intrusion Python acceleration via Cython AOT compilation.
2
+
3
+ Three things only:
4
+ 1. Import hook — transparently compiles .py → .so on import
5
+ 2. Type inference — int→longlong, float→double, range vars→longlong
6
+ 3. Object acceleration — cdef classes + cfunc methods + loop var typing
7
+
8
+ Usage (global auto-activation — recommended)::
9
+
10
+ pip install pythonfaster
11
+ pythonfaster enable # one-time, installs .pth, <1 second
12
+ # Done — all Python processes auto-accelerate projects with
13
+ # pyproject.toml / .git markers in the CWD tree.
14
+
15
+ Usage (explicit, in code — per-project only)::
16
+
17
+ from pythonfaster import Config, install
18
+ install(Config(root="."))
19
+
20
+ When using the explicit form, the .pth file is also auto-installed
21
+ so that future Python processes benefit from global auto-activation
22
+ without needing to run ``pythonfaster enable`` separately.
23
+ """
24
+ __version__ = "1.8.0"
25
+
26
+ __all__ = ["Config", "install"]
27
+
28
+ _pth_installed = False
29
+
30
+
31
+ def _maybe_autoinstall_pth() -> None:
32
+ """Best-effort: install .pth file on first explicit use of pythonfaster.
33
+
34
+ This runs once per process when the user accesses ``pythonfaster.Config``
35
+ or ``pythonfaster.install``. If the .pth file is already present, this
36
+ is a single ``stat()`` call and returns immediately.
37
+ """
38
+ global _pth_installed
39
+ if _pth_installed:
40
+ return
41
+ _pth_installed = True
42
+ try:
43
+ import site
44
+ from pathlib import Path
45
+
46
+ content = "import pythonfaster._bootstrap\n"
47
+ # Check if already installed in any candidate directory.
48
+ candidate_dirs: list[Path] = []
49
+ if site.ENABLE_USER_SITE:
50
+ user_site = site.getusersitepackages()
51
+ if user_site:
52
+ candidate_dirs.append(Path(user_site))
53
+ for prefix in site.getsitepackages():
54
+ d = Path(prefix)
55
+ if d not in candidate_dirs:
56
+ candidate_dirs.append(d)
57
+
58
+ for d in candidate_dirs:
59
+ pth = d / "pythonfaster.pth"
60
+ if pth.exists():
61
+ return # already installed somewhere
62
+
63
+ # Install to the first writable directory.
64
+ for d in candidate_dirs:
65
+ try:
66
+ d.mkdir(parents=True, exist_ok=True)
67
+ (d / "pythonfaster.pth").write_text(content, encoding="utf-8")
68
+ return
69
+ except (PermissionError, OSError):
70
+ continue
71
+ except Exception:
72
+ pass # never break user's import
73
+
74
+
75
+ def __getattr__(name):
76
+ """Lazy-load public API on first access.
77
+
78
+ Keeps ``import pythonfaster`` near-zero cost — essential because the
79
+ ``.pth`` bootstrap does ``import pythonfaster._bootstrap`` on every Python
80
+ startup. Submodules (config, hook, cache, compiler) only load when
81
+ the user actually calls ``Config(...)`` or ``install(...)``.
82
+ Also auto-installs the .pth file on first explicit use.
83
+ """
84
+ if name == "Config":
85
+ _maybe_autoinstall_pth()
86
+ from .config import Config
87
+ return Config
88
+ if name == "install":
89
+ _maybe_autoinstall_pth()
90
+ from .hook import install
91
+ return install
92
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,89 @@
1
+ """Auto-install hook on Python startup via .pth file.
2
+
3
+ When a ``pythonfaster.pth`` file is present in a site-packages directory,
4
+ the ``site`` module executes ``import pythonfaster._bootstrap`` during Python
5
+ startup. This module reads the global activation state and, if enabled,
6
+ auto-detects the project root from the current working directory and
7
+ installs the import hook — all before any user code runs.
8
+
9
+ Safety guarantees:
10
+ - Never raises — any failure silently aborts; Python starts normally.
11
+ - Defaults to **enabled** when no state file exists (fresh install).
12
+ - ``pythonfaster disable`` writes ``{"enabled": false}`` to explicitly turn off.
13
+ - Respects ``PYTHONFASTER_DISABLE=1`` environment variable (fastest exit).
14
+ - Does nothing when no project root is found (no pyproject.toml /
15
+ .git / pythonfaster.toml in the CWD or any parent directory).
16
+ - The package ``__init__.py`` uses lazy ``__getattr__`` so importing
17
+ ``pythonfaster`` is near-zero cost — submodules load only on actual use.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ #: Re-export for convenience (used by _cli.py and tests).
27
+ PTH_NAME = "pythonfaster.pth"
28
+ PTH_CONTENT = "import pythonfaster._bootstrap\n"
29
+
30
+
31
+ def _read_state_fast() -> dict:
32
+ """Read global state without importing pythonfaster.config.
33
+
34
+ Returns ``{"enabled": True, "mode": "all"}`` when no state file
35
+ exists (fresh install) — pythonfaster is on by default. An explicit
36
+ ``{"enabled": false}`` written by ``pythonfaster disable`` turns it off.
37
+ """
38
+ xdg = os.environ.get("XDG_CONFIG_HOME")
39
+ base = Path(xdg) if xdg else Path.home() / ".config"
40
+ state_file = base / "pythonfaster" / "config.json"
41
+ try:
42
+ with open(state_file, "r", encoding="utf-8") as fh:
43
+ state = json.load(fh)
44
+ if isinstance(state, dict):
45
+ return state
46
+ except Exception:
47
+ pass
48
+ return {"enabled": True, "mode": "all"}
49
+
50
+
51
+ def _bootstrap() -> None:
52
+ """Entry point called on every Python startup (via .pth import).
53
+
54
+ Designed to be as fast as possible when pythonfaster is disabled:
55
+ one env-var check + one file read, then return.
56
+ """
57
+ try:
58
+ # --- Fast exit #1: env var (no file I/O at all) ---
59
+ if os.environ.get("PYTHONFASTER_DISABLE") == "1":
60
+ return
61
+
62
+ # --- Fast exit #2: global state disabled (one file read) ---
63
+ state = _read_state_fast()
64
+ if not state.get("enabled"):
65
+ return
66
+
67
+ # --- Enabled: now we can afford the full import cost ---
68
+ from .config import find_project_root, load_config
69
+ from .hook import install
70
+
71
+ # Detect project root from current working directory.
72
+ root = find_project_root(Path.cwd())
73
+ if root is None:
74
+ return
75
+
76
+ # Load project-specific config (pythonfaster.toml / pyproject.toml).
77
+ config = load_config(root)
78
+ if not config.enabled:
79
+ return
80
+
81
+ # Install the import hook.
82
+ install(config)
83
+ except Exception:
84
+ # Never break Python startup.
85
+ pass
86
+
87
+
88
+ # Execute on import (the .pth file does ``import pythonfaster._bootstrap``).
89
+ _bootstrap()
pythonfaster/_cli.py ADDED
@@ -0,0 +1,232 @@
1
+ """pythonfaster CLI — global activation control.
2
+
3
+ After ``pip install pythonfaster``, the state defaults to **enabled**.
4
+ The only missing piece is the ``.pth`` file that triggers the
5
+ bootstrap on every Python startup. Run ``pythonfaster enable`` once
6
+ to install it (takes <1 second) — then you're done forever.
7
+
8
+ Usage::
9
+
10
+ pythonfaster enable Install .pth file + ensure state is enabled (one-time setup).
11
+ pythonfaster disable Disable auto-acceleration globally.
12
+ pythonfaster status Show current activation state and .pth file location.
13
+ pythonfaster install-pth (Re)install the .pth file into site-packages.
14
+ pythonfaster remove-pth Remove the .pth file from all site-packages locations.
15
+
16
+ After ``pythonfaster enable``, every Python process reads the state file on
17
+ startup and auto-installs the import hook when a project root is detected.
18
+ ``pythonfaster disable`` flips the state to ``enabled: false`` (the .pth file
19
+ stays installed so re-enabling is instant — just ``pythonfaster enable`` again).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import site
26
+ import sys
27
+ from pathlib import Path
28
+
29
+ from .config import load_state, save_state, state_path
30
+
31
+ #: Name of the .pth file to install.
32
+ PTH_NAME = "pythonfaster.pth"
33
+
34
+ #: Content of the .pth file — a single import line executed by ``site``.
35
+ PTH_CONTENT = "import pythonfaster._bootstrap\n"
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # .pth file management
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def _pth_candidate_dirs() -> list[Path]:
43
+ """Return candidate directories for .pth installation, best first.
44
+
45
+ Preference order:
46
+ 1. User site-packages (survives venv recreation, no sudo needed).
47
+ 2. Global site-packages (fallback).
48
+ """
49
+ dirs: list[Path] = []
50
+ # User site — shared across venvs (preferred)
51
+ if site.ENABLE_USER_SITE:
52
+ user_site = site.getusersitepackages()
53
+ if user_site:
54
+ dirs.append(Path(user_site))
55
+ # Global site-packages
56
+ for prefix in site.getsitepackages():
57
+ d = Path(prefix)
58
+ if d not in dirs:
59
+ dirs.append(d)
60
+ return dirs
61
+
62
+
63
+ def _find_pth() -> Path | None:
64
+ """Find the currently installed .pth file, if any."""
65
+ for d in _pth_candidate_dirs():
66
+ p = d / PTH_NAME
67
+ if p.exists():
68
+ return p
69
+ return None
70
+
71
+
72
+ def _install_pth() -> Path | None:
73
+ """Install the .pth file into the first writable candidate directory."""
74
+ for d in _pth_candidate_dirs():
75
+ try:
76
+ d.mkdir(parents=True, exist_ok=True)
77
+ p = d / PTH_NAME
78
+ p.write_text(PTH_CONTENT, encoding="utf-8")
79
+ return p
80
+ except (PermissionError, OSError):
81
+ continue
82
+ return None
83
+
84
+
85
+ def _remove_pth() -> bool:
86
+ """Remove the .pth file from all candidate locations."""
87
+ removed = False
88
+ for d in _pth_candidate_dirs():
89
+ p = d / PTH_NAME
90
+ if p.exists():
91
+ try:
92
+ p.unlink()
93
+ removed = True
94
+ except OSError:
95
+ pass
96
+ return removed
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # Commands
101
+ # ---------------------------------------------------------------------------
102
+
103
+ def cmd_enable() -> int:
104
+ """Enable pythonfaster globally: ensure state is on + install .pth file.
105
+
106
+ After ``pip install pythonfaster``, the state is already enabled by default.
107
+ This command mainly installs the ``.pth`` file that triggers the
108
+ bootstrap on every Python startup.
109
+ """
110
+ save_state({"enabled": True, "mode": "all"})
111
+ pth = _install_pth()
112
+ if pth is None:
113
+ print(
114
+ "error: could not write .pth file to any site-packages directory.\n"
115
+ "Try: pip install --user pythonfaster (then re-run pythonfaster enable)",
116
+ file=sys.stderr,
117
+ )
118
+ return 1
119
+ print(f"pythonfaster enabled")
120
+ print(f" state: {state_path()} (enabled=true)")
121
+ print(f" .pth: {pth}")
122
+ print()
123
+ print("All Python processes will now auto-accelerate projects with")
124
+ print("pyproject.toml / pythonfaster.toml / .git markers in the CWD tree.")
125
+ print("Set PYTHONFASTER_DISABLE=1 to temporarily disable for a single process.")
126
+ return 0
127
+
128
+
129
+ def cmd_disable() -> int:
130
+ """Disable pythonfaster globally: flip state to enabled=false."""
131
+ prev = load_state()
132
+ save_state({"enabled": False, "mode": prev.get("mode", "all")})
133
+ # Keep the .pth file installed so re-enable is just a state flip.
134
+ pth = _find_pth()
135
+ print(f"pythonfaster disabled")
136
+ print(f" state: {state_path()} (enabled=false)")
137
+ if pth:
138
+ print(f" .pth: {pth} (kept installed for fast re-enable)")
139
+ print("Run 'pythonfaster enable' to re-activate. No restart needed.")
140
+ return 0
141
+
142
+
143
+ def cmd_status() -> int:
144
+ """Print current activation state and .pth file location."""
145
+ from .config import state_path
146
+ state = load_state()
147
+ pth = _find_pth()
148
+ state_exists = state_path().exists()
149
+
150
+ print(f"State file: {state_path()}")
151
+ if state_exists:
152
+ print(f"Enabled: {state.get('enabled', True)} (explicitly set)")
153
+ else:
154
+ print(f"Enabled: {state.get('enabled', True)} (default — no state file)")
155
+ print(f"Mode: {state.get('mode', 'all')}")
156
+ if pth:
157
+ print(f".pth file: {pth}")
158
+ content = pth.read_text(encoding="utf-8").strip()
159
+ print(f".pth content: {content!r}")
160
+ else:
161
+ print(f".pth file: not installed (run 'pythonfaster enable' to install)")
162
+
163
+ # Show whether the hook is active in the current process.
164
+ active = any(
165
+ type(f).__name__ == "PythonFasterFinder" for f in sys.meta_path
166
+ )
167
+ print(f"Hook active: {active}")
168
+ return 0
169
+
170
+
171
+ def cmd_install_pth() -> int:
172
+ """Force (re)install the .pth file."""
173
+ pth = _install_pth()
174
+ if pth is None:
175
+ print("error: could not write .pth file", file=sys.stderr)
176
+ return 1
177
+ print(f".pth installed at {pth}")
178
+ return 0
179
+
180
+
181
+ def cmd_remove_pth() -> int:
182
+ """Force remove the .pth file from all locations."""
183
+ if _remove_pth():
184
+ print(".pth file removed")
185
+ else:
186
+ print(".pth file was not found")
187
+ return 0
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Entry point
192
+ # ---------------------------------------------------------------------------
193
+
194
+ USAGE = """\
195
+ pythonfaster — zero-intrusion Python acceleration
196
+
197
+ State defaults to enabled after pip install. Run 'pythonfaster enable' once
198
+ to install the .pth file for global auto-activation.
199
+
200
+ Commands:
201
+ enable One-time setup: install .pth file + ensure state is enabled
202
+ disable Disable auto-acceleration globally
203
+ status Show current state and .pth file location
204
+ install-pth (Re)install the .pth file into site-packages
205
+ remove-pth Remove the .pth file from all site-packages locations
206
+ """
207
+
208
+
209
+ def main() -> int:
210
+ args = sys.argv[1:]
211
+ if not args or args[0] in ("-h", "--help", "help"):
212
+ print(USAGE)
213
+ return 0
214
+
215
+ cmd = args[0]
216
+ handlers = {
217
+ "enable": cmd_enable,
218
+ "disable": cmd_disable,
219
+ "status": cmd_status,
220
+ "install-pth": cmd_install_pth,
221
+ "remove-pth": cmd_remove_pth,
222
+ }
223
+ handler = handlers.get(cmd)
224
+ if handler is None:
225
+ print(f"error: unknown command '{cmd}'", file=sys.stderr)
226
+ print(USAGE)
227
+ return 1
228
+ return handler()
229
+
230
+
231
+ if __name__ == "__main__":
232
+ sys.exit(main())
pythonfaster/cache.py ADDED
@@ -0,0 +1,285 @@
1
+ """Cache layout, key computation, skip list and compile locking.
2
+
3
+ Layout (all outside the user project)::
4
+
5
+ $PYTHONFASTER_CACHE_DIR or ~/.cache/pythonfaster/
6
+ ├── cpython-312/
7
+ │ ├── build/<key>/ # transient build area (removed after success)
8
+ │ └── lib/<key>/ # final artifacts: <mod>.cpython-312-*.so
9
+ └── index.json # skip list + compile stats
10
+
11
+ The cache key covers every factor that influences the artifact:
12
+ source bytes, module name, Python version/ABI, Cython version,
13
+ optimization level, compiler flags and the strategy-set version.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+ import importlib.machinery
19
+ import json
20
+ import os
21
+ import platform
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ #: Bump whenever the transform/strategy set changes semantics of artifacts.
27
+ STRATEGY_SET_VERSION = "pythonfaster-1.8" # Passes 9-10: recursive lowering and sum-LICM folding
28
+
29
+ #: Compile flags baked into artifacts (and therefore into the key).
30
+ #: ``-march=native`` is NOT used by default: it can produce artifacts that
31
+ #: crash (SIGILL) when the cache is shared across machines with different
32
+ #: CPU micro-architectures (e.g. AVX2 vs AVX-512). Enable explicitly via
33
+ #: ``PYTHONFASTER_MARCH_NATIVE=1`` for single-machine, profiled use.
34
+ if os.name == "nt": # pragma: no cover - Windows support lands in M2+
35
+ COMPILE_FLAGS = ("/O2",)
36
+ else:
37
+ if os.environ.get("PYTHONFASTER_MARCH_NATIVE") == "1":
38
+ COMPILE_FLAGS = ("-O3", "-march=native")
39
+ else:
40
+ COMPILE_FLAGS = ("-O3",)
41
+
42
+ _LOCK_STALE_SECONDS = 600.0
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # paths & keying
47
+ # ---------------------------------------------------------------------------
48
+
49
+ def cache_root() -> Path:
50
+ env = os.environ.get("PYTHONFASTER_CACHE_DIR")
51
+ if env:
52
+ return Path(env)
53
+ xdg = os.environ.get("XDG_CACHE_HOME")
54
+ base = Path(xdg) if xdg else Path.home() / ".cache"
55
+ return base / "pythonfaster"
56
+
57
+
58
+ def interp_tag() -> str:
59
+ impl = platform.python_implementation().lower()
60
+ return f"{impl}-{sys.version_info.major}{sys.version_info.minor}"
61
+
62
+
63
+ def versioned_root() -> Path:
64
+ return cache_root() / interp_tag()
65
+
66
+
67
+ def ext_suffix() -> str:
68
+ return importlib.machinery.EXTENSION_SUFFIXES[0]
69
+
70
+
71
+ def _cpu_feature_tag() -> str:
72
+ """A coarse CPU-feature tag for cache key isolation.
73
+
74
+ When ``-march=native`` is active, artifacts may use CPU-specific
75
+ instructions. We include a tag derived from the platform machine and,
76
+ when available, a more specific CPU brand string so artifacts from
77
+ different micro-architectures don't collide.
78
+ """
79
+ parts = [platform.machine()]
80
+ if "-march=native" in COMPILE_FLAGS:
81
+ try:
82
+ brand = platform.processor() or ""
83
+ if brand:
84
+ parts.append(brand)
85
+ except Exception:
86
+ pass
87
+ return "|".join(parts)
88
+
89
+
90
+ def cache_key(*, source: bytes, module: str, level: int) -> str:
91
+ """Compute the artifact key. Every influencing factor must appear here."""
92
+ from Cython import __version__ as cython_version # lazy: keep bootstrap fast
93
+
94
+ h = hashlib.sha256()
95
+ h.update(b"pythonfaster-v1\0")
96
+ h.update(source)
97
+ h.update(b"\0")
98
+ h.update(module.encode())
99
+ h.update(b"\0")
100
+ h.update(interp_tag().encode())
101
+ h.update(b"\0")
102
+ h.update(_cpu_feature_tag().encode())
103
+ h.update(b"\0")
104
+ h.update(cython_version.encode())
105
+ h.update(b"\0")
106
+ h.update(str(level).encode())
107
+ h.update(b"\0")
108
+ h.update(" ".join(COMPILE_FLAGS).encode())
109
+ h.update(b"\0")
110
+ h.update(STRATEGY_SET_VERSION.encode())
111
+ return h.hexdigest()[:32]
112
+
113
+
114
+ def so_rel_path(module: str) -> Path:
115
+ """Path of the artifact relative to its lib dir (dotted -> nested)."""
116
+ parts = module.split(".")
117
+ return Path(*parts[:-1], parts[-1] + ext_suffix()) if len(parts) > 1 \
118
+ else Path(parts[0] + ext_suffix())
119
+
120
+
121
+ def so_lookup(key: str, module: str) -> Path | None:
122
+ """Return the cached .so path if present, else None."""
123
+ candidate = versioned_root() / "lib" / key / so_rel_path(module)
124
+ return candidate if candidate.exists() else None
125
+
126
+
127
+ def pyx_rel_path(module: str) -> Path:
128
+ """Path of the stored compiled-source copy relative to its lib dir."""
129
+ rel = so_rel_path(module)
130
+ return rel.parent / rel.name.replace(ext_suffix(), ".pyx")
131
+
132
+
133
+ def pyx_lookup(key: str, module: str) -> Path | None:
134
+ """Return the stored compiled source (.pyx actually compiled) if kept."""
135
+ candidate = versioned_root() / "lib" / key / pyx_rel_path(module)
136
+ return candidate if candidate.exists() else None
137
+
138
+
139
+ def lib_dir(key: str) -> Path:
140
+ return versioned_root() / "lib" / key
141
+
142
+
143
+ def build_dir(key: str) -> Path:
144
+ return versioned_root() / "build" / key
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # index: skip list + stats
149
+ # ---------------------------------------------------------------------------
150
+
151
+ class CacheIndex:
152
+ """Persistent index.json with skip list and compile statistics.
153
+
154
+ Writes are atomic (tmp + rename). Last-writer-wins across processes;
155
+ losing an occasional stat update is acceptable, the skip list is
156
+ best-effort protection against repeated failed compiles.
157
+ """
158
+
159
+ def __init__(self) -> None:
160
+ self.path = cache_root() / "index.json"
161
+ self._data = self._read()
162
+
163
+ def _read(self) -> dict:
164
+ try:
165
+ with open(self.path, "r", encoding="utf-8") as fh:
166
+ data = json.load(fh)
167
+ if isinstance(data, dict):
168
+ data.setdefault("skipped", {})
169
+ data.setdefault("compiled", {})
170
+ return data
171
+ except Exception:
172
+ pass
173
+ return {"version": 1, "skipped": {}, "compiled": {}}
174
+
175
+ def _write(self) -> None:
176
+ try:
177
+ self.path.parent.mkdir(parents=True, exist_ok=True)
178
+ # Use a unique temp file per process to avoid clobbering
179
+ # concurrent writes from other processes.
180
+ tmp = self.path.with_suffix(f".json.{os.getpid()}.tmp")
181
+ with open(tmp, "w", encoding="utf-8") as fh:
182
+ json.dump(self._data, fh, indent=1)
183
+ os.replace(tmp, self.path)
184
+ except Exception as exc:
185
+ # The index is advisory, so never break imports over it. Make
186
+ # persistent cache/index failures diagnosable when requested.
187
+ if os.environ.get("PYTHONFASTER_VERBOSE") == "1":
188
+ print(
189
+ f"[pythonfaster] warning: failed to write cache index: {exc}",
190
+ file=sys.stderr,
191
+ )
192
+
193
+ def refresh(self) -> None:
194
+ self._data = self._read()
195
+
196
+ def is_skipped(self, key: str) -> bool:
197
+ return key in self._data["skipped"]
198
+
199
+ def record_skip(self, key: str, module: str, reason: str) -> None:
200
+ self.refresh()
201
+ self._data["skipped"][key] = {
202
+ "module": module,
203
+ "reason": reason[:500],
204
+ "time": time.time(),
205
+ }
206
+ self._write()
207
+
208
+ def record_success(self, key: str, module: str, seconds: float,
209
+ strategies: list[str] | None = None) -> None:
210
+ self.refresh()
211
+ self._data["compiled"][key] = {
212
+ "module": module,
213
+ "seconds": round(seconds, 3),
214
+ "time": time.time(),
215
+ "strategies": strategies or [],
216
+ }
217
+ self._data["skipped"].pop(key, None)
218
+ self._write()
219
+
220
+ def stats(self) -> dict:
221
+ self.refresh()
222
+ return {
223
+ "compiled": len(self._data["compiled"]),
224
+ "skipped": len(self._data["skipped"]),
225
+ "skipped_detail": dict(self._data["skipped"]),
226
+ }
227
+
228
+
229
+ def cache_size_bytes() -> int:
230
+ total = 0
231
+ root = cache_root()
232
+ if root.exists():
233
+ for dirpath, _dirnames, filenames in os.walk(root):
234
+ for name in filenames:
235
+ try:
236
+ total += os.path.getsize(os.path.join(dirpath, name))
237
+ except OSError:
238
+ pass
239
+ return total
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # compile lock (cross-process, lockfile based)
244
+ # ---------------------------------------------------------------------------
245
+
246
+ class FileLock:
247
+ """Advisory cross-process lock via O_EXCL lockfile.
248
+
249
+ Stale locks (holder died) are broken after ``_LOCK_STALE_SECONDS``.
250
+ """
251
+
252
+ def __init__(self, path: Path, timeout: float = 300.0) -> None:
253
+ self.path = Path(str(path) + ".lock")
254
+ self.timeout = timeout
255
+
256
+ def __enter__(self) -> "FileLock":
257
+ self.path.parent.mkdir(parents=True, exist_ok=True)
258
+ deadline = time.monotonic() + self.timeout
259
+ stale_broken = False
260
+ while True:
261
+ try:
262
+ fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
263
+ os.close(fd)
264
+ return self
265
+ except FileExistsError:
266
+ if time.monotonic() > deadline:
267
+ raise TimeoutError(f"compile lock timeout: {self.path}")
268
+ try:
269
+ age = time.time() - self.path.stat().st_mtime
270
+ except OSError:
271
+ continue # lock vanished between checks; retry immediately
272
+ if age > _LOCK_STALE_SECONDS and not stale_broken:
273
+ stale_broken = True
274
+ try:
275
+ self.path.unlink()
276
+ except OSError:
277
+ pass
278
+ continue
279
+ time.sleep(0.05)
280
+
281
+ def __exit__(self, *exc: object) -> None:
282
+ try:
283
+ self.path.unlink()
284
+ except OSError:
285
+ pass