diff --git a/MANIFEST.in b/MANIFEST.in index e75acb70cde..c0ec52a4949 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -30,7 +30,7 @@ recursive-include test/ansible_test *.py Makefile recursive-include test/integration * recursive-include test/lib/ansible_test/config *.yml *.template recursive-include test/lib/ansible_test/_data *.cfg *.in *.ini *.ps1 *.txt *.yml coveragerc -recursive-include test/lib/ansible_test/_util *.cfg *.json *.ps1 *.psd1 *.py *.sh *.txt *.yml +recursive-include test/lib/ansible_test/_util *.cfg *.ini *.json *.ps1 *.psd1 *.py *.sh *.txt *.yml recursive-include test/lib/ansible_test/_util/controller/sanity/validate-modules validate-modules recursive-include test/sanity *.in *.json *.py *.txt recursive-include test/support *.py *.ps1 *.psm1 *.cs diff --git a/docs/docsite/rst/dev_guide/testing/sanity/mypy.rst b/docs/docsite/rst/dev_guide/testing/sanity/mypy.rst new file mode 100644 index 00000000000..4df6e6e84de --- /dev/null +++ b/docs/docsite/rst/dev_guide/testing/sanity/mypy.rst @@ -0,0 +1,14 @@ +mypy +==== + +The ``mypy`` static type checker is used to check the following code against each Python version supported by the controller: + + * ``lib/ansible/`` + * ``test/lib/ansible_test/_internal/`` + +Additionally, the following code is checked against Python versions supported only on managed nodes: + + * ``lib/ansible/modules/`` + * ``lib/ansible/module_utils/`` + +See https://mypy.readthedocs.io/en/stable/ for additional details. diff --git a/test/lib/ansible_test/_data/requirements/sanity.mypy.in b/test/lib/ansible_test/_data/requirements/sanity.mypy.in new file mode 100644 index 00000000000..b7b8229794a --- /dev/null +++ b/test/lib/ansible_test/_data/requirements/sanity.mypy.in @@ -0,0 +1,9 @@ +mypy[python2] +packaging # type stubs not published separately +types-backports +types-jinja2 +types-paramiko +types-pyyaml < 6 # PyYAML 6+ stubs do not support Python 2.7 +types-requests +types-setuptools +types-toml diff --git a/test/lib/ansible_test/_data/requirements/sanity.mypy.txt b/test/lib/ansible_test/_data/requirements/sanity.mypy.txt new file mode 100644 index 00000000000..d4baf5630dd --- /dev/null +++ b/test/lib/ansible_test/_data/requirements/sanity.mypy.txt @@ -0,0 +1,20 @@ +# edit "sanity.mypy.in" and generate with: hacking/update-sanity-requirements.py --test mypy +mypy==0.931 +mypy-extensions==0.4.3 +packaging==21.2 +pyparsing==2.4.7 +tomli==2.0.1 +typed-ast==1.5.2 +types-backports==0.1.3 +types-cryptography==3.3.15 +types-enum34==1.1.8 +types-ipaddress==1.0.8 +types-Jinja2==2.11.9 +types-MarkupSafe==1.1.10 +types-paramiko==2.8.13 +types-PyYAML==5.4.12 +types-requests==2.27.10 +types-setuptools==57.4.9 +types-toml==0.10.4 +types-urllib3==1.26.9 +typing-extensions==3.10.0.2 diff --git a/test/lib/ansible_test/_internal/commands/sanity/mypy.py b/test/lib/ansible_test/_internal/commands/sanity/mypy.py new file mode 100644 index 00000000000..c384e7e422c --- /dev/null +++ b/test/lib/ansible_test/_internal/commands/sanity/mypy.py @@ -0,0 +1,247 @@ +"""Sanity test which executes mypy.""" +from __future__ import annotations + +import dataclasses +import os +import re +import typing as t + +from . import ( + SanityMultipleVersion, + SanityMessage, + SanityFailure, + SanitySuccess, + SanitySkipped, + SanityTargets, + create_sanity_virtualenv, +) + +from ...constants import ( + CONTROLLER_PYTHON_VERSIONS, + REMOTE_ONLY_PYTHON_VERSIONS, +) + +from ...test import ( + TestResult, +) + +from ...target import ( + TestTarget, +) + +from ...util import ( + SubprocessError, + display, + parse_to_list_of_dict, + ANSIBLE_TEST_CONTROLLER_ROOT, + ApplicationError, + is_subdir, +) + +from ...util_common import ( + intercept_python, +) + +from ...ansible_util import ( + ansible_environment, +) + +from ...config import ( + SanityConfig, +) + +from ...host_configs import ( + PythonConfig, + VirtualPythonConfig, +) + + +class MypyTest(SanityMultipleVersion): + """Sanity test which executes mypy.""" + ansible_only = True + + def filter_targets(self, targets): # type: (t.List[TestTarget]) -> t.List[TestTarget] + """Return the given list of test targets, filtered to include only those relevant for the test.""" + return [target for target in targets if os.path.splitext(target.path)[1] == '.py' and ( + target.path.startswith('lib/ansible/') or target.path.startswith('test/lib/ansible_test/_internal/'))] + + @property + def error_code(self): # type: () -> t.Optional[str] + """Error code for ansible-test matching the format used by the underlying test program, or None if the program does not use error codes.""" + return 'ansible-test' + + @property + def needs_pypi(self): # type: () -> bool + """True if the test requires PyPI, otherwise False.""" + return True + + def test(self, args, targets, python): # type: (SanityConfig, SanityTargets, PythonConfig) -> TestResult + settings = self.load_processor(args, python.version) + + paths = [target.path for target in targets.include] + + virtualenv_python = create_sanity_virtualenv(args, args.controller_python, self.name) + + if args.prime_venvs: + return SanitySkipped(self.name, python_version=python.version) + + if not virtualenv_python: + display.warning(f'Skipping sanity test "{self.name}" due to missing virtual environment support on Python {args.controller_python.version}.') + return SanitySkipped(self.name, python.version) + + contexts = ( + MyPyContext('ansible-test', ['test/lib/ansible_test/_internal/'], CONTROLLER_PYTHON_VERSIONS), + MyPyContext('ansible-core', ['lib/ansible/'], CONTROLLER_PYTHON_VERSIONS), + MyPyContext('modules', ['lib/ansible/modules/', 'lib/ansible/module_utils/'], REMOTE_ONLY_PYTHON_VERSIONS), + ) + + unfiltered_messages = [] # type: t.List[SanityMessage] + + for context in contexts: + if python.version not in context.python_versions: + continue + + unfiltered_messages.extend(self.test_context(args, virtualenv_python, python, context, paths)) + + notices = [] + messages = [] + + for message in unfiltered_messages: + if message.level != 'error': + notices.append(message) + continue + + match = re.search(r'^(?P.*) {2}\[(?P.*)]$', message.message) + + messages.append(SanityMessage( + message=match.group('message'), + path=message.path, + line=message.line, + column=message.column, + level=message.level, + code=match.group('code'), + )) + + for notice in notices: + display.info(notice.format(), verbosity=3) + + # The following error codes from mypy indicate that results are incomplete. + # That prevents the test from completing successfully, just as if mypy were to traceback or generate unexpected output. + fatal_error_codes = { + 'import', + 'syntax', + } + + fatal_errors = [message for message in messages if message.code in fatal_error_codes] + + if fatal_errors: + error_message = '\n'.join(error.format() for error in fatal_errors) + raise ApplicationError(f'Encountered {len(fatal_errors)} fatal errors reported by mypy:\n{error_message}') + + paths_set = set(paths) + + # Only report messages for paths that were specified as targets. + # Imports in our code are followed by mypy in order to perform its analysis, which is important for accurate results. + # However, it will also report issues on those files, which is not the desired behavior. + messages = [message for message in messages if message.path in paths_set] + + results = settings.process_errors(messages, paths) + + if results: + return SanityFailure(self.name, messages=results, python_version=python.version) + + return SanitySuccess(self.name, python_version=python.version) + + @staticmethod + def test_context( + args, # type: SanityConfig + virtualenv_python, # type: VirtualPythonConfig + python, # type: PythonConfig + context, # type: MyPyContext + paths, # type: t.List[str] + ): # type: (...) -> t.List[SanityMessage] + """Run mypy tests for the specified context.""" + context_paths = [path for path in paths if any(is_subdir(path, match_path) for match_path in context.paths)] + + if not context_paths: + return [] + + config_path = os.path.join(ANSIBLE_TEST_CONTROLLER_ROOT, 'sanity', 'mypy', f'{context.name}.ini') + + display.info(f'Checking context "{context.name}"', verbosity=1) + + env = ansible_environment(args, color=False) + + # The --no-site-packages option should not be used, as it will prevent loading of type stubs from the sanity test virtual environment. + + # Enabling the --warn-unused-configs option would help keep the config files clean. + # However, the option can only be used when all files in tested contexts are evaluated. + # Unfortunately sanity tests have no way of making that determination currently. + # The option is also incompatible with incremental mode and caching. + + cmd = [ + # Below are arguments common to all contexts. + # They are kept here to avoid repetition in each config file. + virtualenv_python.path, + '-m', 'mypy', + '--show-column-numbers', + '--show-error-codes', + '--no-error-summary', + # This is a fairly common pattern in our code, so we'll allow it. + '--allow-redefinition', + # Since we specify the path(s) to test, it's important that mypy is configured to use the default behavior of following imports. + '--follow-imports', 'normal', + # Incremental results and caching do not provide significant performance benefits. + # It also prevents the use of the --warn-unused-configs option. + '--no-incremental', + '--cache-dir', '/dev/null', + # The platform is specified here so that results are consistent regardless of what platform the tests are run from. + # In the future, if testing of other platforms is desired, the platform should become part of the test specification, just like the Python version. + '--platform', 'linux', + # Despite what the documentation [1] states, the --python-version option does not cause mypy to search for a corresponding Python executable. + # It will instead use the Python executable that is used to run mypy itself. + # The --python-executable option can be used to specify the Python executable, with the default being the executable used to run mypy. + # As a precaution, that option is used in case the behavior of mypy is updated in the future to match the documentation. + # That should help guarantee that the Python executable providing type hints is the one used to run mypy. + # [1] https://mypy.readthedocs.io/en/stable/command_line.html#cmdoption-mypy-python-version + '--python-executable', virtualenv_python.path, + '--python-version', python.version, + # Below are context specific arguments. + # They are primarily useful for listing individual 'ignore_missing_imports' entries instead of using a global ignore. + '--config-file', config_path, + ] + + cmd.extend(context_paths) + + try: + stdout, stderr = intercept_python(args, virtualenv_python, cmd, env, capture=True) + + if stdout or stderr: + raise SubprocessError(cmd, stdout=stdout, stderr=stderr) + except SubprocessError as ex: + if ex.status != 1 or ex.stderr or not ex.stdout: + raise + + stdout = ex.stdout + + pattern = r'^(?P[^:]*):(?P[0-9]+):((?P[0-9]+):)? (?P[^:]+): (?P.*)$' + + parsed = parse_to_list_of_dict(pattern, stdout) + + messages = [SanityMessage( + level=r['level'], + message=r['message'], + path=r['path'], + line=int(r['line']), + column=int(r.get('column') or '0'), + ) for r in parsed] + + return messages + + +@dataclasses.dataclass(frozen=True) +class MyPyContext: + """Context details for a single run of mypy.""" + name: str + paths: t.List[str] + python_versions: t.Tuple[str, ...] diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini new file mode 100644 index 00000000000..4d93f359289 --- /dev/null +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-core.ini @@ -0,0 +1,119 @@ +# IMPORTANT +# Set "ignore_missing_imports" per package below, rather than globally. +# That will help identify missing type stubs that should be added to the sanity test environment. + +[mypy] +# There are ~20 errors reported in ansible-core when strict optional checking is enabled. +# Until the number of occurrences are reduced, it's better to disable strict checking. +strict_optional = False +# There are ~70 errors reported in ansible-core when checking attributes. +# Until the number of occurrences are reduced, it's better to disable the check. +disable_error_code = attr-defined + +[mypy-ansible.module_utils.six.moves.*] +ignore_missing_imports = True + +[mypy-passlib.*] +ignore_missing_imports = True + +[mypy-pexpect.*] +ignore_missing_imports = True + +[mypy-pypsrp.*] +ignore_missing_imports = True + +[mypy-winrm.*] +ignore_missing_imports = True + +[mypy-kerberos.*] +ignore_missing_imports = True + +[mypy-xmltodict.*] +ignore_missing_imports = True + +[mypy-md5.*] +ignore_missing_imports = True + +[mypy-scp.*] +ignore_missing_imports = True + +[mypy-ncclient.*] +ignore_missing_imports = True + +[mypy-lxml.*] +ignore_missing_imports = True + +[mypy-yum.*] +ignore_missing_imports = True + +[mypy-rpmUtils.*] +ignore_missing_imports = True + +[mypy-rpm.*] +ignore_missing_imports = True + +[mypy-psutil.*] +ignore_missing_imports = True + +[mypy-dnf.*] +ignore_missing_imports = True + +[mypy-apt.*] +ignore_missing_imports = True + +[mypy-apt_pkg.*] +ignore_missing_imports = True + +[mypy-gssapi.*] +ignore_missing_imports = True + +[mypy-_ssl.*] +ignore_missing_imports = True + +[mypy-urllib_gssapi.*] +ignore_missing_imports = True + +[mypy-systemd.*] +ignore_missing_imports = True + +[mypy-sha.*] +ignore_missing_imports = True + +[mypy-distro.*] +ignore_missing_imports = True + +[mypy-selectors2.*] +ignore_missing_imports = True + +[mypy-resolvelib.*] +ignore_missing_imports = True + +[mypy-urlparse.*] +ignore_missing_imports = True + +[mypy-argcomplete.*] +ignore_missing_imports = True + +[mypy-selinux.*] +ignore_missing_imports = True + +[mypy-urllib2.*] +ignore_missing_imports = True + +[mypy-httplib.*] +ignore_missing_imports = True + +[mypy-compiler.*] +ignore_missing_imports = True + +[mypy-aptsources.*] +ignore_missing_imports = True + +[mypy-urllib3.*] +ignore_missing_imports = True + +[mypy-requests.*] +ignore_missing_imports = True + +[mypy-jinja2.nativetypes] +ignore_missing_imports = True diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini new file mode 100644 index 00000000000..6e756c0dc7c --- /dev/null +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/ansible-test.ini @@ -0,0 +1,21 @@ +# IMPORTANT +# Set "ignore_missing_imports" per package below, rather than globally. +# That will help identify missing type stubs that should be added to the sanity test environment. + +[mypy] +# There are ~350 errors reported in ansible-test when strict optional checking is enabled. +# Until the number of occurrences are greatly reduced, it's better to disable strict checking. +strict_optional = False +# There are ~25 errors reported in ansible-test under the 'misc' code. +# The majority of those errors are "Only concrete class can be given", which is due to a limitation of mypy. +# See: https://github.com/python/mypy/issues/5374 +disable_error_code = misc + +[mypy-argcomplete] +ignore_missing_imports = True + +[mypy-coverage] +ignore_missing_imports = True + +[mypy-ansible_release] +ignore_missing_imports = True diff --git a/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini b/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini new file mode 100644 index 00000000000..d6a608f6c7e --- /dev/null +++ b/test/lib/ansible_test/_util/controller/sanity/mypy/modules.ini @@ -0,0 +1,98 @@ +# IMPORTANT +# Set "ignore_missing_imports" per package below, rather than globally. +# That will help identify missing type stubs that should be added to the sanity test environment. + +[mypy] + +[mypy-ansible.module_utils.six.moves.*] +ignore_missing_imports = True + +[mypy-pexpect.*] +ignore_missing_imports = True + +[mypy-md5.*] +ignore_missing_imports = True + +[mypy-yum.*] +ignore_missing_imports = True + +[mypy-rpmUtils.*] +ignore_missing_imports = True + +[mypy-rpm.*] +ignore_missing_imports = True + +[mypy-psutil.*] +ignore_missing_imports = True + +[mypy-dnf.*] +ignore_missing_imports = True + +[mypy-apt.*] +ignore_missing_imports = True + +[mypy-apt_pkg.*] +ignore_missing_imports = True + +[mypy-gssapi.*] +ignore_missing_imports = True + +[mypy-_ssl.*] +ignore_missing_imports = True + +[mypy-urllib_gssapi.*] +ignore_missing_imports = True + +[mypy-systemd.*] +ignore_missing_imports = True + +[mypy-sha.*] +ignore_missing_imports = True + +[mypy-distro.*] +ignore_missing_imports = True + +[mypy-selectors2.*] +ignore_missing_imports = True + +[mypy-selinux.*] +ignore_missing_imports = True + +[mypy-urllib2.*] +ignore_missing_imports = True + +[mypy-httplib.*] +ignore_missing_imports = True + +[mypy-compiler.*] +ignore_missing_imports = True + +[mypy-aptsources.*] +ignore_missing_imports = True + +[mypy-urllib3.*] +ignore_missing_imports = True + +[mypy-requests.*] +ignore_missing_imports = True + +[mypy-pkg_resources.*] +ignore_missing_imports = True + +[mypy-urllib.*] +ignore_missing_imports = True + +[mypy-email.*] +ignore_missing_imports = True + +[mypy-selectors.*] +ignore_missing_imports = True + +[mypy-importlib.*] +ignore_missing_imports = True + +[mypy-collections.*] +ignore_missing_imports = True + +[mypy-http.*] +ignore_missing_imports = True diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 2be802edd10..3b3d5b4f613 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -271,3 +271,75 @@ test/units/utils/collection_loader/fixtures/collections_masked/ansible_collectio 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 +lib/ansible/module_utils/six/__init__.py mypy-2.7:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.8:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.9:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.10:has-type # vendored code +lib/ansible/module_utils/six/__init__.py mypy-2.7:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.8:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.9:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.10:name-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-2.7:assignment # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:assignment # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:assignment # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:assignment # vendored code +lib/ansible/module_utils/six/__init__.py mypy-2.7:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.8:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.9:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.10:misc # vendored code +lib/ansible/module_utils/six/__init__.py mypy-2.7:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-2.7:attr-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.5:attr-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.6:attr-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.7:attr-defined # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.8:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.9:var-annotated # vendored code +lib/ansible/module_utils/six/__init__.py mypy-3.10:var-annotated # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-2.7:arg-type # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.5:valid-type # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.6:valid-type # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.7:valid-type # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-2.7:assignment # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-2.7:attr-defined # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.5:attr-defined # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.6:attr-defined # vendored code +lib/ansible/module_utils/distro/_distro.py mypy-3.7:attr-defined # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-2.7:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.5:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.6:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.7:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.8:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.9:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.10:misc # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-2.7:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.5:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.6:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.7:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.8:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.9:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-3.10:assignment # vendored code +lib/ansible/module_utils/compat/_selectors2.py mypy-2.7:attr-defined # vendored code +lib/ansible/parsing/yaml/dumper.py mypy-3.8:misc +lib/ansible/parsing/yaml/dumper.py mypy-3.9:misc +lib/ansible/parsing/yaml/dumper.py mypy-3.10:misc +lib/ansible/parsing/yaml/dumper.py mypy-3.8:valid-type +lib/ansible/parsing/yaml/dumper.py mypy-3.9:valid-type +lib/ansible/parsing/yaml/dumper.py mypy-3.10:valid-type +lib/ansible/parsing/yaml/loader.py mypy-3.8:misc +lib/ansible/parsing/yaml/loader.py mypy-3.9:misc +lib/ansible/parsing/yaml/loader.py mypy-3.10:misc +lib/ansible/parsing/yaml/loader.py mypy-3.8:valid-type +lib/ansible/parsing/yaml/loader.py mypy-3.9:valid-type +lib/ansible/parsing/yaml/loader.py mypy-3.10:valid-type