summaryrefslogtreecommitdiffstats
path: root/test/sanity
diff options
context:
space:
mode:
Diffstat (limited to 'test/sanity')
-rw-r--r--test/sanity/code-smell/ansible-requirements.json7
-rw-r--r--test/sanity/code-smell/ansible-requirements.py29
-rw-r--r--test/sanity/code-smell/ansible-test-future-boilerplate.json10
-rw-r--r--test/sanity/code-smell/ansible-test-future-boilerplate.py59
-rw-r--r--test/sanity/code-smell/deprecated-config.json10
-rw-r--r--test/sanity/code-smell/deprecated-config.py103
-rw-r--r--test/sanity/code-smell/deprecated-config.requirements.in2
-rw-r--r--test/sanity/code-smell/deprecated-config.requirements.txt6
-rw-r--r--test/sanity/code-smell/no-unwanted-files.json7
-rw-r--r--test/sanity/code-smell/no-unwanted-files.py49
-rw-r--r--test/sanity/code-smell/obsolete-files.json17
-rw-r--r--test/sanity/code-smell/obsolete-files.py17
-rw-r--r--test/sanity/code-smell/package-data.json6
-rw-r--r--test/sanity/code-smell/package-data.py185
-rw-r--r--test/sanity/code-smell/package-data.requirements.in8
-rw-r--r--test/sanity/code-smell/package-data.requirements.txt18
-rw-r--r--test/sanity/code-smell/release-names.json4
-rw-r--r--test/sanity/code-smell/release-names.py48
-rw-r--r--test/sanity/code-smell/release-names.requirements.in1
-rw-r--r--test/sanity/code-smell/release-names.requirements.txt4
-rw-r--r--test/sanity/code-smell/required-and-default-attributes.json9
-rw-r--r--test/sanity/code-smell/required-and-default-attributes.py19
-rw-r--r--test/sanity/code-smell/skip.txt2
-rw-r--r--test/sanity/code-smell/test-constraints.json11
-rw-r--r--test/sanity/code-smell/test-constraints.py126
-rw-r--r--test/sanity/code-smell/update-bundled.json8
-rw-r--r--test/sanity/code-smell/update-bundled.py178
-rw-r--r--test/sanity/code-smell/update-bundled.requirements.in1
-rw-r--r--test/sanity/code-smell/update-bundled.requirements.txt3
-rw-r--r--test/sanity/ignore.txt229
30 files changed, 1176 insertions, 0 deletions
diff --git a/test/sanity/code-smell/ansible-requirements.json b/test/sanity/code-smell/ansible-requirements.json
new file mode 100644
index 0000000..b4b7f2b
--- /dev/null
+++ b/test/sanity/code-smell/ansible-requirements.json
@@ -0,0 +1,7 @@
+{
+ "prefixes": [
+ "requirements.txt",
+ "test/lib/ansible_test/_data/requirements/ansible.txt"
+ ],
+ "output": "path-line-column-message"
+}
diff --git a/test/sanity/code-smell/ansible-requirements.py b/test/sanity/code-smell/ansible-requirements.py
new file mode 100644
index 0000000..4d1a652
--- /dev/null
+++ b/test/sanity/code-smell/ansible-requirements.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+import re
+import sys
+
+
+def read_file(path):
+ try:
+ with open(path, 'r') as f:
+ return f.read()
+ except Exception as ex: # pylint: disable=broad-except
+ print('%s:%d:%d: unable to read required file %s' % (path, 0, 0, re.sub(r'\s+', ' ', str(ex))))
+ return None
+
+
+def main():
+ ORIGINAL_FILE = 'requirements.txt'
+ VENDORED_COPY = 'test/lib/ansible_test/_data/requirements/ansible.txt'
+
+ original_requirements = read_file(ORIGINAL_FILE)
+ vendored_requirements = read_file(VENDORED_COPY)
+
+ if original_requirements is not None and vendored_requirements is not None:
+ if original_requirements != vendored_requirements:
+ print('%s:%d:%d: must be identical to %s' % (VENDORED_COPY, 0, 0, ORIGINAL_FILE))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/ansible-test-future-boilerplate.json b/test/sanity/code-smell/ansible-test-future-boilerplate.json
new file mode 100644
index 0000000..ca4c067
--- /dev/null
+++ b/test/sanity/code-smell/ansible-test-future-boilerplate.json
@@ -0,0 +1,10 @@
+{
+ "extensions": [
+ ".py"
+ ],
+ "prefixes": [
+ "test/sanity/",
+ "test/lib/ansible_test/"
+ ],
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/ansible-test-future-boilerplate.py b/test/sanity/code-smell/ansible-test-future-boilerplate.py
new file mode 100644
index 0000000..9a62225
--- /dev/null
+++ b/test/sanity/code-smell/ansible-test-future-boilerplate.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+import ast
+import sys
+
+
+def main():
+ # The following directories contain code which must work under Python 2.x.
+ py2_compat = (
+ 'test/lib/ansible_test/_util/target/',
+ )
+
+ for path in sys.argv[1:] or sys.stdin.read().splitlines():
+ if any(path.startswith(prefix) for prefix in py2_compat):
+ continue
+
+ with open(path, 'rb') as path_fd:
+ lines = path_fd.read().splitlines()
+
+ missing = True
+ if not lines:
+ # Files are allowed to be empty of everything including boilerplate
+ missing = False
+
+ invalid_future = []
+
+ for text in lines:
+ if text == b'from __future__ import annotations':
+ missing = False
+ break
+
+ if text.startswith(b'from __future__ ') or text == b'__metaclass__ = type':
+ invalid_future.append(text.decode())
+
+ if missing:
+ with open(path) as file:
+ contents = file.read()
+
+ # noinspection PyBroadException
+ try:
+ node = ast.parse(contents)
+
+ # files consisting of only assignments have no need for future import boilerplate
+ # the only exception would be division during assignment, but we'll overlook that for simplicity
+ # the most likely case is that of a documentation only python file
+ if all(isinstance(statement, ast.Assign) for statement in node.body):
+ missing = False
+ except Exception: # pylint: disable=broad-except
+ pass # the compile sanity test will report this error
+
+ if missing:
+ print('%s: missing: from __future__ import annotations' % path)
+
+ for text in invalid_future:
+ print('%s: invalid: %s' % (path, text))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/deprecated-config.json b/test/sanity/code-smell/deprecated-config.json
new file mode 100644
index 0000000..4a88486
--- /dev/null
+++ b/test/sanity/code-smell/deprecated-config.json
@@ -0,0 +1,10 @@
+{
+ "all_targets": true,
+ "output": "path-message",
+ "extensions": [
+ ".py"
+ ],
+ "prefixes": [
+ "lib/ansible/"
+ ]
+}
diff --git a/test/sanity/code-smell/deprecated-config.py b/test/sanity/code-smell/deprecated-config.py
new file mode 100644
index 0000000..474628a
--- /dev/null
+++ b/test/sanity/code-smell/deprecated-config.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# (c) 2018, Matt Martz <matt@sivel.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import annotations
+
+import mmap
+import os
+import re
+import sys
+
+from ansible.module_utils.compat.version import StrictVersion
+
+import yaml
+
+import ansible.config
+
+from ansible.plugins.loader import fragment_loader
+from ansible.release import __version__ as ansible_version
+from ansible.utils.plugin_docs import get_docstring
+
+DOC_RE = re.compile(b'^DOCUMENTATION', flags=re.M)
+ANSIBLE_MAJOR = StrictVersion('.'.join(ansible_version.split('.')[:2]))
+
+
+def find_deprecations(obj, path=None):
+ if not isinstance(obj, (list, dict)):
+ return
+
+ try:
+ items = obj.items()
+ except AttributeError:
+ items = enumerate(obj)
+
+ for key, value in items:
+ if path is None:
+ this_path = []
+ else:
+ this_path = path[:]
+
+ this_path.append(key)
+
+ if key != 'deprecated':
+ for result in find_deprecations(value, path=this_path):
+ yield result
+ else:
+ try:
+ version = value['version']
+ this_path.append('version')
+ except KeyError:
+ version = value['removed_in']
+ this_path.append('removed_in')
+ if StrictVersion(version) <= ANSIBLE_MAJOR:
+ yield (this_path, version)
+
+
+def main():
+ plugins = []
+ for path in sys.argv[1:] or sys.stdin.read().splitlines():
+ with open(path, 'rb') as f:
+ try:
+ mm_file = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
+ except ValueError:
+ continue
+ if DOC_RE.search(mm_file):
+ plugins.append(path)
+ mm_file.close()
+
+ for plugin in plugins:
+ data = {}
+ data['doc'], data['examples'], data['return'], data['metadata'] = get_docstring(plugin, fragment_loader)
+ for result in find_deprecations(data['doc']):
+ print(
+ '%s: %s is scheduled for removal in %s' % (plugin, '.'.join(str(i) for i in result[0][:-2]), result[1])
+ )
+
+ base = os.path.join(os.path.dirname(ansible.config.__file__), 'base.yml')
+ root_path = os.path.dirname(os.path.dirname(os.path.dirname(ansible.__file__)))
+ relative_base = os.path.relpath(base, root_path)
+
+ with open(base) as f:
+ data = yaml.safe_load(f)
+
+ for result in find_deprecations(data):
+ print('%s: %s is scheduled for removal in %s' % (relative_base, '.'.join(str(i) for i in result[0][:-2]), result[1]))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/deprecated-config.requirements.in b/test/sanity/code-smell/deprecated-config.requirements.in
new file mode 100644
index 0000000..859c4ee
--- /dev/null
+++ b/test/sanity/code-smell/deprecated-config.requirements.in
@@ -0,0 +1,2 @@
+jinja2 # ansible-core requirement
+pyyaml
diff --git a/test/sanity/code-smell/deprecated-config.requirements.txt b/test/sanity/code-smell/deprecated-config.requirements.txt
new file mode 100644
index 0000000..338e3f3
--- /dev/null
+++ b/test/sanity/code-smell/deprecated-config.requirements.txt
@@ -0,0 +1,6 @@
+# edit "deprecated-config.requirements.in" and generate with: hacking/update-sanity-requirements.py --test deprecated-config
+# pre-build requirement: pyyaml == 6.0
+# pre-build constraint: Cython < 3.0
+Jinja2==3.1.2
+MarkupSafe==2.1.1
+PyYAML==6.0
diff --git a/test/sanity/code-smell/no-unwanted-files.json b/test/sanity/code-smell/no-unwanted-files.json
new file mode 100644
index 0000000..7a89ebb
--- /dev/null
+++ b/test/sanity/code-smell/no-unwanted-files.json
@@ -0,0 +1,7 @@
+{
+ "include_symlinks": true,
+ "prefixes": [
+ "lib/"
+ ],
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/no-unwanted-files.py b/test/sanity/code-smell/no-unwanted-files.py
new file mode 100644
index 0000000..7e13f53
--- /dev/null
+++ b/test/sanity/code-smell/no-unwanted-files.py
@@ -0,0 +1,49 @@
+"""Prevent unwanted files from being added to the source tree."""
+from __future__ import annotations
+
+import os
+import sys
+
+
+def main():
+ """Main entry point."""
+ paths = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ allowed_extensions = (
+ '.cs',
+ '.ps1',
+ '.psm1',
+ '.py',
+ )
+
+ skip_paths = set([
+ 'lib/ansible/config/ansible_builtin_runtime.yml', # not included in the sanity ignore file since it won't exist until after migration
+ ])
+
+ skip_directories = (
+ 'lib/ansible/galaxy/data/',
+ )
+
+ allow_yaml = ('lib/ansible/plugins/test', 'lib/ansible/plugins/filter')
+
+ for path in paths:
+ if path in skip_paths:
+ continue
+
+ if any(path.startswith(skip_directory) for skip_directory in skip_directories):
+ continue
+
+ if path.startswith('lib/') and not path.startswith('lib/ansible/'):
+ print('%s: all "lib" content must reside in the "lib/ansible" directory' % path)
+ continue
+
+ ext = os.path.splitext(path)[1]
+ if ext in ('.yml', ) and any(path.startswith(yaml_directory) for yaml_directory in allow_yaml):
+ continue
+
+ if ext not in allowed_extensions:
+ print('%s: extension must be one of: %s' % (path, ', '.join(allowed_extensions)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/obsolete-files.json b/test/sanity/code-smell/obsolete-files.json
new file mode 100644
index 0000000..02d3920
--- /dev/null
+++ b/test/sanity/code-smell/obsolete-files.json
@@ -0,0 +1,17 @@
+{
+ "include_symlinks": true,
+ "prefixes": [
+ "test/runner/",
+ "test/sanity/ansible-doc/",
+ "test/sanity/compile/",
+ "test/sanity/import/",
+ "test/sanity/pep8/",
+ "test/sanity/pslint/",
+ "test/sanity/pylint/",
+ "test/sanity/rstcheck/",
+ "test/sanity/shellcheck/",
+ "test/sanity/validate-modules/",
+ "test/sanity/yamllint/"
+ ],
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/obsolete-files.py b/test/sanity/code-smell/obsolete-files.py
new file mode 100644
index 0000000..3c1a4a4
--- /dev/null
+++ b/test/sanity/code-smell/obsolete-files.py
@@ -0,0 +1,17 @@
+"""Prevent files from being added to directories that are now obsolete."""
+from __future__ import annotations
+
+import os
+import sys
+
+
+def main():
+ """Main entry point."""
+ paths = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ for path in paths:
+ print('%s: directory "%s/" is obsolete and should not contain any files' % (path, os.path.dirname(path)))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/package-data.json b/test/sanity/code-smell/package-data.json
new file mode 100644
index 0000000..f7ecd01
--- /dev/null
+++ b/test/sanity/code-smell/package-data.json
@@ -0,0 +1,6 @@
+{
+ "disabled": true,
+ "all_targets": true,
+ "include_symlinks": true,
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/package-data.py b/test/sanity/code-smell/package-data.py
new file mode 100644
index 0000000..7a81b75
--- /dev/null
+++ b/test/sanity/code-smell/package-data.py
@@ -0,0 +1,185 @@
+"""Verify the contents of the built sdist and wheel."""
+from __future__ import annotations
+
+import contextlib
+import fnmatch
+import os
+import pathlib
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import typing as t
+import zipfile
+
+from ansible.release import __version__
+
+
+def collect_sdist_files(complete_file_list: list[str]) -> list[str]:
+ """Return a list of files which should be present in the sdist."""
+ ignore_patterns = (
+ '.azure-pipelines/*',
+ '.cherry_picker.toml',
+ '.git*',
+ '.mailmap',
+ 'changelogs/README.md',
+ 'changelogs/config.yaml',
+ 'changelogs/fragments/*',
+ 'hacking/*',
+ )
+
+ sdist_files = [path for path in complete_file_list if not any(fnmatch.fnmatch(path, ignore) for ignore in ignore_patterns)]
+
+ egg_info = (
+ 'PKG-INFO',
+ 'SOURCES.txt',
+ 'dependency_links.txt',
+ 'entry_points.txt',
+ 'not-zip-safe',
+ 'requires.txt',
+ 'top_level.txt',
+ )
+
+ sdist_files.append('PKG-INFO')
+ sdist_files.extend(f'lib/ansible_core.egg-info/{name}' for name in egg_info)
+
+ return sdist_files
+
+
+def collect_wheel_files(complete_file_list: list[str]) -> list[str]:
+ """Return a list of files which should be present in the wheel."""
+ wheel_files = []
+
+ for path in complete_file_list:
+ if path.startswith('lib/ansible/'):
+ prefix = 'lib'
+ elif path.startswith('test/lib/ansible_test/'):
+ prefix = 'test/lib'
+ else:
+ continue
+
+ wheel_files.append(os.path.relpath(path, prefix))
+
+ dist_info = (
+ 'COPYING',
+ 'METADATA',
+ 'RECORD',
+ 'WHEEL',
+ 'entry_points.txt',
+ 'top_level.txt',
+ )
+
+ wheel_files.append(f'ansible_core-{__version__}.data/scripts/ansible-test')
+ wheel_files.extend(f'ansible_core-{__version__}.dist-info/{name}' for name in dist_info)
+
+ return wheel_files
+
+
+@contextlib.contextmanager
+def clean_repository(complete_file_list: list[str]) -> t.Generator[str, None, None]:
+ """Copy the files to a temporary directory and yield the path."""
+ directories = sorted(set(os.path.dirname(path) for path in complete_file_list))
+ directories.remove('')
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ for directory in directories:
+ os.makedirs(os.path.join(temp_dir, directory))
+
+ for path in complete_file_list:
+ shutil.copy2(path, os.path.join(temp_dir, path), follow_symlinks=False)
+
+ yield temp_dir
+
+
+def build(source_dir: str, tmp_dir: str) -> tuple[pathlib.Path, pathlib.Path]:
+ """Create a sdist and wheel."""
+ create = subprocess.run(
+ [sys.executable, '-m', 'build', '--no-isolation', '--outdir', tmp_dir],
+ stdin=subprocess.DEVNULL,
+ capture_output=True,
+ text=True,
+ check=False,
+ cwd=source_dir,
+ )
+
+ if create.returncode != 0:
+ raise RuntimeError(f'build failed:\n{create.stderr}\n{create.stdout}')
+
+ tmp_dir_files = list(pathlib.Path(tmp_dir).iterdir())
+
+ if len(tmp_dir_files) != 2:
+ raise RuntimeError(f'build resulted in {len(tmp_dir_files)} items instead of 2')
+
+ sdist_path = [path for path in tmp_dir_files if path.suffix == '.gz'][0]
+ wheel_path = [path for path in tmp_dir_files if path.suffix == '.whl'][0]
+
+ return sdist_path, wheel_path
+
+
+def list_sdist(path: pathlib.Path) -> list[str]:
+ """Return a list of the files in the sdist."""
+ item: tarfile.TarInfo
+
+ with tarfile.open(path) as sdist:
+ paths = ['/'.join(pathlib.Path(item.path).parts[1:]) for item in sdist.getmembers() if not item.isdir()]
+
+ return paths
+
+
+def list_wheel(path: pathlib.Path) -> list[str]:
+ """Return a list of the files in the wheel."""
+ with zipfile.ZipFile(path) as wheel:
+ paths = [item.filename for item in wheel.filelist if not item.is_dir()]
+
+ return paths
+
+
+def check_files(source: str, expected: list[str], actual: list[str]) -> list[str]:
+ """Verify the expected files exist and no extra files exist."""
+ missing = sorted(set(expected) - set(actual))
+ extra = sorted(set(actual) - set(expected))
+
+ errors = (
+ [f'{path}: missing from {source}' for path in missing] +
+ [f'{path}: unexpected in {source}' for path in extra]
+ )
+
+ return errors
+
+
+def main() -> None:
+ """Main program entry point."""
+ complete_file_list = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ errors = []
+
+ # Limit visible files to those reported by ansible-test.
+ # This avoids including files which are not committed to git.
+ with clean_repository(complete_file_list) as clean_repo_dir:
+ if __version__.endswith('.dev0'):
+ # Make sure a changelog exists for this version when testing from devel.
+ # When testing from a stable branch the changelog will already exist.
+ major_minor_version = '.'.join(__version__.split('.')[:2])
+ changelog_path = f'changelogs/CHANGELOG-v{major_minor_version}.rst'
+ pathlib.Path(clean_repo_dir, changelog_path).touch()
+ complete_file_list.append(changelog_path)
+
+ expected_sdist_files = collect_sdist_files(complete_file_list)
+ expected_wheel_files = collect_wheel_files(complete_file_list)
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ sdist_path, wheel_path = build(clean_repo_dir, tmp_dir)
+
+ actual_sdist_files = list_sdist(sdist_path)
+ actual_wheel_files = list_wheel(wheel_path)
+
+ errors.extend(check_files('sdist', expected_sdist_files, actual_sdist_files))
+ errors.extend(check_files('wheel', expected_wheel_files, actual_wheel_files))
+
+ for error in errors:
+ print(error)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/package-data.requirements.in b/test/sanity/code-smell/package-data.requirements.in
new file mode 100644
index 0000000..3162feb
--- /dev/null
+++ b/test/sanity/code-smell/package-data.requirements.in
@@ -0,0 +1,8 @@
+build # required to build sdist
+wheel # required to build wheel
+jinja2
+pyyaml # ansible-core requirement
+resolvelib < 0.9.0
+rstcheck < 4 # match version used in other sanity tests
+antsibull-changelog
+setuptools == 45.2.0 # minimum supported setuptools
diff --git a/test/sanity/code-smell/package-data.requirements.txt b/test/sanity/code-smell/package-data.requirements.txt
new file mode 100644
index 0000000..b66079d
--- /dev/null
+++ b/test/sanity/code-smell/package-data.requirements.txt
@@ -0,0 +1,18 @@
+# edit "package-data.requirements.in" and generate with: hacking/update-sanity-requirements.py --test package-data
+# pre-build requirement: pyyaml == 6.0
+# pre-build constraint: Cython < 3.0
+antsibull-changelog==0.16.0
+build==0.10.0
+docutils==0.17.1
+Jinja2==3.1.2
+MarkupSafe==2.1.1
+packaging==21.3
+pyproject_hooks==1.0.0
+pyparsing==3.0.9
+PyYAML==6.0
+resolvelib==0.8.1
+rstcheck==3.5.0
+semantic-version==2.10.0
+setuptools==45.2.0
+tomli==2.0.1
+wheel==0.41.0
diff --git a/test/sanity/code-smell/release-names.json b/test/sanity/code-smell/release-names.json
new file mode 100644
index 0000000..593b765
--- /dev/null
+++ b/test/sanity/code-smell/release-names.json
@@ -0,0 +1,4 @@
+{
+ "no_targets": true,
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/release-names.py b/test/sanity/code-smell/release-names.py
new file mode 100644
index 0000000..81d90d8
--- /dev/null
+++ b/test/sanity/code-smell/release-names.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# (c) 2019, Ansible Project
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+"""
+Test that the release name is present in the list of used up release names
+"""
+
+
+from __future__ import annotations
+
+from yaml import safe_load
+
+from ansible.release import __codename__
+
+
+def main():
+ """Entrypoint to the script"""
+
+ with open('.github/RELEASE_NAMES.yml') as f:
+ releases = safe_load(f.read())
+
+ # Why this format? The file's sole purpose is to be read by a human when they need to know
+ # which release names have already been used. So:
+ # 1) It's easier for a human to find the release names when there's one on each line
+ # 2) It helps keep other people from using the file and then asking for new features in it
+ for name in (r.split(maxsplit=1)[1] for r in releases):
+ if __codename__ == name:
+ break
+ else:
+ print('.github/RELEASE_NAMES.yml: Current codename was not present in the file')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/release-names.requirements.in b/test/sanity/code-smell/release-names.requirements.in
new file mode 100644
index 0000000..c3726e8
--- /dev/null
+++ b/test/sanity/code-smell/release-names.requirements.in
@@ -0,0 +1 @@
+pyyaml
diff --git a/test/sanity/code-smell/release-names.requirements.txt b/test/sanity/code-smell/release-names.requirements.txt
new file mode 100644
index 0000000..bb6a130
--- /dev/null
+++ b/test/sanity/code-smell/release-names.requirements.txt
@@ -0,0 +1,4 @@
+# edit "release-names.requirements.in" and generate with: hacking/update-sanity-requirements.py --test release-names
+# pre-build requirement: pyyaml == 6.0
+# pre-build constraint: Cython < 3.0
+PyYAML==6.0
diff --git a/test/sanity/code-smell/required-and-default-attributes.json b/test/sanity/code-smell/required-and-default-attributes.json
new file mode 100644
index 0000000..dd9ac7b
--- /dev/null
+++ b/test/sanity/code-smell/required-and-default-attributes.json
@@ -0,0 +1,9 @@
+{
+ "prefixes": [
+ "lib/ansible/"
+ ],
+ "extensions": [
+ ".py"
+ ],
+ "output": "path-line-column-message"
+}
diff --git a/test/sanity/code-smell/required-and-default-attributes.py b/test/sanity/code-smell/required-and-default-attributes.py
new file mode 100644
index 0000000..900829d
--- /dev/null
+++ b/test/sanity/code-smell/required-and-default-attributes.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import re
+import sys
+
+
+def main():
+ for path in sys.argv[1:] or sys.stdin.read().splitlines():
+ with open(path, 'r') as path_fd:
+ for line, text in enumerate(path_fd.readlines()):
+ match = re.search(r'(FieldAttribute.*(default|required).*(default|required))', text)
+
+ if match:
+ print('%s:%d:%d: use only one of `default` or `required` with `FieldAttribute`' % (
+ path, line + 1, match.start(1) + 1))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/skip.txt b/test/sanity/code-smell/skip.txt
new file mode 100644
index 0000000..6fb327b
--- /dev/null
+++ b/test/sanity/code-smell/skip.txt
@@ -0,0 +1,2 @@
+deprecated-config.py # disabled by default, to be enabled by the release manager after branching
+update-bundled.py # disabled by default, to be enabled by the release manager after branching
diff --git a/test/sanity/code-smell/test-constraints.json b/test/sanity/code-smell/test-constraints.json
new file mode 100644
index 0000000..8f47beb
--- /dev/null
+++ b/test/sanity/code-smell/test-constraints.json
@@ -0,0 +1,11 @@
+{
+ "all_targets": true,
+ "prefixes": [
+ "test/lib/ansible_test/_data/requirements/",
+ "test/sanity/code-smell/"
+ ],
+ "extensions": [
+ ".txt"
+ ],
+ "output": "path-line-column-message"
+}
diff --git a/test/sanity/code-smell/test-constraints.py b/test/sanity/code-smell/test-constraints.py
new file mode 100644
index 0000000..df30fe1
--- /dev/null
+++ b/test/sanity/code-smell/test-constraints.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import os
+import pathlib
+import re
+import sys
+
+
+def main():
+ constraints_path = 'test/lib/ansible_test/_data/requirements/constraints.txt'
+
+ requirements = {}
+
+ for path in sys.argv[1:] or sys.stdin.read().splitlines():
+ if path == 'test/lib/ansible_test/_data/requirements/ansible.txt':
+ # This file is an exact copy of the ansible requirements.txt and should not conflict with other constraints.
+ continue
+
+ with open(path, 'r') as path_fd:
+ requirements[path] = parse_requirements(path_fd.read().splitlines())
+
+ if path == 'test/lib/ansible_test/_data/requirements/ansible-test.txt':
+ # Special handling is required for ansible-test's requirements file.
+ check_ansible_test(path, requirements.pop(path))
+ continue
+
+ frozen_sanity = {}
+ non_sanity_requirements = set()
+
+ for path, requirements in requirements.items():
+ filename = os.path.basename(path)
+
+ is_sanity = filename.startswith('sanity.') or filename.endswith('.requirements.txt')
+ is_constraints = path == constraints_path
+
+ for lineno, line, requirement in requirements:
+ if not requirement:
+ print('%s:%d:%d: cannot parse requirement: %s' % (path, lineno, 1, line))
+ continue
+
+ name = requirement.group('name').lower()
+ raw_constraints = requirement.group('constraints')
+ constraints = raw_constraints.strip()
+ comment = requirement.group('comment')
+
+ is_pinned = re.search('^ *== *[0-9.]+(\\.post[0-9]+)?$', constraints)
+
+ if is_sanity:
+ sanity = frozen_sanity.setdefault(name, [])
+ sanity.append((path, lineno, line, requirement))
+ elif not is_constraints:
+ non_sanity_requirements.add(name)
+
+ if is_sanity:
+ if not is_pinned:
+ # sanity test requirements must be pinned
+ print('%s:%d:%d: sanity test requirement (%s%s) must be frozen (use `==`)' % (path, lineno, 1, name, raw_constraints))
+
+ continue
+
+ if constraints and not is_constraints:
+ allow_constraints = 'sanity_ok' in comment
+
+ if not allow_constraints:
+ # keeping constraints for tests other than sanity tests in one file helps avoid conflicts
+ print('%s:%d:%d: put the constraint (%s%s) in `%s`' % (path, lineno, 1, name, raw_constraints, constraints_path))
+
+ for name, requirements in frozen_sanity.items():
+ if len(set(req[3].group('constraints').strip() for req in requirements)) != 1:
+ for req in requirements:
+ print('%s:%d:%d: sanity constraint (%s) does not match others for package `%s`' % (
+ req[0], req[1], req[3].start('constraints') + 1, req[3].group('constraints'), name))
+
+
+def check_ansible_test(path: str, requirements: list[tuple[int, str, re.Match]]) -> None:
+ sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent.joinpath('lib')))
+
+ from ansible_test._internal.python_requirements import VIRTUALENV_VERSION
+ from ansible_test._internal.coverage_util import COVERAGE_VERSIONS
+ from ansible_test._internal.util import version_to_str
+
+ expected_lines = set([
+ f"virtualenv == {VIRTUALENV_VERSION} ; python_version < '3'",
+ ] + [
+ f"coverage == {item.coverage_version} ; python_version >= '{version_to_str(item.min_python)}' and python_version <= '{version_to_str(item.max_python)}'"
+ for item in COVERAGE_VERSIONS
+ ])
+
+ for idx, requirement in enumerate(requirements):
+ lineno, line, match = requirement
+
+ if line in expected_lines:
+ expected_lines.remove(line)
+ continue
+
+ print('%s:%d:%d: unexpected line: %s' % (path, lineno, 1, line))
+
+ for expected_line in sorted(expected_lines):
+ print('%s:%d:%d: missing line: %s' % (path, requirements[-1][0] + 1, 1, expected_line))
+
+
+def parse_requirements(lines):
+ # see https://www.python.org/dev/peps/pep-0508/#names
+ pattern = re.compile(r'^(?P<name>[A-Z0-9][A-Z0-9._-]*[A-Z0-9]|[A-Z0-9])(?P<extras> *\[[^]]*])?(?P<constraints>[^;#]*)(?P<markers>[^#]*)(?P<comment>.*)$',
+ re.IGNORECASE)
+
+ matches = [(lineno, line, pattern.search(line)) for lineno, line in enumerate(lines, start=1)]
+ requirements = []
+
+ for lineno, line, match in matches:
+ if not line.strip():
+ continue
+
+ if line.strip().startswith('#'):
+ continue
+
+ if line.startswith('git+https://'):
+ continue # hack to ignore git requirements
+
+ requirements.append((lineno, line, match))
+
+ return requirements
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/update-bundled.json b/test/sanity/code-smell/update-bundled.json
new file mode 100644
index 0000000..379bf4d
--- /dev/null
+++ b/test/sanity/code-smell/update-bundled.json
@@ -0,0 +1,8 @@
+{
+ "all_targets": true,
+ "ignore_self": true,
+ "extensions": [
+ ".py"
+ ],
+ "output": "path-message"
+}
diff --git a/test/sanity/code-smell/update-bundled.py b/test/sanity/code-smell/update-bundled.py
new file mode 100644
index 0000000..4bad77a
--- /dev/null
+++ b/test/sanity/code-smell/update-bundled.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+# (c) 2018, Ansible Project
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+"""
+This test checks whether the libraries we're bundling are out of date and need to be synced with
+a newer upstream release.
+"""
+
+
+from __future__ import annotations
+
+import fnmatch
+import json
+import re
+import sys
+from ansible.module_utils.compat.version import LooseVersion
+
+import packaging.specifiers
+
+from ansible.module_utils.urls import open_url
+
+
+BUNDLED_RE = re.compile(b'\\b_BUNDLED_METADATA\\b')
+
+
+def get_bundled_libs(paths):
+ """
+ Return the set of known bundled libraries
+
+ :arg paths: The paths which the test has been instructed to check
+ :returns: The list of all files which we know to contain bundled libraries. If a bundled
+ library consists of multiple files, this should be the file which has metadata included.
+ """
+ bundled_libs = set()
+ for filename in fnmatch.filter(paths, 'lib/ansible/compat/*/__init__.py'):
+ bundled_libs.add(filename)
+
+ bundled_libs.add('lib/ansible/module_utils/compat/selectors.py')
+ bundled_libs.add('lib/ansible/module_utils/distro/__init__.py')
+ bundled_libs.add('lib/ansible/module_utils/six/__init__.py')
+ # backports.ssl_match_hostname should be moved to its own file in the future
+ bundled_libs.add('lib/ansible/module_utils/urls.py')
+
+ return bundled_libs
+
+
+def get_files_with_bundled_metadata(paths):
+ """
+ Search for any files which have bundled metadata inside of them
+
+ :arg paths: Iterable of filenames to search for metadata inside of
+ :returns: A set of pathnames which contained metadata
+ """
+
+ with_metadata = set()
+ for path in paths:
+ with open(path, 'rb') as f:
+ body = f.read()
+
+ if BUNDLED_RE.search(body):
+ with_metadata.add(path)
+
+ return with_metadata
+
+
+def get_bundled_metadata(filename):
+ """
+ Retrieve the metadata about a bundled library from a python file
+
+ :arg filename: The filename to look inside for the metadata
+ :raises ValueError: If we're unable to extract metadata from the file
+ :returns: The metadata from the python file
+ """
+ with open(filename, 'r') as module:
+ for line in module:
+ if line.strip().startswith('# NOT_BUNDLED'):
+ return None
+
+ if line.strip().startswith('# CANT_UPDATE'):
+ print(
+ '{0} marked as CANT_UPDATE, so skipping. Manual '
+ 'check for CVEs required.'.format(filename))
+ return None
+
+ if line.strip().startswith('_BUNDLED_METADATA'):
+ data = line[line.index('{'):].strip()
+ break
+ else:
+ raise ValueError('Unable to check bundled library for update. Please add'
+ ' _BUNDLED_METADATA dictionary to the library file with'
+ ' information on pypi name and bundled version.')
+ metadata = json.loads(data)
+ return metadata
+
+
+def get_latest_applicable_version(pypi_data, constraints=None):
+ """Get the latest pypi version of the package that we allow
+
+ :arg pypi_data: Pypi information about the data as returned by
+ ``https://pypi.org/pypi/{pkg_name}/json``
+ :kwarg constraints: version constraints on what we're allowed to use as specified by
+ the bundled metadata
+ :returns: The most recent version on pypi that are allowed by ``constraints``
+ """
+ latest_version = "0"
+ if constraints:
+ version_specification = packaging.specifiers.SpecifierSet(constraints)
+ for version in pypi_data['releases']:
+ if version in version_specification:
+ if LooseVersion(version) > LooseVersion(latest_version):
+ latest_version = version
+ else:
+ latest_version = pypi_data['info']['version']
+
+ return latest_version
+
+
+def main():
+ """Entrypoint to the script"""
+
+ paths = sys.argv[1:] or sys.stdin.read().splitlines()
+
+ bundled_libs = get_bundled_libs(paths)
+ files_with_bundled_metadata = get_files_with_bundled_metadata(paths)
+
+ for filename in files_with_bundled_metadata.difference(bundled_libs):
+ if filename.startswith('test/support/'):
+ continue # bundled support code does not need to be updated or tracked
+
+ print('{0}: ERROR: File contains _BUNDLED_METADATA but needs to be added to'
+ ' test/sanity/code-smell/update-bundled.py'.format(filename))
+
+ for filename in bundled_libs:
+ try:
+ metadata = get_bundled_metadata(filename)
+ except ValueError as e:
+ print('{0}: ERROR: {1}'.format(filename, e))
+ continue
+ except (IOError, OSError) as e:
+ if e.errno == 2:
+ print('{0}: ERROR: {1}. Perhaps the bundled library has been removed'
+ ' or moved and the bundled library test needs to be modified as'
+ ' well?'.format(filename, e))
+
+ if metadata is None:
+ continue
+
+ pypi_fh = open_url('https://pypi.org/pypi/{0}/json'.format(metadata['pypi_name']))
+ pypi_data = json.loads(pypi_fh.read().decode('utf-8'))
+
+ constraints = metadata.get('version_constraints', None)
+ latest_version = get_latest_applicable_version(pypi_data, constraints)
+
+ if LooseVersion(metadata['version']) < LooseVersion(latest_version):
+ print('{0}: UPDATE {1} from {2} to {3} {4}'.format(
+ filename,
+ metadata['pypi_name'],
+ metadata['version'],
+ latest_version,
+ 'https://pypi.org/pypi/{0}/json'.format(metadata['pypi_name'])))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/sanity/code-smell/update-bundled.requirements.in b/test/sanity/code-smell/update-bundled.requirements.in
new file mode 100644
index 0000000..748809f
--- /dev/null
+++ b/test/sanity/code-smell/update-bundled.requirements.in
@@ -0,0 +1 @@
+packaging
diff --git a/test/sanity/code-smell/update-bundled.requirements.txt b/test/sanity/code-smell/update-bundled.requirements.txt
new file mode 100644
index 0000000..d9785e7
--- /dev/null
+++ b/test/sanity/code-smell/update-bundled.requirements.txt
@@ -0,0 +1,3 @@
+# edit "update-bundled.requirements.in" and generate with: hacking/update-sanity-requirements.py --test update-bundled
+packaging==21.3
+pyparsing==3.0.9
diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt
new file mode 100644
index 0000000..869522b
--- /dev/null
+++ b/test/sanity/ignore.txt
@@ -0,0 +1,229 @@
+.azure-pipelines/scripts/publish-codecov.py replace-urlopen
+lib/ansible/cli/scripts/ansible_connection_cli_stub.py shebang
+lib/ansible/config/base.yml no-unwanted-files
+lib/ansible/executor/playbook_executor.py pylint:disallowed-name
+lib/ansible/executor/powershell/async_watchdog.ps1 pslint:PSCustomUseLiteralPath
+lib/ansible/executor/powershell/async_wrapper.ps1 pslint:PSCustomUseLiteralPath
+lib/ansible/executor/powershell/exec_wrapper.ps1 pslint:PSCustomUseLiteralPath
+lib/ansible/executor/task_queue_manager.py pylint:disallowed-name
+lib/ansible/keyword_desc.yml no-unwanted-files
+lib/ansible/modules/apt.py validate-modules:parameter-invalid
+lib/ansible/modules/apt_repository.py validate-modules:parameter-invalid
+lib/ansible/modules/assemble.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/async_status.py use-argspec-type-path
+lib/ansible/modules/async_status.py validate-modules!skip
+lib/ansible/modules/async_wrapper.py ansible-doc!skip # not an actual module
+lib/ansible/modules/async_wrapper.py pylint:ansible-bad-function # ignore, required
+lib/ansible/modules/async_wrapper.py use-argspec-type-path
+lib/ansible/modules/blockinfile.py validate-modules:doc-choices-do-not-match-spec
+lib/ansible/modules/blockinfile.py validate-modules:doc-default-does-not-match-spec
+lib/ansible/modules/command.py validate-modules:doc-default-does-not-match-spec # _uses_shell is undocumented
+lib/ansible/modules/command.py validate-modules:doc-missing-type
+lib/ansible/modules/command.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/command.py validate-modules:undocumented-parameter
+lib/ansible/modules/copy.py pylint:disallowed-name
+lib/ansible/modules/copy.py validate-modules:doc-default-does-not-match-spec
+lib/ansible/modules/copy.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/copy.py validate-modules:undocumented-parameter
+lib/ansible/modules/dnf.py validate-modules:doc-required-mismatch
+lib/ansible/modules/dnf.py validate-modules:parameter-invalid
+lib/ansible/modules/file.py validate-modules:undocumented-parameter
+lib/ansible/modules/find.py use-argspec-type-path # fix needed
+lib/ansible/modules/git.py pylint:disallowed-name
+lib/ansible/modules/git.py use-argspec-type-path
+lib/ansible/modules/git.py validate-modules:doc-missing-type
+lib/ansible/modules/git.py validate-modules:doc-required-mismatch
+lib/ansible/modules/iptables.py pylint:disallowed-name
+lib/ansible/modules/lineinfile.py validate-modules:doc-choices-do-not-match-spec
+lib/ansible/modules/lineinfile.py validate-modules:doc-default-does-not-match-spec
+lib/ansible/modules/lineinfile.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/package_facts.py validate-modules:doc-choices-do-not-match-spec
+lib/ansible/modules/pip.py pylint:disallowed-name
+lib/ansible/modules/replace.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/service.py validate-modules:nonexistent-parameter-documented
+lib/ansible/modules/service.py validate-modules:use-run-command-not-popen
+lib/ansible/modules/stat.py validate-modules:doc-default-does-not-match-spec # get_md5 is undocumented
+lib/ansible/modules/stat.py validate-modules:parameter-invalid
+lib/ansible/modules/stat.py validate-modules:parameter-type-not-in-doc
+lib/ansible/modules/stat.py validate-modules:undocumented-parameter
+lib/ansible/modules/systemd_service.py validate-modules:parameter-invalid
+lib/ansible/modules/systemd_service.py validate-modules:return-syntax-error
+lib/ansible/modules/sysvinit.py validate-modules:return-syntax-error
+lib/ansible/modules/uri.py validate-modules:doc-required-mismatch
+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/modules/yum.py pylint:disallowed-name
+lib/ansible/modules/yum.py validate-modules:parameter-invalid
+lib/ansible/modules/yum_repository.py validate-modules:doc-default-does-not-match-spec
+lib/ansible/modules/yum_repository.py validate-modules:parameter-type-not-in-doc
+lib/ansible/modules/yum_repository.py validate-modules:undocumented-parameter
+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 pylint:disallowed-name
+lib/ansible/module_utils/compat/selinux.py import-2.7!skip # pass/fail depends on presence of libselinux.so
+lib/ansible/module_utils/compat/selinux.py import-3.5!skip # pass/fail depends on presence of libselinux.so
+lib/ansible/module_utils/compat/selinux.py import-3.6!skip # pass/fail depends on presence of libselinux.so
+lib/ansible/module_utils/compat/selinux.py import-3.7!skip # pass/fail depends on presence of libselinux.so
+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/distro/_distro.py future-import-boilerplate # ignore bundled
+lib/ansible/module_utils/distro/_distro.py metaclass-boilerplate # ignore bundled
+lib/ansible/module_utils/distro/_distro.py no-assert
+lib/ansible/module_utils/distro/_distro.py pylint:using-constant-test # bundled code we don't want to modify
+lib/ansible/module_utils/distro/_distro.py pep8!skip # bundled code we don't want to modify
+lib/ansible/module_utils/distro/__init__.py empty-init # breaks namespacing, bundled, do not override
+lib/ansible/module_utils/facts/__init__.py empty-init # breaks namespacing, deprecate and eventually remove
+lib/ansible/module_utils/facts/network/linux.py pylint:disallowed-name
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 pslint:PSUseApprovedVerbs
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSProvideCommentHelp # need to agree on best format for comment location
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 pslint:PSUseApprovedVerbs
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 pslint:PSCustomUseLiteralPath
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 pslint:PSProvideCommentHelp
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 pslint:PSCustomUseLiteralPath
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 pslint:PSUseApprovedVerbs
+lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 pslint:PSUseApprovedVerbs
+lib/ansible/module_utils/pycompat24.py no-get-exception
+lib/ansible/module_utils/six/__init__.py empty-init # breaks namespacing, bundled, do not override
+lib/ansible/module_utils/six/__init__.py future-import-boilerplate # ignore bundled
+lib/ansible/module_utils/six/__init__.py metaclass-boilerplate # ignore bundled
+lib/ansible/module_utils/six/__init__.py no-basestring
+lib/ansible/module_utils/six/__init__.py no-dict-iteritems
+lib/ansible/module_utils/six/__init__.py no-dict-iterkeys
+lib/ansible/module_utils/six/__init__.py no-dict-itervalues
+lib/ansible/module_utils/six/__init__.py pylint:self-assigning-variable
+lib/ansible/module_utils/six/__init__.py replace-urlopen
+lib/ansible/module_utils/urls.py pylint:arguments-renamed
+lib/ansible/module_utils/urls.py pylint:disallowed-name
+lib/ansible/module_utils/urls.py replace-urlopen
+lib/ansible/parsing/vault/__init__.py pylint:disallowed-name
+lib/ansible/parsing/yaml/objects.py pylint:arguments-renamed
+lib/ansible/playbook/base.py pylint:disallowed-name
+lib/ansible/playbook/collectionsearch.py required-and-default-attributes # https://github.com/ansible/ansible/issues/61460
+lib/ansible/playbook/helpers.py pylint:disallowed-name
+lib/ansible/playbook/playbook_include.py pylint:arguments-renamed
+lib/ansible/playbook/role/include.py pylint:arguments-renamed
+lib/ansible/plugins/action/normal.py action-plugin-docs # default action plugin for modules without a dedicated action plugin
+lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub for backwards compatibility
+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/plugins/lookup/random_choice.py pylint:arguments-renamed
+lib/ansible/plugins/lookup/sequence.py pylint:disallowed-name
+lib/ansible/plugins/shell/cmd.py pylint:arguments-renamed
+lib/ansible/plugins/strategy/__init__.py pylint:disallowed-name
+lib/ansible/plugins/strategy/linear.py pylint:disallowed-name
+lib/ansible/utils/collection_loader/_collection_finder.py pylint:deprecated-class
+lib/ansible/utils/collection_loader/_collection_meta.py pylint:deprecated-class
+lib/ansible/vars/hostvars.py pylint:disallowed-name
+test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function # ignore, required for testing
+test/integration/targets/ansible-test-sanity/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-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import # ignore, required for testing
+test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py pylint:relative-beyond-top-level
+test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py pep8!skip # vendored code
+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/module_utils/my_util2.py pylint:relative-beyond-top-level
+test/integration/targets/fork_safe_stdio/vendored_pty.py pep8!skip # vendored code
+test/integration/targets/gathering_facts/library/bogus_facts shebang
+test/integration/targets/gathering_facts/library/facts_one shebang
+test/integration/targets/gathering_facts/library/facts_two shebang
+test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 pslint!skip
+test/integration/targets/json_cleanup/library/bad_json shebang
+test/integration/targets/lookup_csvfile/files/crlf.csv line-endings
+test/integration/targets/lookup_ini/lookup-8859-15.ini no-smart-quotes
+test/integration/targets/module_precedence/lib_with_extension/a.ini shebang
+test/integration/targets/module_precedence/lib_with_extension/ping.ini shebang
+test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini shebang
+test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini shebang
+test/integration/targets/module_utils/library/test.py future-import-boilerplate # allow testing of Python 2.x implicit relative imports
+test/integration/targets/module_utils/module_utils/bar0/foo.py pylint:disallowed-name
+test/integration/targets/module_utils/module_utils/foo.py pylint:disallowed-name
+test/integration/targets/module_utils/module_utils/sub/bar/bar.py pylint:disallowed-name
+test/integration/targets/module_utils/module_utils/sub/bar/__init__.py pylint:disallowed-name
+test/integration/targets/module_utils/module_utils/yak/zebra/foo.py pylint:disallowed-name
+test/integration/targets/old_style_modules_posix/library/helloworld.sh shebang
+test/integration/targets/template/files/encoding_1252_utf-8.expected no-smart-quotes
+test/integration/targets/template/files/encoding_1252_windows-1252.expected no-smart-quotes
+test/integration/targets/template/files/foo.dos.txt line-endings
+test/integration/targets/template/templates/encoding_1252.j2 no-smart-quotes
+test/integration/targets/unicode/unicode.yml no-smart-quotes
+test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 pslint!skip
+test/integration/targets/win_exec_wrapper/library/test_fail.ps1 pslint:PSCustomUseLiteralPath
+test/integration/targets/win_exec_wrapper/tasks/main.yml no-smart-quotes # We are explicitly testing smart quote support for env vars
+test/integration/targets/win_fetch/tasks/main.yml no-smart-quotes # We are explictly testing smart quotes in the file name to fetch
+test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1 line-endings # Explicitly tests that we still work with Windows line endings
+test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1 line-endings # Explicitly tests that we still work with Windows line endings
+test/integration/targets/win_script/files/test_script.ps1 pslint:PSAvoidUsingWriteHost # Keep
+test/integration/targets/win_script/files/test_script_removes_file.ps1 pslint:PSCustomUseLiteralPath
+test/integration/targets/win_script/files/test_script_with_args.ps1 pslint:PSAvoidUsingWriteHost # Keep
+test/integration/targets/win_script/files/test_script_with_splatting.ps1 pslint:PSAvoidUsingWriteHost # Keep
+test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 pslint:PSCustomUseLiteralPath # Uses wildcards on purpose
+test/lib/ansible_test/_util/target/setup/ConfigureRemotingForAnsible.ps1 pslint:PSCustomUseLiteralPath
+test/lib/ansible_test/_util/target/setup/requirements.py replace-urlopen
+test/support/integration/plugins/modules/timezone.py pylint:disallowed-name
+test/support/integration/plugins/module_utils/compat/ipaddress.py future-import-boilerplate
+test/support/integration/plugins/module_utils/compat/ipaddress.py metaclass-boilerplate
+test/support/integration/plugins/module_utils/compat/ipaddress.py no-unicode-literals
+test/support/integration/plugins/module_utils/network/common/utils.py future-import-boilerplate
+test/support/integration/plugins/module_utils/network/common/utils.py metaclass-boilerplate
+test/support/integration/plugins/module_utils/network/common/utils.py pylint:use-a-generator
+test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/netconf/netconf.py pylint:used-before-assignment
+test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/filter/network.py pylint:consider-using-dict-comprehension
+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/network/common/facts/facts.py pylint:unnecessary-comprehension
+test/support/network-integration/collections/ansible_collections/ansible/netcommon/plugins/module_utils/network/common/utils.py pylint:use-a-generator
+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/cliconf/ios.py pylint:arguments-renamed
+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/cliconf/vyos.py pylint:arguments-renamed
+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:disallowed-name
+test/support/windows-integration/plugins/action/win_copy.py pylint:used-before-assignment
+test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/module_utils/WebRequest.psm1 pslint!skip
+test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.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/slurp.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_acl.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_certificate_store.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_command.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_copy.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_file.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_get_url.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_lineinfile.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_regedit.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_shell.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_stat.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_tempfile.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_user_right.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_user.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_wait_for.ps1 pslint!skip
+test/support/windows-integration/plugins/modules/win_whoami.ps1 pslint!skip
+test/units/executor/test_play_iterator.py pylint:disallowed-name
+test/units/modules/test_apt.py pylint:disallowed-name
+test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-no-version
+test/units/module_utils/basic/test_deprecate_warn.py pylint:ansible-deprecated-version
+test/units/module_utils/basic/test_run_command.py pylint:disallowed-name
+test/units/module_utils/urls/fixtures/multipart.txt line-endings # Fixture for HTTP tests that use CRLF
+test/units/module_utils/urls/test_fetch_url.py replace-urlopen
+test/units/module_utils/urls/test_gzip.py replace-urlopen
+test/units/module_utils/urls/test_Request.py replace-urlopen
+test/units/parsing/vault/test_vault.py pylint:disallowed-name
+test/units/playbook/role/test_role.py pylint:disallowed-name
+test/units/plugins/test_plugins.py pylint:disallowed-name
+test/units/template/test_templar.py pylint:disallowed-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/modules/__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/__init__.py empty-init # testing that collections don't need inits
+test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py empty-init # testing that collections don't need inits
+test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py empty-init # testing that collections don't need inits
+test/units/utils/collection_loader/test_collection_loader.py pylint:undefined-variable # magic runtime local var splatting