Source code for coma.hooks.base
"""Base of hook utilities implementation."""
from typing import Any, Callable, Optional, TypeVar, Union
from dataclasses import dataclass
from collections.abc import Sequence
from enum import Enum
import argparse
from ..config import ParamData, PersistenceManager
T = TypeVar("T")
# https://stackoverflow.com/questions/69239403/type-hinting-parameters-with-a-sentinel-value-as-the-default
[docs]
class HookSentinels(Enum):
"""
Type for sentinels for hooks.
Attributes:
DEFAULT: Replace a hook marked as this with a default hook.
SHARED: Replace a command-specific hook marked as this with a shared hook.
See also:
* :meth:`~coma.hooks.management.Hooks.merge()`
"""
DEFAULT = 0
SHARED = 1
[docs]
class GeneralSentinel(Enum):
"""Type for the sentinel of general use."""
token = 0
DEFAULT = HookSentinels.DEFAULT
"""
Sentinel for marking a hook to either :func:`~coma.core.command.command()` or
:func:`~coma.core.wake.wake()` as being a default hook. The runtime value of
the hook will be replaced by the corresponding ``coma`` default.
Recall that a hook can also be defined as a (recursive) sequence. Each occurrence
of :obj:`DEFAULT` in said sequence will be replaced by the ``coma`` default.
"""
SHARED = HookSentinels.SHARED
"""
Sentinel for marking that a hook to :func:`~coma.core.command.command` ought be
be replaced at runtime with the runtime value of the corresponding hook from
:func:`~coma.core.wake.wake()`.
Recall that a hook can also be defined as a (recursive) sequence. Each occurrence
of :obj:`SHARED` in said sequence will be replaced by the :obj:`wake()` value.
:obj:`SHARED` is not a legal value for :obj:`wake()` hooks to avoid infinite regress.
"""
SENTINEL = GeneralSentinel.token
"""
A convenient pre-defined sentinel for general use. Unlike other sentinels in ``coma``,
:obj:`SENTINEL` has no special semantic value beyond its existence as a sentinel.
"""
Hook = Callable[[T], Optional[T]]
"""
Base definition of a "raw" ``coma`` hook. A valid hook is typically a procedural
function (modifying its input parameter in place and returning :obj:`None`). However,
returning an instance of the same type :obj:`T` is also permitted to account for cases
where :obj:`T` is an immutable type. A non-:obj:`None` return value takes precedence:
it replaces the procedural parameter is all downstream hooks in a hook pipeline.
Typically, :obj:`T` is a subclass of :class:`~coma.hooks.base.HookData`.
Alias:
"""
HookOrSentinels = Union[Hook, HookSentinels, None]
"""
A :data:`~coma.hooks.base.Hook` or one of the valid hook sentinels:
:data:`~coma.hooks.base.DEFAULT`, :data:`~coma.hooks.base.SHARED`, or :obj:`None`.
Alias:
"""
AugmentedHook = Union[HookOrSentinels, Sequence[HookOrSentinels]]
"""
A :data:`~coma.hooks.base.HookOrSentinels`, or any (recursive) :obj:`Sequence`
thereof.
Example::
parser_hook=(
DEFAULT, (
SHARED, (
add_argument_factory(...),
None,
),
),
add_argument_factory(...),
)
Alias:
"""
CommandName = str
"""
The name under which to register a command with ``coma``. The same value is used to
invoke the command on the command line. Any value allowed by ``argparse`` is allowed.
"""
Command = Union[Callable, type]
"""
Any function or any class with (by default) a no-argument :obj:`run()` method. Configs
are inferred from the command signature. See :func:`~coma.core.command.command()`.
"""
[docs]
@dataclass
class HookData:
"""
Base class for typical :data:`~coma.hooks.base.Hook` arguments.
Attributes:
name (:data:`~coma.hooks.base.CommandName`): The command name.
command (:data:`~coma.hooks.base.Command`, optional): The command itself.
parameters (:class:`~coma.config.cli.ParamData`): The command's parameters.
persistence_manager (:class:`~coma.config.io.PersistenceManager`): The
manager for serializing configs in :obj:`parameters`.
"""
name: CommandName
command: Command
parameters: ParamData
persistence_manager: PersistenceManager
[docs]
@dataclass
class ParserData(HookData):
"""
The :class:`~coma.hooks.base.HookData` for parser hooks.
Attributes:
parser (argparse.ArgumentParser): The sub-parser for this :obj:`command`.
"""
parser: argparse.ArgumentParser
[docs]
@dataclass
class InvocationData(HookData):
"""
The :class:`~coma.hooks.base.HookData` for all invocation hooks.
Attributes:
known_args (typing.Any): The :obj:`namespace` of known command line arguments.
Typically, this is the first return value of `parse_known_args()`_.
unknown_args (list[str]): The list of unknown command line arguments.
Typically, this is the second return value of `parse_known_args()`_.
result (typing.Any, optional): The return value from invoking :obj:`command`.
.. _parse_known_args():
https://docs.python.org/3/library/argparse.html#partial-parsing
"""
known_args: Any
unknown_args: list[str]
result: Optional[Any] = None
[docs]
def identity(arg: T) -> T:
"""A no-op :data:`~coma.hooks.base.Hook`. For convenience."""
return arg
[docs]
class Pipe:
"""
A convenience wrapper for sequences of :data:`~coma.hooks.base.Hook` s.
Recursively composes functions, which can then be invoked with a single call.
Args:
*fns (Union[:data:`~coma.hooks.base.Hook`, Sequence[:data:`~coma.hooks.base.Hook`]]):
The :data:`~coma.hooks.base.Hook` s to recursively compose (in order).
"""
def __init__(self, *fns: Union[Hook, Sequence[Hook]]):
self.fns = [(Pipe(*f) if isinstance(f, Sequence) else f) for f in fns]
self.fns = self.fns or [identity]
def __call__(self, arg: T) -> T:
"""Recursively calls the composed functions, returning the final result."""
for f in self.fns:
arg = f(arg) or arg
return arg