Source code for r2x_core.units._mixins

"""Mixin classes for unit-aware models."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, get_args

from pydantic import PrivateAttr, model_validator

if TYPE_CHECKING:
    from ._specs import UnitSpec


[docs] class HasUnits: """Mixin providing unit-aware field formatting. This mixin provides unit-aware field formatting in repr without per-unit conversion capabilities. Suitable for components that only have absolute unit fields (e.g., voltages in kV, power in MW) without base conversions. Can be combined with any Pydantic BaseModel or Component: class MyComponent(HasUnits, Component): ... """ def __init_subclass__(cls, **kwargs: Any) -> None: """Validate that subclass inherits from BaseModel. Raises ------ TypeError If the subclass does not inherit from pydantic.BaseModel """ super().__init_subclass__(**kwargs) from pydantic import BaseModel # Skip validation for HasPerUnit (which is also a mixin) if cls.__name__ == "HasPerUnit": return # Check if any base class is BaseModel if not any(issubclass(base, BaseModel) for base in cls.__mro__ if base not in (cls, HasUnits)): raise TypeError( f"{cls.__name__} must inherit from pydantic.BaseModel or infrasys.Component. " f"Example: class {cls.__name__}(HasUnits, Component): ..." ) def _get_system_base(self) -> float | None: """Get system base value if this component supports it. Returns ------- float or None System base value, or None if not supported """ return None @classmethod def _get_unit_specs_map(cls) -> dict[str, UnitSpec]: """Build and cache unit specifications for all annotated fields. Returns ------- dict[str, UnitSpec] Mapping of field names to their unit specifications """ # Import here to avoid circular dependency from ._specs import UnitSpec from ._utils import _is_annotated cache_attr = f"_unit_specs_cache_{cls.__name__}" if not hasattr(cls, cache_attr): specs: dict[str, UnitSpec] = {} annotations = getattr(cls, "__annotations__", {}) for fname, ann in annotations.items(): if _is_annotated(ann): for meta in get_args(ann)[1:]: if isinstance(meta, UnitSpec): specs[fname] = meta setattr(cls, cache_attr, specs) cached: dict[str, UnitSpec] = getattr(cls, cache_attr) return cached @model_validator(mode="wrap") @classmethod def _seed_unit_context(cls, values: Any, handler: Any, info: Any) -> Any: """Seed validation context with unit specs and base units map. Parameters ---------- values : Any Input values to validate handler : Any Validation handler info : Any Validation info containing context Returns ------- Any Validated model instance """ ctx = info.context if info.context is not None else {} if isinstance(ctx, dict) and ("unit_specs" not in ctx or "base_units" not in ctx): specs = cls._get_unit_specs_map() base_units = {fname: spec.unit for fname, spec in specs.items() if spec.base is None} ctx.setdefault("unit_specs", specs) ctx.setdefault("base_units", base_units) return handler(values) def __repr_args__(self) -> list[tuple[str | None, Any]]: """Format fields respecting current display mode. Returns ------- list[tuple[str | None, Any]] List of (field_name, formatted_value) tuples for repr """ # Import here to avoid circular dependency from . import get_unit_system from ._utils import _format_for_display repr_args: list[tuple[str | None, Any]] = [] specs_map = type(self)._get_unit_specs_map() unit_system = get_unit_system() for field_name in type(self).model_fields: # type: ignore[attr-defined] if field_name.startswith("_"): continue value = getattr(self, field_name) unit_spec = specs_map.get(field_name) if unit_spec is None: repr_args.append((field_name, value)) else: base_value: float | None = None base_unit: str | None = None if unit_spec.base: base_value = getattr(self, unit_spec.base, None) base_spec = specs_map.get(unit_spec.base) base_unit = base_spec.unit if base_spec else None formatted = _format_for_display( value, unit_spec, unit_system, base_value, base_unit, self._get_system_base() ) repr_args.append((field_name, formatted)) return repr_args
[docs] class HasPerUnit(HasUnits): """Component class with per-unit conversion capabilities. This class extends HasUnits with system-base per-unit display support. Use this for components that have both absolute unit fields (base values) and per-unit fields that reference those bases. Attributes ---------- _system_base : float or None System-wide base power for system-base per-unit display """ _system_base: float | None = PrivateAttr(default=None) def _get_system_base(self) -> float | None: """Get system base value for this component. Returns ------- float or None System base value, or None if not set """ return self.__dict__.get("_system_base")