diff --git a/.azure-pipelines/azure-pipelines.yml b/.azure-pipelines/azure-pipelines.yml index 9d618ca0394..228389367e8 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 8b86c3245fb..437bfbb66e6 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 534a7e4db0e..6eddf2e9e94 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 2975a528b6a..35d883b0181 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 76c756fe195..5c8671fd080 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 b14dcf386fa..ea3f10e26a9 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 94b53fcc8fa..fd827e68e8a 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 482dabfbb01..a79d9b18067 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 da5cacc13bf..2a4ca0f3a71 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 ad28844b8c2..56dca21bbc0 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 c16b21f1f22..59077ec5eb9 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 36035530920..b652ac7dd5e 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 29f271afa81..b51582e4e90 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 ad412aa23df..4d55c8286ee 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 6947c34f21a..7193bfd16bb 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 f2dd0a3f405..07b4474dc64 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 fa461030387..97d7b779c2e 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)