Skip to content

Commit

Permalink
Document and type hint commands part 3 (#4312)
Browse files Browse the repository at this point in the history
Covers commands dependency through idempotence
  • Loading branch information
Qalthos authored Oct 29, 2024
1 parent 42c1c1b commit 3daa7c3
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 93 deletions.
41 changes: 0 additions & 41 deletions .config/pydoclint-baseline.txt
Original file line number Diff line number Diff line change
@@ -1,44 +1,3 @@
src/molecule/command/dependency.py
DOC101: Method `Dependency.execute`: Docstring contains fewer arguments than in function signature.
DOC106: Method `Dependency.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Dependency.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Method `Dependency.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 `dependency`: Docstring contains fewer arguments than in function signature.
DOC106: Function `dependency`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Function `dependency`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `dependency`: 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: , scenario_name: ].
--------------------
src/molecule/command/destroy.py
DOC101: Method `Destroy.execute`: Docstring contains fewer arguments than in function signature.
DOC106: Method `Destroy.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Destroy.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Method `Destroy.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: ].
DOC201: Method `Destroy.execute` does not have a return section in docstring
DOC101: Function `destroy`: Docstring contains fewer arguments than in function signature.
DOC106: Function `destroy`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Function `destroy`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `destroy`: 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: [__all: , ctx: , driver_name: , parallel: , scenario_name: ].
--------------------
src/molecule/command/drivers.py
DOC101: Function `drivers`: Docstring contains fewer arguments than in function signature.
DOC106: Function `drivers`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Function `drivers`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `drivers`: 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: , format: ].
--------------------
src/molecule/command/idempotence.py
DOC101: Method `Idempotence.execute`: Docstring contains fewer arguments than in function signature.
DOC106: Method `Idempotence.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Idempotence.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Method `Idempotence.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: ].
DOC106: Method `Idempotence._is_idempotent`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Idempotence._is_idempotent`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC106: Method `Idempotence._non_idempotent_tasks`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `Idempotence._non_idempotent_tasks`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC101: Function `idempotence`: Docstring contains fewer arguments than in function signature.
DOC106: Function `idempotence`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Function `idempotence`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `idempotence`: 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: [ansible_args: , ctx: , scenario_name: ].
--------------------
src/molecule/command/init/base.py
DOC601: Class `Base`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
DOC603: Class `Base`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [__metaclass__: ]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
Expand Down
27 changes: 20 additions & 7 deletions src/molecule/command/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@


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


LOG = logging.getLogger(__name__)
Expand All @@ -39,9 +39,14 @@
class Dependency(base.Base):
"""Dependency 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 dependency` and returns None."""
self._config.dependency.execute() # type: ignore[union-attr]
def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
"""Execute the actions necessary to perform a `molecule dependency`.
Args:
action_args: Arguments for this command. Unused.
"""
if self._config.dependency:
self._config.dependency.execute() # type: ignore[no-untyped-call]


@base.click_command_ex()
Expand All @@ -52,9 +57,17 @@ def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: AN
default=base.MOLECULE_DEFAULT_SCENARIO_NAME,
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
def dependency(ctx, scenario_name): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201
"""Manage the role's dependencies."""
args = ctx.obj.get("args")
def dependency(
ctx: click.Context,
scenario_name: str,
) -> None: # pragma: no cover
"""Manage the role's dependencies.
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand}

Expand Down
33 changes: 26 additions & 7 deletions src/molecule/command/destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@


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


LOG = logging.getLogger(__name__)
Expand All @@ -44,14 +44,19 @@
class Destroy(base.Base):
"""Destroy 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 destroy` and returns None."""
def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
"""Execute the actions necessary to perform a `molecule destroy`.
Args:
action_args: Arguments for this command. Unused.
"""
if self._config.command_args.get("destroy") == "never":
msg = "Skipping, '--destroy=never' requested."
LOG.warning(msg)
return

self._config.provisioner.destroy() # type: ignore[union-attr]
if self._config.provisioner:
self._config.provisioner.destroy() # type: ignore[no-untyped-call]
self._config.state.reset()


Expand Down Expand Up @@ -80,9 +85,23 @@ def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: AN
default=False,
help="Enable or disable parallel mode. Default is disabled.",
)
def destroy(ctx, scenario_name, driver_name, __all, parallel): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201
"""Use the provisioner to destroy the instances."""
args = ctx.obj.get("args")
def destroy(
ctx: click.Context,
scenario_name: str | None,
driver_name: str,
__all: bool, # noqa: FBT001
parallel: bool, # noqa: FBT001
) -> None: # pragma: no cover
"""Use the provisioner to destroy the instances.
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
driver_name: Molecule driver to use.
__all: Whether molecule should target scenario_name or all scenarios.
parallel: Whether the scenario(s) should be run in parallel mode.
"""
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {
"parallel": parallel,
Expand Down
12 changes: 10 additions & 2 deletions src/molecule/command/drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,16 @@
default="simple",
help="Change output format. (simple)",
)
def drivers(ctx, format): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201, A002, ARG001
"""List drivers."""
def drivers(
ctx: click.Context, # noqa: ARG001
format: str, # noqa: A002
) -> None: # pragma: no cover
"""List drivers.
Args:
ctx: Click context object holding commandline arguments.
format: Output format to use.
"""
drivers = [] # pylint: disable=redefined-outer-name
for driver in api.drivers().values():
description = str(driver)
Expand Down
46 changes: 30 additions & 16 deletions src/molecule/command/idempotence.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@


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


LOG = logging.getLogger(__name__)
Expand All @@ -46,24 +46,29 @@ class Idempotence(base.Base):
the scenario will be considered idempotent.
"""

def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002
"""Execute the actions necessary to perform a `molecule idempotence` and returns None."""
def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002
"""Execute the actions necessary to perform a `molecule idempotence`.
Args:
action_args: Arguments for this command. Unused.
"""
if not self._config.state.converged:
msg = "Instances not converged. Please converge instances first."
util.sysexit_with_message(msg)

output = self._config.provisioner.converge() # type: ignore[union-attr]
if self._config.provisioner:
output = self._config.provisioner.converge() # type: ignore[no-untyped-call]

idempotent = self._is_idempotent(output) # type: ignore[no-untyped-call]
if idempotent:
msg = "Idempotence completed successfully."
LOG.info(msg)
else:
details = "\n".join(self._non_idempotent_tasks(output)) # type: ignore[no-untyped-call]
msg = f"Idempotence test failed because of the following tasks:\n{details}"
util.sysexit_with_message(msg)
idempotent = self._is_idempotent(output)
if idempotent:
msg = "Idempotence completed successfully."
LOG.info(msg)
else:
details = "\n".join(self._non_idempotent_tasks(output))
msg = f"Idempotence test failed because of the following tasks:\n{details}"
util.sysexit_with_message(msg)

def _is_idempotent(self, output): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202
def _is_idempotent(self, output: str) -> bool:
"""Parse the output of the provisioning for changed and returns a bool.
Args:
Expand All @@ -80,7 +85,7 @@ def _is_idempotent(self, output): # type: ignore[no-untyped-def] # noqa: ANN00

return not bool(changed)

def _non_idempotent_tasks(self, output): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202
def _non_idempotent_tasks(self, output: str) -> list[str]:
"""Parse the output to identify the non idempotent tasks.
Args:
Expand Down Expand Up @@ -120,12 +125,21 @@ def _non_idempotent_tasks(self, output): # type: ignore[no-untyped-def] # noqa
help=f"Name of the scenario to target. ({base.MOLECULE_DEFAULT_SCENARIO_NAME})",
)
@click.argument("ansible_args", nargs=-1, type=click.UNPROCESSED)
def idempotence(ctx, scenario_name, ansible_args): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201
def idempotence(
ctx: click.Context,
scenario_name: str,
ansible_args: tuple[str, ...],
) -> None: # pragma: no cover
"""Use the provisioner to configure the instances.
After parse the output to determine idempotence.
Args:
ctx: Click context object holding commandline arguments.
scenario_name: Name of the scenario to target.
ansible_args: Arguments to forward to Ansible.
"""
args = ctx.obj.get("args")
args: MoleculeArgs = ctx.obj.get("args")
subcommand = base._get_subcommand(__name__) # noqa: SLF001
command_args: CommandArgs = {"subcommand": subcommand}

Expand Down
16 changes: 10 additions & 6 deletions tests/unit/command/test_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@


if TYPE_CHECKING:
from unittest.mock import Mock

import pytest

from pytest_mock import MockerFixture

from molecule import config
Expand All @@ -33,15 +37,15 @@
# 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_dependency_execute( # type: ignore[no-untyped-def] # noqa: ANN201, D103
def test_dependency_execute( # noqa: D103
mocker: MockerFixture, # noqa: ARG001
caplog, # noqa: ANN001
patched_ansible_galaxy, # noqa: ANN001
patched_config_validate, # noqa: ANN001, ARG001
caplog: pytest.LogCaptureFixture,
patched_ansible_galaxy: Mock,
patched_config_validate: Mock, # noqa: ARG001
config_instance: config.Config,
):
) -> None:
d = dependency.Dependency(config_instance)
d.execute() # type: ignore[no-untyped-call]
d.execute()

patched_ansible_galaxy.assert_called_once_with()

Expand Down
30 changes: 16 additions & 14 deletions tests/unit/command/test_destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,36 @@


if TYPE_CHECKING:
from unittest.mock import MagicMock, Mock

from pytest_mock import MockerFixture

from molecule import config


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


@pytest.fixture()
def _patched_destroy_setup(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202
def _patched_destroy_setup(mocker: MockerFixture) -> MagicMock:
return mocker.patch("molecule.command.destroy.Destroy._setup")


# 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.
@pytest.mark.skip(reason="destroy not running for delegated")
def test_destroy_execute( # type: ignore[no-untyped-def] # noqa: ANN201, D103
def test_destroy_execute( # noqa: D103
mocker: MockerFixture, # noqa: ARG001
caplog, # noqa: ANN001
patched_config_validate, # noqa: ANN001, ARG001
_patched_ansible_destroy, # noqa: ANN001, PT019
caplog: pytest.LogCaptureFixture,
patched_config_validate: Mock, # noqa: ARG001
_patched_ansible_destroy: Mock, # noqa: PT019
config_instance: config.Config,
):
) -> None:
d = destroy.Destroy(config_instance)
d.execute() # type: ignore[no-untyped-call]
d.execute()

assert "destroy" in caplog.text

Expand All @@ -71,16 +73,16 @@ def test_destroy_execute( # type: ignore[no-untyped-def] # noqa: ANN201, D103
["command_driver_delegated_section_data"], # noqa: PT007
indirect=True,
)
def test_execute_skips_when_destroy_strategy_is_never( # type: ignore[no-untyped-def] # noqa: ANN201, D103
_patched_destroy_setup, # noqa: ANN001, PT019
caplog, # noqa: ANN001
_patched_ansible_destroy, # noqa: ANN001, PT019
def test_execute_skips_when_destroy_strategy_is_never( # noqa: D103
_patched_destroy_setup: Mock, # noqa: PT019
caplog: pytest.LogCaptureFixture,
_patched_ansible_destroy: Mock, # noqa: PT019
config_instance: config.Config,
):
) -> None:
config_instance.command_args = {"destroy": "never"}

d = destroy.Destroy(config_instance)
d.execute() # type: ignore[no-untyped-call]
d.execute()

msg = "Skipping, '--destroy=never' requested."
assert msg in caplog.text
Expand Down

0 comments on commit 3daa7c3

Please sign in to comment.