Skip to content

packager

flavor.packaging.python.packager

Python packager that owns all Python-specific packaging logic.

Classes

PythonPackager

PythonPackager(
    manifest_dir: Path,
    package_name: str,
    entry_point: str,
    build_config: dict[str, Any] | None = None,
    python_version: str = "3.11",
)

Python packager that owns all Python-specific packaging logic.

This class orchestrates the packaging process by delegating to specialized modules: - PythonEnvironmentBuilder: Handles Python environment setup and distribution - PythonSlotBuilder: Manages slot preparation and artifact assembly - WheelBuilder: Builds Python wheels and resolves dependencies - PythonDistManager: Manages Python distributions - PyPaPipManager: Handles PyPA pip operations - UVManager: Handles UV operations

Initialize the Python packager.

Parameters:

Name Type Description Default
manifest_dir Path

Directory containing the package manifest (pyproject.toml)

required
package_name str

Name of the package

required
entry_point str

Entry point for the package (e.g., "module:function")

required
build_config dict[str, Any] | None

Build configuration from pyproject.toml

None
python_version str

Python version to use (e.g., "3.11")

'3.11'
Source code in flavor/packaging/python/packager.py
def __init__(
    self,
    manifest_dir: Path,
    package_name: str,
    entry_point: str,
    build_config: dict[str, Any] | None = None,
    python_version: str = "3.11",
) -> None:
    """
    Initialize the Python packager.

    Args:
        manifest_dir: Directory containing the package manifest (pyproject.toml)
        package_name: Name of the package
        entry_point: Entry point for the package (e.g., "module:function")
        build_config: Build configuration from pyproject.toml
        python_version: Python version to use (e.g., "3.11")
    """
    self.manifest_dir = manifest_dir
    self.package_name = package_name
    self.entry_point = entry_point
    self.build_config = build_config or {}
    self.python_version = python_version

    # Platform detection
    self.is_windows = sys.platform == "win32"
    self.venv_bin_dir = "Scripts" if self.is_windows else "bin"
    self.uv_exe = "uv.exe" if self.is_windows else "uv"

    # Initialize manager instances
    self.pypapip = PyPaPipManager()
    self.uv = UVManager()
    self.uv_manager = self.uv  # Alias for compatibility
    self.wheel_builder = WheelBuilder(python_version=python_version)
    self.dist_manager = PythonDistManager(python_version=python_version)

    # Initialize specialized builders
    self.env_builder = PythonEnvironmentBuilder(
        python_version=python_version,
        is_windows=self.is_windows,
        manylinux_tag=self.MANYLINUX_TAG,
    )
    self.slot_builder = PythonSlotBuilder(
        manifest_dir=manifest_dir,
        package_name=package_name,
        entry_point=entry_point,
        python_version=python_version,
        is_windows=self.is_windows,
        manylinux_tag=self.MANYLINUX_TAG,
        build_config=build_config,
        wheel_builder=self.wheel_builder,
    )

    logger.info(
        "🐍 Building Python package",
        package=package_name,
        entry_point=entry_point,
        python_version=python_version,
        platform=f"{'windows' if self.is_windows else 'unix'}",
    )
Functions
__repr__
__repr__() -> str

String representation of the packager.

Source code in flavor/packaging/python/packager.py
def __repr__(self) -> str:
    """String representation of the packager."""
    return (
        f"PythonPackager(package={self.package_name}, "
        f"python={self.python_version}, "
        f"platform={'windows' if self.is_windows else 'unix'})"
    )
clean_build_artifacts
clean_build_artifacts(work_dir: Path) -> None

Clean up temporary build artifacts.

Parameters:

Name Type Description Default
work_dir Path

Working directory to clean

required
Source code in flavor/packaging/python/packager.py
def clean_build_artifacts(self, work_dir: Path) -> None:
    """
    Clean up temporary build artifacts.

    Args:
        work_dir: Working directory to clean
    """
    logger.debug("🧹 Cleaning build artifacts")

    # Clean specific directories if they exist
    dirs_to_clean = [
        work_dir / "payload",
        work_dir / "metadata_content",
        work_dir / "venv",
        work_dir / "build",
    ]

    for dir_path in dirs_to_clean:
        if dir_path.exists():
            logger.trace(f"Removing {dir_path}")
            try:
                safe_rmtree(dir_path, missing_ok=True)
            except Exception as e:
                logger.debug(f"Failed to remove {dir_path}: {e}")
create_build_environment
create_build_environment(build_dir: Path) -> Path

Create a temporary build environment for wheel building.

Parameters:

Name Type Description Default
build_dir Path

Directory to create environment in

required

Returns:

Type Description
Path

Path to Python executable in the environment

Source code in flavor/packaging/python/packager.py
def create_build_environment(self, build_dir: Path) -> Path:
    """
    Create a temporary build environment for wheel building.

    Args:
        build_dir: Directory to create environment in

    Returns:
        Path to Python executable in the environment
    """

    venv_dir = build_dir / "venv"

    # Try to use UV to create venv
    uv_cmd = self.env_builder.find_uv_command(raise_if_not_found=False)
    if uv_cmd:
        logger.debug("Using UV to create virtual environment")
        self.uv.create_venv(venv_dir, self.python_version)
    else:
        # Fall back to standard venv
        logger.debug("Using standard venv module")
        import venv

        venv.create(venv_dir, with_pip=True)

    python_exe = venv_dir / self.venv_bin_dir / ("python.exe" if self.is_windows else "python")

    # Ensure pip and wheel are installed
    if python_exe.exists():
        logger.debug("Installing pip and wheel in build environment")
        install_cmd = self.pypapip._get_pypapip_install_cmd(python_exe, ["pip", "wheel", "setuptools"])
        from provide.foundation.process import run

        run(install_cmd, check=True, capture_output=True)

    return python_exe
download_uv_binary
download_uv_binary(dest_dir: Path) -> Path | None

Download UV binary for packaging.

Delegates to environment builder for the actual download.

Parameters:

Name Type Description Default
dest_dir Path

Directory to save UV binary to

required

Returns:

Type Description
Path | None

Path to UV binary if successful, None otherwise

Source code in flavor/packaging/python/packager.py
def download_uv_binary(self, dest_dir: Path) -> Path | None:
    """
    Download UV binary for packaging.

    Delegates to environment builder for the actual download.

    Args:
        dest_dir: Directory to save UV binary to

    Returns:
        Path to UV binary if successful, None otherwise
    """
    logger.info("📥 Downloading UV binary for packaging")
    return self.env_builder.download_uv_wheel(dest_dir)
get_build_dependencies
get_build_dependencies() -> list[str]

Get build dependencies from pyproject.toml.

Returns:

Type Description
list[str]

List of build dependency specifications

Source code in flavor/packaging/python/packager.py
def get_build_dependencies(self) -> list[str]:
    """
    Get build dependencies from pyproject.toml.

    Returns:
        List of build dependency specifications
    """
    pyproject_path = self.manifest_dir / "pyproject.toml"
    with pyproject_path.open("rb") as f:
        pyproject_data = tomllib.load(f)

    build_system = pyproject_data.get("build-system", {})
    requires = build_system.get("requires", [])
    return list(requires) if isinstance(requires, list) else []
get_package_metadata
get_package_metadata() -> dict[str, Any]

Get package metadata from pyproject.toml.

Returns:

Type Description
dict[str, Any]

Dictionary with package metadata

Source code in flavor/packaging/python/packager.py
def get_package_metadata(self) -> dict[str, Any]:
    """
    Get package metadata from pyproject.toml.

    Returns:
        Dictionary with package metadata
    """
    pyproject_path = self.manifest_dir / "pyproject.toml"
    with pyproject_path.open("rb") as f:
        pyproject_data = tomllib.load(f)

    project = pyproject_data.get("project", {})
    tool_flavor = pyproject_data.get("tool", {}).get("flavor", {})

    return {
        "name": project.get("name", self.package_name),
        "version": project.get("version", "0.0.1"),
        "description": project.get("description", ""),
        "dependencies": project.get("dependencies", []),
        "python_requires": project.get("requires-python", f">={self.python_version}"),
        "entry_points": project.get("scripts", {}),
        "flavor_config": tool_flavor,
    }
get_python_binary_info
get_python_binary_info() -> dict[str, Any]

Get information about the Python binary to use.

Returns:

Type Description
dict[str, Any]

Dictionary with Python binary information:

dict[str, Any]
  • version: Python version string
dict[str, Any]
  • path: Path to Python executable (if available)
dict[str, Any]
  • is_system: Whether using system Python
Source code in flavor/packaging/python/packager.py
def get_python_binary_info(self) -> dict[str, Any]:
    """
    Get information about the Python binary to use.

    Returns:
        Dictionary with Python binary information:
        - version: Python version string
        - path: Path to Python executable (if available)
        - is_system: Whether using system Python
    """
    try:
        # Try to find UV first
        uv_cmd = self.env_builder.find_uv_command(raise_if_not_found=False)
        if uv_cmd:
            logger.debug("Found UV, will use it to manage Python")
            return {
                "version": self.python_version,
                "path": None,  # UV will handle Python
                "is_system": False,
                "manager": "uv",
            }
    except Exception as e:
        logger.debug(f"UV not found: {e}")

    # Fall back to system Python
    return {
        "version": self.python_version,
        "path": sys.executable,
        "is_system": True,
        "manager": "system",
    }
get_runtime_dependencies
get_runtime_dependencies() -> list[str]

Get runtime dependencies from pyproject.toml.

Returns:

Type Description
list[str]

List of runtime dependency specifications

Source code in flavor/packaging/python/packager.py
def get_runtime_dependencies(self) -> list[str]:
    """
    Get runtime dependencies from pyproject.toml.

    Returns:
        List of runtime dependency specifications
    """
    metadata = self.get_package_metadata()
    deps = metadata.get("dependencies", [])
    return list(deps) if isinstance(deps, list) else []
prepare_artifacts
prepare_artifacts(work_dir: Path) -> dict[str, Path]

Prepare all artifacts needed for flavor assembly.

Delegates to PythonSlotBuilder for the actual preparation.

Returns:

Type Description
dict[str, Path]

Dictionary mapping artifact names to their paths:

dict[str, Path]
  • payload_tgz: The main payload archive
dict[str, Path]
  • metadata_tgz: Metadata archive
dict[str, Path]
  • uv_binary: UV binary (if available)
dict[str, Path]
  • python_tgz: Python distribution
Source code in flavor/packaging/python/packager.py
def prepare_artifacts(self, work_dir: Path) -> dict[str, Path]:
    """
    Prepare all artifacts needed for flavor assembly.

    Delegates to PythonSlotBuilder for the actual preparation.

    Returns:
        Dictionary mapping artifact names to their paths:
        - payload_tgz: The main payload archive
        - metadata_tgz: Metadata archive
        - uv_binary: UV binary (if available)
        - python_tgz: Python distribution
    """
    return self.slot_builder.prepare_artifacts(work_dir)
validate_manifest
validate_manifest() -> bool

Validate that the manifest directory contains a valid Python project.

Returns:

Type Description
bool

True if valid, raises exception otherwise

Source code in flavor/packaging/python/packager.py
def validate_manifest(self) -> bool:
    """
    Validate that the manifest directory contains a valid Python project.

    Returns:
        True if valid, raises exception otherwise
    """
    pyproject_path = self.manifest_dir / "pyproject.toml"
    if not pyproject_path.exists():
        raise FileNotFoundError(f"No pyproject.toml found in {self.manifest_dir}")

    try:
        with pyproject_path.open("rb") as f:
            pyproject_data = tomllib.load(f)

        # Check for required fields
        project = pyproject_data.get("project", {})
        if not project.get("name"):
            raise ValueError("pyproject.toml missing project.name")

        # Check entry point format
        if self.entry_point and ":" not in self.entry_point:
            raise ValueError(
                f"Invalid entry point format: {self.entry_point}. Expected format: 'module:function'"
            )

        return True

    except Exception as e:
        logger.error(f"❌ Manifest validation failed: {e}")
        raise