Source code for descriptor

"""Base descriptor protocol for ODM element attributes.

This module provides the :class:`Descriptor` base class that implements
the Python descriptor protocol for ODM element attributes. All odmlib
attribute types inherit from this class.
"""
from __future__ import annotations
import weakref
from typing import Any, Optional
from odmlib.exceptions import OdmlibRequiredAttributeError
import odmlib.mode as _mode


# Elements instantiated as a side effect of reading an unset optional child
# attribute (the auto-vivification that makes `elem.Child.Sub.append(...)`
# work). Serialization skips members of this set that were never populated,
# so read-only inspection cannot inject spurious empty elements into output.
_AUTO_CREATED: "weakref.WeakSet" = weakref.WeakSet()


def _effectively_empty(elem: Any) -> bool:
    """True when every stored value is None, an empty list, or itself a
    pristine auto-created element."""
    for value in elem.__dict__.values():
        if value is None:
            continue
        if isinstance(value, list):
            if value:
                return False
            continue
        try:
            auto_created = value in _AUTO_CREATED
        except TypeError:
            auto_created = False
        if auto_created and _effectively_empty(value):
            continue
        return False
    return True


[docs] def is_pristine_auto_created(elem: Any) -> bool: """True if *elem* was auto-created by attribute access and never populated. Used by serialization to skip empty elements that exist only because an unset optional child attribute was read. """ try: if elem not in _AUTO_CREATED: return False except TypeError: return False return _effectively_empty(elem)
[docs] class Descriptor: """Base descriptor for all ODM element attributes. Implements the Python descriptor protocol (``__get__``, ``__set__``, ``__delete__``). When accessed on a class, returns the descriptor itself. When accessed on an instance, returns the value from the instance's ``__dict__``. For required attributes without a set value, raises :exc:`~odmlib.exceptions.OdmlibRequiredAttributeError`. For optional list attributes without a value, auto-initializes to [] (stored on the instance so the append idiom works). For unset optional scalar attributes, returns ``None``. For unset single child elements, auto-instantiates the element class when it can be constructed empty (returns ``None`` otherwise); the auto-created child is skipped by serialization unless populated. Args: name (str): Attribute name (set by :class:`ODMMeta` during class creation). required (bool): If True, attribute must be provided at construction. element_class (type): For child element descriptors, the expected class. valid_values (list): List of permitted values (empty = any value). namespace (str): XML namespace prefix (default: "odm"). """ def __init__(self, name: Optional[str] = None, required: bool = False, element_class: Optional[type] = None, valid_values: Optional[list] = None, namespace: str = "odm") -> None: self.name = name self.required = required self.element_class = element_class self.valid_values = valid_values if valid_values is not None else [] self.namespace = namespace def __get__(self, instance: Any, cls: type) -> Any: """Return the descriptor itself (class access) or the stored value (instance access). Args: instance: The object instance, or None if accessed on the class. cls: The owner class. Returns: The descriptor itself when accessed on the class, or the stored attribute value when accessed on an instance. Raises: OdmlibRequiredAttributeError: If the attribute is required and has not been set on the instance. """ if instance is None: return self if self.name in instance.__dict__: return instance.__dict__[self.name] # Attribute not set on instance — raise only for plain required attributes # (not ODMObject/ODMListObject, which have element_class set and auto-initialize) if self.required and self.element_class is None: if not _mode.is_permissive(_mode.ValidationMode.SKIP_REQUIRED): raise OdmlibRequiredAttributeError( f"Missing attribute or element {self.name} in {cls.__name__}", attribute=self.name, element_type=cls.__name__, hint=f"Attribute '{self.name}' is required when constructing {cls.__name__}", ) else: return None # Auto-initialize optional LIST attributes: the stored empty list is # what makes the `elem.Child.append(...)` idiom work. if isinstance(self, list): self.__set__(instance, []) return instance.__dict__[self.name] # Unset optional scalar: read as None without mutating the instance. if self.element_class is None: return None # Unset single child element: auto-create so chained population works # (`rc.ErrorMessage.TranslatedText.append(...)`). The instance is # tracked so serialization can skip it if it is never populated. try: child = self.element_class() except OdmlibRequiredAttributeError: # cannot be constructed empty — leave the attribute unset return None instance.__dict__[self.name] = child _AUTO_CREATED.add(child) return child def __set__(self, instance: Any, value: Any) -> None: """Store ``value`` in the instance's ``__dict__`` under this descriptor's name. Args: instance: The object instance on which to set the value. value: The value to store. """ instance.__dict__[self.name] = value def __delete__(self, instance: Any) -> None: """Remove this attribute from the instance's ``__dict__``. Args: instance: The object instance from which to remove the attribute. """ del instance.__dict__[self.name]