Source code for coma.core.wake

"""Wake from a coma."""

import argparse
import sys
from typing import Any, Callable, Optional, Sequence

from .singleton import Coma, RegistrationData
from ..hooks.base import AugmentedHook, InvocationData, ParserData, DEFAULT
from ..hooks.management import Hooks


[docs] class WakeException(Exception): """Raised when :func:`~coma.core.wake.wake()` fails.""" pass
[docs] def wake( *import_commands: Any, # noqa: Purposefully unused. parser: Optional[argparse.ArgumentParser] = None, cli_args: Optional[Sequence[str]] = None, cli_namespace: Optional[Any] = None, parser_hook: AugmentedHook = DEFAULT, pre_config_hook: AugmentedHook = None, config_hook: AugmentedHook = DEFAULT, post_config_hook: AugmentedHook = None, pre_init_hook: AugmentedHook = None, init_hook: AugmentedHook = DEFAULT, post_init_hook: AugmentedHook = None, pre_run_hook: AugmentedHook = None, run_hook: AugmentedHook = DEFAULT, post_run_hook: AugmentedHook = None, **subparsers_kwargs, ) -> None: """ Wakes from a coma. Starts up ``coma`` with an optional argument parser, optional shared hooks, and optional subparsers keyword arguments. Any shared hooks are applied **globally** to every registered command. .. 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. All hooks default to the :data:`~coma.hooks.base.DEFAULT` sentinel, which means they get replaced at runtime with the corresponding pre-defined default hook. Setting a hook to :obj:`None` disables it. The :data:`~coma.hooks.base.SHARED` sentinel is not allowed in :obj:`wake()` since it would lead to infinite regress. See :func:`~coma.core.command.command()` for usage of :obj:`SHARED`. .. 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. .. note:: A command is only registered (via :func:`~coma.core.command.command()`) if the module in which the command is declared is imported at runtime. This is standard Python behavior: non-imported code is not interpreted by the VM and not available at runtime. This is a bit obscured by the behind-the-scenes magic done by :obj:`command()`. But this magic only works if the command code runs (via being imported) at some point before the call to :obj:`wake()`. One way to achieve this is by having a :obj:`from . import module` statement in the top-level :obj:`__init__.py` for **every** module with a command. That forces each command module to be imported. Alternatively, a common pattern is to put lightweight (one-line) :obj:`command()` wrappers around calls to the main/workhorse functions all in a single module (typically, the same module that calls :obj:`wake()`). Finally, a third alternative is to pass all commands scattered throughout a codebase to :obj:`*import_commands`. The contents of :obj:`*import_commands` is **fully** ignored by :obj:`wake()`. However, it forces the Python VM to import each of the provided modules, thus registering the commands. Providing the imported commands to :obj:`*import_commands` is not required (merely importing them is enough), but doing so prevents linters from complaining of unused import statements. Example:: @coma.command def cmd(...): ... if __name__ == "__main__": coma.wake(parser=..., parser_hook=..., ...) Args: *import_commands (typing.Any): **Fully** ignored. Optional mechanism to forcibly import commands scattered throughout a codebase. parser (:class:`argparse.ArgumentParser`, optional): Top-level :obj:`ArgumentParser` to use. If :obj:`None`, an :obj:`ArgumentParser` with default parameters is used instead. cli_args (typing.Sequence[str], optional): Command line arguments to use with :obj:`parser`. If :obj:`None`, :obj:`sys.argv` is used instead. cli_namespace (typing.Any, optional): The namespace object to pass to `parse_known_args()`_. If :obj:`None`, use the ``argparse`` default. parser_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with parser hook semantics. pre_config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with pre config hook semantics. config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with config hook semantics. post_config_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with post config hook semantics. pre_init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with pre init hook semantics. init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with init hook semantics. post_init_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with post init hook semantics. pre_run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with pre run hook semantics. run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with run hook semantics. post_run_hook (:data:`~coma.hooks.base.AugmentedHook`): An optional shared hook with post run hook semantics. **subparsers_kwargs (typing.Any): Keyword arguments to pass along to `ArgumentParser.add_subparsers()`_ Raises: WakeException: If no commands are registered or the command line arguments fail to invoke a command. ValueError: If any :data:`~coma.hooks.base.Hook` argument is or contains the :data:`~coma.hooks.base.SHARED` sentinel, which would lead to infinite regress. Others: As may be raised by ``argparse`` or any of the hooks or commands. See also: * The online docs for detailed tutorials and examples. * :func:`~coma.core.command.command()` .. _ArgumentParser.add_subparsers(): https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_subparsers .. _parse_known_args(): https://docs.python.org/3/library/argparse.html#partial-parsing .. _Template: https://en.wikipedia.org/wiki/Template_method_pattern .. _hooks: https://en.wikipedia.org/wiki/Hooking """ if not Coma.get_registrations(): raise WakeException( "Waking from a coma with no commands registered. At least one call to " "'@command' is required before waking." ) parser = parser or argparse.ArgumentParser() subparsers = parser.add_subparsers(**subparsers_kwargs) shared_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, ) for data in Coma.get_registrations().values(): subparser = subparsers.add_parser(data.name, **data.parser_kwargs) hooks = Hooks.merge(shared_hooks, data.hooks) hooks.parse(data.to(ParserData, parser=subparser)) subparser.set_defaults(func=invoke_factory(data, hooks)) known_args, unknown_args = parser.parse_known_args(cli_args, cli_namespace) try: known_args.func(known_args, unknown_args) except AttributeError as e: if any("func" in arg for arg in e.args): raise WakeException( "Waking from a coma with no command given on the command line. " f"Invoke with: {sys.argv[0]} <command-name>" ) raise
def invoke_factory(data: RegistrationData, hooks: Hooks) -> Callable: def invoke(known_args: Any, unknown_args: list[str]): inv = data.to(InvocationData, known_args=known_args, unknown_args=unknown_args) hooks.invoke(inv) return invoke