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
@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)