Skip to content

Commit

Permalink
Add tmt about, command showing things about tmt itself
Browse files Browse the repository at this point in the history
  • Loading branch information
happz committed Jan 31, 2025
1 parent 7e5490e commit d55f9e5
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 12 deletions.
1 change: 1 addition & 0 deletions tmt/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tmt/checks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
95 changes: 95 additions & 0 deletions tmt/cli/about.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
""" ``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_list_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.group()
@pass_context
def plugins(context: Context) -> None:
pass


@plugins.command(name='ls')
@option(
'-h', '--how',
choices=['json', 'yaml', 'rest', 'pretty'],
default='pretty',
help='Output format.')
@pass_context
def plugins_ls(context: Context, how: str) -> None:
print = context.obj.logger.print # noqa: A001

if how in ('pretty', 'rest'):
text_output = _render_plugins_list_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))
2 changes: 1 addition & 1 deletion tmt/export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tmt/frameworks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion tmt/package_managers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
45 changes: 44 additions & 1 deletion tmt/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,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(
Expand Down Expand Up @@ -379,6 +381,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]):
"""
Expand Down Expand Up @@ -411,3 +419,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
2 changes: 1 addition & 1 deletion tmt/steps/discover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/execute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/finish/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/prepare/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/prepare/feature/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2363,7 +2363,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
Expand Down
2 changes: 1 addition & 1 deletion tmt/steps/report/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit d55f9e5

Please sign in to comment.