Skip to content

ph

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.

Polytropos helpers for Odoo addons.

This library is meant to be used by Odoo addons at runtime.

OverrideResolver

Descriptor for version-conditional method overrides.

Created by :func:override. When a class is defined, :meth:__set_name__ replaces this descriptor with the resolved function — the first variant whose specifier matches the current Odoo release, or the default.

Source code in src/polytropos/ph.py
class OverrideResolver:
    """Descriptor for version-conditional method overrides.

    Created by :func:`override`. When a class is defined, :meth:`__set_name__`
    replaces this descriptor with the resolved function — the first variant
    whose specifier matches the current Odoo release, or the default.
    """

    def __init__(self: "OverrideResolver", default_func: Callable[..., Any]) -> None:
        self.default = default_func
        self.variants: list[tuple[SpecifierSet, Callable[..., Any]]] = []

    def when(
        self: "OverrideResolver", spec: str
    ) -> Callable[[Callable[..., Any]], "OverrideResolver"]:
        """Register a version-conditional variant of the method.

        The variant replaces the default when the current Odoo release
        matches *spec*. Variants are evaluated in declaration order; the
        first match wins.

        Args:
            spec: PEP 440 version specifier (e.g., ``">=19"``, ``"<19"``,
                ``">=18,<19"``)

        Raises:
            AssertionError: If the variant function's ``__name__`` differs
                from the default method's ``__name__``. All methods in a
                ``@ph.override`` group must share the same name.

        Returns:
            A decorator that registers the variant and returns this resolver.
        """
        specset = SpecifierSet(spec)

        def decorator(variant_func: Callable[..., Any]) -> "OverrideResolver":
            assert variant_func.__name__ == self.default.__name__, (
                f"@ph.override variants must share the same __name__; "
                f"got '{variant_func.__name__}' but expected '{self.default.__name__}'"
            )
            self.variants.append((specset, variant_func))
            return self

        return decorator

    def _resolve(self: "OverrideResolver") -> Callable[..., Any]:
        """Resolve which function to use based on the current Odoo release.

        The resolved function always exposes the original default
        implementation via its ``.default`` attribute, so variants
        can call it when they only need to adjust behavior for a
        specific release.

        Returns:
            The matching variant (with ``.default`` attached), or the
            default (with ``.default`` pointing to itself).
        """
        if series is None:
            self.default.default = self.default  # type: ignore[attr-defined]
            return self.default
        current = Version(series)
        matched: list[str] = []
        for specset, func in self.variants:
            if current in specset:
                matched.append(str(specset))
                if len(matched) == 1:
                    resolved = func
        if not matched:
            self.default.default = self.default  # type: ignore[attr-defined]
            return self.default
        if len(matched) > 1:
            logger.warning(
                "Multiple @ph.override variants of '%s' match Odoo %s: "
                "%s. Using first match (%s).",
                self.default.__name__,
                series,
                ", ".join(matched),
                matched[0],
            )
        resolved.default = self.default  # type: ignore[attr-defined]
        return resolved

    def __set_name__(self: "OverrideResolver", owner: type, name: str) -> None:
        """Replace this descriptor with the resolved function.

        Called by ``type.__new__`` during class creation. The resolved
        function becomes the class attribute, so lookup and ``super()``
        work naturally.
        """
        resolved = self._resolve()
        resolved.__qualname__ = f"{owner.__name__}.{name}"
        setattr(owner, name, resolved)

    def __get__(
        self: "OverrideResolver", obj: object, objtype: type | None = None
    ) -> Callable[..., Any]:
        """Descriptor protocol — resolves and binds on access.

        This is a safety net in case ``__set_name__`` has not been called
        (e.g. module-level use outside a class body) or the descriptor
        was manually kept.
        """
        if obj is None:
            return self  # type: ignore[return-value]
        func = self._resolve()
        return func.__get__(obj, objtype)  # type: ignore[no-any-return]

__get__(obj, objtype=None)

Descriptor protocol — resolves and binds on access.

This is a safety net in case __set_name__ has not been called (e.g. module-level use outside a class body) or the descriptor was manually kept.

Source code in src/polytropos/ph.py
def __get__(
    self: "OverrideResolver", obj: object, objtype: type | None = None
) -> Callable[..., Any]:
    """Descriptor protocol — resolves and binds on access.

    This is a safety net in case ``__set_name__`` has not been called
    (e.g. module-level use outside a class body) or the descriptor
    was manually kept.
    """
    if obj is None:
        return self  # type: ignore[return-value]
    func = self._resolve()
    return func.__get__(obj, objtype)  # type: ignore[no-any-return]

__set_name__(owner, name)

Replace this descriptor with the resolved function.

Called by type.__new__ during class creation. The resolved function becomes the class attribute, so lookup and super() work naturally.

Source code in src/polytropos/ph.py
def __set_name__(self: "OverrideResolver", owner: type, name: str) -> None:
    """Replace this descriptor with the resolved function.

    Called by ``type.__new__`` during class creation. The resolved
    function becomes the class attribute, so lookup and ``super()``
    work naturally.
    """
    resolved = self._resolve()
    resolved.__qualname__ = f"{owner.__name__}.{name}"
    setattr(owner, name, resolved)

_resolve()

Resolve which function to use based on the current Odoo release.

The resolved function always exposes the original default implementation via its .default attribute, so variants can call it when they only need to adjust behavior for a specific release.

Returns:

Type Description
Callable[..., Any]

The matching variant (with .default attached), or the

Callable[..., Any]

default (with .default pointing to itself).

Source code in src/polytropos/ph.py
def _resolve(self: "OverrideResolver") -> Callable[..., Any]:
    """Resolve which function to use based on the current Odoo release.

    The resolved function always exposes the original default
    implementation via its ``.default`` attribute, so variants
    can call it when they only need to adjust behavior for a
    specific release.

    Returns:
        The matching variant (with ``.default`` attached), or the
        default (with ``.default`` pointing to itself).
    """
    if series is None:
        self.default.default = self.default  # type: ignore[attr-defined]
        return self.default
    current = Version(series)
    matched: list[str] = []
    for specset, func in self.variants:
        if current in specset:
            matched.append(str(specset))
            if len(matched) == 1:
                resolved = func
    if not matched:
        self.default.default = self.default  # type: ignore[attr-defined]
        return self.default
    if len(matched) > 1:
        logger.warning(
            "Multiple @ph.override variants of '%s' match Odoo %s: "
            "%s. Using first match (%s).",
            self.default.__name__,
            series,
            ", ".join(matched),
            matched[0],
        )
    resolved.default = self.default  # type: ignore[attr-defined]
    return resolved

when(spec)

Register a version-conditional variant of the method.

The variant replaces the default when the current Odoo release matches spec. Variants are evaluated in declaration order; the first match wins.

Parameters:

Name Type Description Default
spec str

PEP 440 version specifier (e.g., ">=19", "<19", ">=18,<19")

required

Raises:

Type Description
AssertionError

If the variant function's __name__ differs from the default method's __name__. All methods in a @ph.override group must share the same name.

Returns:

Type Description
Callable[[Callable[..., Any]], OverrideResolver]

A decorator that registers the variant and returns this resolver.

Source code in src/polytropos/ph.py
def when(
    self: "OverrideResolver", spec: str
) -> Callable[[Callable[..., Any]], "OverrideResolver"]:
    """Register a version-conditional variant of the method.

    The variant replaces the default when the current Odoo release
    matches *spec*. Variants are evaluated in declaration order; the
    first match wins.

    Args:
        spec: PEP 440 version specifier (e.g., ``">=19"``, ``"<19"``,
            ``">=18,<19"``)

    Raises:
        AssertionError: If the variant function's ``__name__`` differs
            from the default method's ``__name__``. All methods in a
            ``@ph.override`` group must share the same name.

    Returns:
        A decorator that registers the variant and returns this resolver.
    """
    specset = SpecifierSet(spec)

    def decorator(variant_func: Callable[..., Any]) -> "OverrideResolver":
        assert variant_func.__name__ == self.default.__name__, (
            f"@ph.override variants must share the same __name__; "
            f"got '{variant_func.__name__}' but expected '{self.default.__name__}'"
        )
        self.variants.append((specset, variant_func))
        return self

    return decorator

override(f)

Make a method version-aware with :meth:OverrideResolver.when.

The decorated method is the default implementation. Register version-specific alternatives by decorating the same name with .when()::

from polytropos import ph

class MyModel(models.Model):
    @ph.override
    def name_search(self, name, domain=None, operator="ilike", limit=100):
        return super().name_search(
            name, domain=domain, operator=operator, limit=limit,
        )

    @name_search.when("<19")
    def name_search(self, name, args=None, operator="ilike", limit=100):
        return super().name_search(
            name, args=args, operator=operator, limit=limit,
        )

At class creation time the resolver checks the current Odoo release and keeps only the method that matches. Inheritance and super() work as expected.

Raises:

Type Description
TypeError

If Odoo is not installed and no default is reachable (should not happen in normal Odoo operation).

Returns:

Type Description
OverrideResolver

class:OverrideResolver - a descriptor that is replaced by the

OverrideResolver

resolved function when the class is created.

Source code in src/polytropos/ph.py
def override(f: Callable[..., Any]) -> OverrideResolver:
    """Make a method version-aware with :meth:`OverrideResolver.when`.

    The decorated method is the default implementation.  Register
    version-specific alternatives by decorating the *same* name with
    ``.when()``::

        from polytropos import ph

        class MyModel(models.Model):
            @ph.override
            def name_search(self, name, domain=None, operator="ilike", limit=100):
                return super().name_search(
                    name, domain=domain, operator=operator, limit=limit,
                )

            @name_search.when("<19")
            def name_search(self, name, args=None, operator="ilike", limit=100):
                return super().name_search(
                    name, args=args, operator=operator, limit=limit,
                )

    At class creation time the resolver checks the current Odoo release
    and keeps only the method that matches.  Inheritance and ``super()``
    work as expected.

    Raises:
        TypeError: If Odoo is not installed and no default is reachable
            (should not happen in normal Odoo operation).

    Returns:
        :class:`OverrideResolver` - a descriptor that is replaced by the
        resolved function when the class is created.
    """
    return OverrideResolver(f)

rel(spec)

Check if current Odoo release matches a PEP 440 version specifier.

This function checks the Odoo release (e.g., "17.0", "18.0") against a PEP 440 specifier, useful for conditional code execution.

Parameters:

Name Type Description Default
spec str

PEP 440 version specifier (e.g., ">=17.0", ">=18.0,<19.0")

required

Raises:

Type Description
TypeError

If Odoo is not installed.

Returns:

Type Description
bool

True if current Odoo release matches the spec, False otherwise.

Source code in src/polytropos/ph.py
def rel(spec: str) -> bool:
    """Check if current Odoo release matches a PEP 440 version specifier.

    This function checks the Odoo release (e.g., "17.0", "18.0") against
    a PEP 440 specifier, useful for conditional code execution.

    Args:
        spec: PEP 440 version specifier (e.g., ">=17.0", ">=18.0,<19.0")

    Raises:
        TypeError: If Odoo is not installed.

    Returns:
        True if current Odoo release matches the spec, False otherwise.
    """
    if series is None:
        raise TypeError("Odoo is not installed; cannot check release spec.")
    current = Version(series)
    return current in SpecifierSet(spec)