Skip to content

Commit

Permalink
Document and type hint commands part 1 (#4309)
Browse files Browse the repository at this point in the history
Start cleaning up molecule commands.
  • Loading branch information
Qalthos authored Oct 29, 2024
1 parent c20dc08 commit 5f05fe6
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 52 deletions.
22 changes: 0 additions & 22 deletions .config/pydoclint-baseline.txt
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
src/molecule/command/base.py
DOC303: Class `Base`: The __init__() docstring does not need a "Returns" section, because it cannot return anything
DOC302: Class `Base`: The class docstring does not need a "Returns" section, because __init__() cannot return anything
DOC501: Function `execute_cmdline_scenarios` has "raise" statements, but the docstring does not have a "Raises" section
DOC503: Function `execute_cmdline_scenarios` exceptions in the "Raises" section in the docstring do not match those in the function body Raises values in the docstring: []. Raised exceptions in the body: ['SystemExit'].
DOC201: Function `execute_subcommand` does not have a return section in docstring
DOC201: Function `click_group_ex` does not have a return section in docstring
DOC201: Function `click_command_ex` does not have a return section in docstring
DOC101: Function `result_callback`: Docstring contains fewer arguments than in function signature.
DOC106: Function `result_callback`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC103: Function `result_callback`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [**kwargs: , *args: ].
--------------------
src/molecule/command/check.py
DOC101: Method `Check.execute`: Docstring contains fewer arguments than in function signature.
DOC106: Method `Check.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Check.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Method `Check.execute`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [action_args: ].
DOC101: Function `check`: Docstring contains fewer arguments than in function signature.
DOC106: Function `check`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Function `check`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `check`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [ctx: , parallel: , scenario_name: ].
--------------------
src/molecule/command/cleanup.py
DOC101: Method `Cleanup.execute`: Docstring contains fewer arguments than in function signature.
DOC106: Method `Cleanup.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
Expand Down
60 changes: 46 additions & 14 deletions src/molecule/command/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,27 @@

if TYPE_CHECKING:
from collections.abc import Callable
from typing import NoReturn

from molecule.scenario import Scenario
from molecule.types import CommandArgs, MoleculeArgs

ClickCommand = Callable[[Callable[..., None]], click.Command]
ClickGroup = Callable[[Callable[..., None]], click.Group]

LOG = logging.getLogger(__name__)
MOLECULE_GLOB = os.environ.get("MOLECULE_GLOB", "molecule/*/molecule.yml")
MOLECULE_DEFAULT_SCENARIO_NAME = "default"


class Base(metaclass=abc.ABCMeta):
class Base(abc.ABC):
"""An abstract base class used to define the command interface."""

def __init__(self, c: config.Config) -> None:
"""Initialize code for all command classes.
Args:
c: An instance of a Molecule config.
Returns:
None
"""
self._config = c
self._setup()
Expand All @@ -75,7 +76,7 @@ def __init_subclass__(cls) -> None:
"""Decorate execute from all subclasses."""
super().__init_subclass__()
for wrapper in logger.get_section_loggers():
cls.execute = wrapper(cls.execute) # type: ignore # noqa: PGH003
cls.execute = wrapper(cls.execute) # type: ignore[method-assign]

@abc.abstractmethod
def execute(
Expand All @@ -91,8 +92,9 @@ def execute(
def _setup(self) -> None:
"""Prepare Molecule's provisioner and returns None."""
self._config.write()
self._config.provisioner.write_config() # type: ignore[union-attr]
self._config.provisioner.manage_inventory() # type: ignore[union-attr]
if self._config.provisioner is not None:
self._config.provisioner.write_config() # type: ignore[no-untyped-call]
self._config.provisioner.manage_inventory() # type: ignore[no-untyped-call]


def execute_cmdline_scenarios(
Expand All @@ -114,6 +116,9 @@ def execute_cmdline_scenarios(
args: ``args`` dict from ``click`` command context
command_args: dict of command arguments, including the target
ansible_args: Optional tuple of arguments to pass to the `ansible-playbook` command
Raises:
SystemExit: If scenario exits prematurely.
"""
glob_str = MOLECULE_GLOB
if scenario_name:
Expand Down Expand Up @@ -174,6 +179,9 @@ def execute_subcommand(
Args:
current_config: An instance of a Molecule config.
subcommand_and_args: A string representing the subcommand and arguments.
Returns:
The result of the subcommand.
"""
(subcommand, *args) = subcommand_and_args.split(" ")
command_module = getattr(molecule.command, subcommand)
Expand Down Expand Up @@ -204,7 +212,15 @@ def execute_scenario(scenario: Scenario) -> None:
scenario._remove_scenario_state_directory() # noqa: SLF001


def filter_ignored_scenarios(scenario_paths) -> list[str]: # type: ignore[no-untyped-def] # noqa: ANN001, D103
def filter_ignored_scenarios(scenario_paths: list[str]) -> list[str]:
"""Filter out candidate scenario paths that are ignored by git.
Args:
scenario_paths: List of candidate scenario paths.
Returns:
Filtered list of scenario paths.
"""
command = ["git", "check-ignore", *scenario_paths]

with contextlib.suppress(subprocess.CalledProcessError, FileNotFoundError):
Expand Down Expand Up @@ -295,8 +311,12 @@ def _get_subcommand(string: str) -> str:
return string.split(".")[-1]


def click_group_ex(): # type: ignore[no-untyped-def] # noqa: ANN201
"""Return extended version of click.group()."""
def click_group_ex() -> ClickGroup:
"""Return extended version of click.group().
Returns:
Click command group.
"""
# Color coding used to group command types, documented only here as we may
# decide to change them later.
# green : (default) as sequence step
Expand All @@ -322,17 +342,29 @@ def click_group_ex(): # type: ignore[no-untyped-def] # noqa: ANN201
)


def click_command_ex() -> Callable[[Callable[..., Any]], click.Command]:
"""Return extended version of click.command()."""
def click_command_ex() -> ClickCommand:
"""Return extended version of click.command().
Returns:
Click command group.
"""
return click.command(
cls=HelpColorsCommand,
help_headers_color="yellow",
help_options_color="green",
)


def result_callback(*args, **kwargs): # type: ignore[no-untyped-def] # noqa: ANN002, ANN003, ANN201, ARG001
"""Click natural exit callback."""
def result_callback(
*args: object, # noqa: ARG001
**kwargs: object, # noqa: ARG001
) -> NoReturn:
"""Click natural exit callback.
Args:
*args: Unused.
**kwargs: Unused.
"""
# We want to be used we run out custom exit code, regardless if run was
# a success or failure.
util.sysexit(0)
33 changes: 26 additions & 7 deletions src/molecule/command/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@


if TYPE_CHECKING:
from molecule.types import CommandArgs
from molecule.types import CommandArgs, MoleculeArgs


LOG = logging.getLogger(__name__)
Expand All @@ -42,9 +42,17 @@
class Check(base.Base):
"""Check Command Class."""

def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002
"""Execute the actions necessary to perform a `molecule check` and returns None."""
self._config.provisioner.check() # type: ignore[union-attr]
def execute(
self,
action_args: list[str] | None = None, # noqa: ARG002
) -> None:
"""Execute the actions necessary to perform a `molecule check`.
Args:
action_args: Molecule cli arguments. Unused.
"""
if self._config.provisioner is not None:
self._config.provisioner.check() # type: ignore[no-untyped-call]


@base.click_command_ex()
Expand All @@ -60,9 +68,20 @@ def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: AN
default=MOLECULE_PARALLEL,
help="Enable or disable parallel mode. Default is disabled.",
)
def check(ctx, scenario_name, parallel): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201
"""Use the provisioner to perform a Dry-Run (destroy, dependency, create, prepare, converge)."""
args = ctx.obj.get("args")
def check( # pragma: no cover
ctx: click.Context,
scenario_name: str,
*,
parallel: bool,
) -> None:
"""Use the provisioner to perform a Dry-Run (destroy, dependency, create, prepare, converge).
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to run.
parallel: Whether the scenario(s) should be run in parallel.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"parallel": parallel, "subcommand": subcommand}

Expand Down
2 changes: 1 addition & 1 deletion src/molecule/command/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
LOG = logging.getLogger(__name__)


class Matrix(base.Base): # pylint: disable=abstract-method
class Matrix(base.Base):
"""Matrix Command Class.
.. program:: molecule matrix subcommand
Expand Down
2 changes: 1 addition & 1 deletion src/molecule/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def print_version(
ctx.exit()


@click_group_ex() # type: ignore[no-untyped-call, misc]
@click_group_ex()
@click.option(
"--debug/--no-debug",
default=MOLECULE_DEBUG,
Expand Down
16 changes: 9 additions & 7 deletions tests/unit/command/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,30 @@


if TYPE_CHECKING:
from unittest.mock import MagicMock, Mock

from pytest_mock import MockerFixture

from molecule import config


@pytest.fixture()
def _patched_ansible_check(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202
def _patched_ansible_check(mocker: MockerFixture) -> MagicMock:
return mocker.patch("molecule.provisioner.ansible.Ansible.check")


# NOTE(retr0h): The use of the `patched_config_validate` fixture, disables
# config.Config._validate from executing. Thus preventing odd side-effects
# throughout patched.assert_called unit tests.
def test_check_execute( # type: ignore[no-untyped-def] # noqa: ANN201, D103
def test_check_execute( # noqa: D103
mocker: MockerFixture, # noqa: ARG001
caplog, # noqa: ANN001
_patched_ansible_check, # noqa: ANN001, PT019
patched_config_validate, # noqa: ANN001, ARG001
caplog: pytest.LogCaptureFixture,
_patched_ansible_check: Mock, # noqa: PT019
patched_config_validate: Mock, # noqa: ARG001
config_instance: config.Config,
):
) -> None:
c = check.Check(config_instance)
c.execute() # type: ignore[no-untyped-call]
c.execute()

assert "default" in caplog.text
assert "check" in caplog.text

0 comments on commit 5f05fe6

Please sign in to comment.