Source code for granular_configuration_language.yaml.decorators._base

from __future__ import annotations

import abc
import collections.abc as tabc
import dataclasses
import sys
import typing as typ

from ruamel.yaml import MappingNode, Node, SafeConstructor, ScalarNode, SequenceNode

from granular_configuration_language.exceptions import ErrorWhileLoadingTags, TagHadUnsupportArgument
from granular_configuration_language.yaml.classes import RT, StateHolder, T, Tag
from granular_configuration_language.yaml.decorators._tag_tracker import HandlerAttributes, tracker
from granular_configuration_language.yaml.load._constructors import construct_mapping, construct_sequence

if sys.version_info >= (3, 12):
    from typing import override
elif typ.TYPE_CHECKING:
    from typing_extensions import override
else:

    def override(func: tabc.Callable) -> tabc.Callable:
        return func


Category = typ.NewType("Category", str)
SortedAs = typ.NewType("SortedAs", str)
FriendlyType = typ.NewType("FriendlyType", str)


@dataclasses.dataclass(frozen=True, eq=False, slots=True, repr=False)
class TagConstructor:
    """
    Type: frozen :py:func:`dataclass <dataclasses.dataclass>`

    Links the YAML Constructor to the Tag Logic
    """

    tag: Tag
    category: Category
    sort_as: SortedAs
    friendly_type: FriendlyType
    constructor: tabc.Callable[[type[SafeConstructor], StateHolder], None]
    plugin: str = dataclasses.field(default="Unknown", init=False)
    attributes: HandlerAttributes = dataclasses.field(init=False)

    def __post_init__(self) -> None:
        object.__setattr__(self, "attributes", tracker.get(self.constructor))
        self.attributes.set_tag(self.tag)

    def set_plugin(self, plugin: str) -> None:
        object.__setattr__(self, "plugin", plugin)

    def __call__(self, constructor: type[SafeConstructor], state: StateHolder) -> None:
        return self.constructor(constructor, state)

    @override
    def __repr__(self) -> str:
        return f"<TagConstructor(`{self.tag}`): {self.constructor.__module__}.{self.constructor.__name__}>"


[docs] class TagDecoratorBase(typ.Generic[T], abc.ABC): """Base class for Tag Decorator factories. You must implement the :py:attr:`user_friendly_type` property and define the generic type. Example: .. code-block:: python class string_tag(TagDecoratorBase[str]): Type: typing.TypeAlias = str @property def user_friendly_type(self) -> str: return "str" You must override at least one of :py:meth:`scalar_node_type_check`, :py:meth:`sequence_node_type_check`,or :py:meth:`mapping_node_type_check`. - For :py:meth:`scalar_node_type_check` to be called the YAML has already be tested to be a :py:class:`str`. - For :py:meth:`sequence_node_type_check` to be called the YAML has already be tested to be a :py:class:`~collections.abc.Sequence`. - For :py:meth:`mapping_node_type_check` to be called the YAML has already be tested to be a :py:class:`~collections.abc.Mapping`. If these are enough, then you may just return :py:data:`True` in the override method. Otherwise, implement the override as a :py:data:`~typing.TypeGuard`. If the value needs to be altered before being passed to Tag functions, override :py:meth:`scalar_node_transformer`, :py:meth:`scalar_node_transformer`, or :py:meth:`mapping_node_transformer`, as needed. The transformer is called if the associated node type check passes, just before the value is passed to tag function. """ __slots__ = ("tag", "category", "sort_as")
[docs] @typ.final def __init__(self, tag: Tag, category: str = "General", *, sort_as: str | None = None) -> None: """ :param Tag tag: Value of Tag. Expected to be constructed inline using :py:class:`.Tag` (e.g. ``Tag("!Tag")``). Must start with ``!``. :param str, optional category: Category of Tag. Used by :ref:`available_tags <available_tags>` and :ref:`available_plugins <available_plugins>` to organize tags, defaults to ``General``. :param str, optional sort_as: Alternative Tag string. Used for sorting tags different to its explicit value. Used by :ref:`available_tags <available_tags>` and :ref:`available_plugins <available_plugins>`. """ self.tag: typ.Final = tag self.category: typ.Final = Category(category) self.sort_as: typ.Final = SortedAs(sort_as or tag) if not tag.startswith("!"): raise ErrorWhileLoadingTags(f"Tag `{tag}` error: All tags must begin with `!`.")
@property @abc.abstractmethod def user_friendly_type(self) -> str: """User-friendly of the type expected by Tag Decorator. Note: - Use Python types for consistent communication. - This is used when generating exception messages. :returns: User-friendly string :rtype: str """ ...
[docs] def scalar_node_type_check(self, value: str) -> typ.TypeGuard[T]: """Defaults to :py:data:`False`. Override to enable Scalar Node support. Parameters: value (str): YAML value Returns: ~typing.TypeGuard[T]: Return :py:data:`True`, if ``value`` is supported. """ return False
[docs] def sequence_node_type_check(self, value: tabc.Sequence) -> typ.TypeGuard[T]: """Defaults to :py:data:`False`. Override to enable Sequence Node support. Parameters: value (~collections.abc.Sequence): YAML value Returns: ~typing.TypeGuard[T]: Return :py:data:`True`, if ``value`` is supported. """ return False
[docs] def mapping_node_type_check(self, value: tabc.Mapping) -> typ.TypeGuard[T]: """Defaults to :py:data:`False`. Override to enable Mapping Node support. Parameters: value (~collections.abc.Mapping): YAML value Returns: ~typing.TypeGuard[T]: Return :py:data:`True`, if ``value`` is supported. """ return False
[docs] def scalar_node_transformer(self, value: typ.Any) -> T: """Defaults to an identity operation. Override if the value needs to be altered before being passed to Tag functions. Only called if :py:meth:`scalar_node_type_check` return :py:data:`True`. As an example, a float tag could be supported by: .. code-block:: python def scalar_node_type_check(self, value: str) -> TypeGuard[float]: try: float(value) return True except ValueError: return False def scalar_node_transformer(self, value: Any) -> float: return float(value) Parameters: value (~typing.Any): YAML value Returns: T: Transformed value """ return value
[docs] def sequence_node_transformer(self, value: typ.Any) -> T: """Defaults to an identity operation. Override if the value needs to be altered before being passed to Tag functions. Only called if :py:meth:`sequence_node_type_check` return :py:data:`True`. Parameters: value (~typing.Any): YAML value Returns: T: Transformed value """ return value
[docs] def mapping_node_transformer(self, value: typ.Any) -> T: """Defaults to an identity operation. Override if the value needs to be altered before being passed to Tag functions. Only called if :py:meth:`mapping_node_type_check` return :py:data:`True`. Parameters: value (~typing.Any): YAML value Returns: T: Transformed value """ return value
@typ.final def __call__(self, handler: tabc.Callable[[Tag, T, StateHolder], RT], /) -> TagConstructor: # """Takes the wrapped tag function and further wraps it for configuration loading. # :param (~collections.abc.Callable[[Tag, T, StateHolder], RT]) handler: Wrapped Tag Function # :return: Tag Function ready to be used when loading configuration # :rtype: TagConstructor # :meta private: # """ # autodoc refuses to exclude `__call__`, even though this is not a part of the public interface # Don't capture self in the function generation tag = self.tag user_friendly_type = FriendlyType(self.user_friendly_type) category = self.category sort_as = self.sort_as scalar_node_type_check = self.scalar_node_type_check sequence_node_type_check = self.sequence_node_type_check mapping_node_type_check = self.mapping_node_type_check scalar_node_transformer = self.scalar_node_transformer sequence_node_transformers = self.sequence_node_transformer mapping_node_transformer = self.mapping_node_transformer @tracker.wraps(handler) def add_handler( constructor: type[SafeConstructor], state: StateHolder, ) -> None: @tracker.wraps(handler) def type_handler(constructor: SafeConstructor, node: Node) -> RT: try: if isinstance(node, ScalarNode): value = constructor.construct_scalar(node) if isinstance(value, str) and scalar_node_type_check(value): return handler(tag, scalar_node_transformer(value), state) elif isinstance(node, SequenceNode): value = construct_sequence(state.options.sequence_func, constructor, node) if isinstance(value, tabc.Sequence) and sequence_node_type_check(value): return handler(tag, sequence_node_transformers(value), state) elif isinstance(node, MappingNode): value = construct_mapping(state.options.obj_pairs_func, constructor, node) if isinstance(value, tabc.Mapping) and mapping_node_type_check(value): return handler(tag, mapping_node_transformer(value), state) else: pass # pragma: no cover except ValueError: raise TagHadUnsupportArgument( f"`{tag}` supports: {user_friendly_type}. Got: `{repr(node)}`" ) from None # Fallback Exception raise TagHadUnsupportArgument(f"`{tag}` supports: {user_friendly_type}. Got: `{repr(node)}`") constructor.add_constructor(tag, type_handler) return TagConstructor(tag, category, sort_as, user_friendly_type, add_handler)