Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add class for accessing Ansible configuration #10

Merged
merged 1 commit into from Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
57 changes: 56 additions & 1 deletion src/ansible_compat/config.py
@@ -1,15 +1,23 @@
"""Store configuration options as a singleton."""
import ast
import os
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 +85,52 @@ 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, config_dump: Optional[str] = None) -> None:
"""Load config dictionary."""
super().__init__()

if not config_dump:
env = os.environ.copy()
# Avoid possible ANSI garbage
env["ANSIBLE_FORCE_COLOR"] = "0"
config_dump = 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_dump, re.MULTILINE
):
key = match.groupdict()['key']
value = match.groupdict()['value']
try:
self[key] = ast.literal_eval(value)
except (NameError, SyntaxError, ValueError):
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