Skip to content

build.pep517

Private Methods

Private methods, if any (those starting with _), are documented for completeness but do not offer any stability guarantees. They may change or be removed at any time without notice.

PEP 517 build hooks for Polytropos.

This module generates __manifest__.py from pyproject.toml configuration and builds wheels using manifestoo-core for metadata generation.

Only functions required by PEP 517 are public.

_add_directory(tar, dir_path, arcname_prefix, exclude)

Recursively add directory to tar archive.

Parameters:

Name Type Description Default
tar TarFile

TarFile object to add files to

required
dir_path Path

Directory to add

required
arcname_prefix str

Prefix for the archive name

required
exclude set[str]

Set of patterns to exclude

required
Source code in src/polytropos/build/pep517.py
def _add_directory(
    tar: tarfile.TarFile, dir_path: Path, arcname_prefix: str, exclude: set[str]
) -> None:
    """Recursively add directory to tar archive.

    Args:
        tar: TarFile object to add files to
        dir_path: Directory to add
        arcname_prefix: Prefix for the archive name
        exclude: Set of patterns to exclude
    """
    if not dir_path.exists():
        return
    for item in dir_path.iterdir():
        if item.name in exclude or item.name.endswith(".pyc"):
            continue
        if item.is_file():
            tar.add(item, arcname=f"{arcname_prefix}/{item.name}")
        elif item.is_dir():
            _add_directory(tar, item, f"{arcname_prefix}/{item.name}", exclude)

_apply_editable_side_effects(addon_dir, pyproject_data, generated_manifest, file_maps, root, odoo_release)

Apply side effects needed for editable install.

Writes manifest, PKG-INFO, applies file maps, and checks data files.

Parameters:

Name Type Description Default
addon_dir Path

Addon directory (project src dir or root)

required
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required
generated_manifest dict[str, Any]

Generated manifest dictionary

required
file_maps list[dict[str, Any]]

File maps to apply

required
root Path

Project root directory

required
odoo_release str

Odoo release string

required
Source code in src/polytropos/build/pep517.py
def _apply_editable_side_effects(
    addon_dir: Path,
    pyproject_data: dict[str, Any],
    generated_manifest: dict[str, Any],
    file_maps: list[dict[str, Any]],
    root: Path,
    odoo_release: str,
) -> None:
    """Apply side effects needed for editable install.

    Writes manifest, PKG-INFO, applies file maps, and checks data files.

    Args:
        addon_dir: Addon directory (project src dir or root)
        pyproject_data: Parsed pyproject.toml data
        generated_manifest: Generated manifest dictionary
        file_maps: File maps to apply
        root: Project root directory
        odoo_release: Odoo release string
    """
    manifest.write_manifest(generated_manifest, addon_dir / "__manifest__.py")
    _write_pkg_info(addon_dir, pyproject_data, generated_manifest["version"])
    if file_maps:
        _apply_file_maps_editable(addon_dir, file_maps, root, odoo_release)
    _validate_data_files(addon_dir, generated_manifest)

_copy_project_to_addon_dir(src, dst, generated_manifest, src_subdir=None, file_maps=None, odoo_release='')

Copy project files to addon directory for building.

Parameters:

Name Type Description Default
src Path

Source project directory

required
dst Path

Destination addon directory (named after the addon)

required
generated_manifest dict[str, Any]

Generated manifest content

required
src_subdir str | None

Optional subdirectory containing module files (e.g., "src")

None
file_maps list[dict[str, Any]] | None

Optional file maps to apply after copy

None
odoo_release str

Odoo release string for conditional rendering

''
Source code in src/polytropos/build/pep517.py
def _copy_project_to_addon_dir(
    src: Path,
    dst: Path,
    generated_manifest: dict[str, Any],
    src_subdir: str | None = None,
    file_maps: list[dict[str, Any]] | None = None,
    odoo_release: str = "",
) -> None:
    """Copy project files to addon directory for building.

    Args:
        src: Source project directory
        dst: Destination addon directory (named after the addon)
        generated_manifest: Generated manifest content
        src_subdir: Optional subdirectory containing module files (e.g., "src")
        file_maps: Optional file maps to apply after copy
        odoo_release: Odoo release string for conditional rendering
    """
    dst.mkdir(parents=True, exist_ok=True)

    if src_subdir:
        copy_root = src / src_subdir
        if not copy_root.is_dir():
            raise FileNotFoundError(f"Source directory not found: {copy_root}")
    else:
        copy_root = src

    for item in copy_root.iterdir():
        if item.name in ("build", "dist", "__pycache__", ".git", ".venv", "PKG-INFO"):
            continue
        if item.is_file():
            if item.name.endswith(".pyc"):
                continue
            shutil.copy2(item, dst / item.name)
        elif item.is_dir():
            shutil.copytree(item, dst / item.name, dirs_exist_ok=True)

    manifest.write_manifest(generated_manifest, dst / "__manifest__.py")

    if file_maps:
        _apply_file_maps_in_dir(dst, file_maps, odoo_release)

_create_sdist_archive(root, sdist_path, sdist_name, exclude)

Create sdist tarball from project files.

Parameters:

Name Type Description Default
root Path

Project root directory

required
sdist_path Path

Path for the output sdist archive

required
sdist_name str

Name for the sdist archive

required
exclude set[str]

Set of patterns to exclude

required
Source code in src/polytropos/build/pep517.py
def _create_sdist_archive(
    root: Path,
    sdist_path: Path,
    sdist_name: str,
    exclude: set[str],
) -> None:
    """Create sdist tarball from project files.

    Args:
        root: Project root directory
        sdist_path: Path for the output sdist archive
        sdist_name: Name for the sdist archive
        exclude: Set of patterns to exclude
    """
    with tarfile.open(sdist_path, "w:gz") as tar:
        tar.add(root / "pyproject.toml", arcname=f"{sdist_name}/pyproject.toml")

        for item in root.iterdir():
            if item.name in exclude or item.name in (
                "pyproject.toml",
                "__manifest__.py",
            ):
                continue
            if item.is_file() and not item.name.endswith(".pyc"):
                tar.add(item, arcname=f"{sdist_name}/{item.name}")
            elif item.is_dir() and item.suffix != ".pyc":
                _add_directory(tar, item, f"{sdist_name}/{item.name}", exclude)

_get_editable_cache_dir(addon_dir, dist_name, version)

Get the directory path for editable install symlinks.

The directory name includes the addon name, version, and a hash of the absolute addon path, making it human-identifiable while staying unique per source location.

Source code in src/polytropos/build/pep517.py
def _get_editable_cache_dir(addon_dir: Path, dist_name: str, version: str) -> Path:
    """Get the directory path for editable install symlinks.

    The directory name includes the addon name, version, and a hash of the
    absolute addon path, making it human-identifiable while staying unique
    per source location.
    """
    if os.environ.get("POLYTROPOS_EDITABLE_IN_SOURCE"):
        return addon_dir / "build" / "__editable__"
    addon_hash = hashlib.sha256(str(addon_dir.resolve()).encode()).hexdigest()[:12]
    safe_name = _normalize_dist_name(dist_name)
    cache_subdir = f"{safe_name}-{version}-{addon_hash}"
    return (_get_editable_cache_base() / cache_subdir).resolve()

_iter_file_map_entries(addon_dir, file_maps)

Yield file map entries with validated source paths.

Supports two modes: - src_files + dst_dir: each globbed file is mapped to dst_dir - src_dir + dst_dir: the entire src_dir is mapped as a single symlink to dst_dir

Source code in src/polytropos/build/pep517.py
def _iter_file_map_entries(
    addon_dir: Path,
    file_maps: list[dict[str, Any]],
) -> Generator[tuple[dict[str, Any], list[tuple[Path, Path]]], None, None]:
    """Yield file map entries with validated source paths.

    Supports two modes:
    - src_files + dst_dir: each globbed file is mapped to dst_dir
    - src_dir + dst_dir: the entire src_dir is mapped as a single symlink to dst_dir
    """
    for fm in file_maps:
        operation = fm.get("operation", "symlink")
        if operation not in {"symlink", "render"}:
            raise ValueError(f"Invalid file_map operation '{operation}': {fm}")
        dst_dir = fm.get("dst_dir")
        if not dst_dir:
            raise ValueError(f"file_map entry missing dst_dir: {fm}")
        dst_path = addon_dir / dst_dir
        src_files = fm.get("src_files")
        src_dir = fm.get("src_dir")
        if src_files:
            yield (fm, _resolve_src_files_entries(addon_dir, src_files, dst_path))
        elif src_dir:
            entries = _resolve_src_dir_entries(addon_dir, src_dir, dst_path)
            if entries is not None:
                yield (fm, entries)
        else:
            raise ValueError(
                f"file_map entry must have either src_files or src_dir: {fm}"
            )

_prepare_editable_build(config_settings)

Common setup for editable builds.

Parameters:

Name Type Description Default
config_settings dict[str, Any] | None

PEP 517 config_settings

required

Returns:

Type Description
tuple[Path, dict[str, Any], str, str | None, dict[str, Any], list[dict[str, Any]]]

Tuple of (root, pyproject_data, odoo_release, src_dir, generated_manifest, file_maps)

Source code in src/polytropos/build/pep517.py
def _prepare_editable_build(
    config_settings: dict[str, Any] | None,
) -> tuple[Path, dict[str, Any], str, str | None, dict[str, Any], list[dict[str, Any]]]:
    """Common setup for editable builds.

    Args:
        config_settings: PEP 517 config_settings

    Returns:
        Tuple of (root, pyproject_data, odoo_release, src_dir,
                  generated_manifest, file_maps)
    """
    root = Path(os.getcwd())
    pyproject_data = get_pyproject_data(root)
    check_dynamic_version(pyproject_data)
    odoo_release = get_odoo_release(config_settings, pyproject_data)
    check_release_compatibility(odoo_release, pyproject_data)
    src_dir = get_src_dir(root)
    generated_manifest = generate_manifest(pyproject_data, odoo_release, root, src_dir)
    file_maps = parse_file_maps(pyproject_data, odoo_release)
    return root, pyproject_data, odoo_release, src_dir, generated_manifest, file_maps

_prepare_target(target, addon_dir, is_dir_mode)

Prepare target location. Returns False if the target should be skipped.

Source code in src/polytropos/build/pep517.py
def _prepare_target(target: Path, addon_dir: Path, is_dir_mode: bool) -> bool:
    """Prepare target location. Returns False if the target should be skipped."""
    target.parent.mkdir(parents=True, exist_ok=True)
    if not (target.is_dir() and not target.is_symlink()):
        return True
    if is_dir_mode:
        shutil.rmtree(target)
        return True
    logger.warning(
        "file_map target is a real directory, skipping: %s",
        target.relative_to(addon_dir),
    )
    return False

_render_dir_contents(src, target, odoo_release)

Render all XML files from source directory to target directory.

Parameters:

Name Type Description Default
src Path

Source directory path

required
target Path

Target directory path

required
odoo_release str

Odoo release string for conditional rendering

required
Source code in src/polytropos/build/pep517.py
def _render_dir_contents(src: Path, target: Path, odoo_release: str) -> None:
    """Render all XML files from source directory to target directory.

    Args:
        src: Source directory path
        target: Target directory path
        odoo_release: Odoo release string for conditional rendering
    """
    target.mkdir(parents=True, exist_ok=True)
    for item in src.iterdir():
        if item.is_file():
            _render_file(item, target / item.name, odoo_release)
        elif item.is_dir():
            _render_dir_contents(item, target / item.name, odoo_release)

_validate_data_files(addon_dir, generated_manifest)

Validate that all data files referenced in manifest exist.

Parameters:

Name Type Description Default
addon_dir Path

Base directory for data file lookup

required
generated_manifest dict[str, Any]

Generated manifest dictionary

required
Source code in src/polytropos/build/pep517.py
def _validate_data_files(addon_dir: Path, generated_manifest: dict[str, Any]) -> None:
    """Validate that all data files referenced in manifest exist.

    Args:
        addon_dir: Base directory for data file lookup
        generated_manifest: Generated manifest dictionary
    """
    data_files = generated_manifest.get("data", [])
    for data_file in data_files:
        if not (addon_dir / data_file).exists():
            raise FileNotFoundError(f"Data file not found: {data_file}")

_validate_readme_in_metadata(wheel_path, readme_info, root, src_dir=None)

Validate readme file if specified.

Parameters:

Name Type Description Default
wheel_path Path

Path to the built wheel

required
readme_info tuple[str, str, str | None] | None

Tuple of (readme_filename, content_type, description) or None

required
root Path

Project root directory

required
src_dir str | None

Source directory relative to root (optional)

None
Source code in src/polytropos/build/pep517.py
def _validate_readme_in_metadata(
    wheel_path: Path,
    readme_info: tuple[str, str, str | None] | None,
    root: Path,
    src_dir: str | None = None,
) -> None:
    """Validate readme file if specified.

    Args:
        wheel_path: Path to the built wheel
        readme_info: Tuple of (readme_filename, content_type, description) or None
        root: Project root directory
        src_dir: Source directory relative to root (optional)
    """
    if not readme_info:
        return

    readme_filename, expected_content_type, _ = readme_info

    readme_in_src = src_dir and (root / src_dir / readme_filename).is_file()
    readme_path = root / readme_filename
    if readme_in_src:
        return

    if not readme_path.is_file():
        raise ValueError(
            f"readme={readme_filename} specified in pyproject.toml but "
            f"file not found at {readme_path}"
        )

    with ZipFile(wheel_path, "r") as whl:
        metadata_files = [
            n for n in whl.namelist() if n.endswith(".dist-info/METADATA")
        ]
        if not metadata_files:
            return

        metadata_content = whl.read(metadata_files[0]).decode("utf-8")

        has_content_type = False

        for line in metadata_content.splitlines():
            if line.startswith("Description-Content-Type:"):
                content_type = line.split(":", 1)[1].strip()
                has_content_type = True
                if content_type != expected_content_type:
                    logger.warning(
                        "Expected Description-Content-Type '%s', got '%s'",
                        expected_content_type,
                        content_type,
                    )

        if not has_content_type:
            logger.warning(
                "readme=%s specified but Description-Content-Type not in METADATA",
                readme_filename,
            )

_write_pkg_info(addon_dir, pyproject_data, version)

Write PKG-INFO to addon directory for metadata.

Parameters:

Name Type Description Default
addon_dir Path

Path to the addon directory

required
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required
version str

Version string for the package

required
Source code in src/polytropos/build/pep517.py
def _write_pkg_info(
    addon_dir: Path,
    pyproject_data: dict[str, Any],
    version: str,
) -> None:
    """Write PKG-INFO to addon directory for metadata.

    Args:
        addon_dir: Path to the addon directory
        pyproject_data: Parsed pyproject.toml data
        version: Version string for the package
    """
    msg = Message()
    msg["Metadata-Version"] = "2.1"
    msg["Name"] = pyproject_data["project"]["name"]
    msg["Version"] = version

    pkg_info_path = addon_dir / "PKG-INFO"
    with open(pkg_info_path, "w", encoding="utf-8") as f:
        EmailGenerator(f, mangle_from_=False, maxheaderlen=0).flatten(msg)

build_editable(editable_directory, config_settings=None, _metadata_directory=None)

Build an editable install for the Odoo module.

PEP 660 hook.

Parameters:

Name Type Description Default
editable_directory str

Directory to place editable install files

required
config_settings dict[str, Any] | None

Build configuration

None
_metadata_directory str | None

Metadata directory (unused)

None

Returns:

Type Description
str

Basename of the built wheel

Source code in src/polytropos/build/pep517.py
def build_editable(
    editable_directory: str,
    config_settings: dict[str, Any] | None = None,
    _metadata_directory: str | None = None,
) -> str:
    """Build an editable install for the Odoo module.

    PEP 660 hook.

    Args:
        editable_directory: Directory to place editable install files
        config_settings: Build configuration
        _metadata_directory: Metadata directory (unused)

    Returns:
        Basename of the built wheel
    """
    root, pyproject_data, odoo_release, src_dir, generated_manifest, file_maps = (
        _prepare_editable_build(config_settings)
    )
    addon_dir = root / src_dir if src_dir else root
    _apply_editable_side_effects(
        addon_dir, pyproject_data, generated_manifest, file_maps, root, odoo_release
    )

    return _build_wheel(addon_dir, Path(editable_directory), editable=True)

build_sdist(sdist_directory, config_settings=None)

Build a source distribution for the Odoo module.

PEP 517 mandatory hook.

The sdist is version-independent - it contains pyproject.toml and source files, but NOT a version-specific __manifest__.py. The manifest is generated at build time.

Parameters:

Name Type Description Default
sdist_directory str

Directory to place the sdist in

required
config_settings dict[str, Any] | None

PEP 517 config_settings (unused)

None

Returns:

Type Description
str

Path to the built sdist archive

Source code in src/polytropos/build/pep517.py
def build_sdist(
    sdist_directory: str,
    config_settings: dict[str, Any] | None = None,  # noqa: ARG001
) -> str:
    """Build a source distribution for the Odoo module.

    PEP 517 mandatory hook.

    The sdist is version-independent - it contains `pyproject.toml` and source files,
    but NOT a version-specific `__manifest__.py`. The manifest is generated at build time.

    Args:
        sdist_directory: Directory to place the sdist in
        config_settings: PEP 517 config_settings (unused)

    Returns:
        Path to the built sdist archive
    """
    root = Path(os.getcwd())
    sdist_dir = Path(sdist_directory)

    pyproject_data = get_pyproject_data(root)
    check_dynamic_version(pyproject_data)
    module_name = addon_name(pyproject_data["project"]["name"])
    module_version = pyproject_data["project"]["version"]

    sdist_name = f"{module_name}-{module_version}"
    sdist_path = sdist_dir / f"{sdist_name}.tar.gz"

    exclude = get_exclusion_patterns(root)
    exclude.discard("PKG-INFO")  # Required in sdists for publishing tools
    _write_pkg_info(root, pyproject_data, module_version)
    try:
        _create_sdist_archive(root, sdist_path, sdist_name, exclude)
    finally:
        (root / "PKG-INFO").unlink(missing_ok=True)
    return f"{sdist_name}.tar.gz"

build_wheel(wheel_directory, config_settings=None, _metadata_directory=None)

Build a wheel for the Odoo module.

PEP 517 mandatory hook.

Parameters:

Name Type Description Default
wheel_directory str

Directory to place the wheel in

required
config_settings dict[str, Any] | None

PEP 517 config_settings

None
_metadata_directory str | None

Unused, PEP 517 requirement

None

Returns:

Type Description
str

Path to the built wheel file

Source code in src/polytropos/build/pep517.py
def build_wheel(
    wheel_directory: str,
    config_settings: dict[str, Any] | None = None,
    _metadata_directory: str | None = None,
) -> str:
    """Build a wheel for the Odoo module.

    PEP 517 mandatory hook.

    Args:
        wheel_directory: Directory to place the wheel in
        config_settings: PEP 517 config_settings
        _metadata_directory: Unused, PEP 517 requirement

    Returns:
        Path to the built wheel file
    """
    root = Path(os.getcwd())
    wheel_dir = Path(wheel_directory)

    pyproject_data = get_pyproject_data(root)
    check_dynamic_version(pyproject_data)
    odoo_release = get_odoo_release(config_settings, pyproject_data)
    check_release_compatibility(odoo_release, pyproject_data)
    src_dir = get_src_dir(root)
    generated_manifest = generate_manifest(pyproject_data, odoo_release, root, src_dir)
    file_maps = parse_file_maps(pyproject_data, odoo_release)
    module_name = addon_name(pyproject_data["project"]["name"])
    src_subdir = src_dir

    with tempfile.TemporaryDirectory() as tmpdir:
        addon_dir = Path(tmpdir) / module_name
        _copy_project_to_addon_dir(
            root, addon_dir, generated_manifest, src_subdir, file_maps, odoo_release
        )
        wheel_filename = _build_wheel(addon_dir, wheel_dir, editable=False)

        wheel_path = wheel_dir / wheel_filename
        readme_info = get_readme_info(pyproject_data, root, src_dir, module_name)
        _validate_readme_in_metadata(wheel_path, readme_info, root, src_dir)

        return wheel_filename

prepare_metadata_for_build_editable(metadata_directory, config_settings=None)

Prepare metadata for editable build.

PEP 660 optional hook. Builds editable wheel in a temp dir to avoid redundant work during the actual editable install, then extracts .dist-info metadata into the metadata_directory.

Parameters:

Name Type Description Default
metadata_directory str

Directory to place metadata

required
config_settings dict[str, Any] | None

Build configuration

None

Returns:

Type Description
str

Basename of the .dist-info directory

Source code in src/polytropos/build/pep517.py
def prepare_metadata_for_build_editable(
    metadata_directory: str,
    config_settings: dict[str, Any] | None = None,
) -> str:
    """Prepare metadata for editable build.

    PEP 660 optional hook. Builds editable wheel in a temp dir to avoid
    redundant work during the actual editable install, then extracts
    .dist-info metadata into the metadata_directory.

    Args:
        metadata_directory: Directory to place metadata
        config_settings: Build configuration

    Returns:
        Basename of the .dist-info directory
    """
    root, pyproject_data, odoo_release, src_dir, generated_manifest, file_maps = (
        _prepare_editable_build(config_settings)
    )
    addon_dir = root / src_dir if src_dir else root
    _apply_editable_side_effects(
        addon_dir, pyproject_data, generated_manifest, file_maps, root, odoo_release
    )

    with tempfile.TemporaryDirectory() as tmpdir:
        wheel_filename = _build_wheel(addon_dir, Path(tmpdir), editable=True)
        wheel_path = Path(tmpdir) / wheel_filename
        with ZipFile(wheel_path, "r") as whl:
            names = whl.namelist()
            dist_info_prefixes = {n.split("/")[0] for n in names if ".dist-info/" in n}
            dist_info_prefix = next(iter(dist_info_prefixes), "")
            for name in names:
                if name.startswith(dist_info_prefix):
                    target = Path(metadata_directory) / name
                    target.parent.mkdir(parents=True, exist_ok=True)
                    target.write_bytes(whl.read(name))
    return dist_info_prefix