Skip to content

Commit 7862974

Browse files
authored
Enable access to available plugins (#277)
* Enable access to available plugins Related: ansible/ansible-lint#3481
1 parent 34c1459 commit 7862974

File tree

7 files changed

+124
-10
lines changed

7 files changed

+124
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,4 @@ dmypy.json
128128
.pyre/
129129
.test-results
130130
*.lcov
131+
ansible_collections

.vscode/settings.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
"[python]": {
66
"editor.codeActionsOnSave": {
77
"source.fixAll": true,
8-
"source.fixAll.ruff": true,
9-
"source.organizeImports": false,
10-
"source.organizeImports.ruff": true
8+
"source.organizeImports": false
119
}
1210
},
1311
"editor.formatOnSave": true,

ansible.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[defaults]
2+
# isolate testing of ansible-compat from user local setup
3+
collections_path = .

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ disable = [
112112
"import-error",
113113
# already covered by ruff which is faster
114114
"too-many-arguments", # PLR0913
115+
"raise-missing-from",
115116
# Temporary disable duplicate detection we remove old code from prerun
116117
"duplicate-code",
117118
]

src/ansible_compat/runtime.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
import tempfile
1111
import warnings
1212
from collections import OrderedDict
13-
from dataclasses import dataclass
13+
from dataclasses import dataclass, field
1414
from pathlib import Path
15-
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
15+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union, no_type_check
1616

1717
import subprocess_tee
1818
from packaging.version import Version
1919

2020
from ansible_compat.config import (
2121
AnsibleConfig,
2222
ansible_collections_path,
23+
ansible_version,
2324
parse_ansible_version,
2425
)
2526
from ansible_compat.constants import (
@@ -73,6 +74,71 @@ def __init__(self, version: str) -> None:
7374
super().__init__(version)
7475

7576

77+
@dataclass
78+
class Plugins: # pylint: disable=too-many-instance-attributes
79+
"""Dataclass to access installed Ansible plugins, uses ansible-doc to retrieve them."""
80+
81+
runtime: "Runtime"
82+
become: dict[str, str] = field(init=False)
83+
cache: dict[str, str] = field(init=False)
84+
callback: dict[str, str] = field(init=False)
85+
cliconf: dict[str, str] = field(init=False)
86+
connection: dict[str, str] = field(init=False)
87+
httpapi: dict[str, str] = field(init=False)
88+
inventory: dict[str, str] = field(init=False)
89+
lookup: dict[str, str] = field(init=False)
90+
netconf: dict[str, str] = field(init=False)
91+
shell: dict[str, str] = field(init=False)
92+
vars: dict[str, str] = field(init=False) # noqa: A003
93+
module: dict[str, str] = field(init=False)
94+
strategy: dict[str, str] = field(init=False)
95+
test: dict[str, str] = field(init=False)
96+
filter: dict[str, str] = field(init=False) # noqa: A003
97+
role: dict[str, str] = field(init=False)
98+
keyword: dict[str, str] = field(init=False)
99+
100+
@no_type_check
101+
def __getattribute__(self, attr: str): # noqa: ANN204
102+
"""Get attribute."""
103+
if attr in {
104+
"become",
105+
"cache",
106+
"callback",
107+
"cliconf",
108+
"connection",
109+
"httpapi",
110+
"inventory",
111+
"lookup",
112+
"netconf",
113+
"shell",
114+
"vars",
115+
"module",
116+
"strategy",
117+
"test",
118+
"filter",
119+
"role",
120+
"keyword",
121+
}:
122+
try:
123+
result = super().__getattribute__(attr)
124+
except AttributeError as exc:
125+
if ansible_version() < Version("2.14") and attr in {"filter", "test"}:
126+
msg = "Ansible version below 2.14 does not support retrieving filter and test plugins."
127+
raise RuntimeError(msg) from exc
128+
proc = self.runtime.run(
129+
["ansible-doc", "--json", "-l", "-t", attr],
130+
)
131+
data = json.loads(proc.stdout)
132+
if not isinstance(data, dict): # pragma: no cover
133+
msg = "Unexpected output from ansible-doc"
134+
raise AnsibleCompatError(msg) from exc
135+
result = data
136+
else:
137+
result = super().__getattribute__(attr)
138+
139+
return result
140+
141+
76142
# pylint: disable=too-many-instance-attributes
77143
class Runtime:
78144
"""Ansible Runtime manager."""
@@ -83,6 +149,7 @@ class Runtime:
83149
# Used to track if we have already initialized the Ansible runtime as attempts
84150
# to do it multiple tilmes will cause runtime warnings from within ansible-core
85151
initialized: bool = False
152+
plugins: Plugins
86153

87154
def __init__(
88155
self,
@@ -119,6 +186,7 @@ def __init__(
119186
self.isolated = isolated
120187
self.max_retries = max_retries
121188
self.environ = environ or os.environ.copy()
189+
self.plugins = Plugins(runtime=self)
122190
# Reduce noise from paramiko, unless user already defined PYTHONWARNINGS
123191
# paramiko/transport.py:236: CryptographyDeprecationWarning: Blowfish has been deprecated
124192
# https://github.com/paramiko/paramiko/issues/2038

test/test_runtime.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from packaging.version import Version
1616
from pytest_mock import MockerFixture
1717

18+
from ansible_compat.config import ansible_version
1819
from ansible_compat.constants import INVALID_PREREQUISITES_RC
1920
from ansible_compat.errors import (
2021
AnsibleCommandError,
@@ -747,3 +748,44 @@ def test_runtime_exec_env(runtime: Runtime) -> None:
747748
runtime.environ["FOO"] = "bar"
748749
result = runtime.run(["printenv", "FOO"])
749750
assert result.stdout.rstrip() == "bar"
751+
752+
753+
def test_runtime_plugins(runtime: Runtime) -> None:
754+
"""Tests ability to access detected plugins."""
755+
assert len(runtime.plugins.cliconf) == 0
756+
# ansible.netcommon.restconf might be in httpapi
757+
assert isinstance(runtime.plugins.httpapi, dict)
758+
# "ansible.netcommon.default" might be in runtime.plugins.netconf
759+
assert isinstance(runtime.plugins.netconf, dict)
760+
assert isinstance(runtime.plugins.role, dict)
761+
assert "become" in runtime.plugins.keyword
762+
763+
if ansible_version() < Version("2.14.0"):
764+
assert "sudo" in runtime.plugins.become
765+
assert "memory" in runtime.plugins.cache
766+
assert "default" in runtime.plugins.callback
767+
assert "local" in runtime.plugins.connection
768+
assert "ini" in runtime.plugins.inventory
769+
assert "env" in runtime.plugins.lookup
770+
assert "sh" in runtime.plugins.shell
771+
assert "host_group_vars" in runtime.plugins.vars
772+
assert "file" in runtime.plugins.module
773+
assert "free" in runtime.plugins.strategy
774+
# ansible-doc below 2.14 does not support listing 'test' and 'filter' types:
775+
with pytest.raises(RuntimeError):
776+
assert "is_abs" in runtime.plugins.test
777+
with pytest.raises(RuntimeError):
778+
assert "bool" in runtime.plugins.filter
779+
else:
780+
assert "ansible.builtin.sudo" in runtime.plugins.become
781+
assert "ansible.builtin.memory" in runtime.plugins.cache
782+
assert "ansible.builtin.default" in runtime.plugins.callback
783+
assert "ansible.builtin.local" in runtime.plugins.connection
784+
assert "ansible.builtin.ini" in runtime.plugins.inventory
785+
assert "ansible.builtin.env" in runtime.plugins.lookup
786+
assert "ansible.builtin.sh" in runtime.plugins.shell
787+
assert "ansible.builtin.host_group_vars" in runtime.plugins.vars
788+
assert "ansible.builtin.file" in runtime.plugins.module
789+
assert "ansible.builtin.free" in runtime.plugins.strategy
790+
assert "ansible.builtin.is_abs" in runtime.plugins.test
791+
assert "ansible.builtin.bool" in runtime.plugins.filter

tox.ini

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ envlist =
88
py{39,310,311}{,-devel,-ansible212,-ansible213,-ansible214,-ansible215}
99
isolated_build = true
1010
skip_missing_interpreters = True
11-
skipsdist = true
1211

1312
[testenv]
1413
description =
@@ -28,7 +27,8 @@ deps =
2827
devel: ansible-core @ git+https://github.com/ansible/ansible.git # GPLv3+
2928
# avoid installing ansible-core on -devel envs:
3029
!devel: ansible-core
31-
--editable .[test]
30+
extras =
31+
test
3232

3333
commands =
3434
sh -c "ansible --version | head -n 1"
@@ -68,12 +68,14 @@ setenv =
6868
PIP_DISABLE_PIP_VERSION_CHECK = 1
6969
PIP_CONSTRAINT = {toxinidir}/requirements.txt
7070
PRE_COMMIT_COLOR = always
71-
PYTEST_REQPASS = 81
71+
PYTEST_REQPASS = 82
7272
FORCE_COLOR = 1
7373
allowlist_externals =
7474
ansible
7575
git
7676
sh
77+
# https://tox.wiki/en/latest/upgrading.html#editable-mode
78+
package = editable
7779

7880
[testenv:lint]
7981
description = Run all linters
@@ -140,6 +142,5 @@ deps =
140142
description = Build docs
141143
commands =
142144
mkdocs {posargs:build} --strict
143-
deps =
144-
--editable .[docs]
145+
extras = docs
145146
passenv = *

0 commit comments

Comments
 (0)