Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document and type hint commands part 1 #4309

Merged
merged 5 commits into from
Oct 29, 2024
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
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