Drop Python 3.11 controller support (#85590)

This commit is contained in:
Matt Clay
2025-07-30 16:39:38 -07:00
committed by GitHub
parent 8aad1418f6
commit f2612fbe3a
17 changed files with 29 additions and 55 deletions

View File

@@ -112,10 +112,6 @@ stages:
test: rhel/9.6
- name: RHEL 10.0
test: rhel/10.0
- name: FreeBSD 13.5
test: freebsd/13.5
- name: FreeBSD 14.3
test: freebsd/14.3
groups:
- 3
- 4
@@ -183,9 +179,9 @@ stages:
nameFormat: Python {0}
testFormat: galaxy/{0}/1
targets:
- test: 3.11
- test: 3.12
- test: 3.13
- test: 3.14
- stage: Generic
dependsOn: []
jobs:
@@ -194,9 +190,9 @@ stages:
nameFormat: Python {0}
testFormat: generic/{0}/1
targets:
- test: 3.11
- test: 3.12
- test: 3.13
- test: 3.14
- stage: Incidental_Windows
displayName: Incidental Windows
dependsOn: []

View File

@@ -1,3 +1,4 @@
major_changes:
- ansible - Add support for Python 3.14.
- ansible - Drop support for Python 3.8 on targets.
- ansible - Drop support for Python 3.11 on the controller.

View File

@@ -5,7 +5,7 @@ env-setup
---------
The 'env-setup' script modifies your environment to allow you to run
ansible from a git checkout using python >= 3.11.
ansible from a git checkout using a supported Python version.
First, set up your environment to run from the checkout:

View File

@@ -30,10 +30,7 @@ def import_controller_module(module_name: str, /) -> t.Any:
return importlib.import_module(module_name)
_T = t.TypeVar('_T')
def experimental(obj: _T) -> _T:
def experimental[T](obj: T) -> T:
"""
Decorator for experimental types and methods outside the `_internal` package which accept or expose internal types.
As with internal APIs, these are subject to change at any time without notice.

View File

@@ -9,8 +9,6 @@ from ansible.module_utils._internal._ansiballz import _extensions
from ansible.module_utils._internal._ansiballz._extensions import _debugpy, _pydevd, _coverage
from ansible.constants import config
_T = t.TypeVar('_T')
class ExtensionManager:
"""AnsiballZ extension manager."""
@@ -101,7 +99,7 @@ class ExtensionManager:
)
@classmethod
def _get_options(cls, name: str, config_type: type[_T], task_vars: dict[str, object]) -> _T | None:
def _get_options[T](cls, name: str, config_type: type[T], task_vars: dict[str, object]) -> T | None:
"""Parse configuration from the named environment variable as the specified type, or None if not configured."""
if (value := config.get_config_value(name, variables=task_vars)) is None:
return None

View File

@@ -3,26 +3,24 @@ from __future__ import annotations as _annotations
import collections.abc as _c
import typing as _t
_T_co = _t.TypeVar('_T_co', covariant=True)
class SequenceProxy(_c.Sequence[_T_co]):
class SequenceProxy[T](_c.Sequence[T]):
"""A read-only sequence proxy."""
# DTFIX5: needs unit test coverage
__slots__ = ('__value',)
def __init__(self, value: _c.Sequence[_T_co]) -> None:
def __init__(self, value: _c.Sequence[T]) -> None:
self.__value = value
@_t.overload
def __getitem__(self, index: int) -> _T_co: ...
def __getitem__(self, index: int) -> T: ...
@_t.overload
def __getitem__(self, index: slice) -> _c.Sequence[_T_co]: ...
def __getitem__(self, index: slice) -> _c.Sequence[T]: ...
def __getitem__(self, index: int | slice) -> _T_co | _c.Sequence[_T_co]:
def __getitem__(self, index: int | slice) -> T | _c.Sequence[T]:
if isinstance(index, slice):
return self.__class__(self.__value[index])
@@ -34,10 +32,10 @@ class SequenceProxy(_c.Sequence[_T_co]):
def __contains__(self, item: object) -> bool:
return item in self.__value
def __iter__(self) -> _t.Iterator[_T_co]:
def __iter__(self) -> _t.Iterator[T]:
yield from self.__value
def __reversed__(self) -> _c.Iterator[_T_co]:
def __reversed__(self) -> _c.Iterator[T]:
return reversed(self.__value)
def index(self, *args) -> int:

View File

@@ -24,7 +24,6 @@ from ansible._internal._templating import _transform
from ansible.module_utils import _internal
from ansible.module_utils._internal import _datatag
_T = t.TypeVar('_T')
_sentinel = object()
@@ -115,7 +114,7 @@ class AnsibleVariableVisitor:
if func := getattr(super(), '__exit__', None):
func(*args, **kwargs)
def visit(self, value: _T) -> _T:
def visit[T](self, value: T) -> T:
"""
Enforces Ansible's variable type system restrictions before a var is accepted in inventory. Also, conditionally implements template trust
compatibility, depending on the plugin's declared understanding (or lack thereof). This always recursively copies inputs to fully isolate
@@ -143,7 +142,7 @@ class AnsibleVariableVisitor:
return self._visit(None, key) # key=None prevents state tracking from seeing the key as value
def _visit(self, key: t.Any, value: _T) -> _T:
def _visit[T](self, key: t.Any, value: T) -> T:
"""Internal implementation to recursively visit a data structure's contents."""
self._current = key # supports StateTrackingMixIn
@@ -168,7 +167,7 @@ class AnsibleVariableVisitor:
value = value._native_copy()
value_type = type(value)
result: _T
result: T
# DTFIX-FUTURE: Visitor generally ignores dict/mapping keys by default except for debugging and schema-aware checking.
# It could be checking keys destined for variable storage to apply more strict rules about key shape and type.

View File

@@ -29,7 +29,6 @@ from ._utils import LazyOptions, TemplateContext
_display = Display()
_TCallable = t.TypeVar("_TCallable", bound=t.Callable)
_ITERATOR_TYPES: t.Final = (c.Iterator, c.ItemsView, c.KeysView, c.ValuesView, range)
@@ -169,7 +168,7 @@ class _DirectCall:
_marker_attr: t.Final[str] = "_directcall"
@classmethod
def mark(cls, src: _TCallable) -> _TCallable:
def mark[T: t.Callable](cls, src: T) -> T:
setattr(src, cls._marker_attr, True)
return src

View File

@@ -23,7 +23,7 @@ if 1 <= len(sys.argv) <= 2 and os.path.basename(sys.argv[0]) == "ansible" and os
# Used for determining if the system is running a new enough python version
# and should only restrict on our documented minimum versions
_PY_MIN = (3, 11)
_PY_MIN = (3, 12)
if sys.version_info < _PY_MIN:
raise SystemExit(

View File

@@ -1689,12 +1689,12 @@ INTERPRETER_PYTHON:
INTERPRETER_PYTHON_FALLBACK:
name: Ordered list of Python interpreters to check for in discovery
default:
- python3.14
- python3.13
- python3.12
- python3.11
- python3.10
- python3.9
- python3.8
- /usr/bin/python3
- python3
vars:

View File

@@ -1271,11 +1271,7 @@ def test_sdist() -> None:
except FileNotFoundError:
raise ApplicationError(f"Missing sdist: {sdist_file.relative_to(CHECKOUT_DIR)}") from None
# deprecated: description='extractall fallback without filter' python_version='3.11'
if hasattr(tarfile, 'data_filter'):
sdist.extractall(temp_dir, filter='data') # type: ignore[call-arg]
else:
sdist.extractall(temp_dir)
sdist.extractall(temp_dir, filter='data')
pyc_glob = "*.pyc*"
pyc_files = sorted(path.relative_to(temp_dir) for path in temp_dir.rglob(pyc_glob))

View File

@@ -3,7 +3,7 @@ requires = ["setuptools >= 66.1.0, <= 80.3.1", "wheel == 0.45.1"] # lower bound
build-backend = "setuptools.build_meta"
[project]
requires-python = ">=3.11"
requires-python = ">=3.12"
name = "ansible-core"
authors = [
{name = "Ansible Project"},
@@ -20,9 +20,9 @@ classifiers = [
"Natural Language :: English",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
"Topic :: System :: Installation/Setup",
"Topic :: System :: Systems Administration",

View File

@@ -160,11 +160,7 @@ class ValidateModulesTest(SanitySingleVersion):
temp_dir = process_scoped_temporary_directory(args)
with tarfile.open(path) as file:
# deprecated: description='extractall fallback without filter' python_version='3.11'
if hasattr(tarfile, 'data_filter'):
file.extractall(temp_dir, filter='data') # type: ignore[call-arg]
else:
file.extractall(temp_dir)
file.extractall(temp_dir, filter='data')
cmd.extend([
'--original-plugins', temp_dir,

View File

@@ -7,10 +7,10 @@ from __future__ import annotations
REMOTE_ONLY_PYTHON_VERSIONS = (
'3.9',
'3.10',
'3.11',
)
CONTROLLER_PYTHON_VERSIONS = (
'3.11',
'3.12',
'3.13',
'3.14',

View File

@@ -187,12 +187,6 @@ bootstrap_remote_freebsd()
# Declare platform/python version combinations which do not have supporting OS packages available.
# For these combinations ansible-test will use pip to install the requirements instead.
case "${platform_version}/${python_version}" in
13.5/3.11)
# defaults available
;;
14.3/3.11)
# defaults available
;;
*)
# just assume nothing is available
jinja2_pkg="" # not available

View File

@@ -54,7 +54,6 @@ lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub f
lib/ansible/plugins/callback/__init__.py pylint:arguments-renamed
lib/ansible/plugins/inventory/advanced_host_list.py pylint:arguments-renamed
lib/ansible/plugins/inventory/host_list.py pylint:arguments-renamed
lib/ansible/_internal/_wrapt.py mypy-3.11!skip # vendored code
lib/ansible/_internal/_wrapt.py mypy-3.12!skip # vendored code
lib/ansible/_internal/_wrapt.py mypy-3.13!skip # vendored code
lib/ansible/_internal/_wrapt.py mypy-3.14!skip # vendored code
@@ -237,3 +236,4 @@ lib/ansible/utils/encrypt.py pylint:ansible-deprecated-version # TODO: 2.20
lib/ansible/utils/ssh_functions.py pylint:ansible-deprecated-version # TODO: 2.20
lib/ansible/vars/manager.py pylint:ansible-deprecated-version-comment # TODO: 2.20
lib/ansible/vars/plugins.py pylint:ansible-deprecated-version # TODO: 2.20
lib/ansible/galaxy/role.py pylint:ansible-deprecated-python-version-comment # TODO: 2.20

View File

@@ -1,5 +1,5 @@
bcrypt ; python_version >= '3.11' # controller only
passlib ; python_version >= '3.11' # controller only
pexpect ; python_version >= '3.11' # controller only
pywinrm ; python_version >= '3.11' # controller only
bcrypt ; python_version >= '3.12' # controller only
passlib ; python_version >= '3.12' # controller only
pexpect ; python_version >= '3.12' # controller only
pywinrm ; python_version >= '3.12' # controller only
typing_extensions; python_version < '3.11' # some unit tests need Annotated and get_type_hints(include_extras=True)