# Copyright (c) 2015-2021, 2024-2025 by Rocky Bernstein # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import marshal import os.path as osp import py_compile import sys import tempfile import types from datetime import datetime from os import close from struct import pack, unpack from types import CodeType import xdis.marsh import xdis.unmarshal from xdis.dropbox.decrypt25 import fix_dropbox_pyc from xdis.magics import ( GRAAL3_MAGICS, JYTHON_MAGICS, PYPY3_MAGICS, PYTHON_MAGIC_INT, RUSTPYTHON_MAGICS, int2magic, magic2int, magic_int2tuple, magicint2version, py_str2tuple, versions, ) from xdis.version_info import PYTHON3, PYTHON_VERSION_TRIPLE def is_python_source(path) -> bool: try: data = open(path, "r").read() except UnicodeDecodeError: for encoding in ("utf-8", "utf-16", "latin-1", "iso-8859-15"): try: data = open(path, "r", encoding=encoding).read() except UnicodeDecodeError: pass else: break except Exception: return False try: compile(data, path, "exec") except Exception: return False return True def is_bytecode_extension(path: str) -> bool: """ Return True if filename ``path`` is named like a bytecode file, that is, has extension ".pyc" or ".pyo" """ return path.endswith(".pyc") or path.endswith(".pyo") # FIXME: the function name is weird. This checks and returns the path. def check_object_path(path: str) -> str: if not is_bytecode_extension(path) and is_python_source(path): try: import importlib bytecode_path = importlib.util.cache_from_source(path, optimization="") if osp.exists(bytecode_path): return bytecode_path except Exception: try: import imp imp.cache_from_source(path, debug_override=False) except Exception: pass pass basename = osp.basename(path)[0:-3] if PYTHON3: spath = path else: spath = path.decode("utf-8") # It would be better to use a context manager function like WithNamedTemporary. # However we are seeing write errors when this is done in Windows. # So until this is resolved, we'll use mkstemp and explicitly do a close. fd, path = tempfile.mkstemp(prefix=basename + "-", suffix=".pyc", text=False) close(fd) py_compile.compile(spath, cfile=path, doraise=True) if not is_bytecode_extension(path): raise ValueError( f"Path {path} must point to a Python source that can be compiled, or Python bytecode (.pyc, .pyo)\n" ) return path def is_pypy(magic_int: int, filename) -> bool: # PyPy 3.8 starts pyston's trend of using Python's magic numbers. if magic_int in (3413, 3414) and filename.endswith("pypy38.pyc"): return True return magic_int in ((62211 + 7, 3180 + 7) + PYPY3_MAGICS) def load_file(filename: str, out=sys.stdout) -> CodeType: """ load a Python source file and compile it to byte-code _load_file(filename: string): code_object filename: name of file containing Python source code (normally a .py) code_object: code_object compiled from this source code This function does NOT write any file! """ fp = open(filename, "rb") try: source = fp.read() try: if PYTHON_VERSION_TRIPLE < (2, 6): co = compile(source, filename, "exec") else: co = compile(source, filename, "exec", dont_inherit=True) except SyntaxError: out.write(f">>Syntax error in {filename}\n") raise finally: fp.close() return co def load_module( filename: str, code_objects=None, fast_load: bool = False, get_code: bool = True ): """load a module without importing it. Parameters: filename: name of file containing Python byte-code object (normally a .pyc) code_objects: list of additional code_object from this file. This might be a types.CodeType or one of the portable xdis code types, e.g. Code38, Code3, Code2, etc. This can be empty. fast_load: If True, then use Python's builtin loader. This can be done only if the bytecode version matches the current bytecode interpreter. get_code: Parsing the code object takes a bit of parsing time, but sometimes all you want is the module info, time string, code size, python version, etc. For that, set `get_code` to `False`. Return values are as follows: version_tuple: a tuple version number for the given magic_int, e.g. (2, 7) or (3, 4) timestamp: int; the seconds since EPOCH of the time of the bytecode creation, or None if no timestamp was stored magic_int: int, a bytecode-specific version number. This is related to the Python version number, the two aren't quite the same thing. co : code object ispypy : True if this was a PyPy code object source_size: The size of the source code mod 2**32, if that was stored in the bytecode. None otherwise. sip_hash : the SIP Hash for the file (only in Python 3.7 or greater), if the file was created with a SIP hash or None otherwise. Note that if the sip_hash is not none, then the timestamp and source_size will be invalid. """ # Some sanity checks if not osp.exists(filename): raise ImportError(f"File name: '{filename}' doesn't exist") elif not osp.isfile(filename): raise ImportError(f"File name: '{filename}' isn't a file") elif osp.getsize(filename) < 50: raise ImportError( "File name: '%s (%d bytes)' is too short to be a valid pyc file" % (filename, osp.getsize(filename)) ) with open(filename, "rb") as fp: return load_module_from_file_object( fp, filename=filename, code_objects=code_objects, fast_load=fast_load, get_code=get_code, ) def load_module_from_file_object( fp, filename="", code_objects=None, fast_load=False, get_code=True ): """load a module from a file object without importing it. See :func:load_module for a list of return values. """ if code_objects is None: code_objects = {} timestamp = 0 try: magic = fp.read(4) magic_int = magic2int(magic) # For reasons I don't understand, PyPy 3.2 stores a magic # of '0'... The two values below are for Python 2.x and 3.x respectively if magic[0:1] in ["0", b"0"]: magic = int2magic(3180 + 7) try: # FIXME: use the internal routine below tuple_version = magic_int2tuple(magic_int) except KeyError: if len(magic) >= 2: raise ImportError( "Unknown magic number %s in %s" % (ord(magic[0:1]) + 256 * ord(magic[1:2]), filename) ) else: raise ImportError(f"Bad magic number: '{magic}'") if magic_int in [2657, 22138] + list(GRAAL3_MAGICS) + list(RUSTPYTHON_MAGICS) + list(JYTHON_MAGICS): version = magicint2version.get(magic_int, "") raise ImportError(f"Magic int {magic_int} ({version}) is not supported.") if magic_int in ( 3010, 3020, 3030, 3040, 3050, 3060, 3061, 3071, 3361, 3091, 3101, 3103, 3141, 3270, 3280, 3290, 3300, 3320, 3330, 3371, 62071, 62071, 62081, 62091, 62092, 62111, ): raise ImportError( "%s is interim Python %s (%d) bytecode which is " "not supported.\nFinal released versions are " "supported." % (filename, versions[magic], magic2int(magic)) ) elif magic_int == 62135: fp.seek(0) return fix_dropbox_pyc(fp) elif magic_int == 62215: raise ImportError( "%s is a dropbox-hacked Python %s (bytecode %d).\n" "See https://github.com/kholia/dedrop for how to " "decrypt." % (filename, versions[magic], magic2int(magic)) ) try: my_magic_int = PYTHON_MAGIC_INT magic_int = magic2int(magic) version = magic_int2tuple(magic_int) timestamp = None source_size = None sip_hash = None ts = fp.read(4) if magic_int in (3439,) or version >= (3, 7): # PEP 552. https://www.python.org/dev/peps/pep-0552/ pep_bits = ts[-1] if PYTHON_VERSION_TRIPLE <= (2, 7): pep_bits = ord(pep_bits) if (pep_bits & 1) or magic_int == 3393: # 3393 is 3.7.0beta3 # SipHash sip_hash = unpack("= (1, 5) or magic_int in list(PYPY3_MAGICS) + [2657] ): source_size = unpack(" None: """Write bytecode file _bytecode_path_, with code for having Python magic_int (i.e. bytecode associated with some version of Python) """ fp = open(bytecode_path, "wb") version = py_str2tuple(magicint2version[magic_int]) if version >= (3, 0): fp.write(pack("= (3, 7): # pep552 bytes fp.write(pack("= (3, 3): # In Python 3.3+, these 4 bytes are the size of the source code_obj file (mod 2^32) fp.write(pack("