From 035bdd96f4feb916239aa114bce16d3f4577756b Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 21 Nov 2024 15:39:43 -0500 Subject: [PATCH 01/10] Add type hints and docstrings to dependency base --- .config/pydoclint-baseline.txt | 12 ------- src/molecule/dependency/base.py | 62 ++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index 2e801fcd9..afac313cb 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -24,18 +24,6 @@ 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 diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index f8d871e3b..afdca1bce 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -26,14 +26,19 @@ import time from subprocess import CalledProcessError +from typing import TYPE_CHECKING from molecule import util +if TYPE_CHECKING: + from molecule.config import Config + + LOG = logging.getLogger(__name__) -class Base: +class Base(abc.ABC): """Dependency Base Class. Attributes: @@ -42,13 +47,11 @@ 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: @@ -57,7 +60,7 @@ def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN self._config = config self._sh_command: list[str] | None = None - 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 @@ -90,32 +93,42 @@ def execute_with_retries(self): # type: ignore[no-untyped-def] # noqa: ANN201 util.sysexit(exception.returncode) # type: ignore[union-attr] @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) -> dict[str, str]: # 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) # type: ignore[type-var] @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) -> dict[str, str]: + """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"], From 8cf3111dd3c2c7b4175b9794295e783aa4c31d14 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 21 Nov 2024 15:48:52 -0500 Subject: [PATCH 02/10] Add docstrings and type hints to shell dependency. --- .config/pydoclint-baseline.txt | 6 ---- src/molecule/dependency/__init__.py | 2 +- src/molecule/dependency/base.py | 2 +- src/molecule/dependency/shell.py | 47 +++++++++++++++++++++-------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index afac313cb..187ce6a64 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -24,12 +24,6 @@ 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/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/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/base.py b/src/molecule/dependency/base.py index afdca1bce..99027969b 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -58,7 +58,7 @@ def __init__(self, config: Config) -> None: config: An instance of a Molecule config. """ self._config = config - self._sh_command: list[str] | None = None + self._sh_command: str | None = None def execute_with_retries(self) -> None: """Run dependency downloads with retry and timed back-off.""" diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index cf85158b1..d66e794b2 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -22,9 +22,15 @@ import logging +from typing import TYPE_CHECKING + from molecule.dependency import base +if TYPE_CHECKING: + from molecule.config import Config + + LOG = logging.getLogger(__name__) @@ -68,35 +74,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: str | None = None @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) -> dict[str, str]: + """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: 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"] From 524adabc6effea0f72c42877f2cd82a6926f8a92 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 21 Nov 2024 15:54:58 -0500 Subject: [PATCH 03/10] Add docstrings and type hints to base ansiblegalaxy --- .config/pydoclint-baseline.txt | 6 --- .../dependency/ansible_galaxy/__init__.py | 51 ++++++++++++++----- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index 187ce6a64..f5e3c8dc7 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -1,9 +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.) diff --git a/src/molecule/dependency/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index f07d0f32e..b39d31ad9 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -2,12 +2,18 @@ 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 molecule.config import Config + + class AnsibleGalaxy(Base): """Galaxy is the default dependency manager. @@ -88,31 +94,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) # type: ignore[type-var] + return env @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - opts = {} # type: ignore[var-annotated] + def default_options(self) -> dict[str, str]: + """Default options across all invokers. + + Returns: + Merged dictionary of default options for all invokers. + """ + opts: dict[str, str] = {} 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) # type: ignore[type-var] return opts From 6bf2f30e7543c188d1a236b20867a1b8fb075f1c Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 21 Nov 2024 16:11:11 -0500 Subject: [PATCH 04/10] Add docstrings and type hints to ansiblegalaxybase --- .config/pydoclint-baseline.txt | 12 --- .../dependency/ansible_galaxy/__init__.py | 2 +- .../dependency/ansible_galaxy/base.py | 85 ++++++++++++++----- src/molecule/dependency/base.py | 6 +- src/molecule/dependency/shell.py | 2 +- 5 files changed, 68 insertions(+), 39 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index f5e3c8dc7..5edc2b646 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -1,15 +1,3 @@ -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.) diff --git a/src/molecule/dependency/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index b39d31ad9..b70f016b5 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -131,7 +131,7 @@ def default_env(self) -> dict[str, str]: return env @property - def default_options(self) -> dict[str, str]: + def default_options(self) -> dict[str, str | bool]: """Default options across all invokers. Returns: diff --git a/src/molecule/dependency/ansible_galaxy/base.py b/src/molecule/dependency/ansible_galaxy/base.py index 02247f0d7..9cb4fcb62 100644 --- a/src/molecule/dependency/ansible_galaxy/base.py +++ b/src/molecule/dependency/ansible_galaxy/base.py @@ -25,10 +25,16 @@ import logging import os +from typing import TYPE_CHECKING + from molecule import util from molecule.dependency import base +if TYPE_CHECKING: + from molecule.config import Config + + LOG = logging.getLogger(__name__) @@ -39,12 +45,14 @@ class AnsibleGalaxyBase(base.Base): FILTER_OPTS: Keys to remove from the dictionary returned by options(). """ - __metaclass__ = abc.ABCMeta - FILTER_OPTS = () - 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._sh_command = None @@ -52,12 +60,21 @@ def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN @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) -> dict[str, str | bool]: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ + d: dict[str, str | bool] = { "force": False, } if self._config.debug: @@ -65,13 +82,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: dict[str, str | bool], + keys: tuple[str, ...], + ) -> dict[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,7 +109,12 @@ 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 + def options(self) -> dict[str, str | bool]: + """Computed options for this dependency. + + Returns: + Merged and filtered options for this dependency. + """ o = self._config.config["dependency"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. @@ -92,13 +122,19 @@ def options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 o = util.filter_verbose_permutation(o) o = util.merge_dicts(self.default_options, o) - return self.filter_options(o, self.FILTER_OPTS) # type: ignore[no-untyped-call] + return self.filter_options(o, 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) # type: ignore[type-var] + + 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) @@ -110,26 +146,31 @@ def bake(self): # type: ignore[no-untyped-def] # noqa: ANN201 *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] + 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 + def _has_requirements_file(self) -> bool: return os.path.isfile(self.requirements_file) # noqa: PTH113 diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index 99027969b..e8f7069be 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -58,7 +58,7 @@ def __init__(self, config: Config) -> None: config: An instance of a Molecule config. """ self._config = config - self._sh_command: str | None = None + self._sh_command: str | list[str] | None = None def execute_with_retries(self) -> None: """Run dependency downloads with retry and timed back-off.""" @@ -107,7 +107,7 @@ def execute( @property @abc.abstractmethod - def default_options(self) -> dict[str, str]: # pragma: no cover + def default_options(self) -> dict[str, str | bool]: # pragma: no cover """Get default CLI arguments provided to ``cmd``. Returns: @@ -145,7 +145,7 @@ def enabled(self) -> bool: return self._config.config["dependency"]["enabled"] @property - def options(self) -> dict[str, str]: + def options(self) -> dict[str, str | bool]: """Computed dependency options. Returns: diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index d66e794b2..48ea28134 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -93,7 +93,7 @@ def command(self) -> str: return self._config.config["dependency"]["command"] or "" @property - def default_options(self) -> dict[str, str]: + def default_options(self) -> dict[str, str | bool]: """Get default options for shell dependencies (none). Returns: From 1e3039e09c09c81608fc6e981148e1f7fb656eae Mon Sep 17 00:00:00 2001 From: Kate Case Date: Thu, 21 Nov 2024 16:37:54 -0500 Subject: [PATCH 05/10] Add docstrings and type hints to ansiblegalaxy suptypes --- .../dependency/ansible_galaxy/collections.py | 24 ++++++++++++++----- .../dependency/ansible_galaxy/roles.py | 22 ++++++++++++++--- src/molecule/types.py | 11 +++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/molecule/dependency/ansible_galaxy/collections.py b/src/molecule/dependency/ansible_galaxy/collections.py index a0ac19b4f..7c99206da 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -5,21 +5,32 @@ import logging import os +from typing import TYPE_CHECKING + from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase +if TYPE_CHECKING: + from molecule.types import DependencyOptions + + LOG = logging.getLogger(__name__) class Collections(AnsibleGalaxyBase): """Collection-specific Ansible Galaxy dependency handling.""" - FILTER_OPTS = ("role-file",) # type: ignore # noqa: PGH003 + FILTER_OPTS = ("role-file",) COMMANDS = ("collection", "install") @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_options(self) -> DependencyOptions: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ general = super().default_options specific = util.merge_dicts( general, @@ -34,9 +45,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 + Returns: + Path to the requirements file for this dependency. + """ return self.options["requirements-file"] diff --git a/src/molecule/dependency/ansible_galaxy/roles.py b/src/molecule/dependency/ansible_galaxy/roles.py index 6a633213d..f7a6bed4e 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -5,21 +5,32 @@ import logging import os +from typing import TYPE_CHECKING + from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase +if TYPE_CHECKING: + from molecule.types import DependencyOptions + + LOG = logging.getLogger(__name__) class Roles(AnsibleGalaxyBase): """Role-specific Ansible Galaxy dependency handling.""" - FILTER_OPTS = ("requirements-file",) # type: ignore # noqa: PGH003 + FILTER_OPTS = ("requirements-file",) COMMANDS = ("install",) @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_options(self) -> DependencyOptions: + """Default options for this dependency. + + Returns: + Default options for this dependency. + """ general = super().default_options specific = util.merge_dicts( general, @@ -33,5 +44,10 @@ def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 return specific # noqa: RET504 @property - def requirements_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def requirements_file(self) -> str: + """Path to requirements file. + + Returns: + Path to the requirements file for this dependency. + """ return self.options["role-file"] diff --git a/src/molecule/types.py b/src/molecule/types.py index a5ed79eb3..8d80f4b75 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -9,6 +9,13 @@ from typing import Any +DependencyOptions = TypedDict( + "DependencyOptions", + {"force": bool, "requirements-file": str, "role-file": str, "vvv": bool}, + total=False, +) + + class DependencyData(TypedDict, total=False): """Molecule dependency configuration. @@ -23,8 +30,8 @@ class DependencyData(TypedDict, total=False): name: str command: str | None enabled: bool - options: dict[str, Any] - env: dict[str, Any] + options: DependencyOptions + env: dict[str, str] class DriverOptions(TypedDict, total=False): From 1bff327c87ccd13ca51bd86757701728ee2757b4 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Fri, 22 Nov 2024 14:16:59 -0500 Subject: [PATCH 06/10] Additional cleanup The typing in utils is a broken mess but should be fixable --- .../dependency/ansible_galaxy/__init__.py | 9 ++--- .../dependency/ansible_galaxy/base.py | 36 ++++++++++--------- .../dependency/ansible_galaxy/collections.py | 10 +++--- .../dependency/ansible_galaxy/roles.py | 10 +++--- src/molecule/dependency/base.py | 17 +++++---- src/molecule/dependency/shell.py | 9 ++--- src/molecule/types.py | 3 +- src/molecule/util.py | 4 +-- 8 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/molecule/dependency/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index b70f016b5..8a9df1fa6 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -12,6 +12,7 @@ if TYPE_CHECKING: from molecule.config import Config + from molecule.types import DependencyOptions class AnsibleGalaxy(Base): @@ -127,17 +128,17 @@ def default_env(self) -> dict[str, str]: """ env: dict[str, str] = {} for invoker in self.invocations: - env = util.merge_dicts(env, invoker.default_env) # type: ignore[type-var] + env = util.merge_dicts(env, invoker.default_env) return env @property - def default_options(self) -> dict[str, str | bool]: + def default_options(self) -> DependencyOptions: """Default options across all invokers. Returns: Merged dictionary of default options for all invokers. """ - opts: dict[str, str] = {} + opts: DependencyOptions = {} for invoker in self.invocations: - opts = util.merge_dicts(opts, invoker.default_options) # type: ignore[type-var] + 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 9cb4fcb62..284692eb3 100644 --- a/src/molecule/dependency/ansible_galaxy/base.py +++ b/src/molecule/dependency/ansible_galaxy/base.py @@ -25,6 +25,7 @@ import logging import os +from pathlib import Path from typing import TYPE_CHECKING from molecule import util @@ -33,6 +34,7 @@ if TYPE_CHECKING: from molecule.config import Config + from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -43,9 +45,11 @@ 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. """ - FILTER_OPTS = () + FILTER_OPTS: tuple[str, ...] = () + COMMANDS: tuple[str, ...] = () def __init__(self, config: Config) -> None: """Construct AnsibleGalaxy. @@ -54,7 +58,7 @@ def __init__(self, config: Config) -> None: config: Molecule Config instance. """ super().__init__(config) - self._sh_command = None + self._sh_command = [] self.command = "ansible-galaxy" @@ -68,13 +72,13 @@ def requirements_file(self) -> str: # cover """ @property - def default_options(self) -> dict[str, str | bool]: + def default_options(self) -> DependencyOptions: """Default options for this dependency. Returns: Default options for this dependency. """ - d: dict[str, str | bool] = { + d: DependencyOptions = { "force": False, } if self._config.debug: @@ -84,9 +88,9 @@ def default_options(self) -> dict[str, str | bool]: def filter_options( self, - opts: dict[str, str | bool], + opts: DependencyOptions, keys: tuple[str, ...], - ) -> dict[str, str | bool]: + ) -> DependencyOptions: """Filter certain keys from a dictionary. Removes all the values of ``keys`` from the dictionary ``opts``, if @@ -103,26 +107,26 @@ def filter_options( c = copy.copy(opts) for key in keys: if key in c: - del c[key] + del c[key] # type: ignore[misc] return c # NOTE(retr0h): Override the base classes' options() to handle # ``ansible-galaxy`` one-off. @property - def options(self) -> dict[str, str | bool]: + def options(self) -> DependencyOptions: """Computed options for this dependency. Returns: Merged and filtered options for this dependency. """ - o = self._config.config["dependency"]["options"] + 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) + opts = util.merge_dicts(self.default_options, opts) + return self.filter_options(opts, self.FILTER_OPTS) @property def default_env(self) -> dict[str, str]: @@ -132,7 +136,7 @@ def default_env(self) -> dict[str, str]: Default environment variables for this dependency. """ env = dict(os.environ) - return util.merge_dicts(env, self._config.env) # type: ignore[type-var] + 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.""" @@ -141,7 +145,7 @@ def bake(self) -> None: self._sh_command = [ self.command, - *self.COMMANDS, # type: ignore[attr-defined] # pylint: disable=no-member + *self.COMMANDS, *util.dict2args(options), *verbose_flag, ] @@ -163,7 +167,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 LOG.warning(msg) return - if self._sh_command is None: + if not self._sh_command: self.bake() self._setup() @@ -173,4 +177,4 @@ def _setup(self) -> None: """Prepare the system for using ``ansible-galaxy`` and returns None.""" def _has_requirements_file(self) -> bool: - return os.path.isfile(self.requirements_file) # noqa: PTH113 + 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 7c99206da..704a1afa7 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -import os +from pathlib import Path from typing import TYPE_CHECKING from molecule import util @@ -35,9 +35,11 @@ def default_options(self) -> DependencyOptions: 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", + ), ), }, ) diff --git a/src/molecule/dependency/ansible_galaxy/roles.py b/src/molecule/dependency/ansible_galaxy/roles.py index f7a6bed4e..939891685 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -import os +from pathlib import Path from typing import TYPE_CHECKING from molecule import util @@ -35,9 +35,11 @@ def default_options(self) -> DependencyOptions: 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", + ), ), }, ) diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index e8f7069be..f2a5cb5e5 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from molecule.config import Config + from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -58,14 +59,12 @@ def __init__(self, config: Config) -> None: config: An instance of a Molecule config. """ self._config = config - self._sh_command: str | list[str] | None = None + self._sh_command: list[str] = [] 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 @@ -82,7 +81,7 @@ def execute_with_retries(self) -> None: 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 @@ -90,7 +89,7 @@ def execute_with_retries(self) -> None: exception = _exception LOG.error(str(exception)) - util.sysexit(exception.returncode) # type: ignore[union-attr] + util.sysexit(exception.returncode) @abc.abstractmethod def execute( @@ -107,7 +106,7 @@ def execute( @property @abc.abstractmethod - def default_options(self) -> dict[str, str | bool]: # pragma: no cover + def default_options(self) -> DependencyOptions: # pragma: no cover """Get default CLI arguments provided to ``cmd``. Returns: @@ -125,7 +124,7 @@ def default_env(self) -> dict[str, str]: # pragma: no cover # 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) # type: ignore[type-var] + return util.merge_dicts(env, self._config.env) @property def name(self) -> str: @@ -145,7 +144,7 @@ def enabled(self) -> bool: return self._config.config["dependency"]["enabled"] @property - def options(self) -> dict[str, str | bool]: + def options(self) -> DependencyOptions: """Computed dependency options. Returns: diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index 48ea28134..990429f0e 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -29,6 +29,7 @@ if TYPE_CHECKING: from molecule.config import Config + from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -81,7 +82,7 @@ def __init__(self, config: Config) -> None: config: Molecule Config instance. """ super().__init__(config) - self._sh_command: str | None = None + self._sh_command: list[str] = [] @property def command(self) -> str: @@ -93,7 +94,7 @@ def command(self) -> str: return self._config.config["dependency"]["command"] or "" @property - def default_options(self) -> dict[str, str | bool]: + def default_options(self) -> DependencyOptions: """Get default options for shell dependencies (none). Returns: @@ -103,7 +104,7 @@ def default_options(self) -> dict[str, str | bool]: def bake(self) -> None: """Bake a ``shell`` command so it's ready to execute.""" - self._sh_command = self.command + self._sh_command = [self.command] def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 """Execute the dependency solver. @@ -117,7 +118,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 return super().execute() - if self._sh_command is None: + if not self._sh_command: self.bake() self.execute_with_retries() diff --git a/src/molecule/types.py b/src/molecule/types.py index 8d80f4b75..314c89121 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -9,9 +9,10 @@ from typing import Any +# We have to use the alternate form here because dashes are invalid in python identifiers DependencyOptions = TypedDict( "DependencyOptions", - {"force": bool, "requirements-file": str, "role-file": str, "vvv": bool}, + {"force": bool, "requirements-file": str, "role-file": str, "verbose": bool, "vvv": bool}, total=False, ) diff --git a/src/molecule/util.py b/src/molecule/util.py index f01c59fe0..2010522c7 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -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: MutableMapping[str, str | bool]) -> 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: dict[str, str | bool]) -> dict[str, str | bool]: """Clean verbose information. Args: From f83f96b4251ad0fbad59da4679c1f1357ad17f4a Mon Sep 17 00:00:00 2001 From: Kate Case Date: Mon, 2 Dec 2024 10:57:44 -0500 Subject: [PATCH 07/10] Drop DependencyOptions --- src/molecule/command/dependency.py | 2 +- .../dependency/ansible_galaxy/__init__.py | 7 +++--- .../dependency/ansible_galaxy/base.py | 17 +++++++------- .../dependency/ansible_galaxy/collections.py | 6 ++--- .../dependency/ansible_galaxy/roles.py | 6 ++--- src/molecule/dependency/base.py | 7 +++--- src/molecule/dependency/shell.py | 5 +++-- src/molecule/provisioner/ansible.py | 5 +++-- src/molecule/types.py | 22 ++++++------------- src/molecule/util.py | 6 +++-- src/molecule/verifier/testinfra.py | 2 +- 11 files changed, 42 insertions(+), 43 deletions(-) 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/ansible_galaxy/__init__.py b/src/molecule/dependency/ansible_galaxy/__init__.py index 8a9df1fa6..febbcdf39 100644 --- a/src/molecule/dependency/ansible_galaxy/__init__.py +++ b/src/molecule/dependency/ansible_galaxy/__init__.py @@ -11,8 +11,9 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping + from molecule.config import Config - from molecule.types import DependencyOptions class AnsibleGalaxy(Base): @@ -132,13 +133,13 @@ def default_env(self) -> dict[str, str]: return env @property - def default_options(self) -> DependencyOptions: + def default_options(self) -> MutableMapping[str, str | bool]: """Default options across all invokers. Returns: Merged dictionary of default options for all invokers. """ - opts: DependencyOptions = {} + opts: MutableMapping[str, str | bool] = {} for invoker in self.invocations: 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 284692eb3..23ab6a2c0 100644 --- a/src/molecule/dependency/ansible_galaxy/base.py +++ b/src/molecule/dependency/ansible_galaxy/base.py @@ -33,8 +33,9 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping + from molecule.config import Config - from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -72,13 +73,13 @@ def requirements_file(self) -> str: # cover """ @property - def default_options(self) -> DependencyOptions: + def default_options(self) -> MutableMapping[str, str | bool]: """Default options for this dependency. Returns: Default options for this dependency. """ - d: DependencyOptions = { + d: MutableMapping[str, str | bool] = { "force": False, } if self._config.debug: @@ -88,9 +89,9 @@ def default_options(self) -> DependencyOptions: def filter_options( self, - opts: DependencyOptions, + opts: MutableMapping[str, str | bool], keys: tuple[str, ...], - ) -> DependencyOptions: + ) -> MutableMapping[str, str | bool]: """Filter certain keys from a dictionary. Removes all the values of ``keys`` from the dictionary ``opts``, if @@ -107,19 +108,19 @@ def filter_options( c = copy.copy(opts) for key in keys: if key in c: - del c[key] # type: ignore[misc] + del c[key] return c # NOTE(retr0h): Override the base classes' options() to handle # ``ansible-galaxy`` one-off. @property - def options(self) -> DependencyOptions: + 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"] + opts: MutableMapping[str, str | bool] = self._config.config["dependency"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: diff --git a/src/molecule/dependency/ansible_galaxy/collections.py b/src/molecule/dependency/ansible_galaxy/collections.py index 704a1afa7..0f251f58e 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: - from molecule.types import DependencyOptions + from collections.abc import MutableMapping LOG = logging.getLogger(__name__) @@ -25,7 +25,7 @@ class Collections(AnsibleGalaxyBase): COMMANDS = ("collection", "install") @property - def default_options(self) -> DependencyOptions: + def default_options(self) -> MutableMapping[str, str | bool]: """Default options for this dependency. Returns: @@ -53,4 +53,4 @@ def requirements_file(self) -> str: Returns: Path to the requirements file for this dependency. """ - return self.options["requirements-file"] + return self.options["requirements-file"] # type: ignore[return-value] diff --git a/src/molecule/dependency/ansible_galaxy/roles.py b/src/molecule/dependency/ansible_galaxy/roles.py index 939891685..cbddc8634 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: - from molecule.types import DependencyOptions + from collections.abc import MutableMapping LOG = logging.getLogger(__name__) @@ -25,7 +25,7 @@ class Roles(AnsibleGalaxyBase): COMMANDS = ("install",) @property - def default_options(self) -> DependencyOptions: + def default_options(self) -> MutableMapping[str, str | bool]: """Default options for this dependency. Returns: @@ -52,4 +52,4 @@ def requirements_file(self) -> str: Returns: Path to the requirements file for this dependency. """ - return self.options["role-file"] + return self.options["role-file"] # type: ignore[return-value] diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index f2a5cb5e5..423cee9c6 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -32,8 +32,9 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping + from molecule.config import Config - from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -106,7 +107,7 @@ def execute( @property @abc.abstractmethod - def default_options(self) -> DependencyOptions: # pragma: no cover + def default_options(self) -> MutableMapping[str, str | bool]: # pragma: no cover """Get default CLI arguments provided to ``cmd``. Returns: @@ -144,7 +145,7 @@ def enabled(self) -> bool: return self._config.config["dependency"]["enabled"] @property - def options(self) -> DependencyOptions: + def options(self) -> MutableMapping[str, str | bool]: """Computed dependency options. Returns: diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index 990429f0e..a5a216335 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -28,8 +28,9 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping + from molecule.config import Config - from molecule.types import DependencyOptions LOG = logging.getLogger(__name__) @@ -94,7 +95,7 @@ def command(self) -> str: return self._config.config["dependency"]["command"] or "" @property - def default_options(self) -> DependencyOptions: + def default_options(self) -> MutableMapping[str, str | bool]: """Get default options for shell dependencies (none). Returns: diff --git a/src/molecule/provisioner/ansible.py b/src/molecule/provisioner/ansible.py index f09b7a66a..6cfe0b082 100644 --- a/src/molecule/provisioner/ansible.py +++ b/src/molecule/provisioner/ansible.py @@ -39,6 +39,7 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping from typing import Any Vivify = collections.defaultdict[str, Any | "Vivify"] @@ -577,11 +578,11 @@ def config_options(self) -> dict[str, Any]: ) @property - def options(self) -> dict[str, Any]: # noqa: D102 + def options(self) -> MutableMapping[str, str | bool]: # noqa: D102 if self._config.action in ["create", "destroy"]: return self.default_options - o = self._config.config["provisioner"]["options"] + o: MutableMapping[str, str | bool] = self._config.config["provisioner"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: diff --git a/src/molecule/types.py b/src/molecule/types.py index 314c89121..b3cf74c20 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -9,14 +9,6 @@ from typing import Any -# We have to use the alternate form here because dashes are invalid in python identifiers -DependencyOptions = TypedDict( - "DependencyOptions", - {"force": bool, "requirements-file": str, "role-file": str, "verbose": bool, "vvv": bool}, - total=False, -) - - class DependencyData(TypedDict, total=False): """Molecule dependency configuration. @@ -31,7 +23,7 @@ class DependencyData(TypedDict, total=False): name: str command: str | None enabled: bool - options: DependencyOptions + options: dict[str, str | bool] env: dict[str, str] @@ -136,11 +128,11 @@ class ProvisionerData(TypedDict, total=False): """ name: str - config_options: dict[str, Any] + config_options: dict[str, str | bool] ansible_args: list[str] - connection_options: dict[str, Any] - options: dict[str, Any] - env: dict[str, Any] + connection_options: dict[str, str | bool] + options: dict[str, str | bool] + env: dict[str, str] inventory: InventoryData children: dict[str, Any] playbooks: PlaybookData @@ -184,8 +176,8 @@ class VerifierData(TypedDict, total=False): name: str directory: str enabled: bool - options: dict[str, Any] - env: dict[str, Any] + options: dict[str, str | bool] + env: dict[str, str] additional_files_or_dirs: list[str] diff --git a/src/molecule/util.py b/src/molecule/util.py index 2010522c7..ade8dde0a 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: - from collections.abc import Generator, Iterable, MutableMapping + from collections.abc import Generator, Iterable, Mapping, MutableMapping from io import TextIOWrapper from typing import Any, AnyStr, NoReturn, TypeVar from warnings import WarningMessage @@ -378,7 +378,9 @@ def verbose_flag(options: MutableMapping[str, str | bool]) -> list[str]: return flags -def filter_verbose_permutation(options: dict[str, str | bool]) -> dict[str, str | bool]: +def filter_verbose_permutation( + options: Mapping[str, str | bool], +) -> MutableMapping[str, str | bool]: """Clean verbose information. Args: diff --git a/src/molecule/verifier/testinfra.py b/src/molecule/verifier/testinfra.py index d41978650..1bf137d0d 100644 --- a/src/molecule/verifier/testinfra.py +++ b/src/molecule/verifier/testinfra.py @@ -148,7 +148,7 @@ def options(self) -> MutableMapping[str, str | bool]: Returns: The combined dictionary of default options and those specified in the config. """ - o = self._config.config["verifier"]["options"] + o: MutableMapping[str, str | bool] = self._config.config["verifier"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: From b2f7444ae5ac0c941360c2f550b4f1c7103dce6d Mon Sep 17 00:00:00 2001 From: Kate Case Date: Mon, 2 Dec 2024 11:00:13 -0500 Subject: [PATCH 08/10] Fix missing docstring attributes --- .config/pydoclint-baseline.txt | 8 -------- src/molecule/dependency/ansible_galaxy/collections.py | 7 ++++++- src/molecule/dependency/ansible_galaxy/roles.py | 7 ++++++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index 5edc2b646..02be942b5 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -1,11 +1,3 @@ -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.) --------------------- 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/dependency/ansible_galaxy/collections.py b/src/molecule/dependency/ansible_galaxy/collections.py index 0f251f58e..4a2e43c2d 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -19,7 +19,12 @@ class Collections(AnsibleGalaxyBase): - """Collection-specific Ansible Galaxy dependency handling.""" + """Collection-specific Ansible Galaxy dependency handling. + + 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") diff --git a/src/molecule/dependency/ansible_galaxy/roles.py b/src/molecule/dependency/ansible_galaxy/roles.py index cbddc8634..ec2e67b59 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -19,7 +19,12 @@ class Roles(AnsibleGalaxyBase): - """Role-specific Ansible Galaxy dependency handling.""" + """Role-specific Ansible Galaxy dependency handling. + + 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",) From 15f3f6ea14ff005caf1f8ecd9fa9312bffcc02e3 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Mon, 2 Dec 2024 15:15:58 -0500 Subject: [PATCH 09/10] Clean up types --- src/molecule/dependency/ansible_galaxy/base.py | 2 +- .../dependency/ansible_galaxy/collections.py | 4 ++-- src/molecule/dependency/ansible_galaxy/roles.py | 4 ++-- src/molecule/provisioner/ansible.py | 11 ++++++----- src/molecule/types.py | 17 ++++++++++------- src/molecule/util.py | 10 ++++------ src/molecule/verifier/testinfra.py | 2 +- tests/unit/test_util.py | 10 ++++++---- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/molecule/dependency/ansible_galaxy/base.py b/src/molecule/dependency/ansible_galaxy/base.py index 23ab6a2c0..04244f5bf 100644 --- a/src/molecule/dependency/ansible_galaxy/base.py +++ b/src/molecule/dependency/ansible_galaxy/base.py @@ -120,7 +120,7 @@ def options(self) -> MutableMapping[str, str | bool]: Returns: Merged and filtered options for this dependency. """ - opts: MutableMapping[str, str | bool] = self._config.config["dependency"]["options"] + opts = self._config.config["dependency"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: diff --git a/src/molecule/dependency/ansible_galaxy/collections.py b/src/molecule/dependency/ansible_galaxy/collections.py index 4a2e43c2d..0fb584117 100644 --- a/src/molecule/dependency/ansible_galaxy/collections.py +++ b/src/molecule/dependency/ansible_galaxy/collections.py @@ -5,7 +5,7 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase @@ -58,4 +58,4 @@ def requirements_file(self) -> str: Returns: Path to the requirements file for this dependency. """ - return self.options["requirements-file"] # type: ignore[return-value] + 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 ec2e67b59..49d105c44 100644 --- a/src/molecule/dependency/ansible_galaxy/roles.py +++ b/src/molecule/dependency/ansible_galaxy/roles.py @@ -5,7 +5,7 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from molecule import util from molecule.dependency.ansible_galaxy.base import AnsibleGalaxyBase @@ -57,4 +57,4 @@ def requirements_file(self) -> str: Returns: Path to the requirements file for this dependency. """ - return self.options["role-file"] # type: ignore[return-value] + return cast(str, self.options["role-file"]) diff --git a/src/molecule/provisioner/ansible.py b/src/molecule/provisioner/ansible.py index 6cfe0b082..bbd8a0cbf 100644 --- a/src/molecule/provisioner/ansible.py +++ b/src/molecule/provisioner/ansible.py @@ -39,9 +39,10 @@ if TYPE_CHECKING: - from collections.abc import MutableMapping from typing import Any + from molecule.types import Options + Vivify = collections.defaultdict[str, Any | "Vivify"] @@ -578,17 +579,17 @@ def config_options(self) -> dict[str, Any]: ) @property - def options(self) -> MutableMapping[str, str | bool]: # noqa: D102 + def options(self) -> Options: # noqa: D102 if self._config.action in ["create", "destroy"]: return self.default_options - o: MutableMapping[str, str | bool] = 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 b3cf74c20..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,7 +26,7 @@ class DependencyData(TypedDict, total=False): name: str command: str | None enabled: bool - options: dict[str, str | bool] + options: Options env: dict[str, str] @@ -128,10 +131,10 @@ class ProvisionerData(TypedDict, total=False): """ name: str - config_options: dict[str, str | bool] + config_options: dict[str, Any] ansible_args: list[str] - connection_options: dict[str, str | bool] - options: dict[str, str | bool] + connection_options: dict[str, Any] + options: Options env: dict[str, str] inventory: InventoryData children: dict[str, Any] @@ -176,7 +179,7 @@ class VerifierData(TypedDict, total=False): name: str directory: str enabled: bool - options: dict[str, str | bool] + 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 ade8dde0a..844c012b8 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -43,12 +43,12 @@ if TYPE_CHECKING: - from collections.abc import Generator, Iterable, Mapping, MutableMapping + from collections.abc import Generator, Iterable, MutableMapping from io import TextIOWrapper 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) @@ -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, str | bool]) -> list[str]: +def verbose_flag(options: Options) -> list[str]: """Return computed verbosity flag. Args: @@ -378,9 +378,7 @@ def verbose_flag(options: MutableMapping[str, str | bool]) -> list[str]: return flags -def filter_verbose_permutation( - options: Mapping[str, str | bool], -) -> MutableMapping[str, str | bool]: +def filter_verbose_permutation(options: Options) -> Options: """Clean verbose information. Args: diff --git a/src/molecule/verifier/testinfra.py b/src/molecule/verifier/testinfra.py index 1bf137d0d..d41978650 100644 --- a/src/molecule/verifier/testinfra.py +++ b/src/molecule/verifier/testinfra.py @@ -148,7 +148,7 @@ def options(self) -> MutableMapping[str, str | bool]: Returns: The combined dictionary of default options and those specified in the config. """ - o: MutableMapping[str, str | bool] = self._config.config["verifier"]["options"] + o = self._config.config["verifier"]["options"] # NOTE(retr0h): Remove verbose options added by the user while in # debug. if self._config.debug: 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, From ce8ec2191ae9c6e17a529b35bc5947a7b24a023b Mon Sep 17 00:00:00 2001 From: Kate Case Date: Mon, 2 Dec 2024 15:49:12 -0500 Subject: [PATCH 10/10] Fix shell breakage --- src/molecule/dependency/base.py | 2 +- src/molecule/dependency/shell.py | 4 ++-- src/molecule/util.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/molecule/dependency/base.py b/src/molecule/dependency/base.py index 423cee9c6..96530dbec 100644 --- a/src/molecule/dependency/base.py +++ b/src/molecule/dependency/base.py @@ -60,7 +60,7 @@ def __init__(self, config: Config) -> None: config: An instance of a Molecule config. """ self._config = config - self._sh_command: list[str] = [] + self._sh_command: str | list[str] = [] def execute_with_retries(self) -> None: """Run dependency downloads with retry and timed back-off.""" diff --git a/src/molecule/dependency/shell.py b/src/molecule/dependency/shell.py index a5a216335..f1f648fea 100644 --- a/src/molecule/dependency/shell.py +++ b/src/molecule/dependency/shell.py @@ -83,7 +83,7 @@ def __init__(self, config: Config) -> None: config: Molecule Config instance. """ super().__init__(config) - self._sh_command: list[str] = [] + self._sh_command = "" @property def command(self) -> str: @@ -105,7 +105,7 @@ def default_options(self) -> MutableMapping[str, str | bool]: def bake(self) -> None: """Bake a ``shell`` command so it's ready to execute.""" - self._sh_command = [self.command] + self._sh_command = self.command def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 """Execute the dependency solver. diff --git a/src/molecule/util.py b/src/molecule/util.py index 844c012b8..dbd934da7 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -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, *,