Source code for app.util.mixins.models

# SPDX-License-Identifier: GPLv3-or-later
# Copyright © 2025 pygaindalf Rui Pinheiro

import warnings

from pydantic import BaseModel, model_validator, Field, field_validator
from typing import Any, Self, Annotated, override

from . import HierarchicalMutableProtocol, NamedMutableProtocol, HierarchicalMixinMinimal, NamedMixinMinimal, LoggableMixin, HierarchicalProtocol, NamedProtocol



[docs] class HierarchicalModel(BaseModel, HierarchicalMixinMinimal): instance_parent : object | None = Field(default=None, repr=False, exclude=True, description="Parent of this instance in the hierarchy. Can be None if this is a root instance.") @field_validator('instance_parent', mode='after') @classmethod def _validate_instance_parent(cls, obj : object | None) -> HierarchicalProtocol | NamedProtocol | None: if obj is not None and not isinstance(obj, (HierarchicalProtocol | NamedProtocol)): raise TypeError(f"Expected HierarchicalProtocol | NamedProtocol | None, got {type(obj)}") return obj # NOTE: We use object.__setattr__ to avoid triggering Pydantic's validation which would raise an error if the object is not # mutable. # Since it only happens at the model validation stage as part of __init__ we are not breaking the mutability contract. # We do sanity check that the current values are not already set to avoid overwriting them. def _seed_parent_to_object(self, obj : Any) -> None: if isinstance(obj, HierarchicalMutableProtocol) and obj.instance_parent is not self: if obj.instance_parent is not None: raise ValueError(f"Object {obj} already has a parent set: {obj.instance_parent}. Cannot overwrite with {self}.") object.__setattr__(obj, 'instance_parent', self) def _seed_name_to_object(self, obj : Any, name : str) -> None: if isinstance(obj, NamedMutableProtocol) and obj.instance_name != name: if obj.instance_name is not None: raise ValueError(f"Object {obj} already has a name set: {obj.instance_name}. Cannot overwrite with {name}.") object.__setattr__(obj, 'instance_name', name) def _seed_parent_and_name_to_object(self, obj : Any, name : str) -> None: self._seed_parent_to_object(obj) self._seed_name_to_object(obj, name) @model_validator(mode='after') def _validator_seed_parent_and_name(self) -> Any: for fldnm, fldinfo in self.__class__.model_fields.items(): fld = getattr(self, fldnm, None) if fld is None: continue extra = fldinfo.json_schema_extra if isinstance(fldinfo.json_schema_extra, dict) else None hierarchical = extra.get('hierarchical', True) if extra else True if not hierarchical: continue if isinstance(fld, list): for item in fld: self._seed_parent_and_name_to_object(item, fldnm) elif isinstance(fld, dict): for key, item in fld.items(): self._seed_parent_and_name_to_object(item, f"{fldnm}.{key}") else: self._seed_parent_and_name_to_object(fldnm, fld) return self @override def __str__(self): return super(HierarchicalMixinMinimal, self).__str__() @override def __repr__(self): return super(HierarchicalMixinMinimal, self).__repr__()
[docs] class HierarchicalNamedModel(HierarchicalModel): instance_name : str | None = Field(default=None, min_length=1, description="Name of the instance.")
[docs] class LoggableHierarchicalModel(LoggableMixin, HierarchicalModel): pass
[docs] class LoggableHierarchicalNamedModel(LoggableMixin, HierarchicalNamedModel): pass