"""Read project metadata from pyproject.toml for packaging scripts.""" from __future__ import annotations import ast import importlib.util import subprocess import sys from pathlib import Path _BUILD_ROOT = Path(__file__).resolve().parents[1] def _load_wheel_layout_module(): layout_path = _BUILD_ROOT / "wheel" / "layout.py" spec = importlib.util.spec_from_file_location("_orisys_build_wheel_layout", layout_path) if spec is None or spec.loader is None: raise ImportError(f"Cannot load wheel layout helper from {layout_path}") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _load_toml(text: str) -> dict: if sys.version_info >= (3, 11): import tomllib return tomllib.loads(text) try: import tomli except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "tomli", "-q"]) import tomli return tomli.loads(text) def load_pyproject(repo_root: Path) -> dict: path = repo_root / "pyproject.toml" return _load_toml(path.read_text(encoding="utf-8")) def project_metadata(repo_root: Path) -> dict: return load_pyproject(repo_root)["project"] def install_requires(repo_root: Path) -> list[str]: return list(project_metadata(repo_root)["dependencies"]) def extras_require(repo_root: Path) -> dict[str, list[str]]: optional = project_metadata(repo_root).get("optional-dependencies", {}) return {name: list(requirements) for name, requirements in optional.items()} def _resolve_version_attr(repo_root: Path, attr: str) -> str: """Resolve a setuptools dynamic version attr (e.g. orisys.__version__).""" module_name, _, attr_name = attr.partition(".") if not attr_name: raise RuntimeError(f"Invalid dynamic version attr: {attr!r}") # Avoid importing package modules here: __init__ may import heavy runtime deps # (e.g., cv2/numpy) that are not required just to read __version__. rel_module = Path(*module_name.split(".")) module_file = repo_root / "src" / rel_module.with_suffix(".py") if not module_file.is_file(): module_file = repo_root / "src" / rel_module / "__init__.py" if not module_file.is_file(): raise RuntimeError(f"Cannot resolve module file for dynamic version attr: {attr!r}") tree = ast.parse(module_file.read_text(encoding="utf-8"), filename=str(module_file)) target_name = attr_name.split(".")[0] for node in tree.body: if isinstance(node, ast.Assign): for target in node.targets: if isinstance(target, ast.Name) and target.id == target_name: value = ast.literal_eval(node.value) return str(value) raise RuntimeError(f"Attribute {target_name!r} not found in {module_file}") def get_version(repo_root: Path) -> str: """Return project version from pyproject.toml (static or dynamic).""" project = project_metadata(repo_root) if "version" in project: return str(project["version"]) dynamic = load_pyproject(repo_root).get("tool", {}).get("setuptools", {}).get("dynamic", {}) version_cfg = dynamic.get("version") if isinstance(version_cfg, dict) and "attr" in version_cfg: return _resolve_version_attr(repo_root, version_cfg["attr"]) raise RuntimeError("Project version is not defined in pyproject.toml") def bundle_items(repo_root: Path) -> list[dict]: """Return bundle rules from [[tool.orisys.bundle.items]] in pyproject.toml.""" items = load_pyproject(repo_root).get("tool", {}).get("orisys", {}).get("bundle", {}).get("items") if not items: raise RuntimeError("Missing [[tool.orisys.bundle.items]] in pyproject.toml") return list(items) def bundle_data_files(repo_root: Path) -> list[tuple[str, list[str]]]: """Map bundle items marked dev_install=true to setuptools data_files entries.""" resolve_bundle_source_files = _load_wheel_layout_module().resolve_bundle_source_files grouped: dict[str, list[str]] = {} for item in bundle_items(repo_root): if not item.get("dev_install", False): continue src_dir = repo_root / item["source"] base_dest = f"orisys/{item['target']}".replace("\\", "/") for src_file in resolve_bundle_source_files(repo_root, item): rel_src = src_file.relative_to(repo_root).as_posix() if item.get("preserve_tree", False): rel_parent = src_file.relative_to(src_dir).parent.as_posix() dest = base_dest if rel_parent == "." else f"{base_dest}/{rel_parent}" else: dest = base_dest grouped.setdefault(dest, []).append(rel_src) return [(dest, sorted(files)) for dest, files in sorted(grouped.items())]