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.
Files changed (68) hide show
  1. __static__/__init__.py +641 -0
  2. __static__/compiler_flags.py +8 -0
  3. __static__/enum.py +160 -0
  4. __static__/native_utils.py +77 -0
  5. __static__/type_code.py +48 -0
  6. __strict__/__init__.py +39 -0
  7. _cinderx.so +0 -0
  8. cinderx/__init__.py +577 -0
  9. cinderx/__pycache__/__init__.cpython-314.pyc +0 -0
  10. cinderx/_asyncio.py +156 -0
  11. cinderx/compileall.py +710 -0
  12. cinderx/compiler/__init__.py +40 -0
  13. cinderx/compiler/__main__.py +137 -0
  14. cinderx/compiler/config.py +7 -0
  15. cinderx/compiler/consts.py +72 -0
  16. cinderx/compiler/debug.py +70 -0
  17. cinderx/compiler/dis_stable.py +283 -0
  18. cinderx/compiler/errors.py +151 -0
  19. cinderx/compiler/flow_graph_optimizer.py +1287 -0
  20. cinderx/compiler/future.py +91 -0
  21. cinderx/compiler/misc.py +32 -0
  22. cinderx/compiler/opcode_cinder.py +18 -0
  23. cinderx/compiler/opcode_static.py +100 -0
  24. cinderx/compiler/opcodebase.py +158 -0
  25. cinderx/compiler/opcodes.py +991 -0
  26. cinderx/compiler/optimizer.py +547 -0
  27. cinderx/compiler/pyassem.py +3711 -0
  28. cinderx/compiler/pycodegen.py +7660 -0
  29. cinderx/compiler/pysourceloader.py +62 -0
  30. cinderx/compiler/static/__init__.py +1404 -0
  31. cinderx/compiler/static/compiler.py +629 -0
  32. cinderx/compiler/static/declaration_visitor.py +335 -0
  33. cinderx/compiler/static/definite_assignment_checker.py +280 -0
  34. cinderx/compiler/static/effects.py +160 -0
  35. cinderx/compiler/static/module_table.py +666 -0
  36. cinderx/compiler/static/type_binder.py +2176 -0
  37. cinderx/compiler/static/types.py +10580 -0
  38. cinderx/compiler/static/util.py +81 -0
  39. cinderx/compiler/static/visitor.py +91 -0
  40. cinderx/compiler/strict/__init__.py +69 -0
  41. cinderx/compiler/strict/class_conflict_checker.py +249 -0
  42. cinderx/compiler/strict/code_gen_base.py +409 -0
  43. cinderx/compiler/strict/common.py +507 -0
  44. cinderx/compiler/strict/compiler.py +352 -0
  45. cinderx/compiler/strict/feature_extractor.py +130 -0
  46. cinderx/compiler/strict/flag_extractor.py +97 -0
  47. cinderx/compiler/strict/loader.py +827 -0
  48. cinderx/compiler/strict/preprocessor.py +11 -0
  49. cinderx/compiler/strict/rewriter/__init__.py +5 -0
  50. cinderx/compiler/strict/rewriter/remove_annotations.py +84 -0
  51. cinderx/compiler/strict/rewriter/rewriter.py +975 -0
  52. cinderx/compiler/strict/runtime.py +77 -0
  53. cinderx/compiler/symbols.py +1754 -0
  54. cinderx/compiler/unparse.py +414 -0
  55. cinderx/compiler/visitor.py +194 -0
  56. cinderx/jit.py +230 -0
  57. cinderx/opcode.py +202 -0
  58. cinderx/static.py +113 -0
  59. cinderx/strictmodule.py +6 -0
  60. cinderx/test_support.py +341 -0
  61. cinderx-2026.1.16.2.dist-info/METADATA +15 -0
  62. cinderx-2026.1.16.2.dist-info/RECORD +68 -0
  63. cinderx-2026.1.16.2.dist-info/WHEEL +6 -0
  64. cinderx-2026.1.16.2.dist-info/licenses/LICENSE +21 -0
  65. cinderx-2026.1.16.2.dist-info/top_level.txt +5 -0
  66. opcodes/__init__.py +0 -0
  67. opcodes/assign_opcode_numbers.py +272 -0
  68. 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__()