"""Register a command that might be invoked upon waking from a coma."""
from inspect import signature
from typing import Any, Callable, Optional
from boltons.funcutils import wraps
from .singleton import Coma, RegistrationData
from ..config import (
Parameters,
PersistenceManager,
SignatureInspector,
SignatureInspectorProtocol,
)
from ..hooks.base import Command, CommandName, AugmentedHook, SHARED
from ..hooks.management import Hooks
# Implementation note: hooks here default to SHARED and not None (even for those where
# the wake() default is None), so that users who update some hook in wake() have that
# automatically applied to each command. If None was the default here, then adding a
# hook to wake would require explicitly setting that hook to SHARED for *all* commands,
# which is the opposite of what we want. Default behavior: shared. If sharing is not
# desired for a specific command, set to that hook to None for just that command.
[docs]
def command(
cmd: Optional[Command] = None,
*,
name: Optional[CommandName] = None,
parser_hook: AugmentedHook = SHARED,
pre_config_hook: AugmentedHook = SHARED,
config_hook: AugmentedHook = SHARED,
post_config_hook: AugmentedHook = SHARED,
pre_init_hook: AugmentedHook = SHARED,
init_hook: AugmentedHook = SHARED,
post_init_hook: AugmentedHook = SHARED,
pre_run_hook: AugmentedHook = SHARED,
run_hook: AugmentedHook = SHARED,
post_run_hook: AugmentedHook = SHARED,
signature_inspector: Optional[SignatureInspectorProtocol] = None,
persistence_manager: Optional[PersistenceManager] = None,
parser_kwargs: Optional[Parameters] = None,
**supplemental_configs: Any,
):
"""
Registers a command that might be invoked upon waking from a coma.
Registers a command with `ArgumentParser.add_subparsers().add_parser()`_ using
the given registration data (name, hooks, config declarations, persistence manager,
and supplemental configs) and the given parser kwargs.
.. note::
``coma``'s architecture follows the `Template`_ design pattern with `hooks`_
intended to add or modify behavior. Pre-defined hooks specify ``coma``'s
default behavior. Pre-defined hook factories enable one-line deployment of
small tweaks on the core default behavior. ``coma`` has very few baked in
assumptions. Nearly all behavior can be drastically changed with user-defined
hooks. For detailed tutorials and usage examples of both the default behavior
and implementation of user-defined hooks, see the extensive online docs.
Usage modes:
As a decorator:
.. code-block:: python
@command(name="command_name", ...)
def my_cmd(...):
...
As a normal function call:
.. code-block:: python
def my_cmd(main_cfg: SomeConfig, **extra_cli_configs):
...
coma.command(name="command_name", cmd=my_cmd, ...)
Both decorator and procedural modes also accept a class argument:
.. code-block:: python
@coma.command(name="command_name", ...)
class MyCmd(...):
def run(self):
...
or:
.. code-block:: python
class MyCmd(...):
def run(self):
...
coma.command(name="command_name", cmd=MyCmd, ...)
.. note::
It is invalid to specify the :obj:`cmd` parameter in decorator mode.
.. note::
Throughout, we refer to **"the command"**, which applies regardless of
usage mode (decorator or procedural) and regardless of whether the command
object is a function or a class (:obj:`my_cmd` or :obj:`MyCmd`,
respectively, in the above examples). The **"command signature"** refers
directly to the function signature if the command is a function, or to the
signature of the :obj:`__init__()` method if the command is a class.
.. note::
When the command is a function, it gets wrapped in a class internally.
Therefore, unless you really know what you are doing, it is unwise to
inspect or change the :attr:`~coma.hooks.base.HookData.command` in any
user-supplied hooks. Instead, rely on the registered command
:attr:`~coma.hooks.base.HookData.name` (which is guaranteed to be unique)
to delegate reused functionality across the hooks.
Details:
The command's signature is inspected (using :obj:`signature_inspector`) to
infer and separate :class:`~coma.config.base.Config` s from other parameters.
A rich set of options exist for declaring which parameters are config or regular
parameters. See :class:`~coma.config.cli.SignatureInspector` for details.
Additional configs not present in the command signature can be supplied through
:obj:`supplemental_configs`. These can be helpful for providing additional
information to the hooks beyond what the command itself requires.
All hooks default to the :data:`~coma.hooks.base.SHARED` sentinel, which
means they get replaced at runtime with the corresponding shared hook from
:func:`~coma.core.wake.wake()`. Setting a hook to :obj:`None` disables that
hook entirely. Setting a hook to the :data:`~coma.hooks.base.DEFAULT` sentinel,
means it gets replaced at runtime with the corresponding pre-defined default
hook. Because all the shared hooks default to :obj:`DEFAULT`, :obj:`SHARED`
and :obj:`DEFAULT` might feel interchangeable. However, once a shared hook in
:obj:`wake()` is replaced with a user-defined hook, they act differently.
Setting a hook to :obj:`DEFAULT` here recovers the default functionality for
this specific command, whereas :obj:`SHARED` uses the user-defined replacement.
.. note::
Hooks can be "plain" objects as just described, or they can be (recursive)
**sequences** of such "plain" objects. This syntax acts as a convenient
shorthand that enables composing larger hooks from smaller components
without having to define a wrapper hook whose only purpose is to compose
component hooks.
Args:
name (:data:`~coma.hooks.base.CommandName`): Any (unique) valid command name
according to ``argparse``. If :obj:`None`, :obj:`cmd.__name__.lower()`
is used instead.
cmd (:data:`~coma.hooks.base.Command`, optional): A command class or function.
If :obj:`None`, use decorator mode. If given, use procedural mode.
parser_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with parser hook semantics.
pre_config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with pre config hook semantics.
config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with config hook semantics.
post_config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with post config hook semantics.
pre_init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with pre init hook semantics.
init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with init hook semantics.
post_init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with post init hook semantics.
pre_run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with pre run hook semantics.
run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with run hook semantics.
post_run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional
command-specific hook with post run hook semantics.
signature_inspector (:class:`~coma.config.cli.SignatureInspectorProtocol`, optional):
The :obj:`SignatureInspectorProtocol` to use for inspecting the :obj:`cmd`
object's signature. If :obj:`None`, a
:class:`~coma.config.cli.SignatureInspector` with default
parameters is used.
persistence_manager (:class:`~coma.config.io.PersistenceManager`, optional):
Manager for the serializing of configs. If :obj:`None`, a manager with
default parameters is used.
parser_kwargs (:data:`~coma.config.base.Parameters`, optional): Keyword
arguments passed along to the :obj:`ArgumentParser` sub-parser that
will be created just for this command.
**supplemental_configs (typing.Any): Additional configs not present in the
command signature. Any ``omegaconf``-compatible config type is valid.
See also:
* The online docs for detailed tutorials and examples.
* :class:`~coma.config.cli.SignatureInspector`
* :class:`~coma.config.cli.ParamData`
* :class:`~coma.config.io.PersistenceManager`
* :func:`~coma.core.wake.wake()`
.. _Template:
https://en.wikipedia.org/wiki/Template_method_pattern
.. _hooks:
https://en.wikipedia.org/wiki/Hooking
.. _ArgumentParser.add_subparsers().add_parser():
https://docs.python.org/3/library/argparse.html#sub-commands
"""
def decorator(cmd_: Callable):
data = RegistrationData(
name=name or cmd_.__name__.lower(),
command=_maybe_wrap_command(cmd_),
hooks=Hooks(
parser_hook=parser_hook,
pre_config_hook=pre_config_hook,
config_hook=config_hook,
post_config_hook=post_config_hook,
pre_init_hook=pre_init_hook,
init_hook=init_hook,
post_init_hook=post_init_hook,
pre_run_hook=pre_run_hook,
run_hook=run_hook,
post_run_hook=post_run_hook,
),
parameters=(signature_inspector or SignatureInspector())(
signature(cmd_), supplemental_configs
),
persistence_manager=persistence_manager or PersistenceManager(),
parser_kwargs=parser_kwargs or {},
)
Coma.register(data)
return cmd_
# Apply the decorator.
if cmd is None:
return decorator
decorator(cmd)
# If cmd is given, we need to make sure the decorator syntax is not overloaded.
def raise_error(extra_cmd: Any):
cmd_def = _make_def_string(cmd)
extra_cmd_def = _make_def_string(extra_cmd)
raise ValueError(
"Overloaded @command decorator with two commands:\n"
f"@command(cmd={cmd.__name__}, ...)\n{extra_cmd_def}\n"
"Either use the decorator syntax while leaving the 'cmd' parameter "
"None:\n"
f"@command(...)\n{extra_cmd_def}\n"
"or use the procedural syntax while specifying the 'cmd' parameter:\n"
f"{cmd_def}command(cmd={cmd.__name__}, ...)"
)
return raise_error
def _maybe_wrap_command(cmd: Any) -> Command:
if isinstance(cmd, type):
return cmd
@wraps(cmd)
def wrapper(*args, **kwargs):
class Cmd:
@staticmethod
def run():
return cmd(*args, **kwargs)
return Cmd()
return wrapper
def _make_def_string(cmd: Optional[Command]) -> str:
if cmd is None:
# This is disallowed in the language syntax regardless.
raise ValueError("Cannot created definition from None")
if isinstance(cmd, type):
return f"class {cmd.__name__}:\n ...\n"
return f"def {cmd.__name__}(...):\n ...\n"