diff --git a/tmt/__main__.py b/tmt/__main__.py index 68fc7a8062..137bd19d06 100644 --- a/tmt/__main__.py +++ b/tmt/__main__.py @@ -6,6 +6,7 @@ def import_cli_commands() -> None: # TODO: some kind of `import tmt.cli.*` would be nice import tmt.cli._root # type: ignore[reportUnusedImport,unused-ignore] + import tmt.cli.about # noqa: F401,I001,RUF100 import tmt.cli.init # noqa: F401,I001,RUF100 import tmt.cli.lint # noqa: F401,I001,RUF100 import tmt.cli.status # noqa: F401,I001,RUF100 diff --git a/tmt/checks/__init__.py b/tmt/checks/__init__.py index 0fcafb3e38..ad148c9894 100644 --- a/tmt/checks/__init__.py +++ b/tmt/checks/__init__.py @@ -26,7 +26,7 @@ CheckPluginClass = type['CheckPlugin[Any]'] -_CHECK_PLUGIN_REGISTRY: PluginRegistry[CheckPluginClass] = PluginRegistry() +_CHECK_PLUGIN_REGISTRY: PluginRegistry[CheckPluginClass] = PluginRegistry('test.check') def provides_check(check: str) -> Callable[[CheckPluginClass], CheckPluginClass]: diff --git a/tmt/cli/about.py b/tmt/cli/about.py new file mode 100644 index 0000000000..822bc2672f --- /dev/null +++ b/tmt/cli/about.py @@ -0,0 +1,89 @@ +""" ``tmt about`` implementation """ + +import json +import re +import textwrap +from typing import Any + +import tmt.utils +import tmt.utils.rest +from tmt.cli import Context, CustomGroup, pass_context +from tmt.cli._root import main +from tmt.options import option +from tmt.plugins import PluginRegistry, iter_plugin_registries +from tmt.utils import GeneralError +from tmt.utils.templates import render_template + + +@main.group(invoke_without_command=True, cls=CustomGroup) +def about() -> None: + """ Show info about tmt itself """ + + +def _render_plugins_rest() -> str: + registry_intro_map: dict[str, str] = { + r'export\.([a-z]+)': 'Export plugins for {{ MATCH.group(1).lower() }}', + r'test.check': 'Test check plugins', + r'test.framework': 'Test framework plugins', + r'package_manager': 'Package manager plugins', + r'step\.([a-z]+)': '{{ MATCH.group(1).capitalize() }} step plugins' + } + + def find_intro(registry: PluginRegistry[Any]) -> str: + for pattern, intro_template in registry_intro_map.items(): + match = re.match(pattern, registry.name) + + if match is None: + continue + + return render_template(intro_template, MATCH=match) + + raise GeneralError(f"Unknown plugin registry '{registry.name}'.") + + template = textwrap.dedent(""" + {% for registry in REGISTRIES %} + {% set intro = find_intro(registry) %} + {{ intro }} + {# {{ "-" * intro | length }} #} + + {% if registry %} + {% for plugin_id in registry.iter_plugin_ids() %} + * `{{ plugin_id }}` + {% endfor %} + {% else %} + No plugins discovered. + {% endif %} + + ---- + + {% endfor %} + """) + + return render_template(template, REGISTRIES=iter_plugin_registries(), find_intro=find_intro) + + +@about.command() +@option( + '-h', '--how', + choices=['json', 'yaml', 'rest', 'pretty'], + default='pretty', + help='Output format.') +@pass_context +def plugins(context: Context, how: str) -> None: + print = context.obj.logger.print # noqa: A001 + + if how in ('pretty', 'rest'): + text_output = _render_plugins_rest() + + print( + tmt.utils.rest.render_rst(text_output, context.obj.logger) + if how == 'pretty' else text_output) + + elif how in ('json', 'yaml'): + structured_output = { + registry.name: list(registry.iter_plugin_ids()) + for registry in iter_plugin_registries() + } + + print(json.dumps(structured_output) if how == + 'json' else tmt.utils.dict_to_yaml(structured_output)) diff --git a/tmt/export/__init__.py b/tmt/export/__init__.py index 0c2c74d5a4..bce8d4bf4e 100644 --- a/tmt/export/__init__.py +++ b/tmt/export/__init__.py @@ -86,7 +86,7 @@ def get_export_plugin_registry(cls) -> PluginRegistry[ExportClass]: """ Return - or initialize - export plugin registry """ if not hasattr(cls, '_export_plugin_registry'): - cls._export_plugin_registry = PluginRegistry() + cls._export_plugin_registry = PluginRegistry(f'export.{cls.__name__.lower()}') return cls._export_plugin_registry diff --git a/tmt/frameworks/__init__.py b/tmt/frameworks/__init__.py index 8319349b2d..54ff265b96 100644 --- a/tmt/frameworks/__init__.py +++ b/tmt/frameworks/__init__.py @@ -14,7 +14,7 @@ _FRAMEWORK_PLUGIN_REGISTRY: tmt.plugins.PluginRegistry[TestFrameworkClass] = \ - tmt.plugins.PluginRegistry() + tmt.plugins.PluginRegistry('test.framework') def provides_framework(framework: str) -> Callable[[TestFrameworkClass], TestFrameworkClass]: diff --git a/tmt/package_managers/__init__.py b/tmt/package_managers/__init__.py index a888737bfe..192ad872dd 100644 --- a/tmt/package_managers/__init__.py +++ b/tmt/package_managers/__init__.py @@ -68,7 +68,7 @@ def __lt__(self, other: Any) -> bool: _PACKAGE_MANAGER_PLUGIN_REGISTRY: tmt.plugins.PluginRegistry[PackageManagerClass] = \ - tmt.plugins.PluginRegistry() + tmt.plugins.PluginRegistry('package_managers') def provides_package_manager( diff --git a/tmt/plugins/__init__.py b/tmt/plugins/__init__.py index 4e6c314e31..915514d9fe 100644 --- a/tmt/plugins/__init__.py +++ b/tmt/plugins/__init__.py @@ -307,7 +307,9 @@ class PluginRegistry(Generic[RegisterableT]): _plugins: dict[str, RegisterableT] - def __init__(self) -> None: + def __init__(self, name: str) -> None: + self.name = name + self._plugins = {} def register_plugin( @@ -370,6 +372,12 @@ def iter_plugins(self) -> Iterator[RegisterableT]: def items(self) -> Iterator[tuple[str, RegisterableT]]: yield from self._plugins.items() + def __len__(self) -> int: + return len(self._plugins) + + def __bool__(self) -> bool: + return bool(self._plugins) + class ModuleImporter(Generic[ModuleT]): """ @@ -402,3 +410,38 @@ def __call__(self, logger: Logger) -> ModuleT: assert self._module # narrow type return self._module + + +def iter_plugin_registries() -> Iterator[PluginRegistry[Any]]: + # TODO: maybe there is a better way, but as of now, registries do + # not report their existence, there is no method to iterate over + # them. Using a static list for now. + from tmt.base import Plan, Story, Test + from tmt.checks import _CHECK_PLUGIN_REGISTRY + from tmt.frameworks import _FRAMEWORK_PLUGIN_REGISTRY + from tmt.package_managers import _PACKAGE_MANAGER_PLUGIN_REGISTRY + from tmt.steps.discover import DiscoverPlugin + from tmt.steps.execute import ExecutePlugin + from tmt.steps.finish import FinishPlugin + from tmt.steps.prepare import PreparePlugin + from tmt.steps.provision import ProvisionPlugin + from tmt.steps.report import ReportPlugin + + yield Story._export_plugin_registry + yield Plan._export_plugin_registry + yield Test._export_plugin_registry + yield _CHECK_PLUGIN_REGISTRY + yield _FRAMEWORK_PLUGIN_REGISTRY + yield _PACKAGE_MANAGER_PLUGIN_REGISTRY + yield DiscoverPlugin._supported_methods + yield ProvisionPlugin._supported_methods + yield PreparePlugin._supported_methods + yield ExecutePlugin._supported_methods + yield FinishPlugin._supported_methods + yield ReportPlugin._supported_methods + + +def iter_plugins() -> Iterator[tuple[PluginRegistry[RegisterableT], str, RegisterableT]]: + for registry in iter_plugin_registries(): + for plugin_id, plugin_class in registry.items(): + yield registry, plugin_id, plugin_class diff --git a/tmt/steps/discover/__init__.py b/tmt/steps/discover/__init__.py index 383c5c6b18..4e9bb4d578 100644 --- a/tmt/steps/discover/__init__.py +++ b/tmt/steps/discover/__init__.py @@ -83,7 +83,7 @@ class DiscoverPlugin(tmt.steps.GuestlessPlugin[DiscoverStepDataT, None]): _data_class = DiscoverStepData # type: ignore[assignment] # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry() + _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.discover') @classmethod def base_command( diff --git a/tmt/steps/execute/__init__.py b/tmt/steps/execute/__init__.py index f38263eeff..dded6cf588 100644 --- a/tmt/steps/execute/__init__.py +++ b/tmt/steps/execute/__init__.py @@ -609,7 +609,7 @@ class ExecutePlugin(tmt.steps.Plugin[ExecuteStepDataT, None]): _data_class = ExecuteStepData # type: ignore[assignment] # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry() + _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.execute') # Internal executor is the default implementation how = 'tmt' diff --git a/tmt/steps/finish/__init__.py b/tmt/steps/finish/__init__.py index dadc2f5840..0a8d387e6e 100644 --- a/tmt/steps/finish/__init__.py +++ b/tmt/steps/finish/__init__.py @@ -41,7 +41,7 @@ class FinishPlugin(tmt.steps.Plugin[FinishStepDataT, list[PhaseResult]]): _data_class = FinishStepData # type: ignore[assignment] # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[Method] = PluginRegistry() + _supported_methods: PluginRegistry[Method] = PluginRegistry('step.finish') @classmethod def base_command( diff --git a/tmt/steps/prepare/__init__.py b/tmt/steps/prepare/__init__.py index b0504f31bf..7c396213f5 100644 --- a/tmt/steps/prepare/__init__.py +++ b/tmt/steps/prepare/__init__.py @@ -53,7 +53,7 @@ class PreparePlugin(tmt.steps.Plugin[PrepareStepDataT, list[PhaseResult]]): _data_class = PrepareStepData # type: ignore[assignment] # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry() + _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.prepare') @classmethod def base_command( diff --git a/tmt/steps/prepare/feature/__init__.py b/tmt/steps/prepare/feature/__init__.py index b3219e4d49..048b38c816 100644 --- a/tmt/steps/prepare/feature/__init__.py +++ b/tmt/steps/prepare/feature/__init__.py @@ -18,7 +18,7 @@ FEATURE_PLAYEBOOK_DIRECTORY = tmt.utils.resource_files('steps/prepare/feature') FeatureClass = type['Feature'] -_FEATURE_PLUGIN_REGISTRY: PluginRegistry[FeatureClass] = PluginRegistry() +_FEATURE_PLUGIN_REGISTRY: PluginRegistry[FeatureClass] = PluginRegistry('prepare.feature') def provides_feature( diff --git a/tmt/steps/provision/__init__.py b/tmt/steps/provision/__init__.py index 820bba4840..820a716fa6 100644 --- a/tmt/steps/provision/__init__.py +++ b/tmt/steps/provision/__init__.py @@ -2285,7 +2285,7 @@ class ProvisionPlugin(tmt.steps.GuestlessPlugin[ProvisionStepDataT, None]): how = 'virtual' # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry() + _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.provision') # TODO: Generics would provide a better type, https://github.com/teemtee/tmt/issues/1437 _guest: Optional[Guest] = None diff --git a/tmt/steps/report/__init__.py b/tmt/steps/report/__init__.py index ba769e6ccf..02f4f1b779 100644 --- a/tmt/steps/report/__init__.py +++ b/tmt/steps/report/__init__.py @@ -33,7 +33,7 @@ class ReportPlugin(tmt.steps.GuestlessPlugin[ReportStepDataT, None]): how = 'display' # Methods ("how: ..." implementations) registered for the same step. - _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry() + _supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.report') @classmethod def base_command(