Skip to content

Commit 13ec09f

Browse files
Adds experimental support for Ansible 12 (#56)
While most things appear to work fine, there are some things that currently don't work, like passing `extra_vars`. Ansible also appears to be more strict about what it allows to be executed so some commands may need to be modified in order to run. It also looks like there were some backwards incompatible changes to the results structure if there was an error, so you may need to update your error handling code. This also drops support for Ansible 6 and Python 3.8 Co-authored-by: David Salvisberg <[email protected]>
1 parent 5b8c036 commit 13ec09f

File tree

12 files changed

+76
-41
lines changed

12 files changed

+76
-41
lines changed

.github/workflows/python-pr.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ubuntu-latest
99
strategy:
1010
matrix:
11-
python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
11+
python-version: [3.9, '3.10', '3.11', '3.12', '3.13']
1212

1313
steps:
1414
- uses: actions/checkout@v4

.github/workflows/python-tox.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
runs-on: ubuntu-latest
99
strategy:
1010
matrix:
11-
python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13']
11+
python-version: [3.9, '3.10', '3.11', '3.12', '3.13']
1212

1313
steps:
1414
- uses: actions/checkout@v4

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
---------
33

4+
- Adds experimental support for Ansible 12
5+
Some things are still broken like `extra_vars`
6+
[Daverball]
7+
8+
- Drops support for Ansible 6 and Python 3.8
9+
410
0.21.0 (2025-06-12)
511
~~~~~~~~~~~~~~~~~~~
612

README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ The official way to use Ansible from Python is documented here:
3434
Compatibility
3535
-------------
3636

37-
* Python 3.8+
38-
* Ansible 6+
37+
* Python 3.9+
38+
* Ansible 7+
3939
* Mitogen 0.3.7+
4040

4141
Support for older releases is kept only if possible. New Ansible releases

pyproject.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ exclude_dirs = [
2727
skips = ["B101"]
2828

2929
[tool.mypy]
30-
python_version = "3.8"
30+
python_version = "3.9"
3131
follow_imports = "silent"
3232
namespace_packages = true
3333
explicit_package_bases = true
@@ -76,7 +76,7 @@ include = [
7676
]
7777
line-length = 80
7878
indent-width = 4
79-
target-version = "py38"
79+
target-version = "py39"
8080

8181
[tool.ruff.lint]
8282
select = [
@@ -186,20 +186,19 @@ Changelog
186186
legacy_tox_ini = """
187187
[tox]
188188
envlist =
189-
py{38,39,310}-ansible6
190189
py{39,310,311}-ansible7
191190
py{39,310,311}-ansible8
192191
py{310,311,312}-ansible9
193192
py{310,311,312}-ansible10
194193
py{311,312,313}-ansible11
194+
py{311,312,313}-ansible12
195195
ruff
196196
bandit
197197
mypy
198198
report
199199
200200
[gh-actions]
201201
python =
202-
3.8: py38
203202
3.9: py39
204203
3.10: py310
205204
3.11: py311,ruff,bandit,mypy
@@ -210,10 +209,9 @@ python =
210209
usedevelop = true
211210
setenv =
212211
py{38,39,310,311,312,313}: COVERAGE_FILE = .coverage.{envname}
212+
ansible12: UV_PRERELEASE = allow
213213
deps =
214214
-e{toxinidir}[tests]
215-
ansible6: ansible==6.*
216-
ansible6: ansible-core==2.13.*
217215
ansible7: ansible==7.*
218216
ansible7: ansible-core==2.14.*
219217
ansible8: ansible==8.*
@@ -224,6 +222,8 @@ deps =
224222
ansible10: ansible-core==2.17.*
225223
ansible11: ansible==11.*
226224
ansible11: ansible-core==2.18.*
225+
ansible12: ansible==12.*
226+
ansible12: ansible-core==2.19.*
227227
228228
commands = pytest --cov --cov-report= {posargs}
229229
@@ -253,7 +253,6 @@ deps =
253253
pytest
254254
types-paramiko
255255
commands =
256-
mypy -p suitable -p tests --python-version 3.8
257256
mypy -p suitable -p tests --python-version 3.9
258257
mypy -p suitable -p tests --python-version 3.10
259258
mypy -p suitable -p tests --python-version 3.11

setup.cfg

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ classifiers =
1818
Operating System :: OS Independent
1919
Programming Language :: Python
2020
Programming Language :: Python :: 3
21-
Programming Language :: Python :: 3.8
2221
Programming Language :: Python :: 3.9
2322
Programming Language :: Python :: 3.10
2423
Programming Language :: Python :: 3.11
@@ -35,7 +34,7 @@ package_dir =
3534
= src
3635
packages =
3736
suitable
38-
python_requires = >= 3.8
37+
python_requires = >= 3.9
3938
platforms = any
4039
install_requires =
4140
ansible>=6
@@ -66,11 +65,5 @@ tests =
6665
suitable =
6766
py.typed
6867

69-
[flake8]
70-
extend-select = B901,B903,B904,B908
71-
exclude=.venv,.git,.tox,dist,docs,*lib/python*,*egg,build
72-
per_file_ignores =
73-
*.pyi: B,E301,E302,E305,E501,E701,F401,F403,F405,F822,Y065
74-
7568
[bdist_wheel]
7669
universal = 1

src/suitable/callback.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from ansible.plugins.callback import ( # type:ignore[import-untyped]
44
CallbackBase
55
)
6-
from typing import TYPE_CHECKING
6+
from typing import Any, TYPE_CHECKING
77

88
if TYPE_CHECKING:
99
from ansible.executor.task_result import TaskResult # type:ignore
10+
from ansible.utils.display import Display # type:ignore
1011
from suitable.types import ResultData
1112
from typing_extensions import TypedDict
1213

@@ -21,10 +22,19 @@ class SilentCallbackModule(CallbackBase): # type:ignore[misc]
2122
2223
"""
2324

25+
CALLBACK_VERSION = 2.0
26+
CALLBACK_TYPE = 'stdout'
27+
CALLBACK_NAME = 'silent'
28+
2429
unreachable: dict[str, ResultData]
2530
contacted: dict[str, ContactedResult]
2631

27-
def __init__(self) -> None:
32+
def __init__(
33+
self,
34+
display: Display | None = None,
35+
options: dict[str, Any] | None = None
36+
) -> None:
37+
super().__init__(display, options)
2838
self.unreachable = {}
2939
self.contacted = {}
3040

src/suitable/inventory.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,9 @@
33
from typing import TYPE_CHECKING
44
if TYPE_CHECKING:
55
from suitable.types import Hosts, HostVariables, Incomplete
6-
from typing import Dict
76

8-
_Base = Dict[str, HostVariables]
9-
else:
10-
_Base = dict
117

12-
13-
class Inventory(_Base):
8+
class Inventory(dict[str, 'HostVariables']): # noqa: FURB189
149

1510
def __init__(
1611
self,

src/suitable/module_runner.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import signal
88
import sys
99

10+
from ansible import __version__
1011
from ansible.executor.task_queue_manager import TaskQueueManager # type:ignore
1112
from ansible.parsing.dataloader import DataLoader # type:ignore
1213
from ansible.inventory.manager import InventoryManager # type:ignore
@@ -15,6 +16,7 @@
1516
from ansible.vars.manager import VariableManager # type:ignore[import-untyped]
1617
from contextlib import contextmanager
1718
from datetime import datetime
19+
from packaging.version import Version
1820
from pprint import pformat
1921
from suitable._modules import AnsibleModules
2022
from suitable.callback import SilentCallbackModule
@@ -43,6 +45,9 @@ def utcnow() -> datetime:
4345
utcnow = datetime.utcnow
4446

4547

48+
PASS_CALLBACK_BY_NAME = Version(__version__) >= Version('2.19')
49+
50+
4651
@contextmanager
4752
def ansible_verbosity(verbosity: int) -> Generator[None, None, None]:
4853
""" Temporarily changes the ansible verbosity. Relies on a single display
@@ -219,10 +224,7 @@ def execute(self, *args: Any, **kwargs: Any) -> RunnerResults:
219224
'hosts': 'all',
220225
'gather_facts': 'no',
221226
'tasks': [{
222-
'action': {
223-
'module': self.module_name,
224-
'args': module_args,
225-
},
227+
self.module_name: module_args,
226228
'environment': self.api.environment,
227229
}]
228230
}
@@ -265,6 +267,21 @@ def execute(self, *args: Any, **kwargs: Any) -> RunnerResults:
265267
'passwords': getattr(self.api.options, 'passwords', {}),
266268
'stdout_callback': callback
267269
}
270+
if PASS_CALLBACK_BY_NAME:
271+
from ansible.plugins.loader import callback_loader # type: ignore
272+
del kwargs['stdout_callback']
273+
callback_name = 'suitable.callback.silent'
274+
kwargs['stdout_callback_name'] = callback_name
275+
# HACK: This is really not pretty, but creating a working
276+
# ansible collection, just so the plugin finder can
277+
# find our callback and create an instance does not
278+
# seem worth the required effort
279+
orig_get = callback_loader.get
280+
callback_loader.get = lambda name, *a, **kw: (
281+
callback
282+
if name == 'suitable.callback.silent'
283+
else orig_get(name, *a, **kw)
284+
)
268285

269286
if set_global_context:
270287
del kwargs['options']
@@ -296,6 +313,8 @@ def execute(self, *args: Any, **kwargs: Any) -> RunnerResults:
296313
raise
297314
finally:
298315
if task_queue_manager is not None:
316+
if PASS_CALLBACK_BY_NAME:
317+
callback_loader.get = orig_get
299318
task_queue_manager.cleanup()
300319

301320
if set_global_context:

src/suitable/runner_results.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,18 @@
44

55
if TYPE_CHECKING:
66
from suitable.types import ResultData
7-
from typing import Dict
87
from typing_extensions import TypedDict
98

109
class _RunnerResults(TypedDict):
1110
contacted: dict[str, ResultData]
1211
unreachable: dict[str, ResultData]
1312

14-
_Base = Dict[str, Dict[str, ResultData]]
15-
else:
16-
_Base = dict
17-
1813

1914
class ResultsCallback(Protocol):
2015
def __call__(self, server: str | None = None) -> Any: ...
2116

2217

23-
class RunnerResults(_Base):
18+
class RunnerResults(dict[str, dict[str, 'ResultData']]): # noqa: FURB189
2419
"""
2520
Wraps the results of parsed module_runner output.
2621

0 commit comments

Comments
 (0)