From cb654fee709879e555f5a2afb3dfbef97c13557b Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 31 Oct 2024 10:30:53 -0400 Subject: [PATCH] Add docstrings and type hints to init command. (#4314) --- .config/pydoclint-baseline.txt | 16 ----- src/molecule/command/init/__init__.py | 2 +- src/molecule/command/init/base.py | 17 +++-- src/molecule/command/init/init.py | 4 +- src/molecule/command/init/scenario.py | 83 ++++++++++++++++++------ src/molecule/config.py | 6 +- tests/unit/command/init/test_scenario.py | 8 +-- 7 files changed, 86 insertions(+), 50 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index c147e0f56..69b3c962f 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -1,19 +1,3 @@ -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.) --------------------- -src/molecule/command/init/scenario.py - DOC101: Method `Scenario.__init__`: Docstring contains fewer arguments than in function signature. - DOC103: Method `Scenario.__init__`: 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: [command_args: dict[str, str]]. - DOC101: Method `Scenario.execute`: Docstring contains fewer arguments than in function signature. - DOC106: Method `Scenario.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Scenario.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Scenario.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 `scenario`: Docstring contains fewer arguments than in function signature. - DOC106: Function `scenario`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Function `scenario`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Function `scenario`: 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: , dependency_name: , driver_name: , provisioner_name: , scenario_name: ]. --------------------- src/molecule/command/reset.py DOC101: Function `reset`: Docstring contains fewer arguments than in function signature. DOC106: Function `reset`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature diff --git a/src/molecule/command/init/__init__.py b/src/molecule/command/init/__init__.py index d2583e366..6e031999e 100644 --- a/src/molecule/command/init/__init__.py +++ b/src/molecule/command/init/__init__.py @@ -1 +1 @@ -# D104 # noqa: D104, ERA001 +# noqa: D104 diff --git a/src/molecule/command/init/base.py b/src/molecule/command/init/base.py index 3192c6ab6..0acecd9ba 100644 --- a/src/molecule/command/init/base.py +++ b/src/molecule/command/init/base.py @@ -22,7 +22,8 @@ import abc import logging -import os + +from pathlib import Path from molecule import util @@ -30,13 +31,19 @@ LOG = logging.getLogger(__name__) -class Base: +class Base(abc.ABC): """Init Command Base Class.""" - __metaclass__ = abc.ABCMeta + @abc.abstractmethod + def execute(self, action_args: list[str] | None = None) -> None: + """Abstract method to execute the command. + + Args: + action_args: An optional list of arguments to pass to the action. + """ - def _validate_template_dir(self, template_dir): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 - if not os.path.isdir(template_dir): # noqa: PTH112 + def _validate_template_dir(self, template_dir: str) -> None: + if not Path(template_dir).is_dir(): util.sysexit_with_message( "The specified template directory (" + str(template_dir) + ") does not exist", ) diff --git a/src/molecule/command/init/init.py b/src/molecule/command/init/init.py index 92c232a26..1164beb3f 100644 --- a/src/molecule/command/init/init.py +++ b/src/molecule/command/init/init.py @@ -29,8 +29,8 @@ LOG = logging.getLogger(__name__) -@base.click_group_ex() # type: ignore # noqa: PGH003 -def init(): # pragma: no cover # noqa: ANN201 +@base.click_group_ex() +def init() -> None: # pragma: no cover """Initialize a new scenario.""" diff --git a/src/molecule/command/init/scenario.py b/src/molecule/command/init/scenario.py index 849d54e6b..d0c615d5a 100644 --- a/src/molecule/command/init/scenario.py +++ b/src/molecule/command/init/scenario.py @@ -25,6 +25,9 @@ import os import sys +from pathlib import Path +from typing import TYPE_CHECKING + import click from molecule import api, config, util @@ -33,6 +36,27 @@ from molecule.config import DEFAULT_DRIVER, MOLECULE_EMBEDDED_DATA_DIR +if TYPE_CHECKING: + from typing import TypedDict + + class CommandArgs(TypedDict): + """Argument dictionary to pass to init-scenario playbook. + + Attributes: + dependency_name: Name of the dependency to initialize. + driver_name: Name of the driver to initialize. + provisioner_name: Name of the provisioner to initialize. + scenario_name: Name of the scenario to initialize. + subcommand: Name of subcommand to initialize. + """ + + dependency_name: str + driver_name: str + provisioner_name: str + scenario_name: str + subcommand: str + + LOG = logging.getLogger(__name__) @@ -59,20 +83,28 @@ class Scenario(base.Base): Initialize a new scenario using a embedded template. """ - def __init__(self, command_args: dict[str, str]) -> None: - """Construct Scenario.""" + def __init__(self, command_args: CommandArgs) -> None: + """Construct Scenario. + + Args: + command_args: Arguments to pass to init-scenario playbook. + """ self._command_args = command_args - def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002 - """Execute the actions necessary to perform a `molecule init scenario` and returns None.""" + def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 + """Execute the actions necessary to perform a `molecule init scenario`. + + Args: + action_args: Arguments for this command. Unused. + """ scenario_name = self._command_args["scenario_name"] msg = f"Initializing new scenario {scenario_name}..." LOG.info(msg) - molecule_directory = config.molecule_directory(os.getcwd()) # noqa: PTH109 - scenario_directory = os.path.join(molecule_directory, scenario_name) # noqa: PTH118 + molecule_directory = Path(config.molecule_directory(Path.cwd())) + scenario_directory = molecule_directory / scenario_name - if os.path.isdir(scenario_directory): # noqa: PTH112 + if scenario_directory.is_dir(): msg = f"The directory molecule/{scenario_name} exists. Cannot create new scenario." util.sysexit_with_message(msg) @@ -97,14 +129,18 @@ def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: AN LOG.info(msg) -def _role_exists(ctx, param, value: str): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN202, ARG001 +def _role_exists( + ctx: click.Context, # noqa: ARG001 + param: str | None, # noqa: ARG001 + value: str, +) -> str: # pragma: no cover # if role name was not mentioned we assume that current directory is the # one hosting the role and determining the role name. if not value: - value = os.path.basename(os.getcwd()) # noqa: PTH109, PTH119 + value = Path.cwd().name - role_directory = os.path.join(os.pardir, value) # noqa: PTH118 - if not os.path.exists(role_directory): # noqa: PTH110 + role_directory = Path.cwd().parent / value + if not role_directory.exists(): msg = f"The role '{value}' not found. Please choose the proper role name." util.sysexit_with_message(msg) return value @@ -136,18 +172,25 @@ def _role_exists(ctx, param, value: str): # type: ignore[no-untyped-def] # prag default=command_base.MOLECULE_DEFAULT_SCENARIO_NAME, required=False, ) -def scenario( # type: ignore[no-untyped-def] # noqa: ANN201 - ctx, # noqa: ANN001, ARG001 - dependency_name, # noqa: ANN001 - driver_name, # noqa: ANN001 - provisioner_name, # noqa: ANN001 - scenario_name, # noqa: ANN001 -): # pragma: no cover +def scenario( + ctx: click.Context, # noqa: ARG001 + dependency_name: str, + driver_name: str, + provisioner_name: str, + scenario_name: str, +) -> None: # pragma: no cover """Initialize a new scenario for use with Molecule. If name is not specified the 'default' value will be used. + + Args: + ctx: Click context object holding commandline arguments. + dependency_name: Name of dependency to initialize. + driver_name: Name of driver to use. + provisioner_name: Name of provisioner to use. + scenario_name: Name of scenario to initialize. """ - command_args = { + command_args: CommandArgs = { "dependency_name": dependency_name, "driver_name": driver_name, "provisioner_name": provisioner_name, @@ -156,4 +199,4 @@ def scenario( # type: ignore[no-untyped-def] # noqa: ANN201 } s = Scenario(command_args) - s.execute() # type: ignore[no-untyped-call] + s.execute() diff --git a/src/molecule/config.py b/src/molecule/config.py index 44f62cbff..3006fd151 100644 --- a/src/molecule/config.py +++ b/src/molecule/config.py @@ -523,7 +523,7 @@ def _validate(self) -> None: util.sysexit_with_message(msg) -def molecule_directory(path: str) -> str: +def molecule_directory(path: str | Path) -> str: """Return directory of the current scenario. Args: @@ -532,7 +532,9 @@ def molecule_directory(path: str) -> str: Returns: The current scenario's directory. """ - return os.path.join(path, MOLECULE_DIRECTORY) # noqa: PTH118 + if isinstance(path, str): + path = Path(path) + return str(path / MOLECULE_DIRECTORY) def molecule_file(path: str) -> str: diff --git a/tests/unit/command/init/test_scenario.py b/tests/unit/command/init/test_scenario.py index 98e1b11a5..23f264f55 100644 --- a/tests/unit/command/init/test_scenario.py +++ b/tests/unit/command/init/test_scenario.py @@ -46,7 +46,7 @@ def fixture_command_args() -> dict[str, str]: @pytest.fixture(name="instance") -def fixture_instance(command_args: dict[str, str]) -> scenario.Scenario: +def fixture_instance(command_args: scenario.CommandArgs) -> scenario.Scenario: """Provide a scenario instance. Args: @@ -70,7 +70,7 @@ def test_scenario_execute( test_cache_path: Path to the cache directory for the test. """ monkeypatch.chdir(test_cache_path) - instance.execute() # type: ignore[no-untyped-call] + instance.execute() msg = "Initializing new scenario test-scenario..." patched_logger_info.assert_any_call(msg) @@ -97,10 +97,10 @@ def test_execute_scenario_exists( test_cache_path: Path to the cache directory for the test. """ monkeypatch.chdir(test_cache_path) - instance.execute() # type: ignore[no-untyped-call] + instance.execute() with pytest.raises(SystemExit) as e: - instance.execute() # type: ignore[no-untyped-call] + instance.execute() assert e.value.code == 1