cinderx 2026.1.16.2__cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.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.
- __static__/__init__.py +641 -0
- __static__/compiler_flags.py +8 -0
- __static__/enum.py +160 -0
- __static__/native_utils.py +77 -0
- __static__/type_code.py +48 -0
- __strict__/__init__.py +39 -0
- _cinderx.so +0 -0
- cinderx/__init__.py +577 -0
- cinderx/__pycache__/__init__.cpython-314.pyc +0 -0
- cinderx/_asyncio.py +156 -0
- cinderx/compileall.py +710 -0
- cinderx/compiler/__init__.py +40 -0
- cinderx/compiler/__main__.py +137 -0
- cinderx/compiler/config.py +7 -0
- cinderx/compiler/consts.py +72 -0
- cinderx/compiler/debug.py +70 -0
- cinderx/compiler/dis_stable.py +283 -0
- cinderx/compiler/errors.py +151 -0
- cinderx/compiler/flow_graph_optimizer.py +1287 -0
- cinderx/compiler/future.py +91 -0
- cinderx/compiler/misc.py +32 -0
- cinderx/compiler/opcode_cinder.py +18 -0
- cinderx/compiler/opcode_static.py +100 -0
- cinderx/compiler/opcodebase.py +158 -0
- cinderx/compiler/opcodes.py +991 -0
- cinderx/compiler/optimizer.py +547 -0
- cinderx/compiler/pyassem.py +3711 -0
- cinderx/compiler/pycodegen.py +7660 -0
- cinderx/compiler/pysourceloader.py +62 -0
- cinderx/compiler/static/__init__.py +1404 -0
- cinderx/compiler/static/compiler.py +629 -0
- cinderx/compiler/static/declaration_visitor.py +335 -0
- cinderx/compiler/static/definite_assignment_checker.py +280 -0
- cinderx/compiler/static/effects.py +160 -0
- cinderx/compiler/static/module_table.py +666 -0
- cinderx/compiler/static/type_binder.py +2176 -0
- cinderx/compiler/static/types.py +10580 -0
- cinderx/compiler/static/util.py +81 -0
- cinderx/compiler/static/visitor.py +91 -0
- cinderx/compiler/strict/__init__.py +69 -0
- cinderx/compiler/strict/class_conflict_checker.py +249 -0
- cinderx/compiler/strict/code_gen_base.py +409 -0
- cinderx/compiler/strict/common.py +507 -0
- cinderx/compiler/strict/compiler.py +352 -0
- cinderx/compiler/strict/feature_extractor.py +130 -0
- cinderx/compiler/strict/flag_extractor.py +97 -0
- cinderx/compiler/strict/loader.py +827 -0
- cinderx/compiler/strict/preprocessor.py +11 -0
- cinderx/compiler/strict/rewriter/__init__.py +5 -0
- cinderx/compiler/strict/rewriter/remove_annotations.py +84 -0
- cinderx/compiler/strict/rewriter/rewriter.py +975 -0
- cinderx/compiler/strict/runtime.py +77 -0
- cinderx/compiler/symbols.py +1754 -0
- cinderx/compiler/unparse.py +414 -0
- cinderx/compiler/visitor.py +194 -0
- cinderx/jit.py +230 -0
- cinderx/opcode.py +202 -0
- cinderx/static.py +113 -0
- cinderx/strictmodule.py +6 -0
- cinderx/test_support.py +341 -0
- cinderx-2026.1.16.2.dist-info/METADATA +15 -0
- cinderx-2026.1.16.2.dist-info/RECORD +68 -0
- cinderx-2026.1.16.2.dist-info/WHEEL +6 -0
- cinderx-2026.1.16.2.dist-info/licenses/LICENSE +21 -0
- cinderx-2026.1.16.2.dist-info/top_level.txt +5 -0
- opcodes/__init__.py +0 -0
- opcodes/assign_opcode_numbers.py +272 -0
- opcodes/cinderx_opcodes.py +121 -0
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
# pyre-strict
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
try: # ensure all imports in this module are eager, to avoid cycles
|
|
8
|
+
import _imp
|
|
9
|
+
import builtins
|
|
10
|
+
import importlib
|
|
11
|
+
import marshal
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
# pyre-ignore[21]: typeshed doesn't know about this
|
|
17
|
+
from importlib import _bootstrap, _pack_uint32
|
|
18
|
+
|
|
19
|
+
# pyre-ignore[21]: typeshed doesn't know about this
|
|
20
|
+
from importlib._bootstrap_external import (
|
|
21
|
+
_classify_pyc,
|
|
22
|
+
_compile_bytecode,
|
|
23
|
+
_validate_hash_pyc,
|
|
24
|
+
_validate_timestamp_pyc,
|
|
25
|
+
)
|
|
26
|
+
from importlib.abc import Loader
|
|
27
|
+
from importlib.machinery import (
|
|
28
|
+
BYTECODE_SUFFIXES,
|
|
29
|
+
EXTENSION_SUFFIXES,
|
|
30
|
+
ExtensionFileLoader,
|
|
31
|
+
FileFinder,
|
|
32
|
+
ModuleSpec,
|
|
33
|
+
SOURCE_SUFFIXES,
|
|
34
|
+
SourceFileLoader,
|
|
35
|
+
SourcelessFileLoader,
|
|
36
|
+
)
|
|
37
|
+
from importlib.util import cache_from_source, MAGIC_NUMBER
|
|
38
|
+
from io import BytesIO
|
|
39
|
+
from os import getenv, makedirs
|
|
40
|
+
from os.path import dirname, isdir
|
|
41
|
+
from py_compile import (
|
|
42
|
+
_get_default_invalidation_mode,
|
|
43
|
+
PycInvalidationMode,
|
|
44
|
+
PyCompileError,
|
|
45
|
+
)
|
|
46
|
+
from types import CodeType, ModuleType
|
|
47
|
+
from typing import Callable, cast, Collection, final, Iterable, Mapping
|
|
48
|
+
|
|
49
|
+
from _cinderx import StrictModule, watch_sys_modules
|
|
50
|
+
from cinderx.static import install_sp_audit_hook
|
|
51
|
+
|
|
52
|
+
from ..consts import CI_CO_STATICALLY_COMPILED
|
|
53
|
+
from .common import (
|
|
54
|
+
DEFAULT_STUB_PATH,
|
|
55
|
+
FIXED_MODULES,
|
|
56
|
+
MAGIC_NUMBER as STRICT_MAGIC_NUMBER,
|
|
57
|
+
)
|
|
58
|
+
from .compiler import Compiler, Dependencies, SourceInfo, TIMING_LOGGER_TYPE
|
|
59
|
+
from .flag_extractor import Flags
|
|
60
|
+
except BaseException:
|
|
61
|
+
raise
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_MAGIC_STRICT_OR_STATIC: bytes = (STRICT_MAGIC_NUMBER + 2**15).to_bytes(
|
|
65
|
+
2, "little"
|
|
66
|
+
) + b"\r\n"
|
|
67
|
+
# We don't actually need to increment anything here, because the strict modules
|
|
68
|
+
# AST rewrite has no impact on pycs for non-strict modules. So we just always
|
|
69
|
+
# use two zero bytes. This simplifies generating "fake" strict pycs for
|
|
70
|
+
# known-not-to-be-strict third-party modules.
|
|
71
|
+
_MAGIC_NEITHER_STRICT_NOR_STATIC: bytes = (0).to_bytes(2, "little") + b"\r\n"
|
|
72
|
+
_MAGIC_LEN: int = len(_MAGIC_STRICT_OR_STATIC)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@final
|
|
76
|
+
class _PatchState(Enum):
|
|
77
|
+
"""Singleton used for tracking values which have not yet been patched."""
|
|
78
|
+
|
|
79
|
+
Patched = 1
|
|
80
|
+
Deleted = 2
|
|
81
|
+
Unpatched = 3
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Unfortunately module passed in could be a mock object,
|
|
85
|
+
# which also has a `patch` method that clashes with the StrictModule method.
|
|
86
|
+
# Directly get the function to avoid name clash.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _set_patch(module: StrictModule, name: str, value: object) -> None:
|
|
90
|
+
type(module).patch(module, name, value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _del_patch(module: StrictModule, name: str) -> None:
|
|
94
|
+
type(module).patch_delete(module, name)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@final
|
|
98
|
+
class StrictModuleTestingPatchProxy:
|
|
99
|
+
"""Provides a proxy object which enables patching of a strict module if the
|
|
100
|
+
module has been loaded with the StrictSourceWithPatchingFileLoader. The
|
|
101
|
+
proxy can be used as a context manager in which case exiting the with block
|
|
102
|
+
will result in the patches being disabled. The process will be terminated
|
|
103
|
+
if the patches are not unapplied and the proxy is deallocated."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, module: StrictModule) -> None:
|
|
106
|
+
object.__setattr__(self, "module", module)
|
|
107
|
+
object.__setattr__(self, "_patches", {})
|
|
108
|
+
object.__setattr__(self, "__name__", module.__name__)
|
|
109
|
+
object.__setattr__(
|
|
110
|
+
self, "_final_constants", getattr(module, "__final_constants__", ())
|
|
111
|
+
)
|
|
112
|
+
# pyre-ignore[16]: pyre doesn't understand properties well enough
|
|
113
|
+
if not type(module).__patch_enabled__.__get__(module, type(module)):
|
|
114
|
+
raise ValueError(f"strict module {module} does not allow patching")
|
|
115
|
+
|
|
116
|
+
def __setattr__(self, name: str, value: object) -> None:
|
|
117
|
+
patches = object.__getattribute__(self, "_patches")
|
|
118
|
+
prev_patched = patches.get(name, _PatchState.Unpatched)
|
|
119
|
+
module = object.__getattribute__(self, "module")
|
|
120
|
+
final_constants = object.__getattribute__(self, "_final_constants")
|
|
121
|
+
if name in final_constants:
|
|
122
|
+
raise AttributeError(
|
|
123
|
+
f"Cannot patch Final attribute `{name}` of module `{module.__name__}`"
|
|
124
|
+
)
|
|
125
|
+
if value is prev_patched:
|
|
126
|
+
# We're restoring the previous value
|
|
127
|
+
del patches[name]
|
|
128
|
+
elif prev_patched is _PatchState.Unpatched:
|
|
129
|
+
# We're overwriting a value
|
|
130
|
+
# only set patches[name] when name is patched for the first time
|
|
131
|
+
patches[name] = getattr(module, name, _PatchState.Patched)
|
|
132
|
+
|
|
133
|
+
if value is _PatchState.Deleted:
|
|
134
|
+
_del_patch(module, name)
|
|
135
|
+
else:
|
|
136
|
+
_set_patch(module, name, value)
|
|
137
|
+
|
|
138
|
+
def __delattr__(self, name: str) -> None:
|
|
139
|
+
StrictModuleTestingPatchProxy.__setattr__(self, name, _PatchState.Deleted)
|
|
140
|
+
|
|
141
|
+
def __getattribute__(self, name: str) -> object:
|
|
142
|
+
res = getattr(object.__getattribute__(self, "module"), name)
|
|
143
|
+
return res
|
|
144
|
+
|
|
145
|
+
def __enter__(self) -> StrictModuleTestingPatchProxy:
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, *excinfo: object) -> None:
|
|
149
|
+
StrictModuleTestingPatchProxy.cleanup(self)
|
|
150
|
+
|
|
151
|
+
def cleanup(self, ignore: Collection[str] | None = None) -> None:
|
|
152
|
+
patches = object.__getattribute__(self, "_patches")
|
|
153
|
+
module = object.__getattribute__(self, "module")
|
|
154
|
+
for name, value in list(patches.items()):
|
|
155
|
+
if ignore and name in ignore:
|
|
156
|
+
del patches[name]
|
|
157
|
+
continue
|
|
158
|
+
if value is _PatchState.Patched:
|
|
159
|
+
# value is patched means that module originally
|
|
160
|
+
# does not contain this field.
|
|
161
|
+
try:
|
|
162
|
+
_del_patch(module, name)
|
|
163
|
+
except AttributeError:
|
|
164
|
+
pass
|
|
165
|
+
finally:
|
|
166
|
+
del patches[name]
|
|
167
|
+
else:
|
|
168
|
+
setattr(self, name, value)
|
|
169
|
+
assert not patches
|
|
170
|
+
|
|
171
|
+
def __del__(self) -> None:
|
|
172
|
+
patches = object.__getattribute__(self, "_patches")
|
|
173
|
+
if patches:
|
|
174
|
+
print(
|
|
175
|
+
"Patch(es)",
|
|
176
|
+
", ".join(patches.keys()),
|
|
177
|
+
"failed to be detached from strict module",
|
|
178
|
+
"'" + object.__getattribute__(self, "module").__name__ + "'",
|
|
179
|
+
file=sys.stderr,
|
|
180
|
+
)
|
|
181
|
+
# There's a test that depends on this being mocked out.
|
|
182
|
+
os.abort()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__builtins__: ModuleType
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class StrictBytecodeError(ImportError):
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def classify_strict_pyc(
|
|
193
|
+
data: bytes, name: str, exc_details: dict[str, str]
|
|
194
|
+
) -> tuple[int, bool]:
|
|
195
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
196
|
+
flags = _classify_pyc(data[_MAGIC_LEN:], name, exc_details)
|
|
197
|
+
magic = data[:_MAGIC_LEN]
|
|
198
|
+
if magic == _MAGIC_NEITHER_STRICT_NOR_STATIC:
|
|
199
|
+
strict_or_static = False
|
|
200
|
+
elif magic == _MAGIC_STRICT_OR_STATIC:
|
|
201
|
+
strict_or_static = True
|
|
202
|
+
else:
|
|
203
|
+
raise StrictBytecodeError(
|
|
204
|
+
f"Bad magic number {magic!r} in {exc_details['path']}"
|
|
205
|
+
)
|
|
206
|
+
return (flags, strict_or_static)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# A dependency is (name, mtime_and_size_or_hash)
|
|
210
|
+
DependencyTuple = tuple[str, bytes]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def validate_dependencies(
|
|
214
|
+
compiler: Compiler, deps: tuple[DependencyTuple, ...], hash_based: bool
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Raise ImportError if any dependency has changed."""
|
|
217
|
+
for modname, data in deps:
|
|
218
|
+
# empty invalidation data means non-static
|
|
219
|
+
was_static = bool(data)
|
|
220
|
+
source_info = compiler.get_source(
|
|
221
|
+
modname, need_contents=(hash_based or not was_static)
|
|
222
|
+
)
|
|
223
|
+
if source_info is None:
|
|
224
|
+
if was_static:
|
|
225
|
+
raise ImportError(f"{modname} is missing")
|
|
226
|
+
continue
|
|
227
|
+
if was_static:
|
|
228
|
+
expected_data = get_dependency_data(source_info, hash_based)
|
|
229
|
+
if data != expected_data:
|
|
230
|
+
raise ImportError(f"{modname} has changed")
|
|
231
|
+
else:
|
|
232
|
+
assert source_info.source is not None
|
|
233
|
+
# if a previously non-static dependency is now found and has the text
|
|
234
|
+
# "import __static__" in it, we invalidate
|
|
235
|
+
if b"import __static__" in source_info.source:
|
|
236
|
+
raise ImportError(f"{modname} may now be static")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_deps(
|
|
240
|
+
deps: Dependencies | None, hash_based: bool
|
|
241
|
+
) -> tuple[DependencyTuple, ...]:
|
|
242
|
+
ret = []
|
|
243
|
+
if deps is None:
|
|
244
|
+
return ()
|
|
245
|
+
for source_info in deps.static:
|
|
246
|
+
ret.append(
|
|
247
|
+
(
|
|
248
|
+
source_info.name,
|
|
249
|
+
get_dependency_data(source_info, hash_based),
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
for modname in deps.nonstatic:
|
|
253
|
+
ret.append((modname, b""))
|
|
254
|
+
return tuple(ret)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def get_dependency_data(source_info: SourceInfo, hash_based: bool) -> bytes:
|
|
258
|
+
if hash_based:
|
|
259
|
+
assert source_info.source is not None
|
|
260
|
+
return importlib.util.source_hash(source_info.source)
|
|
261
|
+
else:
|
|
262
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
263
|
+
return _pack_uint32(source_info.mtime) + _pack_uint32(source_info.size)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def code_to_strict_timestamp_pyc(
|
|
267
|
+
code: CodeType,
|
|
268
|
+
strict_or_static: bool,
|
|
269
|
+
deps: Dependencies | None,
|
|
270
|
+
mtime: int = 0,
|
|
271
|
+
source_size: int = 0,
|
|
272
|
+
) -> bytearray:
|
|
273
|
+
"Produce the data for a strict timestamp-based pyc."
|
|
274
|
+
data = bytearray(
|
|
275
|
+
_MAGIC_STRICT_OR_STATIC
|
|
276
|
+
if strict_or_static
|
|
277
|
+
else _MAGIC_NEITHER_STRICT_NOR_STATIC
|
|
278
|
+
)
|
|
279
|
+
data.extend(MAGIC_NUMBER)
|
|
280
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
281
|
+
data.extend(_pack_uint32(0))
|
|
282
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
283
|
+
data.extend(_pack_uint32(mtime))
|
|
284
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
285
|
+
data.extend(_pack_uint32(source_size))
|
|
286
|
+
data.extend(marshal.dumps(get_deps(deps, hash_based=False)))
|
|
287
|
+
data.extend(marshal.dumps(code))
|
|
288
|
+
return data
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def code_to_strict_hash_pyc(
|
|
292
|
+
code: CodeType,
|
|
293
|
+
strict_or_static: bool,
|
|
294
|
+
deps: Dependencies | None,
|
|
295
|
+
source_hash: bytes,
|
|
296
|
+
checked: bool = True,
|
|
297
|
+
) -> bytearray:
|
|
298
|
+
"Produce the data for a strict hash-based pyc."
|
|
299
|
+
data = bytearray(
|
|
300
|
+
_MAGIC_STRICT_OR_STATIC
|
|
301
|
+
if strict_or_static
|
|
302
|
+
else _MAGIC_NEITHER_STRICT_NOR_STATIC
|
|
303
|
+
)
|
|
304
|
+
data.extend(MAGIC_NUMBER)
|
|
305
|
+
flags = 0b1 | checked << 1
|
|
306
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
307
|
+
data.extend(_pack_uint32(flags))
|
|
308
|
+
assert len(source_hash) == 8
|
|
309
|
+
data.extend(source_hash)
|
|
310
|
+
data.extend(marshal.dumps(get_deps(deps, hash_based=True)))
|
|
311
|
+
data.extend(marshal.dumps(code))
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class StrictSourceFileLoader(SourceFileLoader):
|
|
316
|
+
compiler: Compiler | None = None
|
|
317
|
+
module: ModuleType | None = None
|
|
318
|
+
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
fullname: str,
|
|
322
|
+
path: str,
|
|
323
|
+
import_path: Iterable[str] | None = None,
|
|
324
|
+
stub_path: str | None = None,
|
|
325
|
+
allow_list_prefix: Iterable[str] | None = None,
|
|
326
|
+
allow_list_exact: Iterable[str] | None = None,
|
|
327
|
+
enable_patching: bool = False,
|
|
328
|
+
log_source_load: Callable[[str, str | None, bool], None] | None = None,
|
|
329
|
+
init_cached_properties: None
|
|
330
|
+
| (
|
|
331
|
+
Callable[
|
|
332
|
+
[Mapping[str, str | tuple[str, bool]]],
|
|
333
|
+
Callable[[type[object]], type[object]],
|
|
334
|
+
]
|
|
335
|
+
) = None,
|
|
336
|
+
log_time_func: Callable[[], TIMING_LOGGER_TYPE] | None = None,
|
|
337
|
+
use_py_compiler: bool = False,
|
|
338
|
+
# The regexes are parsed on the C++ side, so re.Pattern is not accepted.
|
|
339
|
+
allow_list_regex: Iterable[str] | None = None,
|
|
340
|
+
) -> None:
|
|
341
|
+
self.name = fullname
|
|
342
|
+
self.path = path
|
|
343
|
+
self.import_path: Iterable[str] = import_path or list(sys.path)
|
|
344
|
+
configured_stub_path = sys._xoptions.get("strict-module-stubs-path") or getenv(
|
|
345
|
+
"PYTHONSTRICTMODULESTUBSPATH"
|
|
346
|
+
)
|
|
347
|
+
if stub_path is None:
|
|
348
|
+
stub_path = configured_stub_path or DEFAULT_STUB_PATH
|
|
349
|
+
if stub_path and not isdir(stub_path):
|
|
350
|
+
raise ValueError(f"Strict module stubs path does not exist: {stub_path}")
|
|
351
|
+
self.stub_path: str = stub_path
|
|
352
|
+
self.allow_list_prefix: Iterable[str] = allow_list_prefix or []
|
|
353
|
+
self.allow_list_exact: Iterable[str] = allow_list_exact or []
|
|
354
|
+
self.allow_list_regex: Iterable[str] = allow_list_regex or []
|
|
355
|
+
self.enable_patching = enable_patching
|
|
356
|
+
self.log_source_load: Callable[[str, str | None, bool], None] | None = (
|
|
357
|
+
log_source_load
|
|
358
|
+
)
|
|
359
|
+
self.bytecode_found = False
|
|
360
|
+
self.bytecode_path: str | None = None
|
|
361
|
+
self.init_cached_properties = init_cached_properties
|
|
362
|
+
self.log_time_func = log_time_func
|
|
363
|
+
self.use_py_compiler = use_py_compiler
|
|
364
|
+
self.strict_or_static: bool = False
|
|
365
|
+
self.is_static: bool = False
|
|
366
|
+
|
|
367
|
+
@classmethod
|
|
368
|
+
def ensure_compiler(
|
|
369
|
+
cls,
|
|
370
|
+
path: Iterable[str],
|
|
371
|
+
stub_path: str,
|
|
372
|
+
allow_list_prefix: Iterable[str],
|
|
373
|
+
allow_list_exact: Iterable[str],
|
|
374
|
+
log_time_func: Callable[[], TIMING_LOGGER_TYPE] | None,
|
|
375
|
+
enable_patching: bool = False,
|
|
376
|
+
allow_list_regex: Iterable[str] | None = None,
|
|
377
|
+
) -> Compiler:
|
|
378
|
+
if (comp := cls.compiler) is None:
|
|
379
|
+
comp = cls.compiler = Compiler(
|
|
380
|
+
path,
|
|
381
|
+
stub_path,
|
|
382
|
+
allow_list_prefix,
|
|
383
|
+
allow_list_exact,
|
|
384
|
+
raise_on_error=True,
|
|
385
|
+
log_time_func=log_time_func,
|
|
386
|
+
enable_patching=enable_patching,
|
|
387
|
+
allow_list_regex=allow_list_regex or [],
|
|
388
|
+
)
|
|
389
|
+
return comp
|
|
390
|
+
|
|
391
|
+
def get_compiler(self) -> Compiler:
|
|
392
|
+
return self.ensure_compiler(
|
|
393
|
+
self.import_path,
|
|
394
|
+
self.stub_path,
|
|
395
|
+
self.allow_list_prefix,
|
|
396
|
+
self.allow_list_exact,
|
|
397
|
+
self.log_time_func,
|
|
398
|
+
self.enable_patching,
|
|
399
|
+
self.allow_list_regex,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def get_code(self, fullname: str) -> CodeType:
|
|
403
|
+
source_path = self.get_filename(fullname)
|
|
404
|
+
source_mtime = None
|
|
405
|
+
source_bytes = None
|
|
406
|
+
source_hash = None
|
|
407
|
+
hash_based = False
|
|
408
|
+
check_source = True
|
|
409
|
+
try:
|
|
410
|
+
bytecode_path = cache_from_source(source_path)
|
|
411
|
+
except NotImplementedError:
|
|
412
|
+
bytecode_path = None
|
|
413
|
+
else:
|
|
414
|
+
bytecode_path = self.bytecode_path = add_strict_tag(
|
|
415
|
+
bytecode_path, self.enable_patching
|
|
416
|
+
)
|
|
417
|
+
try:
|
|
418
|
+
st = self.path_stats(source_path)
|
|
419
|
+
except OSError:
|
|
420
|
+
pass
|
|
421
|
+
else:
|
|
422
|
+
source_mtime = int(st["mtime"])
|
|
423
|
+
try:
|
|
424
|
+
data = self.get_data(bytecode_path)
|
|
425
|
+
except OSError:
|
|
426
|
+
pass
|
|
427
|
+
else:
|
|
428
|
+
self.bytecode_found = True
|
|
429
|
+
exc_details = {
|
|
430
|
+
"name": fullname,
|
|
431
|
+
"path": bytecode_path,
|
|
432
|
+
}
|
|
433
|
+
try:
|
|
434
|
+
flags, strict_or_static = classify_strict_pyc(
|
|
435
|
+
data, fullname, exc_details
|
|
436
|
+
)
|
|
437
|
+
self.strict_or_static = strict_or_static
|
|
438
|
+
# unmarshal dependencies
|
|
439
|
+
bytes_data = BytesIO(data)
|
|
440
|
+
bytes_data.seek(20)
|
|
441
|
+
deps = marshal.load(bytes_data)
|
|
442
|
+
hash_based = flags & 0b1 != 0
|
|
443
|
+
if hash_based:
|
|
444
|
+
check_source = flags & 0b10 != 0
|
|
445
|
+
if _imp.check_hash_based_pycs != "never" and (
|
|
446
|
+
check_source or _imp.check_hash_based_pycs == "always"
|
|
447
|
+
):
|
|
448
|
+
source_bytes = self.get_data(source_path)
|
|
449
|
+
source_hash = importlib.util.source_hash(source_bytes)
|
|
450
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
451
|
+
_validate_hash_pyc(
|
|
452
|
+
data[_MAGIC_LEN:],
|
|
453
|
+
source_hash,
|
|
454
|
+
fullname,
|
|
455
|
+
exc_details,
|
|
456
|
+
)
|
|
457
|
+
if deps:
|
|
458
|
+
validate_dependencies(
|
|
459
|
+
self.get_compiler(), deps, hash_based=True
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
463
|
+
_validate_timestamp_pyc(
|
|
464
|
+
data[_MAGIC_LEN:],
|
|
465
|
+
source_mtime,
|
|
466
|
+
st["size"],
|
|
467
|
+
fullname,
|
|
468
|
+
exc_details,
|
|
469
|
+
)
|
|
470
|
+
if deps:
|
|
471
|
+
validate_dependencies(
|
|
472
|
+
self.get_compiler(), deps, hash_based=False
|
|
473
|
+
)
|
|
474
|
+
except (ImportError, EOFError):
|
|
475
|
+
pass
|
|
476
|
+
else:
|
|
477
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
478
|
+
_bootstrap._verbose_message(
|
|
479
|
+
"{} matches {}", bytecode_path, source_path
|
|
480
|
+
)
|
|
481
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
482
|
+
return _compile_bytecode(
|
|
483
|
+
memoryview(data)[bytes_data.tell() :],
|
|
484
|
+
name=fullname,
|
|
485
|
+
bytecode_path=bytecode_path,
|
|
486
|
+
source_path=source_path,
|
|
487
|
+
)
|
|
488
|
+
if source_bytes is None:
|
|
489
|
+
source_bytes = self.get_data(source_path)
|
|
490
|
+
code_object = self.source_to_code(source_bytes, source_path)
|
|
491
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
492
|
+
_bootstrap._verbose_message("code object from {}", source_path)
|
|
493
|
+
if (
|
|
494
|
+
not sys.dont_write_bytecode
|
|
495
|
+
and bytecode_path is not None
|
|
496
|
+
and source_mtime is not None
|
|
497
|
+
):
|
|
498
|
+
if self.is_static:
|
|
499
|
+
deps = self.get_compiler().get_dependencies(fullname)
|
|
500
|
+
else:
|
|
501
|
+
deps = None
|
|
502
|
+
if hash_based:
|
|
503
|
+
if source_hash is None:
|
|
504
|
+
source_hash = importlib.util.source_hash(source_bytes)
|
|
505
|
+
data = code_to_strict_hash_pyc(
|
|
506
|
+
code_object,
|
|
507
|
+
self.strict_or_static,
|
|
508
|
+
deps,
|
|
509
|
+
source_hash,
|
|
510
|
+
check_source,
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
data = code_to_strict_timestamp_pyc(
|
|
514
|
+
code_object,
|
|
515
|
+
self.strict_or_static,
|
|
516
|
+
deps,
|
|
517
|
+
source_mtime,
|
|
518
|
+
len(source_bytes),
|
|
519
|
+
)
|
|
520
|
+
try:
|
|
521
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
522
|
+
self._cache_bytecode(source_path, bytecode_path, data)
|
|
523
|
+
except NotImplementedError:
|
|
524
|
+
pass
|
|
525
|
+
return code_object
|
|
526
|
+
|
|
527
|
+
def should_force_strict(self) -> bool:
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
# pyre-fixme[14]: `source_to_code` overrides method defined in `InspectLoader`
|
|
531
|
+
# inconsistently.
|
|
532
|
+
def source_to_code(
|
|
533
|
+
self, data: bytes | str, path: str, *, _optimize: int = -1
|
|
534
|
+
) -> CodeType:
|
|
535
|
+
log_source_load = self.log_source_load
|
|
536
|
+
if log_source_load is not None:
|
|
537
|
+
log_source_load(path, self.bytecode_path, self.bytecode_found)
|
|
538
|
+
# pyre-ignore[28]: typeshed doesn't know about _optimize arg
|
|
539
|
+
code = super().source_to_code(data, path, _optimize=_optimize)
|
|
540
|
+
force = self.should_force_strict()
|
|
541
|
+
if force or "__strict__" in code.co_names or "__static__" in code.co_names:
|
|
542
|
+
# Since a namespace package will never call `source_to_code` (there
|
|
543
|
+
# is no source!), there are only two possibilities here: non-package
|
|
544
|
+
# (submodule_search_paths should be None) or regular package
|
|
545
|
+
# (submodule_search_paths should have one entry, the directory
|
|
546
|
+
# containing the "__init__.py").
|
|
547
|
+
submodule_search_locations = None
|
|
548
|
+
if path.endswith("__init__.py"):
|
|
549
|
+
submodule_search_locations = [path[:12]]
|
|
550
|
+
# Usually _optimize will be -1 (which means "default to the value
|
|
551
|
+
# of sys.flags.optimize"). But this default happens very deep in
|
|
552
|
+
# Python's compiler (in PyAST_CompileObject), so if we just pass
|
|
553
|
+
# around -1 and rely on that, it means we can't make any of our own
|
|
554
|
+
# decisions based on that flag. So instead we do the default right
|
|
555
|
+
# here, so we have the correct optimize flag value throughout our
|
|
556
|
+
# compiler.
|
|
557
|
+
opt = sys.flags.optimize if _optimize == -1 else _optimize
|
|
558
|
+
# Let the ast transform attempt to validate the strict module. This
|
|
559
|
+
# will return an unmodified module if import __strict__ isn't
|
|
560
|
+
# actually at the top-level
|
|
561
|
+
(
|
|
562
|
+
code,
|
|
563
|
+
is_valid_strict,
|
|
564
|
+
is_static,
|
|
565
|
+
) = self.get_compiler().load_compiled_module_from_source(
|
|
566
|
+
data,
|
|
567
|
+
path,
|
|
568
|
+
self.name,
|
|
569
|
+
opt,
|
|
570
|
+
submodule_search_locations,
|
|
571
|
+
override_flags=Flags(is_strict=force),
|
|
572
|
+
)
|
|
573
|
+
self.strict_or_static = is_valid_strict or is_static
|
|
574
|
+
self.is_static = is_static
|
|
575
|
+
assert code is not None
|
|
576
|
+
return code
|
|
577
|
+
|
|
578
|
+
self.strict_or_static = False
|
|
579
|
+
return code
|
|
580
|
+
|
|
581
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
582
|
+
# This ends up being slightly convoluted, because create_module
|
|
583
|
+
# gets called, then source_to_code gets called, so we don't know if
|
|
584
|
+
# we have a strict module until after we were requested to create it.
|
|
585
|
+
# So we'll run the module code we get back in the module that was
|
|
586
|
+
# initially published in sys.modules, check and see if it's a strict
|
|
587
|
+
# module, and then run the strict module body after replacing the
|
|
588
|
+
# entry in sys.modules with a StrictModule entry. This shouldn't
|
|
589
|
+
# really be observable because no user code runs between publishing
|
|
590
|
+
# the normal module in sys.modules and replacing it with the
|
|
591
|
+
# StrictModule.
|
|
592
|
+
code = self.get_code(module.__name__)
|
|
593
|
+
if code is None:
|
|
594
|
+
raise ImportError(
|
|
595
|
+
f"Cannot import module {module.__name__}; get_code() returned None"
|
|
596
|
+
)
|
|
597
|
+
# fix up the pyc path
|
|
598
|
+
cached = getattr(module.__spec__, "cached", None)
|
|
599
|
+
if cached:
|
|
600
|
+
cached = add_strict_tag(cached, self.enable_patching)
|
|
601
|
+
if module.__spec__ is not None:
|
|
602
|
+
module.__spec__.cached = cached
|
|
603
|
+
if sys.version_info < (3, 15):
|
|
604
|
+
module.__cached__ = cached
|
|
605
|
+
spec: ModuleSpec | None = module.__spec__
|
|
606
|
+
|
|
607
|
+
if self.strict_or_static:
|
|
608
|
+
if spec is None:
|
|
609
|
+
raise ImportError(f"Missing module spec for {module.__name__}")
|
|
610
|
+
|
|
611
|
+
new_dict = {
|
|
612
|
+
"<fixed-modules>": cast(object, FIXED_MODULES),
|
|
613
|
+
"<builtins>": builtins.__dict__,
|
|
614
|
+
"<init-cached-properties>": self.init_cached_properties,
|
|
615
|
+
}
|
|
616
|
+
if code.co_flags & CI_CO_STATICALLY_COMPILED:
|
|
617
|
+
init_static_python()
|
|
618
|
+
new_dict["<imported-from>"] = code.co_consts[-1]
|
|
619
|
+
|
|
620
|
+
new_dict.update(module.__dict__)
|
|
621
|
+
strict_mod = StrictModule(new_dict, self.enable_patching)
|
|
622
|
+
|
|
623
|
+
sys.modules[module.__name__] = strict_mod
|
|
624
|
+
|
|
625
|
+
exec(code, new_dict)
|
|
626
|
+
else:
|
|
627
|
+
exec(code, module.__dict__)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class StrictSourceFileLoaderWithPatching(StrictSourceFileLoader):
|
|
631
|
+
def __init__(
|
|
632
|
+
self,
|
|
633
|
+
fullname: str,
|
|
634
|
+
path: str,
|
|
635
|
+
import_path: Iterable[str] | None = None,
|
|
636
|
+
stub_path: str | None = None,
|
|
637
|
+
allow_list_prefix: Iterable[str] | None = None,
|
|
638
|
+
allow_list_exact: Iterable[str] | None = None,
|
|
639
|
+
enable_patching: bool = True,
|
|
640
|
+
log_source_load: Callable[[str, str | None, bool], None] | None = None,
|
|
641
|
+
init_cached_properties: None
|
|
642
|
+
| (
|
|
643
|
+
Callable[
|
|
644
|
+
[Mapping[str, str | tuple[str, bool]]],
|
|
645
|
+
Callable[[type[object]], type[object]],
|
|
646
|
+
]
|
|
647
|
+
) = None,
|
|
648
|
+
log_time_func: Callable[[], TIMING_LOGGER_TYPE] | None = None,
|
|
649
|
+
use_py_compiler: bool = False,
|
|
650
|
+
# The regexes are parsed on the C++ side, so re.Pattern is not accepted.
|
|
651
|
+
allow_list_regex: Iterable[str] | None = None,
|
|
652
|
+
) -> None:
|
|
653
|
+
super().__init__(
|
|
654
|
+
fullname,
|
|
655
|
+
path,
|
|
656
|
+
import_path,
|
|
657
|
+
stub_path,
|
|
658
|
+
allow_list_prefix,
|
|
659
|
+
allow_list_exact,
|
|
660
|
+
enable_patching,
|
|
661
|
+
log_source_load,
|
|
662
|
+
init_cached_properties,
|
|
663
|
+
log_time_func,
|
|
664
|
+
use_py_compiler,
|
|
665
|
+
allow_list_regex,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def add_strict_tag(path: str, enable_patching: bool) -> str:
|
|
670
|
+
base, __, ext = path.rpartition(".")
|
|
671
|
+
enable_patching_marker = ".patch" if enable_patching else ""
|
|
672
|
+
|
|
673
|
+
return f"{base}.strict{enable_patching_marker}.{ext}"
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _get_supported_file_loaders(
|
|
677
|
+
enable_patching: bool = False,
|
|
678
|
+
) -> list[tuple[type[Loader], list[str]]]:
|
|
679
|
+
"""Returns a list of file-based module loaders.
|
|
680
|
+
|
|
681
|
+
Each item is a tuple (loader, suffixes).
|
|
682
|
+
"""
|
|
683
|
+
extensions = ExtensionFileLoader, EXTENSION_SUFFIXES
|
|
684
|
+
source = (
|
|
685
|
+
(
|
|
686
|
+
StrictSourceFileLoaderWithPatching
|
|
687
|
+
if enable_patching
|
|
688
|
+
else StrictSourceFileLoader
|
|
689
|
+
),
|
|
690
|
+
SOURCE_SUFFIXES,
|
|
691
|
+
)
|
|
692
|
+
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
|
|
693
|
+
return [extensions, source, bytecode]
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def strict_compile(
|
|
697
|
+
file: str,
|
|
698
|
+
cfile: str,
|
|
699
|
+
dfile: str | None = None,
|
|
700
|
+
doraise: bool = False,
|
|
701
|
+
optimize: int = -1,
|
|
702
|
+
invalidation_mode: PycInvalidationMode | None = None,
|
|
703
|
+
loader_override: object = None,
|
|
704
|
+
loader_options: dict[str, str | int | bool] | None = None,
|
|
705
|
+
) -> str | None:
|
|
706
|
+
"""Byte-compile one Python source file to Python bytecode, using strict loader.
|
|
707
|
+
|
|
708
|
+
:param file: The source file name.
|
|
709
|
+
:param cfile: The target byte compiled file name.
|
|
710
|
+
:param dfile: Purported file name, i.e. the file name that shows up in
|
|
711
|
+
error messages. Defaults to the source file name.
|
|
712
|
+
:param doraise: Flag indicating whether or not an exception should be
|
|
713
|
+
raised when a compile error is found. If an exception occurs and this
|
|
714
|
+
flag is set to False, a string indicating the nature of the exception
|
|
715
|
+
will be printed, and the function will return to the caller. If an
|
|
716
|
+
exception occurs and this flag is set to True, a PyCompileError
|
|
717
|
+
exception will be raised.
|
|
718
|
+
:param optimize: The optimization level for the compiler. Valid values
|
|
719
|
+
are -1, 0, 1 and 2. A value of -1 means to use the optimization
|
|
720
|
+
level of the current interpreter, as given by -O command line options.
|
|
721
|
+
:return: Path to the resulting byte compiled file.
|
|
722
|
+
|
|
723
|
+
Copied and modified from https://github.com/python/cpython/blob/3.6/Lib/py_compile.py#L65
|
|
724
|
+
|
|
725
|
+
This version does not support cfile=None, since compileall never passes that.
|
|
726
|
+
|
|
727
|
+
"""
|
|
728
|
+
modname = file
|
|
729
|
+
for dir in sys.path:
|
|
730
|
+
if file.startswith(dir):
|
|
731
|
+
modname = file[len(dir) :]
|
|
732
|
+
break
|
|
733
|
+
|
|
734
|
+
modname = modname.replace("/", ".")
|
|
735
|
+
if modname.endswith("__init__.py"):
|
|
736
|
+
modname = modname[: -len("__init__.py")]
|
|
737
|
+
elif modname.endswith(".py"):
|
|
738
|
+
modname = modname[: -len(".py")]
|
|
739
|
+
modname = modname.strip(".")
|
|
740
|
+
|
|
741
|
+
if loader_options is None:
|
|
742
|
+
loader_options = {}
|
|
743
|
+
|
|
744
|
+
# TODO we ignore loader_override
|
|
745
|
+
loader = StrictSourceFileLoader(
|
|
746
|
+
modname,
|
|
747
|
+
file,
|
|
748
|
+
import_path=sys.path,
|
|
749
|
+
**loader_options,
|
|
750
|
+
)
|
|
751
|
+
cfile = add_strict_tag(cfile, enable_patching=loader.enable_patching)
|
|
752
|
+
source_bytes = loader.get_data(file)
|
|
753
|
+
try:
|
|
754
|
+
code = loader.source_to_code(source_bytes, dfile or file, _optimize=optimize)
|
|
755
|
+
deps = loader.get_compiler().get_dependencies(modname)
|
|
756
|
+
except Exception as err:
|
|
757
|
+
raise
|
|
758
|
+
py_exc = PyCompileError(err.__class__, err, dfile or file)
|
|
759
|
+
if doraise:
|
|
760
|
+
raise py_exc
|
|
761
|
+
else:
|
|
762
|
+
sys.stderr.write(py_exc.msg + "\n")
|
|
763
|
+
return
|
|
764
|
+
|
|
765
|
+
makedirs(dirname(cfile), exist_ok=True)
|
|
766
|
+
|
|
767
|
+
if invalidation_mode is None:
|
|
768
|
+
invalidation_mode = _get_default_invalidation_mode()
|
|
769
|
+
if invalidation_mode == PycInvalidationMode.TIMESTAMP:
|
|
770
|
+
source_stats = loader.path_stats(file)
|
|
771
|
+
bytecode = code_to_strict_timestamp_pyc(
|
|
772
|
+
code,
|
|
773
|
+
loader.strict_or_static,
|
|
774
|
+
deps,
|
|
775
|
+
source_stats["mtime"],
|
|
776
|
+
source_stats["size"],
|
|
777
|
+
)
|
|
778
|
+
else:
|
|
779
|
+
source_hash = importlib.util.source_hash(source_bytes)
|
|
780
|
+
bytecode = code_to_strict_hash_pyc(
|
|
781
|
+
code,
|
|
782
|
+
loader.strict_or_static,
|
|
783
|
+
deps,
|
|
784
|
+
source_hash,
|
|
785
|
+
(invalidation_mode == PycInvalidationMode.CHECKED_HASH),
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# pyre-ignore[16]: typeshed doesn't know about this
|
|
789
|
+
loader._cache_bytecode(file, cfile, bytecode)
|
|
790
|
+
return cfile
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def init_static_python() -> None:
|
|
794
|
+
"""Idempotent global initialization of Static Python.
|
|
795
|
+
|
|
796
|
+
Should be called at least once if any Static modules/functions exist.
|
|
797
|
+
"""
|
|
798
|
+
watch_sys_modules()
|
|
799
|
+
install_sp_audit_hook()
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def install(enable_patching: bool = False) -> None:
|
|
803
|
+
"""Installs a loader which is capable of loading and validating strict modules"""
|
|
804
|
+
supported_loaders = _get_supported_file_loaders(enable_patching)
|
|
805
|
+
|
|
806
|
+
for index, hook in enumerate(sys.path_hooks):
|
|
807
|
+
if not isinstance(hook, type):
|
|
808
|
+
sys.path_hooks.insert(index, FileFinder.path_hook(*supported_loaders))
|
|
809
|
+
break
|
|
810
|
+
else:
|
|
811
|
+
sys.path_hooks.insert(0, FileFinder.path_hook(*supported_loaders))
|
|
812
|
+
|
|
813
|
+
# We need to clear the path_importer_cache so that our new FileFinder will
|
|
814
|
+
# start being used for existing directories we've loaded modules from.
|
|
815
|
+
sys.path_importer_cache.clear()
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
if __name__ == "__main__":
|
|
819
|
+
install()
|
|
820
|
+
del sys.argv[0]
|
|
821
|
+
mod: object = __import__(sys.argv[0])
|
|
822
|
+
if not isinstance(mod, StrictModule):
|
|
823
|
+
raise TypeError(
|
|
824
|
+
"compiler.strict.loader should be used to run strict modules: "
|
|
825
|
+
+ type(mod).__name__
|
|
826
|
+
)
|
|
827
|
+
mod.__main__()
|