Skip to content

render

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.

XML rendering with release-based conditional content.

Uses XML namespaces to conditionally include/exclude elements and attributes based on the target Odoo release. The namespace URI encodes the release condition::

xmlns:PREFIX="https://polytropos.moduon.team/v1/rel/SPECIFIER"

Where SPECIFIER is a PEP 440 version specifier (e.g., >=18.0, <=18.0). Use &amp;lt; for the < character in specifiers, since < is not allowed literally in XML attribute values.

_matches_specifier(release_version, specifier)

Check if a release version matches a PEP 440 specifier string.

Source code in src/polytropos/render.py
def _matches_specifier(release_version: Version, specifier: str) -> bool:
    """Check if a release version matches a PEP 440 specifier string."""
    if not specifier:
        return False
    try:
        return release_version in SpecifierSet(specifier)
    except (ValueError, TypeError) as exc:
        logger.debug("Invalid version specifier '%s': %s", specifier, exc)
        return False

_parse_release(release)

Parse Odoo release string to Version object.

Source code in src/polytropos/render.py
def _parse_release(release: str) -> Version:
    """Parse Odoo release string to Version object."""
    normalized = release
    if "." not in normalized:
        normalized = f"{normalized}.0"
    return Version(normalized)

_process_attributes(element, release)

Process namespace-qualified attributes on an element.

Non-matching attributes are removed. Matching attributes are renamed (namespace stripped) and may overwrite existing non-namespace attributes.

Source code in src/polytropos/render.py
def _process_attributes(element: ET.Element, release: Version) -> None:
    """Process namespace-qualified attributes on an element.

    Non-matching attributes are removed. Matching attributes are renamed
    (namespace stripped) and may overwrite existing non-namespace attributes.
    """
    to_rename: dict[str, str] = {}
    to_remove: list[str] = []

    for key in list(element.attrib.keys()):
        ns, local_name = _split_ns(key)
        if not ns:
            continue
        if ns.startswith(NAMESPACE_BASE):
            specifier = ns[len(NAMESPACE_BASE) :]
            if _matches_specifier(release, specifier):
                to_rename[key] = local_name
            else:
                to_remove.append(key)

    for key in to_remove:
        del element.attrib[key]
    for old_key, new_local in to_rename.items():
        element.attrib[new_local] = element.attrib.pop(old_key)

_process_element(element, release)

Process element and its children recursively.

Parameters:

Name Type Description Default
element Element

The XML element to process

required
release Version

Target Odoo release version

required

Returns:

Type Description
bool

True if the element should be removed from its parent

Source code in src/polytropos/render.py
def _process_element(element: ET.Element, release: Version) -> bool:
    """Process element and its children recursively.

    Args:
        element: The XML element to process
        release: Target Odoo release version

    Returns:
        True if the element should be removed from its parent
    """
    _process_attributes(element, release)

    for child in list(element):
        if _process_element(child, release):
            element.remove(child)

    ns, local_name = _split_ns(element.tag)
    if not ns:
        return False

    if ns.startswith(NAMESPACE_BASE):
        specifier = ns[len(NAMESPACE_BASE) :]
        if _matches_specifier(release, specifier):
            element.tag = local_name
            return False
        return True

    return False

_split_ns(tag)

Split a Clark notation tag or attribute key into (namespace, local_name).

Parameters:

Name Type Description Default
tag str

A tag or attribute key, possibly in {ns}local form

required

Returns:

Type Description
tuple[str | None, str]

Tuple of (namespace_uri, local_name). If no namespace, namespace is None.

Source code in src/polytropos/render.py
def _split_ns(tag: str) -> tuple[str | None, str]:
    """Split a Clark notation tag or attribute key into (namespace, local_name).

    Args:
        tag: A tag or attribute key, possibly in ``{ns}local`` form

    Returns:
        Tuple of (namespace_uri, local_name). If no namespace, namespace is None.
    """
    if tag.startswith("{"):
        ns, local = tag[1:].split("}", 1)
        return ns, local
    return None, tag

render_xml(content, odoo_release)

Render XML by removing elements/attributes not matching the target release.

Parameters:

Name Type Description Default
content str

XML source content

required
odoo_release str

Target Odoo release (e.g., "18.0")

required

Returns:

Type Description
str

Rendered XML string with namespace conditions resolved

Raises:

Type Description
ParseError

If the XML content is malformed, with details about the problem location.

Source code in src/polytropos/render.py
def render_xml(content: str, odoo_release: str) -> str:
    """Render XML by removing elements/attributes not matching the target release.

    Args:
        content: XML source content
        odoo_release: Target Odoo release (e.g., "18.0")

    Returns:
        Rendered XML string with namespace conditions resolved

    Raises:
        ET.ParseError: If the XML content is malformed, with details about
            the problem location.
    """
    release = _parse_release(odoo_release)
    try:
        root = ET.fromstring(content)
    except ET.ParseError as exc:
        lines = content.splitlines()
        line_no = exc.position[0] if exc.position else 0
        context = "\n".join(lines[max(0, line_no - 2) : line_no + 1])
        msg = f"Failed to parse XML (line {exc.position[0]}, column {exc.position[1]}): {exc.msg}\nNear:\n{context}"
        raise ET.ParseError(msg) from exc
    if _process_element(root, release):
        return ""
    ET.indent(root)
    return ET.tostring(root, encoding="unicode", xml_declaration=True)