From f2612fbe3ae3c9d5aafab612ee3f04dc2eb24378 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 30 Jul 2025 16:39:38 -0700 Subject: [PATCH] Drop Python 3.11 controller support (#85590) --- .azure-pipelines/azure-pipelines.yml | 8 ++------ changelogs/fragments/python-support.yml | 1 + hacking/README.md | 2 +- lib/ansible/_internal/__init__.py | 5 +---- lib/ansible/_internal/_ansiballz/_builder.py | 4 +--- lib/ansible/_internal/_collection_proxy.py | 16 +++++++--------- lib/ansible/_internal/_json/__init__.py | 7 +++---- .../_internal/_templating/_jinja_plugins.py | 3 +-- lib/ansible/cli/__init__.py | 2 +- lib/ansible/config/base.yml | 2 +- packaging/release.py | 6 +----- pyproject.toml | 4 ++-- .../commands/sanity/validate_modules.py | 6 +----- .../_util/target/common/constants.py | 2 +- .../ansible_test/_util/target/setup/bootstrap.sh | 6 ------ test/sanity/ignore.txt | 2 +- test/units/requirements.txt | 8 ++++---- 17 files changed, 29 insertions(+), 55 deletions(-) diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 9d618ca039..228389367e 100644 --- a/.azure-pipelines/azure-pipelines.yml +++ b/.azure-pipelines/azure-pipelines.yml @@ -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: [] diff --git a/changelogs/fragments/python-support.yml b/changelogs/fragments/python-support.yml index 8b86c3245f..437bfbb66e 100644 --- a/changelogs/fragments/python-support.yml +++ b/changelogs/fragments/python-support.yml @@ -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. diff --git a/hacking/README.md b/hacking/README.md index 534a7e4db0..6eddf2e9e9 100644 --- a/hacking/README.md +++ b/hacking/README.md @@ -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: diff --git a/lib/ansible/_internal/__init__.py b/lib/ansible/_internal/__init__.py index 2975a528b6..35d883b018 100644 --- a/lib/ansible/_internal/__init__.py +++ b/lib/ansible/_internal/__init__.py @@ -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. diff --git a/lib/ansible/_internal/_ansiballz/_builder.py b/lib/ansible/_internal/_ansiballz/_builder.py index 76c756fe19..5c8671fd08 100644 --- a/lib/ansible/_internal/_ansiballz/_builder.py +++ b/lib/ansible/_internal/_ansiballz/_builder.py @@ -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 diff --git a/lib/ansible/_internal/_collection_proxy.py b/lib/ansible/_internal/_collection_proxy.py index b14dcf386f..ea3f10e26a 100644 --- a/lib/ansible/_internal/_collection_proxy.py +++ b/lib/ansible/_internal/_collection_proxy.py @@ -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: diff --git a/lib/ansible/_internal/_json/__init__.py b/lib/ansible/_internal/_json/__init__.py index 94b53fcc8f..fd827e68e8 100644 --- a/lib/ansible/_internal/_json/__init__.py +++ b/lib/ansible/_internal/_json/__init__.py @@ -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. diff --git a/lib/ansible/_internal/_templating/_jinja_plugins.py b/lib/ansible/_internal/_templating/_jinja_plugins.py index 482dabfbb0..a79d9b1806 100644 --- a/lib/ansible/_internal/_templating/_jinja_plugins.py +++ b/lib/ansible/_internal/_templating/_jinja_plugins.py @@ -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 diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index da5cacc13b..2a4ca0f3a7 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -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( diff --git a/lib/ansible/config/base.yml b/lib/ansible/config/base.yml index ad28844b8c..56dca21bbc 100644 --- a/lib/ansible/config/base.yml +++ b/lib/ansible/config/base.yml @@ -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: diff --git a/packaging/release.py b/packaging/release.py index c16b21f1f2..59077ec5eb 100755 --- a/packaging/release.py +++ b/packaging/release.py @@ -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)) diff --git a/pyproject.toml b/pyproject.toml index 3603553092..b652ac7dd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py index 29f271afa8..b51582e4e9 100644 --- a/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py +++ b/test/lib/ansible_test/_internal/commands/sanity/validate_modules.py @@ -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, diff --git a/test/lib/ansible_test/_util/target/common/constants.py b/test/lib/ansible_test/_util/target/common/constants.py index ad412aa23d..4d55c8286e 100644 --- a/test/lib/ansible_test/_util/target/common/constants.py +++ b/test/lib/ansible_test/_util/target/common/constants.py @@ -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', diff --git a/test/lib/ansible_test/_util/target/setup/bootstrap.sh b/test/lib/ansible_test/_util/target/setup/bootstrap.sh index 6947c34f21..7193bfd16b 100644 --- a/test/lib/ansible_test/_util/target/setup/bootstrap.sh +++ b/test/lib/ansible_test/_util/target/setup/bootstrap.sh @@ -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 diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index f2dd0a3f40..07b4474dc6 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -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 diff --git a/test/units/requirements.txt b/test/units/requirements.txt index fa46103038..97d7b779c2 100644 --- a/test/units/requirements.txt +++ b/test/units/requirements.txt @@ -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)