diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index 2e801fcd9..02be942b5 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -1,47 +1,3 @@ -src/molecule/dependency/ansible_galaxy/__init__.py - DOC101: Method `AnsibleGalaxy.__init__`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsibleGalaxy.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsibleGalaxy.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsibleGalaxy.__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: [config: ]. --------------------- -src/molecule/dependency/ansible_galaxy/base.py - DOC601: Class `AnsibleGalaxyBase`: 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 `AnsibleGalaxyBase`: 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.) - DOC101: Method `AnsibleGalaxyBase.__init__`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsibleGalaxyBase.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsibleGalaxyBase.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsibleGalaxyBase.__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: [config: ]. - DOC101: Method `AnsibleGalaxyBase.filter_options`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsibleGalaxyBase.filter_options`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsibleGalaxyBase.filter_options`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsibleGalaxyBase.filter_options`: 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: [keys: , opts: ]. --------------------- -src/molecule/dependency/ansible_galaxy/collections.py - DOC601: Class `Collections`: 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 `Collections`: 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: [COMMANDS: , FILTER_OPTS: ]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) --------------------- -src/molecule/dependency/ansible_galaxy/roles.py - DOC601: Class `Roles`: 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 `Roles`: 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: [COMMANDS: , FILTER_OPTS: ]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) --------------------- -src/molecule/dependency/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.) - DOC106: Method `Base.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Base.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC201: Method `Base.execute_with_retries` does not have a return section in docstring - DOC101: Method `Base.execute`: Docstring contains fewer arguments than in function signature. - DOC106: Method `Base.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Base.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Base.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: ]. - DOC202: Method `Base.default_options` has a return section in docstring, but there are no return statements or annotations --------------------- -src/molecule/dependency/shell.py - DOC101: Method `Shell.__init__`: Docstring contains fewer arguments than in function signature. - DOC106: Method `Shell.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Shell.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Shell.__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: [config: ]. --------------------- tests/conftest.py DOC101: Function `reset_pytest_vars`: Docstring contains fewer arguments than in function signature. DOC106: Function `reset_pytest_vars`: 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/dependency.py b/src/molecule/command/dependency.py index 503bfef02..43ffe2d5c 100644 --- a/src/molecule/command/dependency.py +++ b/src/molecule/command/dependency.py @@ -46,7 +46,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 action_args: Arguments for this command. Unused. """ if self._config.dependency: - self._config.dependency.execute() # type: ignore[no-untyped-call] + self._config.dependency.execute() @base.click_command_ex() diff --git a/src/molecule/dependency/__init__.py b/src/molecule/dependency/__init__.py index d2583e366..6e031999e 100644 --- a/src/molecule/dependency/__init__.py +++ b/src/molecule/dependency/__init__.py @@ -1 +1 @@ -# D104 # noqa: D104, ERA001 +# noqa: D104 diff --git a/src/molecule/dependency/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index f07d0f32e..febbcdf39 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -2,12 +2,20 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from molecule import util from molecule.dependency.ansible_galaxy.collections import Collections from molecule.dependency.ansible_galaxy.roles import Roles from molecule.dependency.base import Base +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from molecule.config import Config + + class AnsibleGalaxy(Base): """Galaxy is the default dependency manager. @@ -88,31 +96,50 @@ class AnsibleGalaxy(Base): [ANSIBLE_HOME]: https://docs.ansible.com/ansible/latest/reference_appendices/config.html#ansible-home """ - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 - """Construct AnsibleGalaxy.""" + def __init__(self, config: Config) -> None: + """Construct AnsibleGalaxy. + + Args: + config: Molecule Config instance. + """ super().__init__(config) self.invocations = [Roles(config), Collections(config)] - def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002, D102 + def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 + """Execute all Ansible Galaxy dependencies. + + Args: + action_args: Arguments for invokers. Unused. + """ for invoker in self.invocations: - invoker.execute() # type: ignore[no-untyped-call] + invoker.execute() - def _has_requirements_file(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _has_requirements_file(self) -> bool: has_file = False for invoker in self.invocations: - has_file = has_file or invoker._has_requirements_file() # type: ignore[no-untyped-call] # noqa: SLF001 + has_file = has_file or invoker._has_requirements_file() # noqa: SLF001 return has_file @property - def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - e = {} # type: ignore[var-annotated] + def default_env(self) -> dict[str, str]: + """Default environment variables across all invokers. + + Returns: + Merged dictionary of default env vars for all invokers. + """ + env: dict[str, str] = {} for invoker in self.invocations: - e = util.merge(e, invoker.default_env) # type: ignore[attr-defined] # pylint: disable=no-member - return e + env = util.merge_dicts(env, invoker.default_env) + return env @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - opts = {} # type: ignore[var-annotated] + def default_options(self) -> MutableMapping[str, str | bool]: + """Default options across all invokers. + + Returns: + Merged dictionary of default options for all invokers. + """ + opts: MutableMapping[str, str | bool] = {} for invoker in self.invocations: - opts = util.merge(opts, invoker.default_opts) # type: ignore[attr-defined] # pylint: disable=no-member + opts = util.merge_dicts(opts, invoker.default_options) return opts diff --git a/src/molecule/dependency/ansible_galaxy/base.py b/src/molecule/dependency/ansible_galaxy/base.py index 02247f0d7..04244f5bf 100644 --- a/src/molecule/dependency/ansible_galaxy/base.py +++ b/src/molecule/dependency/ansible_galaxy/base.py @@ -25,10 +25,19 @@ import logging import os +from pathlib import Path +from typing import TYPE_CHECKING + from molecule import util from molecule.dependency import base +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from molecule.config import Config + + LOG = logging.getLogger(__name__) @@ -37,27 +46,40 @@ class AnsibleGalaxyBase(base.Base): Attributes: FILTER_OPTS: Keys to remove from the dictionary returned by options(). + COMMANDS: Arguments to send to ansible-galaxy to install the appropriate type of content. """ - __metaclass__ = abc.ABCMeta + FILTER_OPTS: tuple[str, ...] = () + COMMANDS: tuple[str, ...] = () - FILTER_OPTS = () + def __init__(self, config: Config) -> None: + """Construct AnsibleGalaxy. - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 - """Construct AnsibleGalaxy.""" + Args: + config: Molecule Config instance. + """ super().__init__(config) - self._sh_command = None + self._sh_command = [] self.command = "ansible-galaxy" @property @abc.abstractmethod - def requirements_file(self): # type: ignore[no-untyped-def] # cover # noqa: ANN201, D102 - pass + def requirements_file(self) -> str: # cover + """Path to requirements file. + + Returns: + Path to the requirements file for this dependency. + """ @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - d = { + def default_options(self) -> MutableMapping[str, str | bool]: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ + d: MutableMapping[str, str | bool] = { "force": False, } if self._config.debug: @@ -65,13 +87,21 @@ def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 return d - def filter_options(self, opts, keys): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201 + def filter_options( + self, + opts: MutableMapping[str, str | bool], + keys: tuple[str, ...], + ) -> MutableMapping[str, str | bool]: """Filter certain keys from a dictionary. Removes all the values of ``keys`` from the dictionary ``opts``, if they are present. Returns the resulting dictionary. Does not modify the existing one. + Args: + opts: Options dictionary. + keys: Key names to exclude from opts. + Returns: A copy of ``opts`` without the value of keys """ @@ -84,52 +114,68 @@ def filter_options(self, opts, keys): # type: ignore[no-untyped-def] # noqa: A # NOTE(retr0h): Override the base classes' options() to handle # ``ansible-galaxy`` one-off. @property - def options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - o = self._config.config["dependency"]["options"] + def options(self) -> MutableMapping[str, str | bool]: + """Computed options for this dependency. + + Returns: + Merged and filtered options for this dependency. + """ + opts = self._config.config["dependency"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: - o = util.filter_verbose_permutation(o) + opts = util.filter_verbose_permutation(opts) - o = util.merge_dicts(self.default_options, o) - return self.filter_options(o, self.FILTER_OPTS) # type: ignore[no-untyped-call] + opts = util.merge_dicts(self.default_options, opts) + return self.filter_options(opts, self.FILTER_OPTS) @property - def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return util.merge_dicts(os.environ, self._config.env) + def default_env(self) -> dict[str, str]: + """Default environment variables for this dependency. - def bake(self): # type: ignore[no-untyped-def] # noqa: ANN201 + Returns: + Default environment variables for this dependency. + """ + env = dict(os.environ) + return util.merge_dicts(env, self._config.env) + + def bake(self) -> None: """Bake an ``ansible-galaxy`` command so it's ready to execute and returns None.""" options = self.options verbose_flag = util.verbose_flag(options) self._sh_command = [ self.command, - *self.COMMANDS, # type: ignore[attr-defined] # pylint: disable=no-member + *self.COMMANDS, *util.dict2args(options), *verbose_flag, ] - def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002, D102 + def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 + """Execute dependency. + + Args: + action_args: Arguments for this dependency. Unused. + """ if not self.enabled: msg = "Skipping, dependency is disabled." LOG.warning(msg) return - super().execute() # type: ignore[no-untyped-call] + super().execute() - if not self._has_requirements_file(): # type: ignore[no-untyped-call] + if not self._has_requirements_file(): msg = "Skipping, missing the requirements file." LOG.warning(msg) return - if self._sh_command is None: - self.bake() # type: ignore[no-untyped-call] + if not self._sh_command: + self.bake() - self._setup() # type: ignore[no-untyped-call] - self.execute_with_retries() # type: ignore[no-untyped-call] + self._setup() + self.execute_with_retries() - def _setup(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _setup(self) -> None: """Prepare the system for using ``ansible-galaxy`` and returns None.""" - def _has_requirements_file(self): # type: ignore[no-untyped-def] # noqa: ANN202 - return os.path.isfile(self.requirements_file) # noqa: PTH113 + def _has_requirements_file(self) -> bool: + return Path(self.requirements_file).is_file() diff --git a/src/molecule/dependency/ansible_galaxy/collections.py b/src/molecule/dependency/ansible_galaxy/collections.py index a0ac19b4f..0fb584117 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -3,30 +3,48 @@ from __future__ import annotations import logging -import os + +from pathlib import Path +from typing import TYPE_CHECKING, cast from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase +if TYPE_CHECKING: + from collections.abc import MutableMapping + + LOG = logging.getLogger(__name__) class Collections(AnsibleGalaxyBase): - """Collection-specific Ansible Galaxy dependency handling.""" + """Collection-specific Ansible Galaxy dependency handling. - FILTER_OPTS = ("role-file",) # type: ignore # noqa: PGH003 + Attributes: + FILTER_OPTS: Keys to remove from the dictionary returned by options(). + COMMANDS: Arguments to send to ansible-galaxy to install the appropriate type of content. + """ + + FILTER_OPTS = ("role-file",) COMMANDS = ("collection", "install") @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_options(self) -> MutableMapping[str, str | bool]: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ general = super().default_options specific = util.merge_dicts( general, { - "requirements-file": os.path.join( # noqa: PTH118 - self._config.scenario.directory, - "collections.yml", + "requirements-file": str( + Path( + self._config.scenario.directory, + "collections.yml", + ), ), }, ) @@ -34,9 +52,10 @@ def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 return specific # noqa: RET504 @property - def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return super().default_env + def requirements_file(self) -> str: + """Path to requirements file. - @property - def requirements_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self.options["requirements-file"] + Returns: + Path to the requirements file for this dependency. + """ + return cast(str, self.options["requirements-file"]) diff --git a/src/molecule/dependency/ansible_galaxy/roles.py b/src/molecule/dependency/ansible_galaxy/roles.py index 6a633213d..49d105c44 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -3,35 +3,58 @@ from __future__ import annotations import logging -import os + +from pathlib import Path +from typing import TYPE_CHECKING, cast from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase +if TYPE_CHECKING: + from collections.abc import MutableMapping + + LOG = logging.getLogger(__name__) class Roles(AnsibleGalaxyBase): - """Role-specific Ansible Galaxy dependency handling.""" + """Role-specific Ansible Galaxy dependency handling. - FILTER_OPTS = ("requirements-file",) # type: ignore # noqa: PGH003 + Attributes: + FILTER_OPTS: Keys to remove from the dictionary returned by options(). + COMMANDS: Arguments to send to ansible-galaxy to install the appropriate type of content. + """ + + FILTER_OPTS = ("requirements-file",) COMMANDS = ("install",) @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_options(self) -> MutableMapping[str, str | bool]: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ general = super().default_options specific = util.merge_dicts( general, { - "role-file": os.path.join( # noqa: PTH118 - self._config.scenario.directory, - "requirements.yml", + "role-file": str( + Path( + self._config.scenario.directory, + "requirements.yml", + ), ), }, ) return specific # noqa: RET504 @property - def requirements_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self.options["role-file"] + def requirements_file(self) -> str: + """Path to requirements file. + + Returns: + Path to the requirements file for this dependency. + """ + return cast(str, self.options["role-file"]) diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index f8d871e3b..96530dbec 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -26,14 +26,21 @@ import time from subprocess import CalledProcessError +from typing import TYPE_CHECKING from molecule import util +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from molecule.config import Config + + LOG = logging.getLogger(__name__) -class Base: +class Base(abc.ABC): """Dependency Base Class. Attributes: @@ -42,27 +49,23 @@ class Base: BACKOFF: Additional number of seconds to sleep for each successive attempt. """ - __metaclass__ = abc.ABCMeta - RETRY = 3 SLEEP = 3 BACKOFF = 3 - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 + def __init__(self, config: Config) -> None: """Initialize code for all :ref:`Dependency` classes. Args: config: An instance of a Molecule config. """ self._config = config - self._sh_command: list[str] | None = None + self._sh_command: str | list[str] = [] - def execute_with_retries(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def execute_with_retries(self) -> None: """Run dependency downloads with retry and timed back-off.""" - exception = None - try: - util.run_command(self._sh_command, debug=self._config.debug, check=True) # type: ignore[arg-type] + util.run_command(self._sh_command, debug=self._config.debug, check=True) msg = "Dependency completed successfully." LOG.info(msg) return # noqa: TRY300 @@ -79,7 +82,7 @@ def execute_with_retries(self): # type: ignore[no-untyped-def] # noqa: ANN201 self.SLEEP += self.BACKOFF try: - util.run_command(self._sh_command, debug=self._config.debug, check=True) # type: ignore[arg-type] + util.run_command(self._sh_command, debug=self._config.debug, check=True) msg = "Dependency completed successfully." LOG.info(msg) return # noqa: TRY300 @@ -87,35 +90,45 @@ def execute_with_retries(self): # type: ignore[no-untyped-def] # noqa: ANN201 exception = _exception LOG.error(str(exception)) - util.sysexit(exception.returncode) # type: ignore[union-attr] + util.sysexit(exception.returncode) @abc.abstractmethod - def execute(self, action_args=None): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN001, ANN201 - """Execute ``cmd`` and returns None.""" + def execute( + self, + action_args: list[str] | None = None, + ) -> None: # pragma: no cover + """Execute ``cmd``. + + Args: + action_args: Arguments for dependency resolvers. Unused. + """ for name, version in self._config.driver.required_collections.items(): self._config.runtime.require_collection(name, version) @property @abc.abstractmethod - def default_options(self): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN201 - """Get default CLI arguments provided to ``cmd`` as a dict. + def default_options(self) -> MutableMapping[str, str | bool]: # pragma: no cover + """Get default CLI arguments provided to ``cmd``. Returns: dict """ @property - def default_env(self): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN201 - """Get default env variables provided to ``cmd`` as a dict. + def default_env(self) -> dict[str, str]: # pragma: no cover + """Get default env variables provided to ``cmd``. Returns: dict """ - env = util.merge_dicts(os.environ, self._config.env) - return env # noqa: RET504 + # I do not know why mypy has a problem here. We are merging two dict[str, str]s into one. + # dict[str, str] should fit the typevar of merge_dicts, and all types are the same, yet + # it still complains. + env = dict(os.environ) + return util.merge_dicts(env, self._config.env) @property - def name(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def name(self) -> str: """Name of the dependency and returns a string. :returns: str @@ -123,18 +136,33 @@ def name(self): # type: ignore[no-untyped-def] # noqa: ANN201 return self._config.config["dependency"]["name"] @property - def enabled(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def enabled(self) -> bool: + """Is the dependency enabled. + + Returns: + Whether the dependency is enabled. + """ return self._config.config["dependency"]["enabled"] @property - def options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def options(self) -> MutableMapping[str, str | bool]: + """Computed dependency options. + + Returns: + Merged dictionary of default options with config options. + """ return util.merge_dicts( self.default_options, self._config.config["dependency"]["options"], ) @property - def env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def env(self) -> dict[str, str]: + """Computed environment variables. + + Returns: + Merged dictionary of default env vars with config env vars. + """ return util.merge_dicts( self.default_env, self._config.config["dependency"]["env"], diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index cf85158b1..f1f648fea 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -22,9 +22,17 @@ import logging +from typing import TYPE_CHECKING + from molecule.dependency import base +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from molecule.config import Config + + LOG = logging.getLogger(__name__) @@ -68,35 +76,52 @@ class Shell(base.Base): ``` """ - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 - """Construct Shell.""" - super().__init__(config) - self._sh_command = None + def __init__(self, config: Config) -> None: + """Construct Shell. - # self.command = config..config['dependency']['command'] + Args: + config: Molecule Config instance. + """ + super().__init__(config) + self._sh_command = "" @property - def command(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._config.config["dependency"]["command"] + def command(self) -> str: + """Return shell command. + + Returns: + Command defined in Molecule config. + """ + return self._config.config["dependency"]["command"] or "" @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_options(self) -> MutableMapping[str, str | bool]: + """Get default options for shell dependencies (none). + + Returns: + An empty dictionary. + """ return {} def bake(self) -> None: """Bake a ``shell`` command so it's ready to execute.""" self._sh_command = self.command - def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002, D102 + def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 + """Execute the dependency solver. + + Args: + action_args: Arguments for the dependency. Unused. + """ if not self.enabled: msg = "Skipping, dependency is disabled." LOG.warning(msg) return - super().execute() # type: ignore[no-untyped-call] + super().execute() - if self._sh_command is None: + if not self._sh_command: self.bake() - self.execute_with_retries() # type: ignore[no-untyped-call] + self.execute_with_retries() - def _has_command_configured(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _has_command_configured(self) -> bool: return "command" in self._config.config["dependency"] diff --git a/src/molecule/provisioner/ansible.py b/src/molecule/provisioner/ansible.py index f09b7a66a..bbd8a0cbf 100644 --- a/src/molecule/provisioner/ansible.py +++ b/src/molecule/provisioner/ansible.py @@ -41,6 +41,8 @@ if TYPE_CHECKING: from typing import Any + from molecule.types import Options + Vivify = collections.defaultdict[str, Any | "Vivify"] @@ -577,17 +579,17 @@ def config_options(self) -> dict[str, Any]: ) @property - def options(self) -> dict[str, Any]: # noqa: D102 + def options(self) -> Options: # noqa: D102 if self._config.action in ["create", "destroy"]: return self.default_options - o = self._config.config["provisioner"]["options"] + opts = self._config.config["provisioner"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: - o = util.filter_verbose_permutation(o) + opts = util.filter_verbose_permutation(opts) - return util.merge_dicts(self.default_options, o) + return util.merge_dicts(self.default_options, opts) @property def env(self) -> dict[str, str]: # noqa: D102 diff --git a/src/molecule/types.py b/src/molecule/types.py index a5ed79eb3..6004f6531 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -2,11 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, TypedDict if TYPE_CHECKING: - from typing import Any + from collections.abc import MutableMapping + from typing import Any, Literal, TypeAlias + + Options: TypeAlias = MutableMapping[str, str | bool] class DependencyData(TypedDict, total=False): @@ -23,8 +26,8 @@ class DependencyData(TypedDict, total=False): name: str command: str | None enabled: bool - options: dict[str, Any] - env: dict[str, Any] + options: Options + env: dict[str, str] class DriverOptions(TypedDict, total=False): @@ -131,8 +134,8 @@ class ProvisionerData(TypedDict, total=False): config_options: dict[str, Any] ansible_args: list[str] connection_options: dict[str, Any] - options: dict[str, Any] - env: dict[str, Any] + options: Options + env: dict[str, str] inventory: InventoryData children: dict[str, Any] playbooks: PlaybookData @@ -176,8 +179,8 @@ class VerifierData(TypedDict, total=False): name: str directory: str enabled: bool - options: dict[str, Any] - env: dict[str, Any] + options: Options + env: dict[str, str] additional_files_or_dirs: list[str] diff --git a/src/molecule/util.py b/src/molecule/util.py index f01c59fe0..dbd934da7 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -48,7 +48,7 @@ from typing import Any, AnyStr, NoReturn, TypeVar from warnings import WarningMessage - from molecule.types import CommandArgs, ConfigData, PlatformData + from molecule.types import CommandArgs, ConfigData, Options, PlatformData NestedDict = MutableMapping[str, Any] _T = TypeVar("_T", bound=NestedDict) @@ -154,7 +154,7 @@ def sysexit_with_message( def run_command( # noqa: PLR0913 - cmd: list[str], + cmd: str | list[str], env: dict[str, str] | None = None, cwd: Path | None = None, *, @@ -355,7 +355,7 @@ def instance_with_scenario_name(instance_name: str, scenario_name: str) -> str: return f"{instance_name}-{scenario_name}" -def verbose_flag(options: MutableMapping[str, Any]) -> list[str]: +def verbose_flag(options: Options) -> list[str]: """Return computed verbosity flag. Args: @@ -378,7 +378,7 @@ def verbose_flag(options: MutableMapping[str, Any]) -> list[str]: return flags -def filter_verbose_permutation(options: dict[str, Any]) -> dict[str, Any]: +def filter_verbose_permutation(options: Options) -> Options: """Clean verbose information. Args: diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index 28f4c03bb..095e10dbf 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -42,6 +42,8 @@ from pytest_mock import MockerFixture + from molecule.types import Options + def test_print_debug() -> None: # noqa: D103 expected = "DEBUG: test_title:\ntest_data\n" @@ -284,7 +286,7 @@ def test_instance_with_scenario_name() -> None: # noqa: D103 def test_verbose_flag() -> None: # noqa: D103 - options = {"verbose": True, "v": True} + options: Options = {"verbose": True, "v": True} assert util.verbose_flag(options) == ["-v"] # pylint: disable=use-implicit-booleaness-not-comparison @@ -292,7 +294,7 @@ def test_verbose_flag() -> None: # noqa: D103 def test_verbose_flag_extra_verbose() -> None: # noqa: D103 - options = {"verbose": True, "vvv": True} + options: Options = {"verbose": True, "vvv": True} assert util.verbose_flag(options) == ["-vvv"] # pylint: disable=use-implicit-booleaness-not-comparison @@ -300,7 +302,7 @@ def test_verbose_flag_extra_verbose() -> None: # noqa: D103 def test_verbose_flag_preserves_verbose_option() -> None: # noqa: D103 - options = {"verbose": True} + options: Options = {"verbose": True} # pylint: disable=use-implicit-booleaness-not-comparison assert util.verbose_flag(options) == [] @@ -308,7 +310,7 @@ def test_verbose_flag_preserves_verbose_option() -> None: # noqa: D103 def test_filter_verbose_permutation() -> None: # noqa: D103 - options = { + options: Options = { "v": True, "vv": True, "vvv": True,