mit neuen venv und exe-Files
This commit is contained in:
465
venv3_12/Lib/site-packages/cx_Freeze/module.py
Normal file
465
venv3_12/Lib/site-packages/cx_Freeze/module.py
Normal file
@@ -0,0 +1,465 @@
|
||||
"""Base class for Module and ConstantsModule."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import socket
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timezone
|
||||
from functools import cached_property, partial
|
||||
from importlib import import_module
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from keyword import iskeyword
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
from cx_Freeze._compat import IS_MINGW, IS_WINDOWS
|
||||
from cx_Freeze._importlib import metadata
|
||||
from cx_Freeze.exception import ModuleError, OptionError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
from types import CodeType
|
||||
|
||||
__all__ = ["ConstantsModule", "Module", "ModuleHook"]
|
||||
|
||||
|
||||
class DistributionCache(metadata.PathDistribution):
|
||||
"""Cache the distribution package."""
|
||||
|
||||
def __init__(self, cache_path: Path, name: str) -> None:
|
||||
"""Construct a distribution.
|
||||
|
||||
:param cache_path: Path indicating where to store the cache.
|
||||
:param name: The name of the distribution package to cache.
|
||||
:raises ModuleError: When the named package's distribution
|
||||
metadata cannot be found.
|
||||
"""
|
||||
try:
|
||||
distribution = metadata.PathDistribution.from_name(name)
|
||||
except metadata.PackageNotFoundError:
|
||||
distribution = None
|
||||
if distribution is None:
|
||||
raise ModuleError(name)
|
||||
# Cache dist-info files in a temporary directory
|
||||
normalized_name = getattr(distribution, "_normalized_name", None)
|
||||
if normalized_name is None:
|
||||
normalized_name = metadata.Prepared.normalize(name)
|
||||
source_path = getattr(distribution, "_path", None)
|
||||
if source_path is None:
|
||||
mask = f"{normalized_name}-{distribution.version}.*-info"
|
||||
dist_path = list(distribution.locate_file(".").glob(mask))
|
||||
if not dist_path:
|
||||
mask = f"{name}-{distribution.version}.*-info"
|
||||
dist_path = list(distribution.locate_file(".").glob(mask))
|
||||
if dist_path:
|
||||
source_path = dist_path[0]
|
||||
if source_path is None or not source_path.exists():
|
||||
raise ModuleError(name)
|
||||
|
||||
dist_name = f"{normalized_name}-{distribution.version}.dist-info"
|
||||
target_path = cache_path / dist_name
|
||||
super().__init__(target_path)
|
||||
self.original = distribution
|
||||
self.normalized_name = normalized_name
|
||||
self.distinfo_name = dist_name
|
||||
if target_path.exists(): # cached
|
||||
return
|
||||
target_path.mkdir(parents=True)
|
||||
|
||||
purelib = None
|
||||
if source_path.name.endswith(".dist-info"):
|
||||
for source in source_path.rglob("*"): # type: Path
|
||||
target = target_path / source.relative_to(source_path)
|
||||
if source.is_dir():
|
||||
target.mkdir(exist_ok=True)
|
||||
else:
|
||||
target.write_bytes(source.read_bytes())
|
||||
elif source_path.is_file():
|
||||
# old egg-info file is converted to dist-info
|
||||
target = target_path / "METADATA"
|
||||
target.write_bytes(source_path.read_bytes())
|
||||
purelib = (source_path.parent / (normalized_name + ".py")).exists()
|
||||
else:
|
||||
# Copy minimal data from egg-info directory into dist-info
|
||||
source = source_path / "PKG-INFO"
|
||||
if source.is_file():
|
||||
target = target_path / "METADATA"
|
||||
target.write_bytes(source.read_bytes())
|
||||
source = source_path / "entry_points.txt"
|
||||
if source.is_file():
|
||||
target = target_path / "entry_points.txt"
|
||||
target.write_bytes(source.read_bytes())
|
||||
source = source_path / "top_level.txt"
|
||||
if source.is_file():
|
||||
target = target_path / "top_level.txt"
|
||||
target.write_bytes(source.read_bytes())
|
||||
purelib = not source_path.joinpath("not-zip-safe").is_file()
|
||||
|
||||
self._write_wheel_distinfo(purelib)
|
||||
self._write_record_distinfo()
|
||||
|
||||
def _write_wheel_distinfo(self, purelib: bool) -> None:
|
||||
"""Create a WHEEL file if it doesn't exist."""
|
||||
target = self.locate_file(f"{self.distinfo_name}/WHEEL")
|
||||
if not target.exists():
|
||||
project = Path(__file__).parent.name # cx_Freeze
|
||||
version = metadata.version(project)
|
||||
root_is_purelib = "true" if purelib else "false"
|
||||
text = [
|
||||
"Wheel-Version: 1.0",
|
||||
f"Generator: {project} ({version})",
|
||||
f"Root-Is-Purelib: {root_is_purelib}",
|
||||
"Tag: py3-none-any",
|
||||
]
|
||||
with target.open(mode="w", encoding="utf_8", newline="") as file:
|
||||
file.write("\n".join(text))
|
||||
|
||||
def _write_record_distinfo(self) -> None:
|
||||
"""Recreate a minimal RECORD file."""
|
||||
distinfo_name = self.distinfo_name
|
||||
target = self.locate_file(f"{distinfo_name}/RECORD")
|
||||
target_dir = target.parent
|
||||
record = [
|
||||
f"{distinfo_name}/{file.name},," for file in target_dir.iterdir()
|
||||
]
|
||||
record.append(f"{distinfo_name}/RECORD,,")
|
||||
with target.open(mode="w", encoding="utf_8", newline="") as file:
|
||||
file.write("\n".join(record))
|
||||
|
||||
@property
|
||||
def binary_files(self) -> list[str]:
|
||||
"""Return the binary files included in the package."""
|
||||
if IS_MINGW or IS_WINDOWS:
|
||||
return [
|
||||
file
|
||||
for file in self.original.files
|
||||
if file.suffix.lower() == ".dll"
|
||||
]
|
||||
extensions = tuple([ext for ext in EXTENSION_SUFFIXES if ext != ".so"])
|
||||
return [
|
||||
file
|
||||
for file in self.original.files
|
||||
if file.match("*.so*") and not file.name.endswith(extensions)
|
||||
]
|
||||
|
||||
@property
|
||||
def installer(self) -> str:
|
||||
"""Return the installer (pip, conda) for the distribution package."""
|
||||
# consider 'uv' as 'pip'
|
||||
value = self.read_text("INSTALLER") or "pip"
|
||||
return value.splitlines()[0].replace("uv", "pip")
|
||||
|
||||
@property
|
||||
def requires(self) -> list[str]:
|
||||
"""Generated requirements specified for this Distribution."""
|
||||
package_names = []
|
||||
requires = super().requires
|
||||
if requires:
|
||||
for requirement_string in requires:
|
||||
require = Requirement(requirement_string)
|
||||
if require.marker is None or require.marker.evaluate():
|
||||
package_names.append(require.name)
|
||||
return package_names
|
||||
|
||||
@property
|
||||
def version(self) -> tuple[int, ...] | str | None:
|
||||
"""Return the 'Version' metadata for the distribution package."""
|
||||
value = super().version
|
||||
with suppress(ValueError):
|
||||
value = tuple(map(int, value.split(".")))
|
||||
return value
|
||||
|
||||
|
||||
class Module:
|
||||
"""The Module class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
path: Sequence[Path | str] | None = None,
|
||||
filename: Path | str | None = None,
|
||||
parent: Module | None = None,
|
||||
) -> None:
|
||||
self.name: str = name
|
||||
self.path: list[Path] | None = list(map(Path, path)) if path else None
|
||||
self._file: Path | None = self._file_validate(filename)
|
||||
self.parent: Module | None = parent
|
||||
self.root: Module = parent.root if parent else self
|
||||
self.code: CodeType | None = None
|
||||
self.cache_path: Path | None = None
|
||||
self.distribution: DistributionCache | None = None
|
||||
self.hook: ModuleHook | Callable | None = None
|
||||
self.exclude_names: set[str] = set()
|
||||
self.global_names: set[str] = set()
|
||||
self.ignore_names: set[str] = set()
|
||||
self.in_import: bool = True
|
||||
self.source_is_string: bool = False
|
||||
self.source_is_zip_file: bool = False
|
||||
self._in_file_system: int = 1
|
||||
# add the load hook
|
||||
self.load_hook()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
parts = [f"name={self.name!r}"]
|
||||
if self.file is not None:
|
||||
parts.append(f"file={self.file!r}")
|
||||
if self.path is not None:
|
||||
parts.append(f"path={self.path!r}")
|
||||
join_parts = ", ".join(parts)
|
||||
return f"<Module {join_parts}>"
|
||||
|
||||
@property
|
||||
def file(self) -> Path | None:
|
||||
"""Module filename."""
|
||||
return self._file
|
||||
|
||||
@file.setter
|
||||
def file(self, filename: Path | str | None) -> None:
|
||||
self._file = self._file_validate(filename)
|
||||
|
||||
def _file_validate(self, filename: Path | str | None) -> Path | None:
|
||||
if "stub_code" in self.__dict__:
|
||||
del self.__dict__["stub_code"] # clear the cache
|
||||
if not filename:
|
||||
return None
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
return filename
|
||||
|
||||
@cached_property
|
||||
def stub_code(self) -> CodeType | None:
|
||||
cache_path: Path = self.cache_path
|
||||
filename = self._file
|
||||
if filename is None:
|
||||
return None
|
||||
ext = "".join(filename.suffixes)
|
||||
if ext not in EXTENSION_SUFFIXES:
|
||||
return None
|
||||
source_dir = self.root.file.parent
|
||||
package = filename.parent.relative_to(source_dir.parent)
|
||||
stem = filename.name.partition(ext)[0]
|
||||
stub_name = f"{stem}.pyi"
|
||||
# search for the stub file already parsed in the distribution
|
||||
importshed = Path(__file__).resolve().parent / "importshed"
|
||||
source_file = importshed / package / stub_name
|
||||
if source_file.exists():
|
||||
imports_only = source_file.read_text(encoding="utf_8")
|
||||
if imports_only:
|
||||
return compile(
|
||||
imports_only, stub_name, "exec", dont_inherit=True
|
||||
)
|
||||
# search for a stub file along side the python extension module
|
||||
source_file = filename.parent / stub_name
|
||||
if not source_file.exists():
|
||||
return None
|
||||
if cache_path:
|
||||
target_file = cache_path / package / stub_name
|
||||
if target_file.exists():
|
||||
# a parsed stub exists in the cache
|
||||
imports_only = target_file.read_text(encoding="utf_8")
|
||||
else:
|
||||
imports_only = self.get_imports_from_file(source_file)
|
||||
if imports_only:
|
||||
# cache the parsed stub
|
||||
target_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_file.write_text(
|
||||
"# Generated by cx_Freeze\n\n" + imports_only,
|
||||
encoding="utf_8",
|
||||
)
|
||||
else:
|
||||
imports_only = self.get_imports_from_file(source_file)
|
||||
if imports_only:
|
||||
return compile(imports_only, stub_name, "exec", dont_inherit=True)
|
||||
return None
|
||||
|
||||
def get_imports_from_file(self, source_file: Path) -> str | None:
|
||||
"""Get the implicit imports in a stub file."""
|
||||
if not source_file.is_file():
|
||||
return None
|
||||
ignore = {"__future__", "builtins", "typing", "_typeshed"}
|
||||
source = source_file.read_text(encoding="utf_8")
|
||||
try:
|
||||
rootnode = ast.parse(source, source_file.name)
|
||||
except SyntaxError:
|
||||
return None
|
||||
lines = []
|
||||
for node in ast.walk(rootnode):
|
||||
if isinstance(node, ast.Import):
|
||||
names = {name.name for name in node.names}
|
||||
names.difference_update(ignore)
|
||||
if names:
|
||||
lines.append("import " + ", ".join(sorted(names)))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level == 0 and node.module in ignore:
|
||||
continue
|
||||
names = {name.name for name in node.names}
|
||||
line = "from "
|
||||
if node.level > 0:
|
||||
line += "." * node.level
|
||||
if node.module:
|
||||
line += node.module
|
||||
line += " import " + ", ".join(sorted(names))
|
||||
lines.append(line)
|
||||
return "\n".join([*lines, ""]) if lines else None
|
||||
|
||||
@property
|
||||
def in_file_system(self) -> int:
|
||||
"""Returns a value indicating where the module/package will be stored:
|
||||
0. in a zip file (not directly in the file system)
|
||||
1. in the file system, package with modules and data
|
||||
2. in the file system, only detected modules.
|
||||
"""
|
||||
if self.parent is not None:
|
||||
return self.parent.in_file_system
|
||||
if self.path is None or self.file is None:
|
||||
return 0
|
||||
return self._in_file_system
|
||||
|
||||
@in_file_system.setter
|
||||
def in_file_system(self, value: int) -> None:
|
||||
self._in_file_system: int = value
|
||||
|
||||
def load_hook(self) -> None:
|
||||
"""Load hook for the given module if one is present.
|
||||
|
||||
For instance, a load hook for PyQt5.QtCore:
|
||||
- Using ModuleHook class:
|
||||
# module and hook methods are lowercased.
|
||||
hook = pyqt5.Hook()
|
||||
hook.qt_qtcore()
|
||||
- For functions present in hooks.__init__:
|
||||
# module and load hook functions use the original case.
|
||||
load_PyQt5_QtCore()
|
||||
- For functions in a separated module:
|
||||
# module and load hook functions are lowercased.
|
||||
pyqt5.load_pyqt5_qtcore()
|
||||
"""
|
||||
if not isinstance(self.root.hook, ModuleHook):
|
||||
try:
|
||||
# new style hook using ModuleHook class - top-level call
|
||||
root_name = self.root.name.lower()
|
||||
hooks = import_module(f"cx_Freeze.hooks.{root_name}")
|
||||
hook_cls = getattr(hooks, "Hook", None)
|
||||
if hook_cls and issubclass(hook_cls, ModuleHook):
|
||||
self.root.hook = hook_cls(self.root)
|
||||
else:
|
||||
# old style hook with lowercased functions
|
||||
name = self.name.replace(".", "_").lower()
|
||||
func = getattr(hooks, f"load_{name}", None)
|
||||
self.hook = partial(func, module=self) if func else None
|
||||
return
|
||||
except ImportError:
|
||||
# old style hook with functions at hooks.__init__
|
||||
hooks = import_module("cx_Freeze.hooks")
|
||||
name = self.name.replace(".", "_")
|
||||
func = getattr(hooks, f"load_{name}", None)
|
||||
self.hook = partial(func, module=self) if func else None
|
||||
return
|
||||
# new style hook using ModuleHook class - lower level call
|
||||
root_hook = self.root.hook
|
||||
if isinstance(root_hook, ModuleHook) and self.parent is not None:
|
||||
name = "_".join(self.name.lower().split(".")[1:])
|
||||
func = getattr(root_hook, f"{root_hook.name}_{name}", None)
|
||||
self.hook = partial(func, module=self) if func else None
|
||||
|
||||
def update_distribution(self, name: str | None = None) -> None:
|
||||
"""Update the distribution cache based on its name.
|
||||
This method may be used to link an distribution's name to a module.
|
||||
|
||||
Example: ModuleFinder cannot detects the distribution of _cffi_backend
|
||||
but in a hook we can link it to 'cffi'.
|
||||
"""
|
||||
cache_path: Path = self.cache_path
|
||||
if cache_path is None:
|
||||
return
|
||||
if name is None:
|
||||
name = self.name
|
||||
try:
|
||||
distribution = DistributionCache(cache_path, name)
|
||||
except ModuleError:
|
||||
return
|
||||
for req_name in distribution.requires:
|
||||
with suppress(ModuleError):
|
||||
DistributionCache(cache_path, req_name)
|
||||
self.distribution = distribution
|
||||
|
||||
|
||||
class ModuleHook:
|
||||
"""The Module Hook class."""
|
||||
|
||||
def __init__(self, module: Module) -> None:
|
||||
self.module = module
|
||||
self.name = module.name.replace(".", "_").lower()
|
||||
|
||||
def __call__(self, finder) -> None:
|
||||
# redirect to the top level hook
|
||||
method = getattr(self, self.name, None)
|
||||
if method:
|
||||
method(finder, self.module)
|
||||
|
||||
|
||||
class ConstantsModule:
|
||||
"""Base ConstantsModule class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
release_string: str | None = None,
|
||||
copyright_string: str | None = None,
|
||||
module_name: str = "BUILD_CONSTANTS",
|
||||
time_format: str = "%B %d, %Y %H:%M:%S",
|
||||
constants: list[str] | None = None,
|
||||
) -> None:
|
||||
self.module_name: str = module_name
|
||||
self.time_format: str = time_format
|
||||
self.values: dict[str, str] = {}
|
||||
self.values["BUILD_RELEASE_STRING"] = release_string
|
||||
self.values["BUILD_COPYRIGHT"] = copyright_string
|
||||
if constants:
|
||||
for constant in constants:
|
||||
parts = constant.split("=", maxsplit=1)
|
||||
if len(parts) == 1:
|
||||
name = constant
|
||||
value = None
|
||||
else:
|
||||
name, string_value = parts
|
||||
value = ast.literal_eval(string_value)
|
||||
if (not name.isidentifier()) or iskeyword(name):
|
||||
msg = (
|
||||
f"Invalid constant name in ConstantsModule ({name!r})"
|
||||
)
|
||||
raise OptionError(msg)
|
||||
self.values[name] = value
|
||||
|
||||
def create(self, tmp_path: Path, modules: list[Module]) -> Path:
|
||||
"""Create the module which consists of declaration statements for each
|
||||
of the values.
|
||||
"""
|
||||
today = datetime.now(tz=timezone.utc)
|
||||
source_timestamp = 0
|
||||
for module in modules:
|
||||
if module.file is None or module.source_is_string:
|
||||
continue
|
||||
if module.source_is_zip_file:
|
||||
continue
|
||||
if not module.file.exists():
|
||||
msg = (
|
||||
f"No file named {module.file!s} (for module {module.name})"
|
||||
)
|
||||
raise OptionError(msg)
|
||||
timestamp = module.file.stat().st_mtime
|
||||
source_timestamp = max(source_timestamp, timestamp)
|
||||
stamp = datetime.fromtimestamp(source_timestamp, tz=timezone.utc)
|
||||
self.values["BUILD_TIMESTAMP"] = today.strftime(self.time_format)
|
||||
self.values["BUILD_HOST"] = socket.gethostname().split(".")[0]
|
||||
self.values["SOURCE_TIMESTAMP"] = stamp.strftime(self.time_format)
|
||||
parts = []
|
||||
for name in sorted(self.values.keys()):
|
||||
value = self.values[name]
|
||||
parts.append(f"{name} = {value!r}")
|
||||
module_path = tmp_path.joinpath(self.module_name).with_suffix(".py")
|
||||
with module_path.open(mode="w", encoding="utf_8", newline="") as file:
|
||||
file.write("\n".join(parts))
|
||||
return module_path
|
||||
Reference in New Issue
Block a user