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

View File

@@ -1,3 +1,4 @@
major_changes: major_changes:
- ansible - Add support for Python 3.14. - ansible - Add support for Python 3.14.
- ansible - Drop support for Python 3.8 on targets. - 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 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: 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) return importlib.import_module(module_name)
_T = t.TypeVar('_T') def experimental[T](obj: T) -> T:
def experimental(obj: _T) -> _T:
""" """
Decorator for experimental types and methods outside the `_internal` package which accept or expose internal types. 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. 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.module_utils._internal._ansiballz._extensions import _debugpy, _pydevd, _coverage
from ansible.constants import config from ansible.constants import config
_T = t.TypeVar('_T')
class ExtensionManager: class ExtensionManager:
"""AnsiballZ extension manager.""" """AnsiballZ extension manager."""
@@ -101,7 +99,7 @@ class ExtensionManager:
) )
@classmethod @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.""" """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: if (value := config.get_config_value(name, variables=task_vars)) is None:
return None return None

View File

@@ -3,26 +3,24 @@ from __future__ import annotations as _annotations
import collections.abc as _c import collections.abc as _c
import typing as _t import typing as _t
_T_co = _t.TypeVar('_T_co', covariant=True)
class SequenceProxy[T](_c.Sequence[T]):
class SequenceProxy(_c.Sequence[_T_co]):
"""A read-only sequence proxy.""" """A read-only sequence proxy."""
# DTFIX5: needs unit test coverage # DTFIX5: needs unit test coverage
__slots__ = ('__value',) __slots__ = ('__value',)
def __init__(self, value: _c.Sequence[_T_co]) -> None: def __init__(self, value: _c.Sequence[T]) -> None:
self.__value = value self.__value = value
@_t.overload @_t.overload
def __getitem__(self, index: int) -> _T_co: ... def __getitem__(self, index: int) -> T: ...
@_t.overload @_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): if isinstance(index, slice):
return self.__class__(self.__value[index]) return self.__class__(self.__value[index])
@@ -34,10 +32,10 @@ class SequenceProxy(_c.Sequence[_T_co]):
def __contains__(self, item: object) -> bool: def __contains__(self, item: object) -> bool:
return item in self.__value return item in self.__value
def __iter__(self) -> _t.Iterator[_T_co]: def __iter__(self) -> _t.Iterator[T]:
yield from self.__value yield from self.__value
def __reversed__(self) -> _c.Iterator[_T_co]: def __reversed__(self) -> _c.Iterator[T]:
return reversed(self.__value) return reversed(self.__value)
def index(self, *args) -> int: 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 import _internal
from ansible.module_utils._internal import _datatag from ansible.module_utils._internal import _datatag
_T = t.TypeVar('_T')
_sentinel = object() _sentinel = object()
@@ -115,7 +114,7 @@ class AnsibleVariableVisitor:
if func := getattr(super(), '__exit__', None): if func := getattr(super(), '__exit__', None):
func(*args, **kwargs) 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 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 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 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.""" """Internal implementation to recursively visit a data structure's contents."""
self._current = key # supports StateTrackingMixIn self._current = key # supports StateTrackingMixIn
@@ -168,7 +167,7 @@ class AnsibleVariableVisitor:
value = value._native_copy() value = value._native_copy()
value_type = type(value) 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. # 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. # 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() _display = Display()
_TCallable = t.TypeVar("_TCallable", bound=t.Callable)
_ITERATOR_TYPES: t.Final = (c.Iterator, c.ItemsView, c.KeysView, c.ValuesView, range) _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" _marker_attr: t.Final[str] = "_directcall"
@classmethod @classmethod
def mark(cls, src: _TCallable) -> _TCallable: def mark[T: t.Callable](cls, src: T) -> T:
setattr(src, cls._marker_attr, True) setattr(src, cls._marker_attr, True)
return src 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 # Used for determining if the system is running a new enough python version
# and should only restrict on our documented minimum versions # and should only restrict on our documented minimum versions
_PY_MIN = (3, 11) _PY_MIN = (3, 12)
if sys.version_info < _PY_MIN: if sys.version_info < _PY_MIN:
raise SystemExit( raise SystemExit(

View File

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

View File

@@ -1271,11 +1271,7 @@ def test_sdist() -> None:
except FileNotFoundError: except FileNotFoundError:
raise ApplicationError(f"Missing sdist: {sdist_file.relative_to(CHECKOUT_DIR)}") from None raise ApplicationError(f"Missing sdist: {sdist_file.relative_to(CHECKOUT_DIR)}") from None
# deprecated: description='extractall fallback without filter' python_version='3.11' sdist.extractall(temp_dir, filter='data')
if hasattr(tarfile, 'data_filter'):
sdist.extractall(temp_dir, filter='data') # type: ignore[call-arg]
else:
sdist.extractall(temp_dir)
pyc_glob = "*.pyc*" pyc_glob = "*.pyc*"
pyc_files = sorted(path.relative_to(temp_dir) for path in temp_dir.rglob(pyc_glob)) 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" build-backend = "setuptools.build_meta"
[project] [project]
requires-python = ">=3.11" requires-python = ">=3.12"
name = "ansible-core" name = "ansible-core"
authors = [ authors = [
{name = "Ansible Project"}, {name = "Ansible Project"},
@@ -20,9 +20,9 @@ classifiers = [
"Natural Language :: English", "Natural Language :: English",
"Operating System :: POSIX", "Operating System :: POSIX",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Topic :: System :: Installation/Setup", "Topic :: System :: Installation/Setup",
"Topic :: System :: Systems Administration", "Topic :: System :: Systems Administration",

View File

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

View File

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

View File

@@ -187,12 +187,6 @@ bootstrap_remote_freebsd()
# Declare platform/python version combinations which do not have supporting OS packages available. # 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. # For these combinations ansible-test will use pip to install the requirements instead.
case "${platform_version}/${python_version}" in case "${platform_version}/${python_version}" in
13.5/3.11)
# defaults available
;;
14.3/3.11)
# defaults available
;;
*) *)
# just assume nothing is available # just assume nothing is available
jinja2_pkg="" # not 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/callback/__init__.py pylint:arguments-renamed
lib/ansible/plugins/inventory/advanced_host_list.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/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.12!skip # vendored code
lib/ansible/_internal/_wrapt.py mypy-3.13!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 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/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/manager.py pylint:ansible-deprecated-version-comment # TODO: 2.20
lib/ansible/vars/plugins.py pylint:ansible-deprecated-version # 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 bcrypt ; python_version >= '3.12' # controller only
passlib ; python_version >= '3.11' # controller only passlib ; python_version >= '3.12' # controller only
pexpect ; python_version >= '3.11' # controller only pexpect ; python_version >= '3.12' # controller only
pywinrm ; python_version >= '3.11' # 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) typing_extensions; python_version < '3.11' # some unit tests need Annotated and get_type_hints(include_extras=True)