Skip to content

build.manifest

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.

Module for generating Odoo __manifest__.py files.

_extract_github_username(email)

Extract GitHub username from a noreply email address.

Parameters:

Name Type Description Default
email str

Email address to parse

required

Returns:

Type Description
str | None

GitHub username if email is a noreply address, None otherwise

Source code in src/polytropos/build/manifest.py
def _extract_github_username(email: str) -> str | None:
    """Extract GitHub username from a noreply email address.

    Args:
        email: Email address to parse

    Returns:
        GitHub username if email is a noreply address, None otherwise
    """
    match = GITHUB_NOREPLY_PATTERN.match(email)
    if match:
        return match.group("username")
    return None

_manual_manifest(pyproject_data, odoo_release)

Get version-specific overrides from pyproject.toml, if any.

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required
odoo_release str

Odoo release (e.g., "19.0") to match against

required

Returns:

Type Description
dict[str, Any]

Merged overrides dictionary for matching releases

Source code in src/polytropos/build/manifest.py
def _manual_manifest(
    pyproject_data: dict[str, Any], odoo_release: str
) -> dict[str, Any]:
    """Get version-specific overrides from `pyproject.toml`, if any.

    Args:
        pyproject_data: Parsed `pyproject.toml` data
        odoo_release: Odoo release (e.g., "19.0") to match against

    Returns:
        Merged overrides dictionary for matching releases
    """
    manifest_config = (
        pyproject_data.get("tool", {}).get("polytropos", {}).get("manifest", [])
    )
    if not isinstance(manifest_config, list):
        raise ValueError(
            "[tool.polytropos.manifest] must be a list, if defined. "
            "Use [[tool.polytropos.manifest]] (double brackets) in pyproject.toml."
        )
    manifest_config = deepcopy(manifest_config)
    parsed_release = parse_release(odoo_release)

    result: dict[str, Any] = {}
    for item in manifest_config:
        releases_spec = item.pop("releases", None)
        if releases_spec and not matches_release_specifier(
            parsed_release, releases_spec
        ):
            continue
        result = deep_merge(result, item)

    return result

_standard_manifest(pyproject_data, odoo_release, root=None, src_dir=None)

Convert standard pyproject.toml values to __manifest__.py.

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required
odoo_release str

Odoo release (e.g., "19.0")

required
root Path | None

Project root directory (optional, for reading README)

None
src_dir str | None

Source subdirectory (optional, e.g., "src")

None

Returns:

Type Description
dict[str, Any]

Dictionary representing standard __manifest__.py content

Source code in src/polytropos/build/manifest.py
def _standard_manifest(
    pyproject_data: dict[str, Any],
    odoo_release: str,
    root: Path | None = None,
    src_dir: str | None = None,
) -> dict[str, Any]:
    """Convert standard `pyproject.toml` values to `__manifest__.py`.

    Args:
        pyproject_data: Parsed `pyproject.toml` data
        odoo_release: Odoo release (e.g., "19.0")
        root: Project root directory (optional, for reading README)
        src_dir: Source subdirectory (optional, e.g., "src")

    Returns:
        Dictionary representing standard `__manifest__.py` content
    """
    name = addon_name(pyproject_data["project"]["name"])
    human_name = name.replace("_", " ").title()

    project = pyproject_data["project"]
    module_version = project["version"]
    full_version = build_full_version(module_version, odoo_release)

    manifest: dict[str, Any] = {
        "name": human_name,
        "version": full_version,
    }
    authors = get_author_value(pyproject_data)
    if authors:
        manifest["author"] = authors

    maintainers = get_maintainer_value(pyproject_data)
    if maintainers:
        manifest["maintainers"] = maintainers

    with suppress(KeyError):
        manifest["website"] = pyproject_data["project"]["urls"]["Homepage"]
    with suppress(KeyError):
        manifest["license"] = pyproject_data["project"]["license"]
    with suppress(KeyError):
        manifest["summary"] = pyproject_data["project"]["description"]

    addon_deps, py_deps = parse_project_dependencies(pyproject_data)
    if addon_deps:
        manifest["depends"] = addon_deps
    if py_deps:
        manifest["external_dependencies"] = {"python": py_deps}

    if "description" not in manifest:
        if readme_info := get_readme_info(pyproject_data, root, src_dir, name):
            _, _, readme_desc = readme_info
            if readme_desc:
                manifest["description"] = readme_desc

    return manifest

addon_name(pkg_name)

Get addon name from package name.

Parameters:

Name Type Description Default
pkg_name str

Package name (e.g., "odoo-addon-example-module")

required

Returns:

Type Description
str

Addon name (e.g., "example_module")

Raises:

Type Description
ValueError

If package name doesn't start with ADDON_PREFIX

Source code in src/polytropos/build/manifest.py
def addon_name(pkg_name: str) -> str:
    """Get addon name from package name.

    Args:
        pkg_name: Package name (e.g., "odoo-addon-example-module")

    Returns:
        Addon name (e.g., "example_module")

    Raises:
        ValueError: If package name doesn't start with ADDON_PREFIX
    """
    if not pkg_name.startswith(ADDON_PREFIX):
        raise ValueError(
            f"Invalid package name: '{pkg_name}'. "
            "Package name must start with 'odoo-addon-' "
            "(e.g., 'odoo-addon-example-module')."
        )
    return pkg_name.removeprefix(ADDON_PREFIX).replace("-", "_")

generate_manifest(pyproject_data, odoo_release, root=None, src_dir=None)

Convert standard pyproject.toml values to __manifest__.py.

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required
odoo_release str

Odoo release (e.g., "19.0")

required
root Path | None

Project root directory (optional, for reading README)

None
src_dir str | None

Source subdirectory (optional, e.g., "src")

None

Returns:

Type Description
dict[str, Any]

Dictionary representing __manifest__.py content

Source code in src/polytropos/build/manifest.py
def generate_manifest(
    pyproject_data: dict[str, Any],
    odoo_release: str,
    root: Path | None = None,
    src_dir: str | None = None,
) -> dict[str, Any]:
    """Convert standard `pyproject.toml` values to `__manifest__.py`.

    Args:
        pyproject_data: Parsed `pyproject.toml` data
        odoo_release: Odoo release (e.g., "19.0")
        root: Project root directory (optional, for reading README)
        src_dir: Source subdirectory (optional, e.g., "src")

    Returns:
        Dictionary representing `__manifest__.py` content
    """
    standard = _standard_manifest(pyproject_data, odoo_release, root, src_dir)
    overrides = _manual_manifest(pyproject_data, odoo_release)

    manifest = deep_merge(standard, overrides)
    return manifest

generate_manifest_content(manifest)

Generate manifest.py file content.

Parameters:

Name Type Description Default
manifest dict[str, Any]

Manifest dictionary

required

Returns:

Type Description
str

String content for manifest.py

Source code in src/polytropos/build/manifest.py
def generate_manifest_content(manifest: dict[str, Any]) -> str:
    """Generate __manifest__.py file content.

    Args:
        manifest: Manifest dictionary

    Returns:
        String content for __manifest__.py
    """
    return pprint.pformat(manifest)

get_author_value(pyproject_data)

Extract author value from project authors list.

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required

Returns:

Type Description
str | None

Comma-separated author names, or None if not found

Source code in src/polytropos/build/manifest.py
def get_author_value(pyproject_data: dict[str, Any]) -> str | None:
    """Extract author value from project authors list.

    Args:
        pyproject_data: Parsed `pyproject.toml` data

    Returns:
        Comma-separated author names, or `None` if not found
    """
    try:
        authors = pyproject_data["project"]["authors"]
    except KeyError:
        return None
    if not authors:
        return None
    return ", ".join(author["name"] for author in authors if "name" in author)

get_maintainer_value(pyproject_data)

Extract maintainer value from project maintainers list.

Uses GitHub noreply emails to extract usernames. Falls back to name if email is not a GitHub noreply address.

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required

Returns:

Type Description
list[str] | None

List of GitHub usernames or maintainer names, or None if not found

Source code in src/polytropos/build/manifest.py
def get_maintainer_value(pyproject_data: dict[str, Any]) -> list[str] | None:
    """Extract maintainer value from project maintainers list.

    Uses GitHub noreply emails to extract usernames. Falls back to name if
    email is not a GitHub noreply address.

    Args:
        pyproject_data: Parsed `pyproject.toml` data

    Returns:
        List of GitHub usernames or maintainer names, or `None` if not found
    """
    try:
        maintainers = pyproject_data["project"]["maintainers"]
    except KeyError:
        return None
    if not maintainers:
        return None

    result: list[str] = []
    for m in maintainers:
        if "email" in m:
            github_user = _extract_github_username(m["email"])
            if github_user:
                result.append(github_user)
            elif "name" in m:
                result.append(m["name"])
            else:
                result.append(m["email"])
        elif "name" in m:
            result.append(m["name"])
    return result if result else None

parse_project_dependencies(pyproject_data)

Parse project.dependencies to extract depends and external_dependencies.python.

  • odoo-addon-* packages become depends (with prefix stripped)
  • odoo>=X.Y becomes depends as just 'odoo'
  • Other packages become external_dependencies.python

Parameters:

Name Type Description Default
pyproject_data dict[str, Any]

Parsed pyproject.toml data

required

Returns:

Type Description
tuple[list[str], list[str]]

Tuple of (depends list, external_dependencies.python list)

Source code in src/polytropos/build/manifest.py
def parse_project_dependencies(
    pyproject_data: dict[str, Any],
) -> tuple[list[str], list[str]]:
    """Parse project.dependencies to extract depends and external_dependencies.python.

    - odoo-addon-* packages become depends (with prefix stripped)
    - odoo>=X.Y becomes depends as just 'odoo'
    - Other packages become external_dependencies.python

    Args:
        pyproject_data: Parsed pyproject.toml data

    Returns:
        Tuple of (depends list, external_dependencies.python list)
    """
    project = pyproject_data.get("project", {})
    dependencies: list[str] = project.get("dependencies", [])

    depends: list[str] = []
    external_deps: list[str] = []

    for dep in dependencies:
        req = Requirement(dep)
        name = req.name

        if name.startswith(ADDON_PREFIX):
            depends.append(addon_name(name))
        elif name == "odoo":
            continue
        else:
            external_deps.append(dep)

    return depends, external_deps

write_manifest(manifest, output_path)

Write manifest to manifest.py file.

Parameters:

Name Type Description Default
manifest dict[str, Any]

Manifest dictionary

required
output_path Path

Path where to write manifest.py

required
Source code in src/polytropos/build/manifest.py
def write_manifest(manifest: dict[str, Any], output_path: Path) -> None:
    """Write manifest to __manifest__.py file.

    Args:
        manifest: Manifest dictionary
        output_path: Path where to write __manifest__.py
    """
    content = generate_manifest_content(manifest)
    output_path.write_text(content)