Add collection config support to ansible-test.

This commit is contained in:
Matt Clay 2021-04-22 11:55:16 -07:00
parent bacede7a2b
commit c4e76a7f80
13 changed files with 404 additions and 95 deletions

View file

@ -24,7 +24,7 @@ recursive-include licenses *.txt
recursive-include packaging * recursive-include packaging *
recursive-include test/ansible_test *.py Makefile recursive-include test/ansible_test *.py Makefile
recursive-include test/integration * recursive-include test/integration *
recursive-include test/lib/ansible_test/config *.template recursive-include test/lib/ansible_test/config *.yml *.template
recursive-include test/lib/ansible_test/_data *.cfg *.ini *.json *.ps1 *.psd1 *.py *.sh *.txt *.yml coveragerc inventory recursive-include test/lib/ansible_test/_data *.cfg *.ini *.json *.ps1 *.psd1 *.py *.sh *.txt *.yml coveragerc inventory
recursive-include test/lib/ansible_test/_data/injector ansible ansible-config ansible-connection ansible-console ansible-doc ansible-galaxy ansible-playbook ansible-pull ansible-test ansible-vault pytest recursive-include test/lib/ansible_test/_data/injector ansible ansible-config ansible-connection ansible-console ansible-doc ansible-galaxy ansible-playbook ansible-pull ansible-test ansible-vault pytest
recursive-include test/lib/ansible_test/_data/sanity/validate-modules validate-modules recursive-include test/lib/ansible_test/_data/sanity/validate-modules validate-modules

View file

@ -0,0 +1,9 @@
minor_changes:
- ansible-test - Add support for an ansible-test configuration file in collections under ``tests/config.yml``.
- ansible-test - Collections can limit the Python versions used for testing their remote-only code (modules/module_utils and related tests).
- ansible-test - Collections can declare their remote-only code (modules/module_utils and related tests) as controller-only.
- ansible-test - Sanity test warnings relating to Python version support have been improved.
major_changes:
- ansible-test - The ``import`` and ``compile`` sanity tests limit remote-only Python version checks to remote-only code.
- ansible-test - The ``future-import-boilerplate`` and ``metaclass-boilerplate`` sanity tests are limited to remote-only code.
Additionally, they are skipped for collections which declare no support for Python 2.x.

View file

@ -2,5 +2,6 @@
"extensions": [ "extensions": [
".py" ".py"
], ],
"py2_compat": true,
"output": "path-message" "output": "path-message"
} }

View file

@ -2,5 +2,6 @@
"extensions": [ "extensions": [
".py" ".py"
], ],
"py2_compat": true,
"output": "path-message" "output": "path-message"
} }

View file

@ -0,0 +1,12 @@
"""Packaging compatibility."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
try:
from packaging.specifiers import SpecifierSet
from packaging.version import Version
PACKAGING_IMPORT_ERROR = None
except ImportError as ex:
SpecifierSet = None
Version = None
PACKAGING_IMPORT_ERROR = ex

View file

@ -0,0 +1,21 @@
"""PyYAML compatibility."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from functools import (
partial,
)
try:
import yaml as _yaml
YAML_IMPORT_ERROR = None
except ImportError as ex:
yaml_load = None # pylint: disable=invalid-name
YAML_IMPORT_ERROR = ex
else:
try:
_SafeLoader = _yaml.CSafeLoader
except AttributeError:
_SafeLoader = _yaml.SafeLoader
yaml_load = partial(_yaml.load, Loader=_SafeLoader)

View file

@ -0,0 +1,155 @@
"""Content configuration."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from . import types as t
from .compat.packaging import (
PACKAGING_IMPORT_ERROR,
SpecifierSet,
Version,
)
from .compat.yaml import (
YAML_IMPORT_ERROR,
yaml_load,
)
from .io import (
read_text_file,
)
from .util import (
ApplicationError,
CONTROLLER_PYTHON_VERSIONS,
SUPPORTED_PYTHON_VERSIONS,
display,
str_to_version,
)
from .data import (
data_context,
)
MISSING = object()
class BaseConfig:
"""Base class for content configuration."""
def __init__(self, data): # type: (t.Any) -> None
if not isinstance(data, dict):
raise Exception('config must be type `dict` not `%s`' % type(data))
class ModulesConfig(BaseConfig):
"""Configuration for modules."""
def __init__(self, data): # type: (t.Any) -> None
super(ModulesConfig, self).__init__(data)
python_requires = data.get('python_requires', MISSING)
if python_requires == MISSING:
raise KeyError('python_requires is required')
self.python_requires = python_requires
self.python_versions = parse_python_requires(python_requires)
self.controller_only = python_requires == 'controller'
class ContentConfig(BaseConfig):
"""Configuration for all content."""
def __init__(self, data): # type: (t.Any) -> None
super(ContentConfig, self).__init__(data)
# Configuration specific to modules/module_utils.
self.modules = ModulesConfig(data.get('modules', {}))
# Python versions supported by the controller, combined with Python versions supported by modules/module_utils.
# Mainly used for display purposes and to limit the Python versions used for sanity tests.
self.python_versions = [version for version in SUPPORTED_PYTHON_VERSIONS
if version in CONTROLLER_PYTHON_VERSIONS or version in self.modules.python_versions]
# True if Python 2.x is supported.
self.py2_support = any(version for version in self.python_versions if str_to_version(version)[0] == 2)
def load_config(path): # type: (str) -> t.Optional[ContentConfig]
"""Load and parse the specified config file and return the result or None if loading/parsing failed."""
if YAML_IMPORT_ERROR:
raise ApplicationError('The "PyYAML" module is required to parse config: %s' % YAML_IMPORT_ERROR)
if PACKAGING_IMPORT_ERROR:
raise ApplicationError('The "packaging" module is required to parse config: %s' % PACKAGING_IMPORT_ERROR)
value = read_text_file(path)
try:
yaml_value = yaml_load(value)
except Exception as ex: # pylint: disable=broad-except
display.warning('Ignoring config "%s" due to a YAML parsing error: %s' % (path, ex))
return None
try:
config = ContentConfig(yaml_value)
except Exception as ex: # pylint: disable=broad-except
display.warning('Ignoring config "%s" due a config parsing error: %s' % (path, ex))
return None
display.info('Loaded configuration: %s' % path, verbosity=1)
return config
def get_content_config(): # type: () -> ContentConfig
"""
Parse and return the content configuration (if any) for the current collection.
For ansible-core, a default configuration is used.
Results are cached.
"""
try:
return get_content_config.config
except AttributeError:
pass
collection_config_path = 'tests/config.yml'
config = None
if data_context().content.collection and os.path.exists(collection_config_path):
config = load_config(collection_config_path)
if not config:
config = ContentConfig(dict(
modules=dict(
python_requires='default',
),
))
get_content_config.config = config
if not config.modules.python_versions:
raise ApplicationError('This collection does not declare support for modules/module_utils on any known Python version.\n'
'Ansible supports modules/module_utils on Python versions: %s\n'
'This collection provides the Python requirement: %s' % (
', '.join(SUPPORTED_PYTHON_VERSIONS), config.modules.python_requires))
return config
def parse_python_requires(value): # type: (t.Any) -> t.List[str]
"""Parse the given 'python_requires' version specifier and return the matching Python versions."""
if not isinstance(value, str):
raise ValueError('python_requires must must be of type `str` not type `%s`' % type(value))
if value == 'default':
versions = list(SUPPORTED_PYTHON_VERSIONS)
elif value == 'controller':
versions = list(CONTROLLER_PYTHON_VERSIONS)
else:
specifier_set = SpecifierSet(value)
versions = [version for version in SUPPORTED_PYTHON_VERSIONS if specifier_set.contains(Version(version))]
return versions

View file

@ -33,6 +33,7 @@ from ..util import (
str_to_version, str_to_version,
SUPPORTED_PYTHON_VERSIONS, SUPPORTED_PYTHON_VERSIONS,
CONTROLLER_PYTHON_VERSIONS, CONTROLLER_PYTHON_VERSIONS,
REMOTE_ONLY_PYTHON_VERSIONS,
) )
from ..util_common import ( from ..util_common import (
@ -74,6 +75,10 @@ from ..data import (
data_context, data_context,
) )
from ..content_config import (
get_content_config,
)
COMMAND = 'sanity' COMMAND = 'sanity'
SANITY_ROOT = os.path.join(ANSIBLE_TEST_DATA_ROOT, 'sanity') SANITY_ROOT = os.path.join(ANSIBLE_TEST_DATA_ROOT, 'sanity')
@ -142,18 +147,21 @@ def command_sanity(args):
options = '' options = ''
if test.supported_python_versions and version not in test.supported_python_versions: if test.supported_python_versions and version not in test.supported_python_versions:
display.warning("Skipping sanity test '%s' on Python %s. Supported Python versions: %s" % ( # There are two ways this situation can occur:
test.name, version, ', '.join(test.supported_python_versions))) #
result = SanitySkipped(test.name, skip_version) # - A specific Python version was requested with the `--python` option and that version is not supported by the test.
elif not args.python and version not in available_versions: # This means that the test supports only a subset of the controller supported Python versions, and not the one given by the `--python` option.
display.warning("Skipping sanity test '%s' on Python %s due to missing interpreter." % (test.name, version)) # Or that a remote-only Python version was specified for a Python based sanity test that is not multi-version.
#
# - No specific Python version was requested and no supported version was found on the system.
# This means that the test supports only a subset of the controller supported Python versions, and not the one used to run ansible-test.
# Or that the Python version used to run ansible-test is not supported by the controller, a condition which will soon not be possible.
#
# Neither of these are affected by the Python versions supported by a collection.
result = SanitySkipped(test.name, skip_version) result = SanitySkipped(test.name, skip_version)
result.reason = "Skipping sanity test '%s' on Python %s. Supported Python versions: %s" % (
test.name, version, ', '.join(test.supported_python_versions))
else: else:
if test.supported_python_versions:
display.info("Running sanity test '%s' with Python %s" % (test.name, version))
else:
display.info("Running sanity test '%s'" % test.name)
if isinstance(test, SanityCodeSmellTest): if isinstance(test, SanityCodeSmellTest):
settings = test.load_processor(args) settings = test.load_processor(args)
elif isinstance(test, SanityMultipleVersion): elif isinstance(test, SanityMultipleVersion):
@ -177,11 +185,27 @@ def command_sanity(args):
all_targets = SanityTargets.filter_and_inject_targets(test, all_targets) all_targets = SanityTargets.filter_and_inject_targets(test, all_targets)
usable_targets = SanityTargets.filter_and_inject_targets(test, usable_targets) usable_targets = SanityTargets.filter_and_inject_targets(test, usable_targets)
usable_targets = sorted(test.filter_targets(list(usable_targets))) usable_targets = sorted(test.filter_targets_by_version(list(usable_targets), version))
usable_targets = settings.filter_skipped_targets(usable_targets) usable_targets = settings.filter_skipped_targets(usable_targets)
sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets)) sanity_targets = SanityTargets(tuple(all_targets), tuple(usable_targets))
if usable_targets or test.no_targets: test_needed = bool(usable_targets or test.no_targets)
result = None
if test_needed and not args.python and version not in available_versions:
# Deferred checking of Python availability. Done here since it is now known to be required for running the test.
# Earlier checking could cause a spurious warning to be generated for a collection which does not support the Python version.
# If the `--python` option was used, this warning will be skipped and an error will be reported when running the test instead.
result = SanitySkipped(test.name, skip_version)
result.reason = "Skipping sanity test '%s' on Python %s due to missing interpreter." % (test.name, version)
if not result:
if test.supported_python_versions:
display.info("Running sanity test '%s' with Python %s" % (test.name, version))
else:
display.info("Running sanity test '%s'" % test.name)
if test_needed and not result:
install_command_requirements(args, version, context=test.name, enable_pyyaml_check=True) install_command_requirements(args, version, context=test.name, enable_pyyaml_check=True)
if isinstance(test, SanityCodeSmellTest): if isinstance(test, SanityCodeSmellTest):
@ -195,6 +219,8 @@ def command_sanity(args):
result = test.test(args, sanity_targets) result = test.test(args, sanity_targets)
else: else:
raise Exception('Unsupported test type: %s' % type(test)) raise Exception('Unsupported test type: %s' % type(test))
elif result:
pass
else: else:
result = SanitySkipped(test.name, skip_version) result = SanitySkipped(test.name, skip_version)
@ -274,13 +300,18 @@ class SanityIgnoreParser:
for test in sanity_get_tests(): for test in sanity_get_tests():
test_targets = SanityTargets.filter_and_inject_targets(test, targets) test_targets = SanityTargets.filter_and_inject_targets(test, targets)
paths_by_test[test.name] = set(target.path for target in test.filter_targets(test_targets))
if isinstance(test, SanityMultipleVersion): if isinstance(test, SanityMultipleVersion):
versioned_test_names.add(test.name) versioned_test_names.add(test.name)
tests_by_name.update(dict(('%s-%s' % (test.name, python_version), test) for python_version in test.supported_python_versions))
for python_version in test.supported_python_versions:
test_name = '%s-%s' % (test.name, python_version)
paths_by_test[test_name] = set(target.path for target in test.filter_targets_by_version(test_targets, python_version))
tests_by_name[test_name] = test
else: else:
unversioned_test_names.update(dict(('%s-%s' % (test.name, python_version), test.name) for python_version in SUPPORTED_PYTHON_VERSIONS)) unversioned_test_names.update(dict(('%s-%s' % (test.name, python_version), test.name) for python_version in SUPPORTED_PYTHON_VERSIONS))
paths_by_test[test.name] = set(target.path for target in test.filter_targets_by_version(test_targets, ''))
tests_by_name[test.name] = test tests_by_name[test.name] = test
for line_no, line in enumerate(lines, start=1): for line_no, line in enumerate(lines, start=1):
@ -347,7 +378,7 @@ class SanityIgnoreParser:
self.parse_errors.append((line_no, 1, "Sanity test '%s' does not support directory paths" % test_name)) self.parse_errors.append((line_no, 1, "Sanity test '%s' does not support directory paths" % test_name))
continue continue
if path not in paths_by_test[test.name] and not test.no_targets: if path not in paths_by_test[test_name] and not test.no_targets:
self.parse_errors.append((line_no, 1, "Sanity test '%s' does not test path '%s'" % (test_name, path))) self.parse_errors.append((line_no, 1, "Sanity test '%s' does not test path '%s'" % (test_name, path)))
continue continue
@ -657,6 +688,11 @@ class SanityTest(ABC):
"""True if the test targets should include symlinks.""" """True if the test targets should include symlinks."""
return False return False
@property
def py2_compat(self): # type: () -> bool
"""True if the test only applies to code that runs on Python 2.x."""
return False
@property @property
def supported_python_versions(self): # type: () -> t.Optional[t.Tuple[str, ...]] def supported_python_versions(self): # type: () -> t.Optional[t.Tuple[str, ...]]
"""A tuple of supported Python versions or None if the test does not depend on specific Python versions.""" """A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
@ -669,6 +705,47 @@ class SanityTest(ABC):
raise NotImplementedError('Sanity test "%s" must implement "filter_targets" or set "no_targets" to True.' % self.name) raise NotImplementedError('Sanity test "%s" must implement "filter_targets" or set "no_targets" to True.' % self.name)
def filter_targets_by_version(self, targets, python_version): # type: (t.List[TestTarget], str) -> t.List[TestTarget]
"""Return the given list of test targets, filtered to include only those relevant for the test, taking into account the Python version."""
del python_version # python_version is not used here, but derived classes may make use of it
targets = self.filter_targets(targets)
if self.py2_compat:
# This sanity test is a Python 2.x compatibility test.
content_config = get_content_config()
if content_config.py2_support:
# This collection supports Python 2.x.
# Filter targets to include only those that require support for remote-only Python versions.
targets = self.filter_remote_targets(targets)
else:
# This collection does not support Python 2.x.
# There are no targets to test.
targets = []
return targets
def filter_remote_targets(self, targets): # type: (t.List[TestTarget]) -> t.List[TestTarget]
"""Return a filtered list of the given targets, including only those that require support for remote-only Python versions."""
targets = [target for target in targets if (
is_subdir(target.path, data_context().content.module_path) or
is_subdir(target.path, data_context().content.module_utils_path) or
is_subdir(target.path, data_context().content.unit_module_path) or
is_subdir(target.path, data_context().content.unit_module_utils_path) or
# include modules/module_utils within integration test library directories
re.search('^%s/.*/library/' % re.escape(data_context().content.integration_targets_path), target.path) or
# special handling for content in ansible-core
(data_context().content.is_ansible and (
# temporary solution until ansible-test code is reorganized when the split controller/remote implementation is complete
is_subdir(target.path, 'test/lib/ansible_test/') or
# integration test support modules/module_utils continue to require support for remote-only Python versions
re.search('^test/support/integration/.*/(modules|module_utils)/', target.path)
))
)]
return targets
class SanityCodeSmellTest(SanityTest): class SanityCodeSmellTest(SanityTest):
"""Sanity test script.""" """Sanity test script."""
@ -701,6 +778,7 @@ class SanityCodeSmellTest(SanityTest):
self.__no_targets = self.config.get('no_targets') # type: bool self.__no_targets = self.config.get('no_targets') # type: bool
self.__include_directories = self.config.get('include_directories') # type: bool self.__include_directories = self.config.get('include_directories') # type: bool
self.__include_symlinks = self.config.get('include_symlinks') # type: bool self.__include_symlinks = self.config.get('include_symlinks') # type: bool
self.__py2_compat = self.config.get('py2_compat', False) # type: bool
else: else:
self.output = None self.output = None
self.extensions = [] self.extensions = []
@ -715,6 +793,7 @@ class SanityCodeSmellTest(SanityTest):
self.__no_targets = True self.__no_targets = True
self.__include_directories = False self.__include_directories = False
self.__include_symlinks = False self.__include_symlinks = False
self.__py2_compat = False
if self.no_targets: if self.no_targets:
mutually_exclusive = ( mutually_exclusive = (
@ -753,6 +832,11 @@ class SanityCodeSmellTest(SanityTest):
"""True if the test targets should include symlinks.""" """True if the test targets should include symlinks."""
return self.__include_symlinks return self.__include_symlinks
@property
def py2_compat(self): # type: () -> bool
"""True if the test only applies to code that runs on Python 2.x."""
return self.__py2_compat
@property @property
def supported_python_versions(self): # type: () -> t.Optional[t.Tuple[str, ...]] def supported_python_versions(self): # type: () -> t.Optional[t.Tuple[str, ...]]
"""A tuple of supported Python versions or None if the test does not depend on specific Python versions.""" """A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
@ -937,6 +1021,25 @@ class SanityMultipleVersion(SanityFunc):
"""A tuple of supported Python versions or None if the test does not depend on specific Python versions.""" """A tuple of supported Python versions or None if the test does not depend on specific Python versions."""
return SUPPORTED_PYTHON_VERSIONS return SUPPORTED_PYTHON_VERSIONS
def filter_targets_by_version(self, targets, python_version): # type: (t.List[TestTarget], str) -> t.List[TestTarget]
"""Return the given list of test targets, filtered to include only those relevant for the test, taking into account the Python version."""
if not python_version:
raise Exception('python_version is required to filter multi-version tests')
targets = super(SanityMultipleVersion, self).filter_targets_by_version(targets, python_version)
if python_version in REMOTE_ONLY_PYTHON_VERSIONS:
content_config = get_content_config()
if python_version not in content_config.modules.python_versions:
# when a remote-only python version is not supported there are no paths to test
return []
# when a remote-only python version is supported, tests must be applied only to targets that support remote-only Python versions
targets = self.filter_remote_targets(targets)
return targets
SANITY_TESTS = ( SANITY_TESTS = (
) )

View file

@ -228,16 +228,29 @@ class TestSuccess(TestResult):
class TestSkipped(TestResult): class TestSkipped(TestResult):
"""Test skipped.""" """Test skipped."""
def __init__(self, command, test, python_version=None):
"""
:type command: str
:type test: str
:type python_version: str
"""
super(TestSkipped, self).__init__(command, test, python_version)
self.reason = None # type: t.Optional[str]
def write_console(self): def write_console(self):
"""Write results to console.""" """Write results to console."""
display.info('No tests applicable.', verbosity=1) if self.reason:
display.warning(self.reason)
else:
display.info('No tests applicable.', verbosity=1)
def write_junit(self, args): def write_junit(self, args):
""" """
:type args: TestConfig :type args: TestConfig
""" """
test_case = self.junit.TestCase(classname=self.command, name=self.name) test_case = self.junit.TestCase(classname=self.command, name=self.name)
test_case.add_skipped_info('No tests applicable.') test_case.add_skipped_info(self.reason or 'No tests applicable.')
self.save_junit(args, test_case) self.save_junit(args, test_case)

View file

@ -60,6 +60,10 @@ from ..executor import (
install_command_requirements, install_command_requirements,
) )
from ..content_config import (
get_content_config,
)
class TestContext: class TestContext:
"""Contexts that unit tests run in based on the type of content.""" """Contexts that unit tests run in based on the type of content."""
@ -80,8 +84,18 @@ def command_units(args):
paths = [target.path for target in include] paths = [target.path for target in include]
module_paths = [path for path in paths if is_subdir(path, data_context().content.unit_module_path)] content_config = get_content_config()
module_utils_paths = [path for path in paths if is_subdir(path, data_context().content.unit_module_utils_path)] supported_remote_python_versions = content_config.modules.python_versions
if content_config.modules.controller_only:
# controller-only collections run modules/module_utils unit tests as controller-only tests
module_paths = []
module_utils_paths = []
else:
# normal collections run modules/module_utils unit tests isolated from controller code due to differences in python version requirements
module_paths = [path for path in paths if is_subdir(path, data_context().content.unit_module_path)]
module_utils_paths = [path for path in paths if is_subdir(path, data_context().content.unit_module_utils_path)]
controller_paths = sorted(path for path in set(paths) - set(module_paths) - set(module_utils_paths)) controller_paths = sorted(path for path in set(paths) - set(module_paths) - set(module_utils_paths))
remote_paths = module_paths or module_utils_paths remote_paths = module_paths or module_utils_paths
@ -96,10 +110,20 @@ def command_units(args):
raise AllTargetsSkipped() raise AllTargetsSkipped()
if args.python and args.python in REMOTE_ONLY_PYTHON_VERSIONS: if args.python and args.python in REMOTE_ONLY_PYTHON_VERSIONS:
if args.python not in supported_remote_python_versions:
display.warning('Python %s is not supported by this collection. Supported Python versions are: %s' % (
args.python, ', '.join(content_config.python_versions)))
raise AllTargetsSkipped()
if not remote_paths: if not remote_paths:
display.warning('Python %s is only supported by module and module_utils unit tests, but none were selected.' % args.python) display.warning('Python %s is only supported by module and module_utils unit tests, but none were selected.' % args.python)
raise AllTargetsSkipped() raise AllTargetsSkipped()
if args.python and args.python not in supported_remote_python_versions and not controller_paths:
display.warning('Python %s is not supported by this collection for modules/module_utils. Supported Python versions are: %s' % (
args.python, ', '.join(supported_remote_python_versions)))
raise AllTargetsSkipped()
if args.delegate: if args.delegate:
raise Delegate(require=changes, exclude=args.exclude) raise Delegate(require=changes, exclude=args.exclude)
@ -118,6 +142,9 @@ def command_units(args):
if test_context == TestContext.controller: if test_context == TestContext.controller:
if version not in CONTROLLER_PYTHON_VERSIONS: if version not in CONTROLLER_PYTHON_VERSIONS:
continue continue
else:
if version not in supported_remote_python_versions:
continue
if not paths: if not paths:
continue continue

View file

@ -0,0 +1,41 @@
# Sample ansible-test configuration file for collections.
# Support for this feature was first added in ansible-core 2.12.
# Use of this file is optional.
# If used, this file must be placed in "tests/config.yml" relative to the base of the collection.
modules:
# Configuration for modules/module_utils.
# These settings do not apply to other content in the collection.
python_requires: default
# Python versions supported by modules/module_utils.
# This setting is required.
#
# Possible values:
#
# - 'default' - All Python versions supported by Ansible.
# This is the default value if no configuration is provided.
# - 'controller' - All Python versions supported by the Ansible controller.
# This indicates the modules/module_utils can only run on the controller.
# Intended for use only with modules/module_utils that depend on ansible-connection, which only runs on the controller.
# Unit tests for modules/module_utils will be permitted to import any Ansible code, instead of only module_utils.
# - SpecifierSet - A PEP 440 specifier set indicating the supported Python versions.
# This is only needed when modules/module_utils do not support all Python versions supported by Ansible.
# It is not necessary to exclude versions which Ansible does not support, as this will be done automatically.
#
# What does this affect?
#
# - Unit tests will be skipped on any unsupported Python version.
# - Sanity tests that are Python version specific will be skipped on any unsupported Python version that is not supported by the controller.
#
# Sanity tests that are Python version specific will always be executed for Python versions supported by the controller, regardless of this setting.
# Reasons for this restriction include, but are not limited to:
#
# - AnsiballZ must be able to AST parse modules/module_utils on the controller, even though they may execute on a managed node.
# - ansible-doc must be able to AST parse modules/module_utils on the controller to display documentation.
# - ansible-test must be able to AST parse modules/module_utils to perform static analysis on them.
# - ansible-test must be able to execute portions of modules/module_utils to validate their argument specs.
#
# These settings only apply to modules/module_utils.
# It is not possible to declare supported Python versions for controller-only code.
# All Python versions supported by the controller must be supported by controller-only code.

View file

@ -6,32 +6,7 @@ examples/scripts/my_test_facts.py shebang # example module but not in a normal m
examples/scripts/my_test_info.py shebang # example module but not in a normal module location examples/scripts/my_test_info.py shebang # example module but not in a normal module location
examples/scripts/upgrade_to_ps3.ps1 pslint:PSCustomUseLiteralPath examples/scripts/upgrade_to_ps3.ps1 pslint:PSCustomUseLiteralPath
examples/scripts/upgrade_to_ps3.ps1 pslint:PSUseApprovedVerbs examples/scripts/upgrade_to_ps3.ps1 pslint:PSUseApprovedVerbs
hacking/build_library/build_ansible/announce.py compile-2.6!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/announce.py compile-2.7!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/announce.py compile-3.5!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_config.py compile-2.6!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_config.py compile-2.7!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_config.py compile-3.5!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_keywords.py compile-2.6!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_keywords.py compile-2.7!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/dump_keywords.py compile-3.5!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/generate_man.py compile-2.6!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/generate_man.py compile-2.7!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/generate_man.py compile-3.5!skip # docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/porting_guide.py compile-2.6!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/porting_guide.py compile-2.7!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/porting_guide.py compile-3.5!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/release_announcement.py compile-2.6!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/release_announcement.py compile-2.7!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/release_announcement.py compile-3.5!skip # release process only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/update_intersphinx.py compile-2.6!skip # release process and docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/update_intersphinx.py compile-2.7!skip # release process and docs build only, 3.6+ required
hacking/build_library/build_ansible/command_plugins/update_intersphinx.py compile-3.5!skip # release process and docs build only, 3.6+ required
hacking/build_library/build_ansible/commands.py compile-2.6!skip # release and docs process only, 3.6+ required
hacking/build_library/build_ansible/commands.py compile-2.7!skip # release and docs process only, 3.6+ required
hacking/build_library/build_ansible/commands.py compile-3.5!skip # release and docs process only, 3.6+ required
lib/ansible/cli/console.py pylint:blacklisted-name lib/ansible/cli/console.py pylint:blacklisted-name
lib/ansible/cli/galaxy.py compile-2.6!skip # 'ansible-galaxy collection' requires 2.7+
lib/ansible/cli/scripts/ansible_cli_stub.py pylint:ansible-deprecated-version lib/ansible/cli/scripts/ansible_cli_stub.py pylint:ansible-deprecated-version
lib/ansible/cli/scripts/ansible_cli_stub.py shebang lib/ansible/cli/scripts/ansible_cli_stub.py shebang
lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang
@ -41,10 +16,6 @@ lib/ansible/executor/powershell/async_watchdog.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/executor/task_queue_manager.py pylint:blacklisted-name lib/ansible/executor/task_queue_manager.py pylint:blacklisted-name
lib/ansible/galaxy/collection/__init__.py compile-2.6!skip # 'ansible-galaxy collection' requires 2.7+
lib/ansible/galaxy/collection/galaxy_api_proxy.py compile-2.6!skip # 'ansible-galaxy collection' requires 2.7+
lib/ansible/galaxy/dependency_resolution/dataclasses.py compile-2.6!skip # 'ansible-galaxy collection' requires 2.7+
lib/ansible/galaxy/dependency_resolution/providers.py compile-2.6!skip # 'ansible-galaxy collection' requires 2.7+
lib/ansible/keyword_desc.yml no-unwanted-files lib/ansible/keyword_desc.yml no-unwanted-files
lib/ansible/module_utils/compat/_selectors2.py future-import-boilerplate # ignore bundled lib/ansible/module_utils/compat/_selectors2.py future-import-boilerplate # ignore bundled
lib/ansible/module_utils/compat/_selectors2.py metaclass-boilerplate # ignore bundled lib/ansible/module_utils/compat/_selectors2.py metaclass-boilerplate # ignore bundled
@ -165,7 +136,6 @@ test/integration/targets/ansible-test/ansible_collections/ns/col/tests/integrati
test/integration/targets/ansible-test/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from # ignore, required for testing test/integration/targets/ansible-test/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from # ignore, required for testing
test/integration/targets/ansible-test/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py pylint:relative-beyond-top-level test/integration/targets/ansible-test/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py pylint:relative-beyond-top-level
test/integration/targets/ansible-test/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level test/integration/targets/ansible-test/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level
test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py future-import-boilerplate # testing Python 2.x implicit relative imports
test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py pylint:relative-beyond-top-level test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py pylint:relative-beyond-top-level
test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py pylint:relative-beyond-top-level test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py pylint:relative-beyond-top-level
test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py pylint:relative-beyond-top-level test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py pylint:relative-beyond-top-level
@ -230,55 +200,13 @@ test/support/integration/plugins/module_utils/postgres.py future-import-boilerpl
test/support/integration/plugins/module_utils/postgres.py metaclass-boilerplate test/support/integration/plugins/module_utils/postgres.py metaclass-boilerplate
test/support/integration/plugins/modules/lvg.py pylint:blacklisted-name test/support/integration/plugins/modules/lvg.py pylint:blacklisted-name
test/support/integration/plugins/modules/timezone.py pylint:blacklisted-name test/support/integration/plugins/modules/timezone.py pylint:blacklisted-name
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/netconf.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/netconf.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/network_agnostic.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/doc_fragments/network_agnostic.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py no-unicode-literals test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py no-unicode-literals
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py pep8:E203 test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/compat/ipaddress.py pep8:E203
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/cfg/base.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/config.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py pylint:unnecessary-comprehension test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/facts/facts.py pylint:unnecessary-comprehension
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/netconf.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/network.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/parsing.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/restconf/restconf.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/restconf/restconf.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py pylint:unnecessary-comprehension test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/netconf/default.py pylint:unnecessary-comprehension
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/doc_fragments/ios.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/module_utils/network/ios/ios.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_command.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py pep8:E501 test/support/network-integration/collections/ansible_collections/cisco/ios/plugins/modules/ios_config.py pep8:E501
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/doc_fragments/vyos.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/doc_fragments/vyos.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/module_utils/network/vyos/vyos.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pep8:E231 test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pep8:E231
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pylint:blacklisted-name test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_command.py pylint:blacklisted-name
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_config.py metaclass-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_facts.py future-import-boilerplate
test/support/network-integration/collections/ansible_collections/vyos/vyos/plugins/modules/vyos_facts.py metaclass-boilerplate
test/support/windows-integration/plugins/modules/async_status.ps1 pslint!skip test/support/windows-integration/plugins/modules/async_status.ps1 pslint!skip
test/support/windows-integration/plugins/modules/setup.ps1 pslint!skip test/support/windows-integration/plugins/modules/setup.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_copy.ps1 pslint!skip test/support/windows-integration/plugins/modules/win_copy.ps1 pslint!skip
@ -303,8 +231,6 @@ test/units/playbook/role/test_role.py pylint:blacklisted-name
test/units/plugins/test_plugins.py pylint:blacklisted-name test/units/plugins/test_plugins.py pylint:blacklisted-name
test/units/template/test_templar.py pylint:blacklisted-name test/units/template/test_templar.py pylint:blacklisted-name
test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py pylint:relative-beyond-top-level test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py pylint:relative-beyond-top-level
test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py future-import-boilerplate # test expects no boilerplate
test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py metaclass-boilerplate # test expects no boilerplate
test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py empty-init # testing that collections don't need inits
test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py empty-init # testing that collections don't need inits
test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py empty-init # testing that collections don't need inits test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py empty-init # testing that collections don't need inits