Skip to content

Commit

Permalink
Add AnsibleConfig class
Browse files Browse the repository at this point in the history
This enables easy access to Ansible configuration.
  • Loading branch information
ssbarnea committed Jun 29, 2021
1 parent 2c58919 commit 4078459
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Expand Up @@ -78,6 +78,7 @@ repos:
# empty args needed in order to match mypy cli behavior
args: ["--strict"]
additional_dependencies:
- .
- flaky
- packaging
- pytest
Expand All @@ -88,6 +89,7 @@ repos:
hooks:
- id: pylint
additional_dependencies:
- .
- flaky
- pytest
- PyYAML
Expand Down
2 changes: 2 additions & 0 deletions .pylintrc
Expand Up @@ -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,
23 changes: 22 additions & 1 deletion 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
```
2 changes: 2 additions & 0 deletions setup.cfg
Expand Up @@ -69,6 +69,8 @@ test =
flaky
pytest
pytest-cov
pytest-markdown
pytest-plus

[options.packages.find]
where = src
Expand Down
54 changes: 53 additions & 1 deletion src/ansible_compat/config.py
Expand Up @@ -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] = []

Expand Down Expand Up @@ -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<key>[A-Za-z0-9_]+).* = (?P<value>.*)$", 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(':')
22 changes: 22 additions & 0 deletions 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)
2 changes: 2 additions & 0 deletions tox.ini
Expand Up @@ -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 \
Expand All @@ -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
Expand Down

0 comments on commit 4078459

Please sign in to comment.