Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tmt/base/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ def lint_execute_unknown_method(self) -> LinterReturn:

yield from self._lint_step_methods(
'execute',
tmt.steps.execute.ExecutePlugin, # type: ignore[type-abstract]
tmt.steps.execute.ExecutePlugin,
)

def lint_discover_unknown_method(self) -> LinterReturn:
Expand All @@ -1038,7 +1038,7 @@ def lint_discover_unknown_method(self) -> LinterReturn:

yield from self._lint_step_methods(
'discover',
tmt.steps.discover.DiscoverPlugin, # type: ignore[type-abstract]
tmt.steps.discover.DiscoverPlugin,
)

def lint_fmf_remote_ids_valid(self) -> LinterReturn:
Expand Down
79 changes: 72 additions & 7 deletions tmt/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Generic,
Literal,
Optional,
Expand Down Expand Up @@ -53,7 +54,7 @@
option_to_key,
simple_field,
)
from tmt.options import option
from tmt.options import ClickOptionDecoratorType, option
from tmt.result import ResultOutcome
from tmt.utils import (
DEFAULT_NAME,
Expand Down Expand Up @@ -1366,7 +1367,7 @@ def prune(self, logger: tmt.log.Logger) -> None:

# Do not prune plugin workdirs, each plugin decides what should
# be pruned from the workdir and what should be kept there
plugins = self.phases(classes=BasePlugin) # type: ignore[type-abstract]
plugins = self.phases(classes=BasePlugin)
for plugin in plugins:
if plugin.workdir is not None:
preserved_members = {*preserved_members, plugin.workdir.name}
Expand Down Expand Up @@ -1537,6 +1538,11 @@ class BasePlugin(
# subclasses.
_supported_methods: 'tmt.plugins.PluginRegistry[Method]'

#: A sequence of :py:func:`click.option`-like decorators that should
#: be added to the step base command created by :py:meth:`base_command`.
#: Decorators are applied in the same order they are in this sequence.
_base_command_options: tuple[ClickOptionDecoratorType, ...] = (PHASE_OPTIONS,)

_data_class: type[StepDataT]

@classmethod
Expand All @@ -1552,6 +1558,21 @@ def get_data_class(cls) -> type[StepDataT]:

data: StepDataT

#: Point back to the :py:class:`Step` subclass implementing the step
#: which owns this family of plugins.
#:
#: .. note::
#:
#: This "backlink" is initialized at the end of Python modules
#: holding the respective step implementations. Each ``Step``
#: subclass points at the base class of its plugin family, via
#: simple class-level attribute, and it would be impossible to
#: establish the same kind of link in the opposite direction.
#: When the plugin base class is defined, the step class does not
#: even exist yet. Therefore this link is set after both classes,
#: step and its plugin base, are finalized.
_step_class: ClassVar[type[Step]]

@classmethod
def get_step_name(cls) -> str:
match = _PLUGIN_CLASS_NAME_TO_STEP_PATTERN.match(cls.__module__)
Expand Down Expand Up @@ -1633,17 +1654,61 @@ def safe_name(self) -> str:
return self.pathless_safe_name

@classmethod
@abc.abstractmethod
def base_command(
cls,
usage: str,
method_class: Optional[type[click.Command]] = None,
) -> click.Command:
"""
Create base click command (common for all step plugins)
"""

raise NotImplementedError
Create base :py:mod:`click` command for plugins of the step.
"""

step_name = cls._step_class.__name__.lower()

# Prepare general usage message for the step
if method_class:
usage = cls._step_class.usage(method_overview=usage)

# Instead of the well-known way `@option(...)` decorators are
# used, we get rid of the syntax sugar they add, and apply them
# in a way they are actually applied. We want to include the
# extra decorators, and we can't simply `@cls._base_command_extra_options`.
# Note that the order is opposite to what one would expect when
# looking at decorators, which is correct, they are indeed applied
# from bottom to top.

# First, the actual command code.
def base_command(context: 'tmt.cli.Context', **kwargs: Any) -> None:
context.obj.steps.add(step_name)
cls._step_class.store_cli_invocation(context)

# Then apply the custom options by invoking them as if they were
# decorators.
for options_decorator in cls._base_command_options:
base_command = options_decorator(base_command)

# And then the rest, `@option(...)` for `--how`, context, and
# finally `@click.command(...)`.
base_command = option(
'-h',
'--how',
# Cannot use `choices=...` because we want to allow values
# that are not on the list *as long as they are clearly
# matching values on the list*. For example, `virtual` is
# absolutely acceptable as long as some `virtual.*` plugin
# is available.
metavar='|'.join(
sorted([method.name for method in cls._supported_methods.iter_plugins()])
),
help=f'Use specified method for {step_name} phase.',
)(base_command)

# ignore[arg-type]: `pass_context` annotations add `Context`
# parameter, but we already have that one, because we use it
# in the command code. This is probably much less visible when
# `pass_context` is used as a decorator.
base_command = click.pass_context(base_command) # type: ignore[arg-type]
return click.command(cls=method_class, help=usage, name=step_name)(base_command)

@classmethod
def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecoratorType]:
Expand Down
36 changes: 5 additions & 31 deletions tmt/steps/cleanup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import copy
from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast
from typing import Optional, TypeVar, cast

import click
import fmf.utils

import tmt.log
import tmt.steps
import tmt.utils
from tmt.container import container
from tmt.guest import Guest
from tmt.options import option
from tmt.plugins import PluginRegistry
from tmt.result import PhaseResult, ResultGuestData, ResultOutcome
from tmt.steps import (
Expand All @@ -19,9 +17,6 @@
PluginTask,
)

if TYPE_CHECKING:
import tmt.cli


@container
class CleanupStepData(tmt.steps.StepData):
Expand All @@ -46,31 +41,6 @@ class CleanupPlugin(tmt.steps.Plugin[CleanupStepDataT, PluginOutcome]):
# Internal cleanup plugin is the default implementation
how = 'tmt'

@classmethod
def base_command(
cls,
usage: str,
method_class: Optional[type[click.Command]] = None,
) -> click.Command:
"""
Create base click command (common for all cleanup plugins)
"""

# Prepare general usage message for the step
if method_class:
usage = Cleanup.usage(method_overview=usage)

# Create the command
@click.command(cls=method_class, help=usage)
@click.pass_context
@option('-h', '--how', metavar='METHOD', help='Use specified method for cleanup tasks.')
@tmt.steps.PHASE_OPTIONS
def cleanup(context: 'tmt.cli.Context', **kwargs: Any) -> None:
context.obj.steps.add('cleanup')
Cleanup.store_cli_invocation(context)

return cleanup

def go(
self,
*,
Expand Down Expand Up @@ -263,3 +233,7 @@ def _record_exception(
# Update status and save
self.status('done')
self.save()


# Establish the "plugin class -> step class" link.
CleanupPlugin._step_class = Cleanup
Comment thread
LecrisUT marked this conversation as resolved.
30 changes: 4 additions & 26 deletions tmt/steps/discover/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import tmt.utils.filesystem
import tmt.utils.git
import tmt.utils.url
from tmt.options import option
from tmt.plugins import PluginRegistry
from tmt.steps import Action
from tmt.utils import Command, Environment, EnvVarValue, GeneralError, Path
Expand Down Expand Up @@ -192,31 +191,6 @@ def test_dir(self) -> Path:
def source_dir(self) -> Path:
return self.phase_workdir / 'source'

@classmethod
def base_command(
cls,
usage: str,
method_class: Optional[type[click.Command]] = None,
) -> click.Command:
"""
Create base click command (common for all discover plugins)
"""

# Prepare general usage message for the step
if method_class:
usage = Discover.usage(method_overview=usage)

# Create the command
@click.command(cls=method_class, help=usage)
@click.pass_context
@option('-h', '--how', metavar='METHOD', help='Use specified method to discover tests.')
@tmt.steps.PHASE_OPTIONS
def discover(context: 'tmt.cli.Context', **kwargs: Any) -> None:
context.obj.steps.add('discover')
Discover.store_cli_invocation(context)

return discover

def go(self, *, path: Optional[Path] = None, logger: Optional[tmt.log.Logger] = None) -> None:
"""
Perform actions shared among plugins when beginning their tasks
Expand Down Expand Up @@ -1020,3 +994,7 @@ def _iter_tests() -> Iterator['TestOrigin']:
return [
test_origin for test_origin in _iter_tests() if test_origin.test.enabled is enabled
]


# Establish the "plugin class -> step class" link.
DiscoverPlugin._step_class = Discover
35 changes: 8 additions & 27 deletions tmt/steps/execute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from collections.abc import Iterator, Sequence
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast

import click
import fmf.utils

import tmt
Expand All @@ -21,7 +20,6 @@
from tmt.checks import Check, CheckEvent, CheckPlugin
from tmt.container import container, field, simple_field
from tmt.guest import Guest
from tmt.options import option
from tmt.plugins import PluginRegistry
from tmt.result import (
CheckResult,
Expand Down Expand Up @@ -54,7 +52,6 @@

if TYPE_CHECKING:
import tmt.base.plan
import tmt.cli
import tmt.result
import tmt.steps.discover

Expand Down Expand Up @@ -639,6 +636,10 @@ class ExecutePlugin(tmt.steps.Plugin[ExecuteStepDataT, None]):
# Methods ("how: ..." implementations) registered for the same step.
_supported_methods: PluginRegistry[tmt.steps.Method] = PluginRegistry('step.execute')

# No additional options, `execute` does not support modifications of
# phases.
_base_command_options = ()

# Internal executor is the default implementation
how = 'tmt'

Expand All @@ -660,30 +661,6 @@ def __init__(
if tmt.steps.Login._opt('test'):
self._login_after_test = tmt.steps.Login(logger=logger, step=self.step, order=90)

@classmethod
def base_command(
cls,
usage: str,
method_class: Optional[type[click.Command]] = None,
) -> click.Command:
"""
Create base click command (common for all execute plugins)
"""

# Prepare general usage message for the step
if method_class:
usage = Execute.usage(method_overview=usage)

# Create the command
@click.command(cls=method_class, help=usage)
@click.pass_context
@option('-h', '--how', metavar='METHOD', help='Use specified method for test execution.')
def execute(context: 'tmt.cli.Context', **kwargs: Any) -> None:
context.obj.steps.add('execute')
Execute.store_cli_invocation(context)

return execute

def go(
self,
*,
Expand Down Expand Up @@ -1366,3 +1343,7 @@ def _assert_required_tests_executed(self) -> None:
raise tmt.utils.ExecuteError(
f"Required test '{result.name}' on guest '{result.guest.name}' was skipped."
)


# Establish the "plugin class -> step class" link.
ExecutePlugin._step_class = Execute
36 changes: 5 additions & 31 deletions tmt/steps/finish/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import copy
from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast
from typing import Optional, TypeVar, cast

import click
import fmf

import tmt
import tmt.steps
from tmt.container import container
from tmt.guest import Guest
from tmt.options import option
from tmt.plugins import PluginRegistry
from tmt.result import PhaseResult, ResultGuestData, ResultOutcome
from tmt.steps import (
Expand All @@ -21,9 +19,6 @@
sync_with_guests,
)

if TYPE_CHECKING:
import tmt.cli


@container
class FinishStepData(tmt.steps.WhereableStepData, tmt.steps.StepData):
Expand All @@ -45,31 +40,6 @@ class FinishPlugin(tmt.steps.Plugin[FinishStepDataT, PluginOutcome]):
# Methods ("how: ..." implementations) registered for the same step.
_supported_methods: PluginRegistry[Method] = PluginRegistry('step.finish')

@classmethod
def base_command(
cls,
usage: str,
method_class: Optional[type[click.Command]] = None,
) -> click.Command:
"""
Create base click command (common for all finish plugins)
"""

# Prepare general usage message for the step
if method_class:
usage = Finish.usage(method_overview=usage)

# Create the command
@click.command(cls=method_class, help=usage)
@click.pass_context
@option('-h', '--how', metavar='METHOD', help='Use specified method for finishing tasks.')
@tmt.steps.PHASE_OPTIONS
def finish(context: 'tmt.cli.Context', **kwargs: Any) -> None:
context.obj.steps.add('finish')
Finish.store_cli_invocation(context)

return finish

def go(
self,
*,
Expand Down Expand Up @@ -277,3 +247,7 @@ def _is_failed() -> bool:
# Update status and save
self.status('done')
self.save()


# Establish the "plugin class -> step class" link.
FinishPlugin._step_class = Finish
Loading
Loading