From 02d89f5d93dee479deb3379e90a207c9b54c4dee Mon Sep 17 00:00:00 2001 From: David Brochart Date: Wed, 10 Jan 2024 11:42:50 +0100 Subject: [PATCH] Add --help-all CLI --- src/asphalt/core/cli.py | 13 +++++++- src/asphalt/core/component.py | 19 +++++++++++- src/asphalt/core/runner.py | 14 ++++++++- tests/test_cli.py | 56 ++++++++++++++++++++++++++++++++++- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/asphalt/core/cli.py b/src/asphalt/core/cli.py index ac6e3ad2..4d4ca04c 100644 --- a/src/asphalt/core/cli.py +++ b/src/asphalt/core/cli.py @@ -10,7 +10,8 @@ from ruamel.yaml import YAML, ScalarNode from ruamel.yaml.loader import Loader -from .runner import policies, run_application +from .component import component_set_help_all +from .runner import policies, run_application, runner_set_help_all from .utils import merge_config, qualified_name @@ -63,13 +64,23 @@ def main() -> None: type=str, help="set configuration", ) +@click.option( + "--help-all", + is_flag=True, + default=False, + help="show all components' configuration parameters and exit", +) def run( configfile, unsafe: bool, loop: str | None, service: str | None, set_: list[str], + help_all: bool, ) -> None: + component_set_help_all(help_all) + runner_set_help_all(help_all) + yaml = YAML(typ="unsafe" if unsafe else "safe") yaml.constructor.add_constructor("!Env", env_constructor) yaml.constructor.add_constructor("!TextFile", text_file_constructor) diff --git a/src/asphalt/core/component.py b/src/asphalt/core/component.py index 5aef6869..813d1b1d 100644 --- a/src/asphalt/core/component.py +++ b/src/asphalt/core/component.py @@ -3,6 +3,7 @@ __all__ = ("Component", "ContainerComponent", "CLIApplicationComponent") import asyncio +import inspect import sys from abc import ABCMeta, abstractmethod from asyncio import Future @@ -14,6 +15,13 @@ from .context import Context from .utils import PluginContainer, merge_config, qualified_name +help_all = False + + +def component_set_help_all(val): + global help_all + help_all = val + class Component(metaclass=ABCMeta): """This is the base class for all Asphalt components.""" @@ -54,11 +62,12 @@ class ContainerComponent(Component): :vartype component_configs: Dict[str, Optional[Dict[str, Any]]] """ - __slots__ = "child_components", "component_configs" + __slots__ = "child_components", "component_configs", "_hierarchy" def __init__(self, components: dict[str, dict[str, Any] | None] | None = None) -> None: self.child_components: OrderedDict[str, Component] = OrderedDict() self.component_configs = components or {} + self._hierarchy = f"{self.__class__.__name__}." def add_component(self, alias: str, type: str | type | None = None, **config) -> None: """ @@ -95,8 +104,13 @@ def add_component(self, alias: str, type: str | type | None = None, **config) -> config = merge_config(config, override_config) component = component_types.create_object(**config) + component._hierarchy = f"{self._hierarchy}{alias}." self.child_components[alias] = component + if help_all: + signature = inspect.signature(component.__init__) + print(f"{self._hierarchy}{alias} {signature}") + async def start(self, ctx: Context) -> None: """ Create child components that have been configured but not yet created and then calls their @@ -156,6 +170,9 @@ def start_run_task() -> None: task.add_done_callback(run_complete) await super().start(ctx) + if help_all: + sys.exit(0) + ctx.loop.call_later(0.1, start_run_task) @abstractmethod diff --git a/src/asphalt/core/runner.py b/src/asphalt/core/runner.py index 0e83803a..5eb80f45 100644 --- a/src/asphalt/core/runner.py +++ b/src/asphalt/core/runner.py @@ -3,6 +3,7 @@ __all__ = ("run_application",) import asyncio +import inspect import signal import sys from asyncio.events import AbstractEventLoop @@ -11,12 +12,19 @@ from logging.config import dictConfig from typing import Any, cast -from .component import Component, component_types +from .component import Component, ContainerComponent, component_types from .context import Context, _current_context from .utils import PluginContainer, qualified_name policies = PluginContainer("asphalt.core.event_loop_policies") +help_all = False + + +def runner_set_help_all(val): + global help_all + help_all = val + def sigterm_handler(logger: Logger, event_loop: AbstractEventLoop) -> None: if event_loop.is_running(): @@ -95,6 +103,10 @@ def run_application( if isinstance(component, dict): component = cast(Component, component_types.create_object(**component)) + if help_all: + signature = inspect.signature(component.__init__) # type: ignore + print(component.__class__.__name__, signature) + logger.info("Starting application") context = Context() exception: BaseException | None = None diff --git a/tests/test_cli.py b/tests/test_cli.py index c90a22d7..87fe28fb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,7 +7,20 @@ from _pytest.monkeypatch import MonkeyPatch from click.testing import CliRunner -from asphalt.core import Component, Context, cli +from asphalt.core import Component, ContainerComponent, Context, cli + + +class Container0(ContainerComponent): + async def start(self, ctx: Context) -> None: + self.add_component("container1", Container1) + self.add_component("container2", Container1) + await super().start(ctx) + + +class Container1(ContainerComponent): + async def start(self, ctx: Context) -> None: + self.add_component("dummy", DummyComponent) + await super().start(ctx) class DummyComponent(Component): @@ -24,6 +37,47 @@ def runner() -> CliRunner: return CliRunner() +@pytest.mark.parametrize("help_all", [False, True]) +@pytest.mark.parametrize("root_component", [DummyComponent, Container1, Container0]) +def test_help_all( + runner: CliRunner, + help_all: bool, + root_component: Component, +) -> None: + config = f"""\ + component: + type: {root_component.__module__}:{root_component.__name__} +""" + cmd = ["test.yml"] + if help_all: + cmd.append("--help-all") + with runner.isolated_filesystem(): + Path("test.yml").write_text(config) + result = runner.invoke(cli.run, cmd) + if help_all: + if root_component == DummyComponent: + assert result.exit_code == 1 + assert result.stdout == ("DummyComponent (dummyval1=None, dummyval2=None)\n") + elif root_component == Container1: + assert result.exit_code == 1 + assert result.stdout == """\ +Container1 (components: 'dict[str, dict[str, Any] | None] | None' = None) -> 'None' +Container1.dummy (dummyval1=None, dummyval2=None) +""" + elif root_component == Container0: + assert result.exit_code == 1 + assert result.stdout == """\ +Container0 (components: 'dict[str, dict[str, Any] | None] | None' = None) -> 'None' +Container0.container1 (components: 'dict[str, dict[str, Any] | None] | None' = None) -> 'None' +Container0.container2 (components: 'dict[str, dict[str, Any] | None] | None' = None) -> 'None' +Container0.container1.dummy (dummyval1=None, dummyval2=None) +Container0.container2.dummy (dummyval1=None, dummyval2=None) +""" + else: + assert result.exit_code == 1 + assert result.stdout == ("") + + @pytest.mark.parametrize("loop", [None, "uvloop"], ids=["default", "override"]) @pytest.mark.parametrize("unsafe", [False, True], ids=["safe", "unsafe"]) def test_run(