From 8de790059289ddfe85397ca5e8d39c188975e66b Mon Sep 17 00:00:00 2001 From: Kate Case Date: Wed, 20 Nov 2024 12:53:45 -0500 Subject: [PATCH] Add docstrings and type hints to provisioner (#4319) --- .config/pydoclint-baseline.txt | 57 ---- src/molecule/command/base.py | 4 +- src/molecule/command/check.py | 2 +- src/molecule/command/cleanup.py | 2 +- src/molecule/command/converge.py | 2 +- src/molecule/command/create.py | 2 +- src/molecule/command/destroy.py | 2 +- src/molecule/command/idempotence.py | 2 +- src/molecule/command/prepare.py | 2 +- src/molecule/command/side_effect.py | 2 +- src/molecule/command/syntax.py | 2 +- src/molecule/config.py | 2 +- src/molecule/provisioner/__init__.py | 2 +- src/molecule/provisioner/ansible.py | 291 +++++++++++------- src/molecule/provisioner/ansible_playbook.py | 115 ++++--- src/molecule/provisioner/ansible_playbooks.py | 166 +++++++--- src/molecule/provisioner/base.py | 34 +- src/molecule/types.py | 6 +- src/molecule/util.py | 6 +- tests/unit/provisioner/test_ansible.py | 127 ++++++-- .../unit/provisioner/test_ansible_playbook.py | 14 +- 21 files changed, 506 insertions(+), 336 deletions(-) diff --git a/.config/pydoclint-baseline.txt b/.config/pydoclint-baseline.txt index aef87e8c3..72c4d5fdc 100644 --- a/.config/pydoclint-baseline.txt +++ b/.config/pydoclint-baseline.txt @@ -95,63 +95,6 @@ src/molecule/model/schema_v3.py DOC103: Function `validate`: 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: [c: ]. DOC201: Function `validate` does not have a return section in docstring -------------------- -src/molecule/provisioner/ansible.py - DOC106: Method `Ansible.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Ansible.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC106: Method `Ansible.converge`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Ansible.converge`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Ansible.converge`: 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: [**kwargs: ]. Arguments in the docstring but not in the function signature: [kwargs: ]. - DOC101: Method `Ansible.side_effect`: Docstring contains fewer arguments than in function signature. - DOC106: Method `Ansible.side_effect`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Ansible.side_effect`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Ansible.side_effect`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [action_args: ]. - DOC101: Method `Ansible.verify`: Docstring contains fewer arguments than in function signature. - DOC106: Method `Ansible.verify`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Ansible.verify`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Ansible.verify`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [action_args: ]. - DOC201: Method `Ansible.verify` does not have a return section in docstring - DOC106: Method `Ansible._get_ansible_playbook`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `Ansible._get_ansible_playbook`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `Ansible._get_ansible_playbook`: 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: [**kwargs: ]. Arguments in the docstring but not in the function signature: [kwargs: ]. - DOC201: Method `Ansible._get_ansible_playbook` does not have a return section in docstring - DOC201: Method `Ansible._vivify` does not have a return section in docstring - DOC201: Method `Ansible._get_modules_directories` does not have a return section in docstring - DOC201: Method `Ansible._get_filter_plugins_directories` does not have a return section in docstring --------------------- -src/molecule/provisioner/ansible_playbook.py - DOC106: Method `AnsiblePlaybook.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybook.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC201: Method `AnsiblePlaybook.bake` does not have a return section in docstring - DOC101: Method `AnsiblePlaybook.execute`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsiblePlaybook.execute`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybook.execute`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsiblePlaybook.execute`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [action_args: ]. - DOC106: Method `AnsiblePlaybook.add_cli_arg`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybook.add_cli_arg`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC106: Method `AnsiblePlaybook.add_env_arg`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybook.add_env_arg`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints --------------------- -src/molecule/provisioner/ansible_playbooks.py - DOC106: Method `AnsiblePlaybooks.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybooks.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC101: Method `AnsiblePlaybooks._get_playbook`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsiblePlaybooks._get_playbook`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybooks._get_playbook`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsiblePlaybooks._get_playbook`: 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: [section: ]. - DOC201: Method `AnsiblePlaybooks._get_playbook` does not have a return section in docstring - DOC101: Method `AnsiblePlaybooks._normalize_playbook`: Docstring contains fewer arguments than in function signature. - DOC106: Method `AnsiblePlaybooks._normalize_playbook`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature - DOC107: Method `AnsiblePlaybooks._normalize_playbook`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints - DOC103: Method `AnsiblePlaybooks._normalize_playbook`: 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: [playbook: ]. - DOC201: Method `AnsiblePlaybooks._normalize_playbook` does not have a return section in docstring --------------------- -src/molecule/provisioner/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 - DOC202: Method `Base.name` has a return section in docstring, but there are no return statements or annotations --------------------- src/molecule/verifier/base.py DOC601: Class `Verifier`: 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 `Verifier`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [__metaclass__: ]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.) diff --git a/src/molecule/command/base.py b/src/molecule/command/base.py index 8d78eef81..10124c479 100644 --- a/src/molecule/command/base.py +++ b/src/molecule/command/base.py @@ -93,8 +93,8 @@ def _setup(self) -> None: """Prepare Molecule's provisioner and returns None.""" self._config.write() if self._config.provisioner is not None: - self._config.provisioner.write_config() # type: ignore[no-untyped-call] - self._config.provisioner.manage_inventory() # type: ignore[no-untyped-call] + self._config.provisioner.write_config() + self._config.provisioner.manage_inventory() def execute_cmdline_scenarios( diff --git a/src/molecule/command/check.py b/src/molecule/command/check.py index 52603c173..c879ca081 100644 --- a/src/molecule/command/check.py +++ b/src/molecule/command/check.py @@ -49,7 +49,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 action_args: Arguments for this command. Unused. """ if self._config.provisioner is not None: - self._config.provisioner.check() # type: ignore[no-untyped-call] + self._config.provisioner.check() @base.click_command_ex() diff --git a/src/molecule/command/cleanup.py b/src/molecule/command/cleanup.py index f4d11bef0..4c1241bed 100644 --- a/src/molecule/command/cleanup.py +++ b/src/molecule/command/cleanup.py @@ -51,7 +51,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 LOG.warning(msg) return - self._config.provisioner.cleanup() # type: ignore[no-untyped-call] + self._config.provisioner.cleanup() @base.click_command_ex() diff --git a/src/molecule/command/converge.py b/src/molecule/command/converge.py index 566c42410..7526b5d14 100644 --- a/src/molecule/command/converge.py +++ b/src/molecule/command/converge.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.provisioner: - self._config.provisioner.converge() # type: ignore[no-untyped-call] + self._config.provisioner.converge() self._config.state.change_state("converged", value=True) diff --git a/src/molecule/command/create.py b/src/molecule/command/create.py index 6edd87edd..5f15cbf34 100644 --- a/src/molecule/command/create.py +++ b/src/molecule/command/create.py @@ -55,7 +55,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 return if self._config.provisioner: - self._config.provisioner.create() # type: ignore[no-untyped-call] + self._config.provisioner.create() self._config.state.change_state("created", value=True) diff --git a/src/molecule/command/destroy.py b/src/molecule/command/destroy.py index 84adc6569..438a06eb4 100644 --- a/src/molecule/command/destroy.py +++ b/src/molecule/command/destroy.py @@ -56,7 +56,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 return if self._config.provisioner: - self._config.provisioner.destroy() # type: ignore[no-untyped-call] + self._config.provisioner.destroy() self._config.state.reset() diff --git a/src/molecule/command/idempotence.py b/src/molecule/command/idempotence.py index a97ec4b91..d8f0e9a23 100644 --- a/src/molecule/command/idempotence.py +++ b/src/molecule/command/idempotence.py @@ -57,7 +57,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 util.sysexit_with_message(msg) if self._config.provisioner: - output = self._config.provisioner.converge() # type: ignore[no-untyped-call] + output = self._config.provisioner.converge() idempotent = self._is_idempotent(output) if idempotent: diff --git a/src/molecule/command/prepare.py b/src/molecule/command/prepare.py index 3342f1c05..2047bc578 100644 --- a/src/molecule/command/prepare.py +++ b/src/molecule/command/prepare.py @@ -108,7 +108,7 @@ def execute(self, action_args: list[str] | None = None) -> None: # noqa: ARG002 LOG.warning(msg) return - self._config.provisioner.prepare() # type: ignore[no-untyped-call] + self._config.provisioner.prepare() self._config.state.change_state("prepared", value=True) diff --git a/src/molecule/command/side_effect.py b/src/molecule/command/side_effect.py index cb7edbd08..6cf50ffb5 100644 --- a/src/molecule/command/side_effect.py +++ b/src/molecule/command/side_effect.py @@ -54,7 +54,7 @@ def execute(self, action_args: list[str] | None = None) -> None: LOG.warning(msg) return - self._config.provisioner.side_effect(action_args) # type: ignore[no-untyped-call] + self._config.provisioner.side_effect(action_args) @base.click_command_ex() diff --git a/src/molecule/command/syntax.py b/src/molecule/command/syntax.py index cad3df406..8f33572f5 100644 --- a/src/molecule/command/syntax.py +++ b/src/molecule/command/syntax.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.provisioner: - self._config.provisioner.syntax() # type: ignore[no-untyped-call] + self._config.provisioner.syntax() @base.click_command_ex() diff --git a/src/molecule/config.py b/src/molecule/config.py index 3006fd151..5ce932c81 100644 --- a/src/molecule/config.py +++ b/src/molecule/config.py @@ -135,7 +135,7 @@ def write(self) -> None: # noqa: D102 @property def ansible_collections_path( self, - ) -> Literal["ANSIBLE_COLLECTIONS_PATH", "ANSIBLE_COLLECTIONS_PATHS"]: + ) -> str: """Return collection path variable for current version of Ansible.""" # https://github.com/ansible/ansible/pull/70007 if self.runtime.version >= Version("2.10.0.dev0"): diff --git a/src/molecule/provisioner/__init__.py b/src/molecule/provisioner/__init__.py index d2583e366..6e031999e 100644 --- a/src/molecule/provisioner/__init__.py +++ b/src/molecule/provisioner/__init__.py @@ -1 +1 @@ -# D104 # noqa: D104, ERA001 +# noqa: D104 diff --git a/src/molecule/provisioner/ansible.py b/src/molecule/provisioner/ansible.py index 31656dfd4..f59c66415 100644 --- a/src/molecule/provisioner/ansible.py +++ b/src/molecule/provisioner/ansible.py @@ -1,3 +1,4 @@ +# pylint: disable=too-many-lines # Copyright (c) 2015-2018 Cisco Systems, Inc. # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -28,7 +29,7 @@ import shutil from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING from ansible_compat.ports import cached_property @@ -37,6 +38,12 @@ from molecule.provisioner import ansible_playbook, ansible_playbooks, base +if TYPE_CHECKING: + from typing import Any + + Vivify = collections.defaultdict[str, Any | "Vivify"] + + LOG = logging.getLogger(__name__) @@ -414,17 +421,13 @@ class Ansible(base.Base): ``` """ - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # pylint: disable=useless-parent-delegation # noqa: ANN001 - """Initialize a new ansible class and returns None. - - Args: - config: An instance of a Molecule config. - """ - super().__init__(config) - @property def default_config_options(self) -> dict[str, Any]: - """Provide Default options to construct ansible.cfg and returns a dict.""" + """Provide default options to construct ansible.cfg. + + Returns: + Default config options. + """ return { "defaults": { "ansible_managed": "Ansible managed: Do NOT edit this file manually!", @@ -442,20 +445,30 @@ def default_config_options(self) -> dict[str, Any]: } @property - def default_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - d = {"skip-tags": "molecule-notest,notest"} + def default_options(self) -> dict[str, str | bool]: + """Provide default options. + + Returns: + Default options. + """ + d: dict[str, str | bool] = {"skip-tags": "molecule-notest,notest"} if self._config.action == "idempotence": - d["skip-tags"] += ",molecule-idempotence-notest" + d["skip-tags"] += ",molecule-idempotence-notest" # type: ignore[assignment, operator] if self._config.debug: - d["vvv"] = True # type: ignore[assignment] - d["diff"] = True # type: ignore[assignment] + d["vvv"] = True + d["diff"] = True return d @property - def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def default_env(self) -> dict[str, str]: + """Provide default environment variables. + + Returns: + Default set of environment variables. + """ # Finds if the current project is part of an ansible_collections hierarchy collection_indicator = "ansible_collections" # isolating test environment by injects ephemeral scenario directory on @@ -517,10 +530,10 @@ def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 list(map(util.abs_path, os.environ["ANSIBLE_ROLES_PATH"].split(":"))), ) - env = util.merge_dicts( - os.environ, + env = util.merge_dicts( # type: ignore[type-var] + dict(os.environ), { - "ANSIBLE_CONFIG": self._config.provisioner.config_file, + "ANSIBLE_CONFIG": self.config_file, "ANSIBLE_ROLES_PATH": ":".join(roles_path_list), self._config.ansible_collections_path: ":".join(collections_path_list), "ANSIBLE_LIBRARY": ":".join(self._get_modules_directories()), @@ -529,27 +542,42 @@ def default_env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 ), }, ) - env = util.merge_dicts(env, self._config.env) + env = util.merge_dicts(env, self._config.env) # type: ignore[type-var] return env # noqa: RET504 @property - def name(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def name(self) -> str: + """Provisioner name. + + Returns: + The provisioner name. + """ return self._config.config["provisioner"]["name"] @property - def ansible_args(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def ansible_args(self) -> list[str]: + """Provisioner ansible args. + + Returns: + The provisioner ansible_args. + """ return self._config.config["provisioner"]["ansible_args"] @property - def config_options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def config_options(self) -> dict[str, Any]: + """Provisioner config options. + + Returns: + The provisioner config options. + """ return util.merge_dicts( self.default_config_options, self._config.config["provisioner"]["config_options"], ) @property - def options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def options(self) -> dict[str, Any]: # noqa: D102 if self._config.action in ["create", "destroy"]: return self.default_options @@ -562,7 +590,7 @@ def options(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 return util.merge_dicts(self.default_options, o) @property - def env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def env(self) -> dict[str, str]: # noqa: D102 default_env = self.default_env env = self._config.config["provisioner"]["env"].copy() # ensure that all keys and values are strings @@ -572,13 +600,13 @@ def env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 filter_plugins_path = default_env["ANSIBLE_FILTER_PLUGINS"] try: - path = self._absolute_path_for(env, "ANSIBLE_LIBRARY") # type: ignore[no-untyped-call] + path = self._absolute_path_for(env, "ANSIBLE_LIBRARY") library_path = f"{library_path}:{path}" except KeyError: pass try: - path = self._absolute_path_for(env, "ANSIBLE_FILTER_PLUGINS") # type: ignore[no-untyped-call] + path = self._absolute_path_for(env, "ANSIBLE_FILTER_PLUGINS") filter_plugins_path = f"{filter_plugins_path}:{path}" except KeyError: pass @@ -589,23 +617,23 @@ def env(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 return util.merge_dicts(default_env, env) @property - def hosts(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def hosts(self) -> dict[str, str]: # noqa: D102 return self._config.config["provisioner"]["inventory"]["hosts"] @property - def host_vars(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def host_vars(self) -> dict[str, str]: # noqa: D102 return self._config.config["provisioner"]["inventory"]["host_vars"] @property - def group_vars(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def group_vars(self) -> dict[str, str]: # noqa: D102 return self._config.config["provisioner"]["inventory"]["group_vars"] @property - def links(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def links(self) -> dict[str, str]: # noqa: D102 return self._config.config["provisioner"]["inventory"]["links"] @property - def inventory(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def inventory(self) -> dict[str, str]: """Create an inventory structure and returns a dict. ``` yaml @@ -628,11 +656,11 @@ def inventory(self): # type: ignore[no-untyped-def] # noqa: ANN201 ansible_connection: docker ``` """ - dd = self._vivify() # type: ignore[no-untyped-call] + dd = self._vivify() for platform in self._config.platforms.instances: for group in platform.get("groups", ["ungrouped"]): instance_name = platform["name"] - connection_options = self.connection_options(instance_name) # type: ignore[no-untyped-call] + connection_options = self.connection_options(instance_name) molecule_vars = { "molecule_file": "{{ lookup('env', 'MOLECULE_FILE') }}", "molecule_ephemeral_directory": "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}", # noqa: E501 @@ -655,139 +683,162 @@ def inventory(self): # type: ignore[no-untyped-def] # noqa: ANN201 for child_group in platform.get("children", []): dd[group]["children"][child_group]["hosts"][instance_name] = connection_options - return self._default_to_regular(dd) # type: ignore[no-untyped-call] + return self._default_to_regular(dd) @property - def inventory_directory(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def inventory_directory(self) -> str: # noqa: D102 return self._config.scenario.inventory_directory @property - def inventory_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return os.path.join(self.inventory_directory, "ansible_inventory.yml") # noqa: PTH118 + def inventory_file(self) -> str: # noqa: D102 + return str(Path(self.inventory_directory, "ansible_inventory.yml")) @property - def config_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return os.path.join( # noqa: PTH118 - self._config.scenario.ephemeral_directory, - "ansible.cfg", + def config_file(self) -> str: # noqa: D102 + return str( + Path( + self._config.scenario.ephemeral_directory, + "ansible.cfg", + ), ) @cached_property - def playbooks(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 + def playbooks(self) -> ansible_playbooks.AnsiblePlaybooks: # noqa: D102 return ansible_playbooks.AnsiblePlaybooks(self._config) @property - def directory(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return os.path.join( # noqa: PTH118 - os.path.dirname(__file__), # noqa: PTH120 - os.path.pardir, - os.path.pardir, - "molecule", - "provisioner", - "ansible", - ) + def directory(self) -> str: # noqa: D102 + return str(Path(__file__).parent.parent.parent / "molecule" / "provisioner" / "ansible") - def cleanup(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def cleanup(self) -> None: """Execute `ansible-playbook` against the cleanup playbook and returns None.""" - pb = self._get_ansible_playbook(self.playbooks.cleanup) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(self.playbooks.cleanup) pb.execute() - def connection_options(self, instance_name): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D102 - d = self._config.driver.ansible_connection_options(instance_name) + def connection_options( + self, + instance_name: str, + ) -> dict[str, Any]: + """Computed connection options combining instance and provisioner options. + + Args: + instance_name: The instance to base the connection options on. + + Returns: + The combined connection options. + """ + d = self._config.driver.ansible_connection_options(instance_name) # type: ignore[no-untyped-call] return util.merge_dicts( d, self._config.config["provisioner"]["connection_options"], ) - def check(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def check(self) -> None: """Execute ``ansible-playbook`` against the converge playbook with the ``--check`` flag.""" - pb = self._get_ansible_playbook(self.playbooks.converge) # type: ignore[no-untyped-call] - pb.add_cli_arg("check", True) # noqa: FBT003 + pb = self._get_ansible_playbook(self.playbooks.converge) + pb.add_cli_arg("check", value=True) pb.execute() - def converge(self, playbook=None, **kwargs): # type: ignore[no-untyped-def] # noqa: ANN001, ANN003, ANN201 + def converge(self, playbook: str = "", **kwargs: object) -> str: """Execute ``ansible-playbook`` against the converge playbook. unless specified otherwise. Args: playbook: An optional string containing an absolute path to a playbook. - kwargs: An optional keyword arguments. + **kwargs: An optional keyword arguments. Returns: str: The output from the ``ansible-playbook`` command. """ - pb = self._get_ansible_playbook(playbook or self.playbooks.converge, **kwargs) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(playbook or self.playbooks.converge, **kwargs) # type: ignore[arg-type] return pb.execute() - def destroy(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def destroy(self) -> None: """Execute ``ansible-playbook`` against the destroy playbook and returns None.""" - pb = self._get_ansible_playbook(self.playbooks.destroy) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(self.playbooks.destroy) pb.execute() - def side_effect(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201 - """Execute ``ansible-playbook`` against the side_effect playbook and returns None.""" + def side_effect(self, action_args: list[str] | None = None) -> None: + """Execute ``ansible-playbook`` against the side_effect playbook. + + Args: + action_args: Arguments to pass to the side_effect playbook. + """ + playbooks = [] if action_args: playbooks = [ - self._get_ansible_playbook(self._config.provisioner.abs_path(playbook)) # type: ignore[no-untyped-call] - for playbook in action_args + self._get_ansible_playbook(self.abs_path(playbook)) for playbook in action_args ] - else: - playbooks = [self._get_ansible_playbook(self.playbooks.side_effect)] # type: ignore[no-untyped-call] + playbooks = [self._get_ansible_playbook(self.playbooks.side_effect)] for pb in playbooks: pb.execute() - def create(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def create(self) -> None: """Execute ``ansible-playbook`` against the create playbook and returns None.""" - pb = self._get_ansible_playbook(self.playbooks.create) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(self.playbooks.create) pb.execute() - def prepare(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def prepare(self) -> None: """Execute ``ansible-playbook`` against the prepare playbook and returns None.""" - pb = self._get_ansible_playbook(self.playbooks.prepare) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(self.playbooks.prepare) pb.execute() - def syntax(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def syntax(self) -> None: """Execute `ansible-playbook` against the converge playbook with the -syntax-check flag.""" - pb = self._get_ansible_playbook(self.playbooks.converge) # type: ignore[no-untyped-call] - pb.add_cli_arg("syntax-check", True) # noqa: FBT003 + pb = self._get_ansible_playbook(self.playbooks.converge) + pb.add_cli_arg("syntax-check", value=True) pb.execute() - def verify(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201 - """Execute ``ansible-playbook`` against the verify playbook and returns None.""" + def verify(self, action_args: list[str] | None = None) -> None: + """Execute ``ansible-playbook`` against the verify playbook. + + Args: + action_args: Arguments to pass on to the verify playbook. + """ + playbooks = [] if action_args: - playbooks = [self._config.provisioner.abs_path(playbook) for playbook in action_args] - else: + playbooks = [self.abs_path(playbook) for playbook in action_args] + elif self.playbooks.verify: playbooks = [self.playbooks.verify] if not playbooks: LOG.warning("Skipping, verify playbook not configured.") return for playbook in playbooks: # Get ansible playbooks for `verify` instead of `provision` - pb = self._get_ansible_playbook(playbook, verify=True) # type: ignore[no-untyped-call] + pb = self._get_ansible_playbook(playbook, verify=True) pb.execute() - def write_config(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def write_config(self) -> None: """Write the provisioner's config file to disk and returns None.""" template = util.render_template( - self._get_config_template(), # type: ignore[no-untyped-call] + self._get_config_template(), config_options=self.config_options, ) util.write_file(self.config_file, template) - def manage_inventory(self): # type: ignore[no-untyped-def] # noqa: ANN201 + def manage_inventory(self) -> None: """Manage inventory for Ansible and returns None.""" - self._write_inventory() # type: ignore[no-untyped-call] - self._remove_vars() # type: ignore[no-untyped-call] + self._write_inventory() + self._remove_vars() if not self.links: - self._add_or_update_vars() # type: ignore[no-untyped-call] + self._add_or_update_vars() else: - self._link_or_update_vars() # type: ignore[no-untyped-call] + self._link_or_update_vars() - def abs_path(self, path: str) -> str | None: # noqa: D102 - return util.abs_path(os.path.join(self._config.scenario.directory, path)) # noqa: PTH118 + def abs_path(self, path: str | Path) -> str: + """Return absolute scenario-adjacent path. - def _add_or_update_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 + Args: + path: Scenario-adjacent relative path. + + Returns: + Absolute path. + """ + path = Path(self._config.scenario.directory) / path + return str(util.abs_path(path)) + + def _add_or_update_vars(self) -> None: """Create host and/or group vars and returns None.""" # Create the hosts extra inventory source (only if not empty) hosts_file = os.path.join(self.inventory_directory, "hosts") # noqa: PTH118 @@ -814,13 +865,13 @@ def _add_or_update_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 path = target_vars_directory / target util.write_file(path, util.safe_dump(target_var_content)) - def _write_inventory(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _write_inventory(self) -> None: """Write the provisioner's inventory file to disk and returns None.""" - self._verify_inventory() # type: ignore[no-untyped-call] + self._verify_inventory() util.write_file(self.inventory_file, util.safe_dump(self.inventory)) - def _remove_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _remove_vars(self) -> None: """Remove hosts/host_vars/group_vars and returns None.""" for name in ("hosts", "group_vars", "host_vars"): d = os.path.join(self.inventory_directory, name) # noqa: PTH118 @@ -829,7 +880,7 @@ def _remove_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 elif os.path.isdir(d): # noqa: PTH112 shutil.rmtree(d) - def _link_or_update_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _link_or_update_vars(self) -> None: """Create or updates the symlink to group_vars and returns None.""" for d, source in self.links.items(): target = os.path.join(self.inventory_directory, d) # noqa: PTH118 @@ -850,29 +901,37 @@ def _link_or_update_vars(self): # type: ignore[no-untyped-def] # noqa: ANN202 LOG.debug(msg) os.symlink(source, target) - def _get_ansible_playbook(self, playbook, verify=False, **kwargs): # type: ignore[no-untyped-def] # noqa: ANN001, ANN003, ANN202, FBT002 + def _get_ansible_playbook( + self, + playbook: str | None, + verify: bool = False, # noqa: FBT001, FBT002 + **kwargs: object, + ) -> ansible_playbook.AnsiblePlaybook: """Get an instance of AnsiblePlaybook and returns it. Args: playbook: A string containing an absolute path to a provisioner's playbook. verify: An optional bool to toggle the Playbook mode between provision and verify. False: provision; True: verify. Default is False. - kwargs: An optional keyword arguments. + **kwargs: An optional keyword arguments. + + Returns: + An AnsiblePlaybook object. """ return ansible_playbook.AnsiblePlaybook( playbook, self._config, - verify, + verify=verify, **kwargs, ) - def _verify_inventory(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _verify_inventory(self) -> None: """Verify the inventory is valid and returns None.""" if not self.inventory: msg = "Instances missing from the 'platform' section of molecule.yml." util.sysexit_with_message(msg) - def _get_config_template(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _get_config_template(self) -> str: """Return a config template string. Returns: @@ -887,17 +946,20 @@ def _get_config_template(self): # type: ignore[no-untyped-def] # noqa: ANN202 {% endfor -%} """.strip() - def _vivify(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _vivify(self) -> Vivify: """Return an autovivification default dict. - Return: - dict + Returns: + A defaultdict whose default value is other defaultdict objects (and so on). """ return collections.defaultdict(self._vivify) - def _default_to_regular(self, d): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 + def _default_to_regular( + self, + d: dict[str, Any] | collections.defaultdict[str, Any], + ) -> dict[str, Any]: if isinstance(d, collections.defaultdict): - d = {k: self._default_to_regular(v) for k, v in d.items()} # type: ignore[no-untyped-call] + d = {k: self._default_to_regular(v) for k, v in d.items()} return d @@ -908,6 +970,9 @@ def _get_modules_directories(self) -> list[str]: """Return list of ansible module includes directories. Adds modules directory from molecule and its plugins. + + Returns: + List of module includes directories. """ paths: list[str | None] = [] if os.environ.get("ANSIBLE_LIBRARY"): @@ -946,11 +1011,15 @@ def _get_modules_directories(self) -> list[str]: return [path for path in paths if path is not None] - def _get_filter_plugin_directory(self): # type: ignore[no-untyped-def] # noqa: ANN202 + def _get_filter_plugin_directory(self) -> str: return util.abs_path(os.path.join(self._get_plugin_directory(), "filter")) # noqa: PTH118 def _get_filter_plugins_directories(self) -> list[str]: - """Return list of ansible filter plugins includes directories.""" + """Return list of ansible filter plugins includes directories. + + Returns: + List of filter includes directories. + """ paths: list[str | None] = [] if os.environ.get("ANSIBLE_FILTER_PLUGINS"): paths = list( @@ -959,7 +1028,7 @@ def _get_filter_plugins_directories(self) -> list[str]: paths.extend( [ - self._get_filter_plugin_directory(), # type: ignore[no-untyped-call] + self._get_filter_plugin_directory(), util.abs_path( os.path.join( # noqa: PTH118 self._config.scenario.ephemeral_directory, @@ -988,5 +1057,5 @@ def _get_filter_plugins_directories(self) -> list[str]: return [path for path in paths if path is not None] - def _absolute_path_for(self, env, key): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 - return ":".join([self.abs_path(p) for p in env[key].split(":")]) # type: ignore[misc] + def _absolute_path_for(self, env: dict[str, str], key: str) -> str: + return ":".join([self.abs_path(p) for p in env[key].split(":")]) diff --git a/src/molecule/provisioner/ansible_playbook.py b/src/molecule/provisioner/ansible_playbook.py index 4d6dce369..bac897604 100644 --- a/src/molecule/provisioner/ansible_playbook.py +++ b/src/molecule/provisioner/ansible_playbook.py @@ -24,18 +24,30 @@ import shlex import warnings +from typing import TYPE_CHECKING + from molecule import util from molecule.api import MoleculeRuntimeWarning +if TYPE_CHECKING: + from molecule.config import Config + + LOG = logging.getLogger(__name__) class AnsiblePlaybook: """Provisioner Playbook.""" - def __init__(self, playbook, config, verify=False) -> None: # type: ignore[no-untyped-def] # noqa: ANN001, FBT002 - """Set up the requirements to execute ``ansible-playbook`` and returns None. + def __init__( + self, + playbook: str | None, + config: Config, + *, + verify: bool = False, + ) -> None: + """Set up the requirements to execute ``ansible-playbook``. Args: playbook: A string containing the path to the playbook. @@ -43,74 +55,79 @@ def __init__(self, playbook, config, verify=False) -> None: # type: ignore[no-u verify: An optional bool to toggle the Playbook mode between provision and verify. False provision; True: verify. Default is False. """ - self._ansible_command = None + self._ansible_command: list[str] = [] self._playbook = playbook self._config = config - self._cli = {} # type: ignore[var-annotated] + self._cli: dict[str, str | bool] = {} + self._env: dict[str, str] = {} if verify: self._env = util.merge_dicts( self._config.verifier.env, self._config.config["verifier"]["env"], ) - else: + elif self._config.provisioner: self._env = self._config.provisioner.env - def bake(self): # type: ignore[no-untyped-def] # noqa: ANN201 - """Bake an ``ansible-playbook`` command so it's ready to execute and returns ``None``.""" + def bake(self) -> None: + """Bake an ``ansible-playbook`` command so it's ready to execute.""" if not self._playbook: return - # Pass a directory as inventory to let Ansible merge the multiple - # inventory sources located under - self.add_cli_arg("inventory", self._config.provisioner.inventory_directory) # type: ignore[no-untyped-call] - options = util.merge_dicts(self._config.provisioner.options, self._cli) - verbose_flag = util.verbose_flag(options) - if self._playbook != self._config.provisioner.playbooks.converge: # noqa: SIM102 - if options.get("become"): - del options["become"] - - # We do not pass user-specified Ansible arguments to the create and - # destroy invocations because playbooks involved in those two - # operations are not always provided by end users. And in those cases, - # custom Ansible arguments can break the creation and destruction - # processes. - # - # If users need to modify the creation of deletion, they can supply - # custom playbooks and specify them in the scenario configuration. - if self._config.action not in ["create", "destroy"]: - ansible_args = list(self._config.provisioner.ansible_args) + list( - self._config.ansible_args, - ) - else: - ansible_args = [] - - self._ansible_command = [ # type: ignore[assignment] - "ansible-playbook", - *util.dict2args(options), - *util.bool2args(verbose_flag), - *ansible_args, - self._playbook, # must always go last - ] + if self._config.provisioner: + # Pass a directory as inventory to let Ansible merge the multiple + # inventory sources located under + self.add_cli_arg("inventory", self._config.provisioner.inventory_directory) + options = util.merge_dicts(self._config.provisioner.options, self._cli) + verbose_flag = util.verbose_flag(options) + if self._playbook != self._config.provisioner.playbooks.converge: # noqa: SIM102 + if options.get("become"): + del options["become"] + + # We do not pass user-specified Ansible arguments to the create and + # destroy invocations because playbooks involved in those two + # operations are not always provided by end users. And in those cases, + # custom Ansible arguments can break the creation and destruction + # processes. + # + # If users need to modify the creation of deletion, they can supply + # custom playbooks and specify them in the scenario configuration. + if self._config.action not in ["create", "destroy"]: + ansible_args = list(self._config.provisioner.ansible_args) + list( + self._config.ansible_args, + ) + else: + ansible_args = [] + + self._ansible_command = [ + "ansible-playbook", + *util.dict2args(options), + *util.bool2args(verbose_flag), + *ansible_args, + self._playbook, # must always go last + ] + + def execute(self, action_args: list[str] | None = None) -> str: # noqa: ARG002 + """Execute ``ansible-playbook``. - def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, ARG002 - """Execute ``ansible-playbook`` and returns a string. + Args: + action_args: Arguments to forward to the action. Unused. Returns: - str + Output from ansible-playbook. """ - if self._ansible_command is None: - self.bake() # type: ignore[no-untyped-call] + if not self._ansible_command: + self.bake() if not self._playbook: LOG.warning("Skipping, %s action has no playbook.", self._config.action) - return None + return "" with warnings.catch_warnings(record=True) as warns: warnings.filterwarnings("default", category=MoleculeRuntimeWarning) self._config.driver.sanity_checks() cwd = self._config.scenario_path result = util.run_command( - cmd=self._ansible_command, # type: ignore[arg-type] + cmd=self._ansible_command, env=self._env, debug=self._config.debug, cwd=cwd, @@ -127,8 +144,8 @@ def execute(self, action_args=None): # type: ignore[no-untyped-def] # noqa: AN return result.stdout - def add_cli_arg(self, name, value): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201 - """Add argument to CLI passed to ansible-playbook and returns None. + def add_cli_arg(self, name: str, value: str | bool) -> None: + """Add argument to CLI passed to ansible-playbook. Args: name: A string containing the name of argument to be added. @@ -137,8 +154,8 @@ def add_cli_arg(self, name, value): # type: ignore[no-untyped-def] # noqa: ANN if value: self._cli[name] = value - def add_env_arg(self, name, value): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201 - """Add argument to environment passed to ansible-playbook and returns None. + def add_env_arg(self, name: str, value: str) -> None: + """Add argument to environment passed to ansible-playbook. Args: name: A string containing the name of argument to be added. diff --git a/src/molecule/provisioner/ansible_playbooks.py b/src/molecule/provisioner/ansible_playbooks.py index a5c2a8c5f..dd794bcd1 100644 --- a/src/molecule/provisioner/ansible_playbooks.py +++ b/src/molecule/provisioner/ansible_playbooks.py @@ -23,17 +23,36 @@ import logging import os +from pathlib import Path +from typing import TYPE_CHECKING + from molecule import util +if TYPE_CHECKING: + from typing import Literal + + from molecule.config import Config + + Section = Literal[ + "cleanup", + "create", + "converge", + "destroy", + "prepare", + "side_effect", + "verify", + ] + + LOG = logging.getLogger(__name__) class AnsiblePlaybooks: """A class to act as a module to namespace playbook properties.""" - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 - """Initialize a new namespace class and returns None. + def __init__(self, config: Config) -> None: + """Initialize a new namespace class. Args: config: An instance of a Molecule config. @@ -41,61 +60,108 @@ def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN self._config = config @property - def cleanup(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("cleanup") # type: ignore[no-untyped-call] + def cleanup(self) -> str | None: + """Get the cleanup playbook path. + + Returns: + Path to cleanup.yml. + """ + return self._get_playbook("cleanup") @property - def create(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("create") # type: ignore[no-untyped-call] + def create(self) -> str | None: + """Get the create playbook path. + + Returns: + Path to create.yml. + """ + return self._get_playbook("create") @property - def converge(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("converge") # type: ignore[no-untyped-call] + def converge(self) -> str | None: + """Get the converge playbook path. + + Returns: + Path to converge.yml. + """ + return self._get_playbook("converge") @property - def destroy(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("destroy") # type: ignore[no-untyped-call] + def destroy(self) -> str | None: + """Get the destroy playbook path. + + Returns: + Path to destroy.yml. + """ + return self._get_playbook("destroy") @property - def prepare(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("prepare") # type: ignore[no-untyped-call] + def prepare(self) -> str | None: + """Get the prepare playbook path. + + Returns: + Path to prepare.yml. + """ + return self._get_playbook("prepare") @property - def side_effect(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("side_effect") # type: ignore[no-untyped-call] + def side_effect(self) -> str | None: + """Get the side_effect playbook path. + + Returns: + Path to side_effect.yml. + """ + return self._get_playbook("side_effect") @property - def verify(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102 - return self._get_playbook("verify") # type: ignore[no-untyped-call] + def verify(self) -> str | None: + """Get the verify playbook path. - def _get_playbook_directory(self): # type: ignore[no-untyped-def] # noqa: ANN202 - return util.abs_path( - os.path.join(self._config.provisioner.directory, "playbooks"), # noqa: PTH118 - ) + Returns: + Path to verify.yml. + """ + return self._get_playbook("verify") + + def _get_playbook_directory(self) -> Path: + if self._config.provisioner: + return util.abs_path( + Path(self._config.provisioner.directory, "playbooks"), + ) + return Path() - def _get_playbook(self, section): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 + def _get_playbook(self, section: Section) -> str | None: """Return path to playbook or None if playbook is not needed. Return None when there is no playbook configured and when action is considered skippable. + + Args: + section: Named section to retrieve playbook for. + + Returns: + The playbook path, or none if one is not needed. """ c = self._config.config - driver_dict = c["provisioner"]["playbooks"].get(self._config.driver.name) + driver_dict: dict[Section, str | None] | None = c["provisioner"]["playbooks"].get( # type: ignore[assignment] + self._config.driver.name, + ) - playbook = c["provisioner"]["playbooks"][section] + playbook: str | None = c["provisioner"]["playbooks"][section] if driver_dict: try: playbook = driver_dict[section] except Exception as exc: LOG.exception(exc) # noqa: TRY401 - if playbook is not None: + if self._config.provisioner and playbook is not None: playbook = self._config.provisioner.abs_path(playbook) - playbook = self._normalize_playbook(playbook) # type: ignore[no-untyped-call] + if playbook: + playbook = self._normalize_playbook(playbook) - if os.path.exists(playbook): # noqa: PTH110 - return playbook - if os.path.exists(self._get_bundled_driver_playbook(section)): # type: ignore[no-untyped-call] # noqa: PTH110 - return self._get_bundled_driver_playbook(section) # type: ignore[no-untyped-call] + if os.path.exists(playbook): # noqa: PTH110 + return playbook + + if os.path.exists(self._get_bundled_driver_playbook(section)): # noqa: PTH110 + return self._get_bundled_driver_playbook(section) if section not in [ # these playbooks can be considered optional "prepare", @@ -108,45 +174,49 @@ def _get_playbook(self, section): # type: ignore[no-untyped-def] # noqa: ANN00 return playbook return None - def _get_bundled_driver_playbook(self, section): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 - path = self._config.driver.get_playbook(section) + def _get_bundled_driver_playbook(self, section: Section) -> str: + path = self._config.driver.get_playbook(section) # type: ignore[no-untyped-call] if path: - return path + return path # type: ignore[no-any-return] - path = os.path.join( # noqa: PTH118 - self._get_playbook_directory(), # type: ignore[no-untyped-call] + path = Path( + self._get_playbook_directory(), self._config.driver.name, self._config.config["provisioner"]["playbooks"][section], ) - if os.path.exists(path): # noqa: PTH110 - return path - path = os.path.join( # noqa: PTH118 + if path.exists(): + return str(path) + path = Path( self._config.driver._path, # noqa: SLF001 "playbooks", self._config.config["provisioner"]["playbooks"][section], ) - return path # noqa: RET504 + return str(path) - def _normalize_playbook(self, playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 + def _normalize_playbook(self, playbook: str) -> str: """Return current filename to use for a playbook by allowing fallbacks. Currently used to deprecate use of playbook.yml in favour of converge.yml + + Args: + playbook: Playbook path to alter. + + Returns: + Normalized playbook path. """ - if not playbook or os.path.isfile(playbook): # noqa: PTH113 + play_path = Path(playbook) + if not playbook or play_path.is_file(): return playbook pb_rename_map = {"converge.yml": "playbook.yml"} - basename = os.path.basename(playbook) # noqa: PTH119 + basename = play_path.name if basename in pb_rename_map: - fb_playbook = os.path.join( # noqa: PTH118 - os.path.dirname(playbook), # noqa: PTH120 - pb_rename_map[basename], - ) - if os.path.isfile(fb_playbook): # noqa: PTH113 + fb_playbook = play_path.parent / pb_rename_map[basename] + if fb_playbook.is_file(): LOG.warning( "%s was deprecated, rename it to %s", - pb_rename_map[basename], + fb_playbook.name, basename, ) - playbook = fb_playbook + playbook = str(fb_playbook) return playbook diff --git a/src/molecule/provisioner/base.py b/src/molecule/provisioner/base.py index 4f35de101..26aeb1d12 100644 --- a/src/molecule/provisioner/base.py +++ b/src/molecule/provisioner/base.py @@ -22,13 +22,17 @@ import abc +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from molecule.config import Config -class Base: - """Provisioner Base Class.""" - __metaclass__ = abc.ABCMeta +class Base(abc.ABC): + """Provisioner Base Class.""" - def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001 + def __init__(self, config: Config) -> None: """Initialize code for all :ref:`Provisioner` classes. Args: @@ -38,27 +42,27 @@ def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN @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 | bool]: # pragma: no cover + """Get default CLI arguments provided to ``cmd``. - Return: - dict + Returns: + The default CLI arguments. """ @property @abc.abstractmethod - 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``. - Return: - dict + Returns: + The default env variables. """ @property @abc.abstractmethod - def name(self): # type: ignore[no-untyped-def] # pragma: no cover # noqa: ANN201 - """Name of the provisioner and returns a string. + def name(self) -> str: # pragma: no cover + """Name of the provisioner. Returns: - str + The provisioner's name. """ diff --git a/src/molecule/types.py b/src/molecule/types.py index b66034c2a..052b9d1b2 100644 --- a/src/molecule/types.py +++ b/src/molecule/types.py @@ -61,14 +61,18 @@ class InventoryData(TypedDict): links: dict[str, str] -class PlatformData(TypedDict): +class PlatformData(TypedDict, total=False): """Platform data for a Molecule run. Attributes: name: Name of the platform. + groups: Optional list of groups. + children: Optional list of child groups. """ name: str + groups: list[str] + children: list[str] class PlaybookData(TypedDict, total=False): diff --git a/src/molecule/util.py b/src/molecule/util.py index 7ce9bc8a4..68b599ab1 100644 --- a/src/molecule/util.py +++ b/src/molecule/util.py @@ -182,13 +182,11 @@ def run_command( # noqa: PLR0913 Raises: CalledProcessError: If return code is nonzero and check is True. """ - args = cmd - if debug: print_environment_vars(env) result = app.runtime.run( - args=args, + args=cmd, env=env, cwd=cwd, tee=True, @@ -234,7 +232,7 @@ def os_walk( yield str(filename) -def render_template(template: str, **kwargs: str) -> str: +def render_template(template: str, **kwargs: str | dict[str, str]) -> str: """Render a jinaj2 template. Args: diff --git a/tests/unit/provisioner/test_ansible.py b/tests/unit/provisioner/test_ansible.py index 37aa14c64..3e87db4f2 100644 --- a/tests/unit/provisioner/test_ansible.py +++ b/tests/unit/provisioner/test_ansible.py @@ -33,12 +33,13 @@ if TYPE_CHECKING: from pathlib import Path + from unittest.mock import MagicMock, Mock from pytest_mock import MockerFixture @pytest.fixture -def _patched_ansible_playbook(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 +def _patched_ansible_playbook(mocker: MockerFixture) -> MagicMock: m = mocker.patch("molecule.provisioner.ansible_playbook.AnsiblePlaybook") m.return_value.execute.return_value = b"patched-ansible-playbook-stdout" @@ -46,17 +47,17 @@ def _patched_ansible_playbook(mocker): # type: ignore[no-untyped-def] # noqa: @pytest.fixture -def _patched_write_inventory(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 +def _patched_write_inventory(mocker: MockerFixture) -> MagicMock: return mocker.patch("molecule.provisioner.ansible.Ansible._write_inventory") @pytest.fixture -def _patched_remove_vars(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 +def _patched_remove_vars(mocker: MockerFixture) -> MagicMock: return mocker.patch("molecule.provisioner.ansible.Ansible._remove_vars") @pytest.fixture -def _patched_link_or_update_vars(mocker): # type: ignore[no-untyped-def] # noqa: ANN001, ANN202 +def _patched_link_or_update_vars(mocker: MockerFixture) -> MagicMock: return mocker.patch("molecule.provisioner.ansible.Ansible._link_or_update_vars") @@ -352,128 +353,182 @@ def test_playbooks_side_effect_property(instance): # type: ignore[no-untyped-de assert instance.playbooks.side_effect is None -def test_check(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_check( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.check() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.converge, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with( "check", - True, # noqa: FBT003 + value=True, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_converge(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_converge( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: result = instance.converge() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.converge, instance._config, - False, # noqa: FBT003 + verify=False, ) # NOTE(retr0h): This is not the true return type. This is a mock return # which didn't go through str.decode(). - assert result == b"patched-ansible-playbook-stdout" + assert result == b"patched-ansible-playbook-stdout" # type: ignore[comparison-overlap] _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_converge_with_playbook( # type: ignore[no-untyped-def] # noqa: ANN201, D103 - instance, # noqa: ANN001 +def test_converge_with_playbook( # noqa: D103 + instance: ansible.Ansible, mocker: MockerFixture, # noqa: ARG001 - _patched_ansible_playbook, # noqa: ANN001, PT019 -): + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: result = instance.converge("playbook") _patched_ansible_playbook.assert_called_once_with( "playbook", instance._config, - False, # noqa: FBT003 + verify=False, ) # NOTE(retr0h): This is not the true return type. This is a mock return # which didn't go through str.decode(). - assert result == b"patched-ansible-playbook-stdout" + assert result == b"patched-ansible-playbook-stdout" # type: ignore[comparison-overlap] _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_cleanup(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_cleanup( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.cleanup() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.cleanup, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_destroy(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_destroy( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.destroy() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.destroy, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_side_effect(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_side_effect( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.side_effect() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.side_effect, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_create(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_create( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.create() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.create, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_prepare(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_prepare( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.prepare() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.prepare, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_syntax(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_syntax( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.syntax() + assert instance._config.provisioner + _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.converge, instance._config, - False, # noqa: FBT003 + verify=False, ) _patched_ansible_playbook.return_value.add_cli_arg.assert_called_once_with( "syntax-check", - True, # noqa: FBT003 + value=True, ) _patched_ansible_playbook.return_value.execute.assert_called_once_with() -def test_verify(instance, mocker: MockerFixture, _patched_ansible_playbook): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, ARG001, D103 +def test_verify( # noqa: D103 + instance: ansible.Ansible, + mocker: MockerFixture, # noqa: ARG001 + _patched_ansible_playbook: Mock, # noqa: PT019 +) -> None: instance.verify() + assert instance._config.provisioner + if instance._config.provisioner.playbooks.verify: _patched_ansible_playbook.assert_called_once_with( instance._config.provisioner.playbooks.verify, @@ -679,7 +734,7 @@ def test_link_vars(instance): # type: ignore[no-untyped-def] # noqa: ANN001, A assert os.path.lexists(target_host_vars) -def test_link_vars_raises_when_source_not_found(instance, caplog): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 +def test_link_vars_raises_when_source_not_found(instance: ansible.Ansible, caplog): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 c = instance._config.config c["provisioner"]["inventory"]["links"] = {"foo": "../bar"} @@ -761,7 +816,7 @@ def test_get_modules_directories_default( assert paths[4] == "/usr/share/ansible/plugins/modules" -def test_get_modules_directories_single_ansible_library(instance, monkeypatch): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 +def test_get_modules_directories_single_ansible_library(instance: ansible.Ansible, monkeypatch): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 monkeypatch.setenv("ANSIBLE_LIBRARY", "/abs/path/lib") paths = instance._get_modules_directories() @@ -770,7 +825,7 @@ def test_get_modules_directories_single_ansible_library(instance, monkeypatch): assert paths[0] == "/abs/path/lib" -def test_get_modules_directories_multi_ansible_library(instance, monkeypatch): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 +def test_get_modules_directories_multi_ansible_library(instance: ansible.Ansible, monkeypatch): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 monkeypatch.setenv("ANSIBLE_LIBRARY", "relpath/lib:/abs/path/lib") paths = instance._get_modules_directories() @@ -788,7 +843,11 @@ def test_get_filter_plugin_directory(instance): # type: ignore[no-untyped-def] assert x == parts[-5:] -def test_get_filter_plugins_directories_default(instance, monkeypatch, test_cache_path: Path): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, D103 +def test_get_filter_plugins_directories_default( # noqa: D103 + instance: ansible.Ansible, + monkeypatch: pytest.MonkeyPatch, + test_cache_path: Path, +) -> None: monkeypatch.delenv("ANSIBLE_FILTER_PLUGINS", raising=False) paths = instance._get_filter_plugins_directories() diff --git a/tests/unit/provisioner/test_ansible_playbook.py b/tests/unit/provisioner/test_ansible_playbook.py index 0b773dd5c..4a981d7a9 100644 --- a/tests/unit/provisioner/test_ansible_playbook.py +++ b/tests/unit/provisioner/test_ansible_playbook.py @@ -28,7 +28,7 @@ @pytest.fixture -def _instance(config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN202 +def _instance(config_instance: config.Config) -> ansible_playbook.AnsiblePlaybook: _instance = ansible_playbook.AnsiblePlaybook("playbook", config_instance) return _instance # noqa: RET504 @@ -54,7 +54,11 @@ def _provisioner_verifier_section_data(): # type: ignore[no-untyped-def] # noq @pytest.fixture def _instance_for_verifier_env(config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN202 - _instance = ansible_playbook.AnsiblePlaybook("playbook", config_instance, True) # noqa: FBT003 + _instance = ansible_playbook.AnsiblePlaybook( + "playbook", + config_instance, + verify=True, + ) return _instance # noqa: RET504 @@ -90,8 +94,10 @@ def _inventory_directory(_instance): # type: ignore[no-untyped-def] # noqa: AN return _instance._config.provisioner.inventory_directory -def test_ansible_command_private_member(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103 - assert _instance._ansible_command is None +def test_ansible_command_private_member( # noqa: D103 + _instance: ansible_playbook.AnsiblePlaybook, # noqa: PT019 +) -> None: + assert _instance._ansible_command == [] def test_ansible_playbook_private_member(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103