From 407845940528f77d5582ab4974ac9750ce932e03 Mon Sep 17 00:00:00 2001 From: Sorin Sbarnea Date: Mon, 28 Jun 2021 13:53:18 +0100 Subject: [PATCH] Add AnsibleConfig class This enables easy access to Ansible configuration. --- .pre-commit-config.yaml | 2 ++ .pylintrc | 2 ++ README.md | 23 ++++++++++++++- setup.cfg | 2 ++ src/ansible_compat/config.py | 54 +++++++++++++++++++++++++++++++++++- test/test_config.py | 22 +++++++++++++++ tox.ini | 2 ++ 7 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 test/test_config.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62e89ac7..3547fa05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,6 +78,7 @@ repos: # empty args needed in order to match mypy cli behavior args: ["--strict"] additional_dependencies: + - . - flaky - packaging - pytest @@ -88,6 +89,7 @@ repos: hooks: - id: pylint additional_dependencies: + - . - flaky - pytest - PyYAML diff --git a/.pylintrc b/.pylintrc index 774d482f..a144fabe 100644 --- a/.pylintrc +++ b/.pylintrc @@ -17,3 +17,5 @@ preferred-modules = disable = # On purpose disabled as we rely on black line-too-long, + # local imports do not work well with pre-commit hook + import-error, diff --git a/README.md b/README.md index 70b73eba..909a1b6c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # ansible-compat -A python package containing functions that help interacting with various versions of Ansible + +A python package contains functions that facilitates working with various +versions of Ansible, 2.9 and newer. + +## Access to Ansible configuration + +As you may not want to parse `ansible-config dump` yourself, you +can make use of a simple python class that facilitates access to +it, using python data types. + +```python +from ansible_compat.config import AnsibleConfig + + +def test_example_config(): + cfg = AnsibleConfig() + assert isinstance(cfg.ACTION_WARNINGS, bool) + # you can also use lowercase: + assert isinstance(cfg.action_warnings, bool) + # you can also use it as dictionary + assert cfg['action_warnings'] == cfg.action_warnings +``` diff --git a/setup.cfg b/setup.cfg index ff66df4b..a496f5fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,6 +69,8 @@ test = flaky pytest pytest-cov + pytest-markdown + pytest-plus [options.packages.find] where = src diff --git a/src/ansible_compat/config.py b/src/ansible_compat/config.py index d846a847..4eccab26 100644 --- a/src/ansible_compat/config.py +++ b/src/ansible_compat/config.py @@ -3,13 +3,20 @@ import re import subprocess import sys +from collections import UserDict from functools import lru_cache -from typing import List, Optional, Tuple +from typing import TYPE_CHECKING, List, Optional, Tuple from packaging.version import Version from ansible_compat.constants import ANSIBLE_MISSING_RC +if TYPE_CHECKING: + # https://github.com/PyCQA/pylint/issues/3285 + _UserDict = UserDict[str, object] # pylint: disable=unsubscriptable-object) +else: + _UserDict = UserDict + # Used to store collection list paths (with mock paths if needed) collection_list: List[str] = [] @@ -77,5 +84,50 @@ def ansible_version(version: str = "") -> Version: return Version(version) +class AnsibleConfig(_UserDict): # pylint: disable=too-many-ancestors + """Interface to query Ansible configuration. + + This should allow user to access everything provided by `ansible-config dump` without having to parse the data himself. + """ + + _aliases = { + 'COLLECTIONS_PATHS': 'COLLECTIONS_PATH', # 2.9 -> 2.10+ + 'COLLECTIONS_PATH': 'COLLECTIONS_PATHS', # 2.10+ -> 2.9 + } + + def __init__(self) -> None: + """Load config dictionary.""" + super().__init__() + env = os.environ.copy() + # Avoid possible ANSI garbage + env["ANSIBLE_FORCE_COLOR"] = "0" + + config = subprocess.check_output( + ["ansible-config", "dump"], universal_newlines=True, env=env + ) + for match in re.finditer( + r"^(?P[A-Za-z0-9_]+).* = (?P.*)$", config, re.MULTILINE + ): + key = match.groupdict()['key'] + value = match.groupdict()['value'] + try: + self[key] = eval(value) # pylint: disable=eval-used + except (NameError, SyntaxError): + self[key] = value + + def __getattr__(self, attr_name: str) -> object: + """Allow access of config options as attributes.""" + name = attr_name.upper() + if name in self.data: + return self.data[name] + if name in self._aliases: + return self.data[self._aliases[name]] + raise AttributeError(attr_name) + + def __getitem__(self, name: str) -> object: + """Allow access to config options using indexing.""" + return super().__getitem__(name.upper()) + + if ansible_collections_path() in os.environ: collection_list = os.environ[ansible_collections_path()].split(':') diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 00000000..87b5f6dc --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,22 @@ +"""Tests for ansible_compat.config submodule.""" +import pytest + +import ansible_compat + + +def test_config() -> None: + """Checks that config vars are loaded with their expected type.""" + config = ansible_compat.config.AnsibleConfig() + assert isinstance(config.ACTION_WARNINGS, bool) + assert isinstance(config.CACHE_PLUGIN_PREFIX, str) + assert isinstance(config.CONNECTION_FACTS_MODULES, dict) + assert config.ANSIBLE_COW_PATH is None + assert isinstance(config.NETWORK_GROUP_MODULES, list) + assert isinstance(config.DEFAULT_GATHER_TIMEOUT, int) + + # check lowercase and older name aliasing + assert isinstance(config.collections_paths, list) + assert isinstance(config.collections_path, list) + + with pytest.raises(AttributeError): + print(config.THIS_DOES_NOT_EXIST) diff --git a/tox.ini b/tox.ini index 303702b8..1ed245ff 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ commands = {envpython} -m pytest \ --junitxml "{toxworkdir}/junit.{envname}.xml" \ {posargs:\ + --no-success-flaky-report \ --cov ansible_compat \ --cov "{envsitepackagesdir}/ansible_compat" \ --cov-report term-missing:skip-covered \ @@ -54,6 +55,7 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 PIP_CONSTRAINT = {toxinidir}/constraints.txt PRE_COMMIT_COLOR = always + PYTEST_REQPASS = 28 FORCE_COLOR = 1 allowlist_externals = sh