Add support for Python 3.14 and drop Python 3.8 (#85576)

pull/85577/head
Matt Clay 5 months ago committed by GitHub
parent 647e7409eb
commit 6b2b665ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -54,12 +54,12 @@ stages:
nameFormat: Python {0}
testFormat: units/{0}
targets:
- test: 3.8
- test: 3.9
- test: '3.10'
- test: 3.11
- test: 3.12
- test: 3.13
- test: 3.14
- stage: Windows
dependsOn: []
jobs:

@ -0,0 +1,3 @@
major_changes:
- ansible - Add support for Python 3.14.
- ansible - Drop support for Python 3.8 on targets.

@ -23,10 +23,12 @@ 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
if sys.version_info < (3, 11):
_PY_MIN = (3, 11)
if sys.version_info < _PY_MIN:
raise SystemExit(
'ERROR: Ansible requires Python 3.11 or newer on the controller. '
'Current version: %s' % ''.join(sys.version.splitlines())
f"ERROR: Ansible requires Python {'.'.join(map(str, _PY_MIN))} or newer on the controller. "
f"Current version: {''.join(sys.version.splitlines())}"
)

@ -500,17 +500,9 @@ class AnsibleDatatagBase(AnsibleSerializableDataclass, metaclass=abc.ABCMeta):
_known_tag_type_map: t.Dict[str, t.Type[AnsibleDatatagBase]] = {}
_known_tag_types: t.Set[t.Type[AnsibleDatatagBase]] = set()
if sys.version_info >= (3, 9):
# Include the key and value types in the type hints on Python 3.9 and later.
# Earlier versions do not support subscriptable dict.
# deprecated: description='always use subscriptable dict' python_version='3.8'
class _AnsibleTagsMapping(dict[type[_TAnsibleDatatagBase], _TAnsibleDatatagBase]):
__slots__ = _NO_INSTANCE_STORAGE
else:
class _AnsibleTagsMapping(dict):
__slots__ = _NO_INSTANCE_STORAGE
class _AnsibleTagsMapping(dict[type[_TAnsibleDatatagBase], _TAnsibleDatatagBase]):
__slots__ = _NO_INSTANCE_STORAGE
class _EmptyROInternalTagsMapping(dict):

@ -11,12 +11,13 @@ import typing as t
# 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, 8)
_PY_MIN = (3, 9)
if sys.version_info < _PY_MIN:
print(json.dumps(dict(
failed=True,
msg=f"ansible-core requires a minimum of Python version {'.'.join(map(str, _PY_MIN))}. Current version: {''.join(sys.version.splitlines())}",
msg=f"Ansible requires Python {'.'.join(map(str, _PY_MIN))} or newer on the target. "
f"Current version: {''.join(sys.version.splitlines())}",
)))
sys.exit(1)

@ -26,20 +26,15 @@ from . import _to_bytes, _to_text
from ._collection_config import AnsibleCollectionConfig
try:
try:
# Available on Python >= 3.11
# We ignore the import error that will trigger when running mypy with
# older Python versions.
from importlib.resources.abc import TraversableResources # type: ignore[import]
except ImportError:
# Used with Python 3.9 and 3.10 only
# This member is still available as an alias up until Python 3.14 but
# is deprecated as of Python 3.12.
from importlib.abc import TraversableResources # deprecated: description='TraversableResources move' python_version='3.10'
# Available on Python >= 3.11
# We ignore the import error that will trigger when running mypy with
# older Python versions.
from importlib.resources.abc import TraversableResources # type: ignore[import]
except ImportError:
# Python < 3.9
# deprecated: description='TraversableResources fallback' python_version='3.8'
TraversableResources = object # type: ignore[assignment,misc]
# Used with Python 3.9 and 3.10 only
# This member is still available as an alias up until Python 3.14 but
# is deprecated as of Python 3.12.
from importlib.abc import TraversableResources # deprecated: description='TraversableResources move' python_version='3.10'
# NB: this supports import sanity test providing a different impl
try:

@ -1,6 +1,6 @@
base image=quay.io/ansible/base-test-container:8.2.0 python=3.13,3.8,3.9,3.10,3.11,3.12
default image=quay.io/ansible/default-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection
default image=quay.io/ansible/ansible-core-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
base image=quay.io/ansible/base-test-container:9.1.0 python=3.13,3.9,3.10,3.11,3.12,3.14
default image=quay.io/ansible/default-test-container:12.0.0 python=3.13,3.9,3.10,3.11,3.12,3.14 context=collection
default image=quay.io/ansible/ansible-core-test-container:12.0.0 python=3.13,3.9,3.10,3.11,3.12,3.14 context=ansible-core
alpine322 image=quay.io/ansible/alpine322-test-container:10.0.0 python=3.12 cgroup=none audit=none
fedora42 image=quay.io/ansible/fedora42-test-container:10.0.0 python=3.13 cgroup=v2-only
ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:10.0.0 python=3.10

@ -1,4 +0,0 @@
[pytest]
xfail_strict = true
mock_use_standalone_module = true
junit_family = xunit1

@ -1,3 +1,2 @@
# The test-constraints sanity test verifies this file, but changes must be made manually to keep it in up-to-date.
coverage == 7.10.0 ; python_version >= '3.9' and python_version <= '3.14'
coverage == 7.6.1 ; python_version >= '3.8' and python_version <= '3.8'

@ -69,7 +69,7 @@ def controller_python(version: t.Optional[str]) -> t.Optional[str]:
def get_fallback_remote_controller() -> str:
"""Return the remote fallback platform for the controller."""
platform = 'freebsd' # lower cost than RHEL and macOS
platform = 'fedora' # Fedora is lower cost than other remotes and always supports a recent Python version
candidates = [item for item in filter_completion(remote_completion()).values() if item.controller_supported and item.platform == platform]
fallback = sorted(candidates, key=lambda value: str_to_version(value.version), reverse=True)[0]
return fallback.name

@ -241,19 +241,7 @@ def command_units(args: UnitsConfig) -> None:
sys.exit()
for test_context, python, paths, env in test_sets:
# When using pytest-mock, make sure that features introduced in Python 3.8 are available to older Python versions.
# This is done by enabling the mock_use_standalone_module feature, which forces use of mock even when unittest.mock is available.
# Later Python versions have not introduced additional unittest.mock features, so use of mock is not needed as of Python 3.8.
# If future Python versions introduce new unittest.mock features, they will not be available to older Python versions.
# Having the cutoff at Python 3.8 also eases packaging of ansible-core since no supported controller version requires the use of mock.
#
# NOTE: This only affects use of pytest-mock.
# Collection unit tests may directly import mock, which will be provided by ansible-test when it installs requirements using pip.
# Although mock is available for ansible-core unit tests, they should import unittest.mock instead.
if str_to_version(python.version) < (3, 8):
config_name = 'legacy.ini'
else:
config_name = 'default.ini'
config_name = 'default.ini'
cmd = [
'pytest',

@ -70,7 +70,6 @@ class CoverageVersion:
COVERAGE_VERSIONS = (
# IMPORTANT: Keep this in sync with the ansible-test.txt requirements file.
CoverageVersion('7.10.0', 7, (3, 9), (3, 14)),
CoverageVersion('7.6.1', 7, (3, 8), (3, 8)),
)
"""
This tuple specifies the coverage version to use for Python version ranges.

@ -5,7 +5,6 @@
from __future__ import annotations
REMOTE_ONLY_PYTHON_VERSIONS = (
'3.8',
'3.9',
'3.10',
)
@ -14,4 +13,5 @@ CONTROLLER_PYTHON_VERSIONS = (
'3.11',
'3.12',
'3.13',
'3.14',
)

@ -21,6 +21,8 @@ def main() -> None:
remote_only_python_versions = os.environ['ANSIBLE_TEST_REMOTE_ONLY_PYTHON_VERSIONS'].split(',')
fix_mode = bool(int(os.environ['ANSIBLE_TEST_FIX_MODE']))
controller_python_versions.remove('3.14') # black does not yet support formatting for Python 3.14
target_python_versions = remote_only_python_versions + controller_python_versions
black(controller_paths, controller_python_versions, fix_mode)

@ -27,12 +27,12 @@ lib/ansible/modules/systemd_service.py validate-modules:parameter-invalid
lib/ansible/modules/user.py validate-modules:doc-default-does-not-match-spec
lib/ansible/modules/user.py validate-modules:use-run-command-not-popen
lib/ansible/module_utils/basic.py pylint:unused-import # deferring resolution to allow enabling the rule now
lib/ansible/module_utils/compat/selinux.py import-3.8!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.9!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.10!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.11!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.12!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.13!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py import-3.14!skip # pass/fail depends on presence of libselinux.so
lib/ansible/module_utils/compat/selinux.py pylint:unidiomatic-typecheck
lib/ansible/module_utils/distro/_distro.py no-assert
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 pslint:PSUseApprovedVerbs
@ -57,6 +57,7 @@ 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
lib/ansible/_internal/_wrapt.py pep8!skip # vendored code
lib/ansible/_internal/_wrapt.py pylint!skip # vendored code
lib/ansible/_internal/ansible_collections/ansible/_protomatter/README.md no-unwanted-files
@ -211,16 +212,17 @@ test/units/module_utils/facts/test_facts.py mypy-3.9:assignment
test/units/modules/mount_facts_data.py mypy-3.9:arg-type
test/units/modules/test_apt.py mypy-3.9:name-match
test/units/modules/test_mount_facts.py mypy-3.9:index
test/units/module_utils/basic/test_exit_json.py mypy-3.8:assignment
test/units/module_utils/basic/test_exit_json.py mypy-3.8:misc
test/units/module_utils/facts/other/test_facter.py mypy-3.8:assignment
test/units/module_utils/facts/other/test_ohai.py mypy-3.8:assignment
test/units/module_utils/facts/system/test_lsb.py mypy-3.8:assignment
test/units/module_utils/facts/test_collectors.py mypy-3.8:assignment
test/units/module_utils/facts/test_facts.py mypy-3.8:assignment
test/units/modules/mount_facts_data.py mypy-3.8:arg-type
test/units/modules/test_apt.py mypy-3.8:name-match
test/units/modules/test_mount_facts.py mypy-3.8:index
test/units/module_utils/basic/test_exit_json.py mypy-3.14:assignment
test/units/module_utils/basic/test_exit_json.py mypy-3.14:misc
test/units/module_utils/facts/other/test_facter.py mypy-3.14:assignment
test/units/module_utils/facts/other/test_ohai.py mypy-3.14:assignment
test/units/module_utils/facts/system/test_lsb.py mypy-3.14:assignment
test/units/module_utils/facts/test_collectors.py mypy-3.14:assignment
test/units/module_utils/facts/test_facts.py mypy-3.14:assignment
test/units/modules/mount_facts_data.py mypy-3.14:arg-type
test/units/modules/test_apt.py mypy-3.14:name-match
test/units/modules/test_mount_facts.py mypy-3.14:index
test/units/playbook/test_base.py mypy-3.14:assignment
test/integration/targets/interpreter_discovery_python/library/test_non_python_interpreter.py shebang # test needs non-standard shebang
test/integration/targets/inventory_script/bad_shebang shebang # test needs an invalid shebang
test/integration/targets/ansible-test-sanity-pylint/ansible_collections/ns/col/plugins/lookup/deprecated.py pylint!skip # validated as a collection

@ -10,6 +10,7 @@ import typing as t
import pytest
from ansible.module_utils._internal import _patches
from ansible.module_utils._internal._patches import _socket_patch
from ansible.module_utils.common._utils import get_all_subclasses
module_to_patch = sys.modules[__name__]
@ -36,6 +37,7 @@ def get_patch_required_test_cases() -> list:
xfail_patch_when: dict[type[_patches.CallablePatch], bool] = {
# Example:
# _patches._some_patch_module.SomePatchClass: sys.version_info >= (3, 13),
_socket_patch.GetAddrInfoPatch: sys.version_info >= (3, 14),
}
patches = sorted(get_all_subclasses(_patches.CallablePatch), key=lambda item: item.__name__)

@ -130,7 +130,7 @@ class TestImmutableDict:
# ImmutableDict is unhashable when one of its values is unhashable
imdict = ImmutableDict({u'café': u'くらとみ', 1: [1, 2]})
expected_reason = r"^unhashable type: 'list'$"
expected_reason = r"unhashable type: 'list'"
with pytest.raises(TypeError, match=expected_reason):
hash(imdict)

@ -4,6 +4,8 @@
from __future__ import annotations
import sys
import pytest
from ansible.module_utils.common.text.converters import to_native
@ -84,4 +86,5 @@ def test_check_required_arguments_missing_none():
def test_check_required_arguments_no_params(arguments_terms):
with pytest.raises(TypeError) as te:
check_required_arguments(arguments_terms, None)
assert "'NoneType' is not iterable" in to_native(te.value)
expected = "argument of type 'NoneType' is not a container or iterable" if sys.version_info >= (3, 14) else "'NoneType' is not iterable"
assert expected in to_native(te.value)

Loading…
Cancel
Save