From 3daa7c3d6ddd7e93aca1591ac9cd01a9bbf32746 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Tue, 29 Oct 2024 14:16:35 -0400 Subject: [PATCH] Document and type hint commands part 3 (#4312) Covers commands dependency through idempotence --- .config/pydoclint-baseline.txt | 41 ------------------------ src/molecule/command/dependency.py | 27 ++++++++++++---- src/molecule/command/destroy.py | 33 +++++++++++++++---- src/molecule/command/drivers.py | 12 +++++-- src/molecule/command/idempotence.py | 46 +++++++++++++++++---------- tests/unit/command/test_dependency.py | 16 ++++++---- tests/unit/command/test_destroy.py | 30 +++++++++-------- 7 files changed, 112 insertions(+), 93 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index 17c986b1a..b91c0c280 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -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.) diff --git a/src/molecule/command/dependency.py b/src/molecule/command/dependency.py index 3ffa17e6a..503bfef02 100644 --- a/src/molecule/command/dependency.py +++ b/src/molecule/command/dependency.py @@ -30,7 +30,7 @@ if TYPE_CHECKING: - from molecule.types import CommandArgs + from molecule.types import CommandArgs, MoleculeArgs LOG = logging.getLogger(__name__) @@ -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() @@ -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} diff --git a/src/molecule/command/destroy.py b/src/molecule/command/destroy.py index 7b84974a8..84adc6569 100644 --- a/src/molecule/command/destroy.py +++ b/src/molecule/command/destroy.py @@ -34,7 +34,7 @@ if TYPE_CHECKING: - from molecule.types import CommandArgs + from molecule.types import CommandArgs, MoleculeArgs LOG = logging.getLogger(__name__) @@ -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() @@ -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, diff --git a/src/molecule/command/drivers.py b/src/molecule/command/drivers.py index 707532e05..08fcceab9 100644 --- a/src/molecule/command/drivers.py +++ b/src/molecule/command/drivers.py @@ -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) diff --git a/src/molecule/command/idempotence.py b/src/molecule/command/idempotence.py index 74475627c..a97ec4b91 100644 --- a/src/molecule/command/idempotence.py +++ b/src/molecule/command/idempotence.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: - from molecule.types import CommandArgs + from molecule.types import CommandArgs, MoleculeArgs LOG = logging.getLogger(__name__) @@ -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: @@ -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: @@ -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} diff --git a/tests/unit/command/test_dependency.py b/tests/unit/command/test_dependency.py index f6f4b04fd..41333d73d 100644 --- a/tests/unit/command/test_dependency.py +++ b/tests/unit/command/test_dependency.py @@ -25,6 +25,10 @@ if TYPE_CHECKING: + from unittest.mock import Mock + + import pytest + from pytest_mock import MockerFixture from molecule import config @@ -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() diff --git a/tests/unit/command/test_destroy.py b/tests/unit/command/test_destroy.py index daa1e832e..5fca7d778 100644 --- a/tests/unit/command/test_destroy.py +++ b/tests/unit/command/test_destroy.py @@ -27,18 +27,20 @@ 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") @@ -46,15 +48,15 @@ def _patched_destroy_setup(mocker): # type: ignore[no-untyped-def] # noqa: ANN # 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 @@ -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