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.
- pythonfaster/__init__.py +92 -0
- pythonfaster/_bootstrap.py +89 -0
- pythonfaster/_cli.py +232 -0
- pythonfaster/cache.py +285 -0
- pythonfaster/compiler.py +425 -0
- pythonfaster/config.py +165 -0
- pythonfaster/hook.py +307 -0
- pythonfaster/py.typed +0 -0
- pythonfaster/transform/__init__.py +10 -0
- pythonfaster/transform/engine.py +3033 -0
- pythonfaster/transform/recpass.py +522 -0
- pythonfaster-1.8.0.data/data/pythonfaster.pth +1 -0
- pythonfaster-1.8.0.dist-info/METADATA +362 -0
- pythonfaster-1.8.0.dist-info/RECORD +18 -0
- pythonfaster-1.8.0.dist-info/WHEEL +5 -0
- pythonfaster-1.8.0.dist-info/entry_points.txt +2 -0
- pythonfaster-1.8.0.dist-info/licenses/LICENSE +21 -0
- pythonfaster-1.8.0.dist-info/top_level.txt +1 -0
pythonfaster/__init__.py
ADDED
|
@@ -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
|