ansible-test - Update sanity test requirements (#85980)

This commit is contained in:
Matt Clay
2025-10-13 12:32:39 -07:00
committed by GitHub
parent 82b64d4b69
commit 9ee667030f
25 changed files with 140 additions and 105 deletions

View File

@@ -0,0 +1,5 @@
minor_changes:
- ansible-test - Update pinned sanity test requirements, including upgrading to pylint 4.0.0.
- ansible-test - Filter out pylint messages for invalid filenames and display a notice when doing so.
- ansible-test - Update astroid imports in custom pylint checkers.
- ansible-test - Default to Python 3.14 in the ``base`` and ``default`` test containers.

View File

@@ -12,4 +12,4 @@ packaging
# NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69 # NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69
# NOTE: When updating the upper bound, also update the latest version used # NOTE: When updating the upper bound, also update the latest version used
# NOTE: in the ansible-galaxy-collection test suite. # NOTE: in the ansible-galaxy-collection test suite.
resolvelib >= 0.5.3, < 2.0.0 # dependency resolver used by ansible-galaxy resolvelib >= 0.8.0, < 2.0.0 # dependency resolver used by ansible-galaxy

View File

@@ -14,4 +14,4 @@ import string # pylint: disable=unused-import
# 'Call' object has no attribute 'value' # 'Call' object has no attribute 'value'
result = {None: None}[{}.get('something')] result = {None: None}[{}.get('something')]
foo = {}.keys() foo = {}.keys() # should trigger disallowed-name, but doesn't in pylint 4.0.0, probably due to https://github.com/pylint-dev/pylint/issues/10652

View File

@@ -1,7 +1,6 @@
plugins/modules/bad.py import plugins/modules/bad.py import
plugins/modules/bad.py pylint:ansible-bad-module-import plugins/modules/bad.py pylint:ansible-bad-module-import
plugins/lookup/bad.py import plugins/lookup/bad.py import
plugins/plugin_utils/check_pylint.py pylint:disallowed-name
tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function tests/integration/targets/hello/files/bad.py pylint:ansible-bad-function
tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import
tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from tests/integration/targets/hello/files/bad.py pylint:ansible-bad-import-from

View File

@@ -1,6 +1,6 @@
base image=quay.io/ansible/base-test-container:v2.20-1 python=3.13,3.9,3.10,3.11,3.12,3.14 base image=quay.io/ansible/base-test-container:v2.20-1 python=3.14,3.9,3.10,3.11,3.12,3.13
default image=quay.io/ansible/default-test-container:v2.20-2 python=3.13,3.9,3.10,3.11,3.12,3.14 context=collection default image=quay.io/ansible/default-test-container:v2.20-2 python=3.14,3.9,3.10,3.11,3.12,3.13 context=collection
default image=quay.io/ansible/ansible-core-test-container:v2.20-2 python=3.13,3.9,3.10,3.11,3.12,3.14 context=ansible-core default image=quay.io/ansible/ansible-core-test-container:v2.20-2 python=3.14,3.9,3.10,3.11,3.12,3.13 context=ansible-core
alpine322 image=quay.io/ansible/alpine-test-container:3.22-v2.20-0 python=3.12 cgroup=none audit=none alpine322 image=quay.io/ansible/alpine-test-container:3.22-v2.20-0 python=3.12 cgroup=none audit=none
fedora42 image=quay.io/ansible/fedora-test-container:42-v2.20-0 python=3.13 cgroup=v2-only fedora42 image=quay.io/ansible/fedora-test-container:42-v2.20-0 python=3.13 cgroup=v2-only
ubuntu2204 image=quay.io/ansible/ubuntu-test-container:22.04-v2.20-0 python=3.10 ubuntu2204 image=quay.io/ansible/ubuntu-test-container:22.04-v2.20-0 python=3.10

View File

@@ -12,4 +12,4 @@ packaging
# NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69 # NOTE: Ref: https://github.com/sarugaku/resolvelib/issues/69
# NOTE: When updating the upper bound, also update the latest version used # NOTE: When updating the upper bound, also update the latest version used
# NOTE: in the ansible-galaxy-collection test suite. # NOTE: in the ansible-galaxy-collection test suite.
resolvelib >= 0.5.3, < 2.0.0 # dependency resolver used by ansible-galaxy resolvelib >= 0.8.0, < 2.0.0 # dependency resolver used by ansible-galaxy

View File

@@ -1,5 +1,5 @@
# edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc # edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
packaging==25.0 packaging==25.0
PyYAML==6.0.2 PyYAML==6.0.3

View File

@@ -2,7 +2,7 @@
antsibull-changelog==0.29.0 antsibull-changelog==0.29.0
docutils==0.18.1 docutils==0.18.1
packaging==25.0 packaging==25.0
PyYAML==6.0.2 PyYAML==6.0.3
rstcheck==5.0.0 rstcheck==5.0.0
semantic-version==2.10.0 semantic-version==2.10.0
types-docutils==0.18.3 types-docutils==0.18.3

View File

@@ -1,4 +1,4 @@
# edit "sanity.import.plugin.in" and generate with: hacking/update-sanity-requirements.py --test import.plugin # edit "sanity.import.plugin.in" and generate with: hacking/update-sanity-requirements.py --test import.plugin
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
PyYAML==6.0.2 PyYAML==6.0.3

View File

@@ -1,2 +1,2 @@
# edit "sanity.import.in" and generate with: hacking/update-sanity-requirements.py --test import # edit "sanity.import.in" and generate with: hacking/update-sanity-requirements.py --test import
PyYAML==6.0.2 PyYAML==6.0.3

View File

@@ -1,2 +1,2 @@
# edit "sanity.integration-aliases.in" and generate with: hacking/update-sanity-requirements.py --test integration-aliases # edit "sanity.integration-aliases.in" and generate with: hacking/update-sanity-requirements.py --test integration-aliases
PyYAML==6.0.2 PyYAML==6.0.3

View File

@@ -1,9 +1,9 @@
# edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint # edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint
astroid==3.3.11 astroid==4.0.1
dill==0.4.0 dill==0.4.0
isort==6.0.1 isort==7.0.0
mccabe==0.7.0 mccabe==0.7.0
platformdirs==4.4.0 platformdirs==4.5.0
pylint==3.3.8 pylint==4.0.0
PyYAML==6.0.2 PyYAML==6.0.3
tomlkit==0.13.3 tomlkit==0.13.3

View File

@@ -1,3 +1,3 @@
# edit "sanity.runtime-metadata.in" and generate with: hacking/update-sanity-requirements.py --test runtime-metadata # edit "sanity.runtime-metadata.in" and generate with: hacking/update-sanity-requirements.py --test runtime-metadata
PyYAML==6.0.2 PyYAML==6.0.3
voluptuous==0.15.2 voluptuous==0.15.2

View File

@@ -1,6 +1,6 @@
# edit "sanity.validate-modules.in" and generate with: hacking/update-sanity-requirements.py --test validate-modules # edit "sanity.validate-modules.in" and generate with: hacking/update-sanity-requirements.py --test validate-modules
antsibull-docs-parser==1.0.0 antsibull-docs-parser==1.0.0
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
PyYAML==6.0.2 PyYAML==6.0.3
voluptuous==0.15.2 voluptuous==0.15.2

View File

@@ -1,4 +1,4 @@
# edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint # edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint
pathspec==0.12.1 pathspec==0.12.1
PyYAML==6.0.2 PyYAML==6.0.3
yamllint==1.37.1 yamllint==1.37.1

View File

@@ -301,4 +301,15 @@ class PylintTest(SanitySingleVersion):
else: else:
messages = [] messages = []
expected_paths = set(paths)
unexpected_messages = [message for message in messages if message["path"] not in expected_paths]
messages = [message for message in messages if message["path"] in expected_paths]
for unexpected_message in unexpected_messages:
display.info(f"Unexpected message: {json.dumps(unexpected_message)}", verbosity=4)
if unexpected_messages:
display.notice(f"Discarded {len(unexpected_messages)} unexpected messages. Use -vvvv to display.")
return messages return messages

View File

@@ -11,9 +11,11 @@ import functools
import pathlib import pathlib
import re import re
import astroid import astroid.bases
import astroid.context import astroid.exceptions
import astroid.nodes
import astroid.typing import astroid.typing
import astroid.util
import pylint.lint import pylint.lint
import pylint.checkers import pylint.checkers
@@ -42,7 +44,7 @@ class DeprecationCallArgs:
def all_args_dynamic(self) -> bool: def all_args_dynamic(self) -> bool:
"""True if all args are dynamic or None, otherwise False.""" """True if all args are dynamic or None, otherwise False."""
return all(arg is None or isinstance(arg, astroid.NodeNG) for arg in dataclasses.asdict(self).values()) return all(arg is None or isinstance(arg, astroid.nodes.NodeNG) for arg in dataclasses.asdict(self).values())
class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker): class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
@@ -177,7 +179,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.module_cache: dict[str, astroid.Module] = {} self.module_cache: dict[str, astroid.nodes.Module] = {}
@functools.cached_property @functools.cached_property
def collection_name(self) -> str | None: def collection_name(self) -> str | None:
@@ -226,7 +228,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
return None return None
@pylint.checkers.utils.only_required_for_messages(*(msgs.keys())) @pylint.checkers.utils.only_required_for_messages(*(msgs.keys()))
def visit_call(self, node: astroid.Call) -> None: def visit_call(self, node: astroid.nodes.Call) -> None:
"""Visit a call node.""" """Visit a call node."""
if inferred := self.infer(node.func): if inferred := self.infer(node.func):
name = self.get_fully_qualified_name(inferred) name = self.get_fully_qualified_name(inferred)
@@ -234,50 +236,50 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
if args := self.DEPRECATION_FUNCTIONS.get(name): if args := self.DEPRECATION_FUNCTIONS.get(name):
self.check_call(node, name, args) self.check_call(node, name, args)
def infer(self, node: astroid.NodeNG) -> astroid.NodeNG | None: def infer(self, node: astroid.nodes.NodeNG) -> astroid.nodes.NodeNG | None:
"""Return the inferred node from the given node, or `None` if it cannot be unambiguously inferred.""" """Return the inferred node from the given node, or `None` if it cannot be unambiguously inferred."""
names: list[str] = [] names: list[str] = []
target: astroid.NodeNG | None = node target: astroid.nodes.NodeNG | None = node
inferred: astroid.typing.InferenceResult | None = None inferred: astroid.typing.InferenceResult | None = None
while target: while target:
if inferred := astroid.util.safe_infer(target): if inferred := astroid.util.safe_infer(target):
break break
if isinstance(target, astroid.Call): if isinstance(target, astroid.nodes.Call):
inferred = self.infer(target.func) inferred = self.infer(target.func)
break break
if isinstance(target, astroid.FunctionDef): if isinstance(target, astroid.nodes.FunctionDef):
inferred = target inferred = target
break break
if isinstance(target, astroid.Name): if isinstance(target, astroid.nodes.Name):
target = self.infer_name(target) target = self.infer_name(target)
elif isinstance(target, astroid.AssignName) and isinstance(target.parent, astroid.Assign): elif isinstance(target, astroid.nodes.AssignName) and isinstance(target.parent, astroid.nodes.Assign):
target = target.parent.value target = target.parent.value
elif isinstance(target, astroid.Attribute): elif isinstance(target, astroid.nodes.Attribute):
names.append(target.attrname) names.append(target.attrname)
target = target.expr target = target.expr
else: else:
break break
for name in reversed(names): for name in reversed(names):
if isinstance(inferred, astroid.Instance): if isinstance(inferred, astroid.bases.Instance):
try: try:
attr = next(iter(inferred.getattr(name)), None) attr = next(iter(inferred.getattr(name)), None)
except astroid.AttributeInferenceError: except astroid.exceptions.AttributeInferenceError:
break break
if isinstance(attr, astroid.AssignAttr): if isinstance(attr, astroid.nodes.AssignAttr):
inferred = self.get_ansible_module(attr) inferred = self.get_ansible_module(attr)
continue continue
if isinstance(attr, astroid.FunctionDef): if isinstance(attr, astroid.nodes.FunctionDef):
inferred = attr inferred = attr
continue continue
if not isinstance(inferred, (astroid.Module, astroid.ClassDef)): if not isinstance(inferred, (astroid.nodes.Module, astroid.nodes.ClassDef)):
inferred = None inferred = None
break break
@@ -288,15 +290,15 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
else: else:
inferred = self.infer(inferred) inferred = self.infer(inferred)
if isinstance(inferred, astroid.FunctionDef) and isinstance(inferred.parent, astroid.ClassDef): if isinstance(inferred, astroid.nodes.FunctionDef) and isinstance(inferred.parent, astroid.nodes.ClassDef):
inferred = astroid.BoundMethod(inferred, inferred.parent) inferred = astroid.bases.BoundMethod(inferred, inferred.parent)
return inferred return inferred
def infer_name(self, node: astroid.Name) -> astroid.NodeNG | None: def infer_name(self, node: astroid.nodes.Name) -> astroid.nodes.NodeNG | None:
"""Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred.""" """Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred."""
scope = node.scope() scope = node.scope()
inferred: astroid.NodeNG | None = None inferred: astroid.nodes.NodeNG | None = None
name = node.name name = node.name
while scope: while scope:
@@ -306,12 +308,12 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
scope = scope.parent.scope() if scope.parent else None scope = scope.parent.scope() if scope.parent else None
continue continue
if isinstance(assignment, astroid.AssignName) and isinstance(assignment.parent, astroid.Assign): if isinstance(assignment, astroid.nodes.AssignName) and isinstance(assignment.parent, astroid.nodes.Assign):
inferred = assignment.parent.value inferred = assignment.parent.value
elif ( elif (
isinstance(scope, astroid.FunctionDef) isinstance(scope, astroid.nodes.FunctionDef)
and isinstance(assignment, astroid.AssignName) and isinstance(assignment, astroid.nodes.AssignName)
and isinstance(assignment.parent, astroid.Arguments) and isinstance(assignment.parent, astroid.nodes.Arguments)
and assignment.parent.annotations and assignment.parent.annotations
): ):
idx, _node = assignment.parent.find_argname(name) idx, _node = assignment.parent.find_argname(name)
@@ -322,12 +324,12 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
except IndexError: except IndexError:
pass pass
else: else:
if isinstance(annotation, astroid.Name): if isinstance(annotation, astroid.nodes.Name):
name = annotation.name name = annotation.name
continue continue
elif isinstance(assignment, astroid.ClassDef): elif isinstance(assignment, astroid.nodes.ClassDef):
inferred = assignment inferred = assignment
elif isinstance(assignment, astroid.ImportFrom): elif isinstance(assignment, astroid.nodes.ImportFrom):
if module := self.get_module(assignment): if module := self.get_module(assignment):
name = assignment.real_name(name) name = assignment.real_name(name)
scope = module.scope() scope = module.scope()
@@ -337,7 +339,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
return inferred return inferred
def get_module(self, node: astroid.ImportFrom) -> astroid.Module | None: def get_module(self, node: astroid.nodes.ImportFrom) -> astroid.nodes.Module | None:
"""Import the requested module if possible and cache the result.""" """Import the requested module if possible and cache the result."""
module_name = pylint.checkers.utils.get_import_name(node, node.modname) module_name = pylint.checkers.utils.get_import_name(node, node.modname)
@@ -357,21 +359,21 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
return module return module
@staticmethod @staticmethod
def get_fully_qualified_name(node: astroid.NodeNG) -> str | None: def get_fully_qualified_name(node: astroid.nodes.NodeNG) -> str | None:
"""Return the fully qualified name of the given inferred node.""" """Return the fully qualified name of the given inferred node."""
parent = node.parent parent = node.parent
parts: tuple[str, ...] | None parts: tuple[str, ...] | None
if isinstance(node, astroid.FunctionDef) and isinstance(parent, astroid.Module): if isinstance(node, astroid.nodes.FunctionDef) and isinstance(parent, astroid.nodes.Module):
parts = (parent.name, node.name) parts = (parent.name, node.name)
elif isinstance(node, astroid.BoundMethod) and isinstance(parent, astroid.ClassDef) and isinstance(parent.parent, astroid.Module): elif isinstance(node, astroid.bases.BoundMethod) and isinstance(parent, astroid.nodes.ClassDef) and isinstance(parent.parent, astroid.nodes.Module):
parts = (parent.parent.name, parent.name, node.name) parts = (parent.parent.name, parent.name, node.name)
else: else:
parts = None parts = None
return '.'.join(parts) if parts else None return '.'.join(parts) if parts else None
def check_call(self, node: astroid.Call, name: str, args: tuple[str, ...]) -> None: def check_call(self, node: astroid.nodes.Call, name: str, args: tuple[str, ...]) -> None:
"""Check the given deprecation call node for valid arguments.""" """Check the given deprecation call node for valid arguments."""
call_args = self.get_deprecation_call_args(node, args) call_args = self.get_deprecation_call_args(node, args)
@@ -400,7 +402,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
self.check_version(node, name, call_args) self.check_version(node, name, call_args)
@staticmethod @staticmethod
def get_deprecation_call_args(node: astroid.Call, args: tuple[str, ...]) -> DeprecationCallArgs: def get_deprecation_call_args(node: astroid.nodes.Call, args: tuple[str, ...]) -> DeprecationCallArgs:
"""Get the deprecation call arguments from the given node.""" """Get the deprecation call arguments from the given node."""
fields: dict[str, object] = {} fields: dict[str, object] = {}
@@ -413,12 +415,12 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
fields[keyword.arg] = keyword.value fields[keyword.arg] = keyword.value
for key, value in fields.items(): for key, value in fields.items():
if isinstance(value, astroid.Const): if isinstance(value, astroid.nodes.Const):
fields[key] = value.value fields[key] = value.value
return DeprecationCallArgs(**fields) return DeprecationCallArgs(**fields)
def check_collection_name(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None: def check_collection_name(self, node: astroid.nodes.Call, name: str, args: DeprecationCallArgs) -> None:
"""Check the collection name provided to the given call node.""" """Check the collection name provided to the given call node."""
deprecator_requirement = self.is_deprecator_required() deprecator_requirement = self.is_deprecator_required()
@@ -459,14 +461,14 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
if args.collection_name and args.collection_name != expected_collection_name: if args.collection_name and args.collection_name != expected_collection_name:
self.add_message('wrong-collection-deprecated', node=node, args=(args.collection_name, name)) self.add_message('wrong-collection-deprecated', node=node, args=(args.collection_name, name))
def check_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None: def check_version(self, node: astroid.nodes.Call, name: str, args: DeprecationCallArgs) -> None:
"""Check the version provided to the given call node.""" """Check the version provided to the given call node."""
if self.collection_name: if self.collection_name:
self.check_collection_version(node, name, args) self.check_collection_version(node, name, args)
else: else:
self.check_core_version(node, name, args) self.check_core_version(node, name, args)
def check_core_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None: def check_core_version(self, node: astroid.nodes.Call, name: str, args: DeprecationCallArgs) -> None:
"""Check the core version provided to the given call node.""" """Check the core version provided to the given call node."""
try: try:
if not isinstance(args.version, str) or not args.version: if not isinstance(args.version, str) or not args.version:
@@ -480,7 +482,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
if self.ANSIBLE_VERSION >= strict_version: if self.ANSIBLE_VERSION >= strict_version:
self.add_message('ansible-deprecated-version', node=node, args=(args.version, name)) self.add_message('ansible-deprecated-version', node=node, args=(args.version, name))
def check_collection_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None: def check_collection_version(self, node: astroid.nodes.Call, name: str, args: DeprecationCallArgs) -> None:
"""Check the collection version provided to the given call node.""" """Check the collection version provided to the given call node."""
try: try:
if not isinstance(args.version, str) or not args.version: if not isinstance(args.version, str) or not args.version:
@@ -497,7 +499,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
if semantic_version.major != 0 and (semantic_version.minor != 0 or semantic_version.patch != 0): if semantic_version.major != 0 and (semantic_version.minor != 0 or semantic_version.patch != 0):
self.add_message('removal-version-must-be-major', node=node, args=(args.version,)) self.add_message('removal-version-must-be-major', node=node, args=(args.version,))
def check_date(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None: def check_date(self, node: astroid.nodes.Call, name: str, args: DeprecationCallArgs) -> None:
"""Check the date provided to the given call node.""" """Check the date provided to the given call node."""
try: try:
date_parsed = self.parse_isodate(args.date) date_parsed = self.parse_isodate(args.date)
@@ -515,18 +517,19 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
raise TypeError(type(value)) raise TypeError(type(value))
def get_ansible_module(self, node: astroid.AssignAttr) -> astroid.Instance | None: def get_ansible_module(self, node: astroid.nodes.AssignAttr) -> astroid.bases.Instance | None:
"""Infer an AnsibleModule instance node from the given assignment.""" """Infer an AnsibleModule instance node from the given assignment."""
if isinstance(node.parent, astroid.Assign) and isinstance(node.parent.type_annotation, astroid.Name): if isinstance(node.parent, astroid.nodes.Assign) and isinstance(node.parent.type_annotation, astroid.nodes.Name):
inferred = self.infer_name(node.parent.type_annotation) inferred = self.infer_name(node.parent.type_annotation)
elif isinstance(node.parent, astroid.Assign) and isinstance(node.parent.parent, astroid.FunctionDef) and isinstance(node.parent.value, astroid.Name): elif (isinstance(node.parent, astroid.nodes.Assign) and isinstance(node.parent.parent, astroid.nodes.FunctionDef) and
isinstance(node.parent.value, astroid.nodes.Name)):
inferred = self.infer_name(node.parent.value) inferred = self.infer_name(node.parent.value)
elif isinstance(node.parent, astroid.AnnAssign) and isinstance(node.parent.annotation, astroid.Name): elif isinstance(node.parent, astroid.nodes.AnnAssign) and isinstance(node.parent.annotation, astroid.nodes.Name):
inferred = self.infer_name(node.parent.annotation) inferred = self.infer_name(node.parent.annotation)
else: else:
inferred = None inferred = None
if isinstance(inferred, astroid.ClassDef) and inferred.name == 'AnsibleModule': if isinstance(inferred, astroid.nodes.ClassDef) and inferred.name == 'AnsibleModule':
return inferred.instantiate_class() return inferred.instantiate_class()
return None return None

View File

@@ -5,7 +5,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
import astroid import astroid.bases
import astroid.exceptions
import astroid.nodes
try: try:
from pylint.checkers.utils import check_messages from pylint.checkers.utils import check_messages
@@ -39,22 +41,22 @@ class AnsibleStringFormatChecker(BaseChecker):
def visit_call(self, node): def visit_call(self, node):
"""Visit a call node.""" """Visit a call node."""
func = utils.safe_infer(node.func) func = utils.safe_infer(node.func)
if (isinstance(func, astroid.BoundMethod) if (isinstance(func, astroid.bases.BoundMethod)
and isinstance(func.bound, astroid.Instance) and isinstance(func.bound, astroid.bases.Instance)
and func.bound.name in ('str', 'unicode', 'bytes')): and func.bound.name in ('str', 'unicode', 'bytes')):
if func.name == 'format': if func.name == 'format':
self._check_new_format(node, func) self._check_new_format(node, func)
def _check_new_format(self, node, func): def _check_new_format(self, node, func):
""" Check the new string formatting """ """ Check the new string formatting """
if (isinstance(node.func, astroid.Attribute) if (isinstance(node.func, astroid.nodes.Attribute)
and not isinstance(node.func.expr, astroid.Const)): and not isinstance(node.func.expr, astroid.nodes.Const)):
return return
try: try:
strnode = next(func.bound.infer()) strnode = next(func.bound.infer())
except astroid.InferenceError: except astroid.exceptions.InferenceError:
return return
if not isinstance(strnode, astroid.Const): if not isinstance(strnode, astroid.nodes.Const):
return return
if isinstance(strnode.value, bytes): if isinstance(strnode.value, bytes):

View File

@@ -6,7 +6,8 @@ import functools
import os import os
import typing as t import typing as t
import astroid import astroid.exceptions
import astroid.nodes
from pylint.checkers import BaseChecker from pylint.checkers import BaseChecker
@@ -146,21 +147,21 @@ class AnsibleUnwantedChecker(BaseChecker):
"""True if ansible-core is being tested.""" """True if ansible-core is being tested."""
return not self.linter.config.collection_name return not self.linter.config.collection_name
def visit_import(self, node): # type: (astroid.node_classes.Import) -> None def visit_import(self, node: astroid.nodes.Import) -> None:
"""Visit an import node.""" """Visit an import node."""
for name in node.names: for name in node.names:
self._check_import(node, name[0]) self._check_import(node, name[0])
def visit_importfrom(self, node): # type: (astroid.node_classes.ImportFrom) -> None def visit_importfrom(self, node: astroid.nodes.ImportFrom) -> None:
"""Visit an import from node.""" """Visit an import from node."""
self._check_importfrom(node, node.modname, node.names) self._check_importfrom(node, node.modname, node.names)
def visit_attribute(self, node): # type: (astroid.node_classes.Attribute) -> None def visit_attribute(self, node: astroid.nodes.Attribute) -> None:
"""Visit an attribute node.""" """Visit an attribute node."""
last_child = node.last_child() last_child = node.last_child()
# this is faster than using type inference and will catch the most common cases # this is faster than using type inference and will catch the most common cases
if not isinstance(last_child, astroid.node_classes.Name): if not isinstance(last_child, astroid.nodes.Name):
return return
module = last_child.name module = last_child.name
@@ -171,13 +172,13 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file, node.attrname): if entry.applies_to(self.linter.current_file, node.attrname):
self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node) self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node)
def visit_call(self, node): # type: (astroid.node_classes.Call) -> None def visit_call(self, node: astroid.nodes.Call) -> None:
"""Visit a call node.""" """Visit a call node."""
try: try:
for i in node.func.inferred(): for i in node.func.inferred():
func = None func = None
if isinstance(i, astroid.scoped_nodes.FunctionDef) and isinstance(i.parent, astroid.scoped_nodes.Module): if isinstance(i, astroid.nodes.FunctionDef) and isinstance(i.parent, astroid.nodes.Module):
func = '%s.%s' % (i.parent.name, i.name) func = '%s.%s' % (i.parent.name, i.name)
if not func: if not func:
@@ -190,7 +191,7 @@ class AnsibleUnwantedChecker(BaseChecker):
except astroid.exceptions.InferenceError: except astroid.exceptions.InferenceError:
pass pass
def _check_import(self, node, modname): # type: (astroid.node_classes.Import, str) -> None def _check_import(self, node: astroid.nodes.Import, modname: str) -> None:
"""Check the imports on the specified import node.""" """Check the imports on the specified import node."""
self._check_module_import(node, modname) self._check_module_import(node, modname)
@@ -202,7 +203,7 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file): if entry.applies_to(self.linter.current_file):
self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node) self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node)
def _check_importfrom(self, node, modname, names): # type: (astroid.node_classes.ImportFrom, str, t.List[str]) -> None def _check_importfrom(self, node: astroid.nodes.ImportFrom, modname: str, names: list[tuple[str, str | None]]) -> None:
"""Check the imports on the specified import from node.""" """Check the imports on the specified import from node."""
self._check_module_import(node, modname) self._check_module_import(node, modname)
@@ -215,7 +216,7 @@ class AnsibleUnwantedChecker(BaseChecker):
if entry.applies_to(self.linter.current_file, name[0]): if entry.applies_to(self.linter.current_file, name[0]):
self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node) self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node)
def _check_module_import(self, node, modname): # type: (t.Union[astroid.node_classes.Import, astroid.node_classes.ImportFrom], str) -> None def _check_module_import(self, node: astroid.nodes.Import | astroid.nodes.ImportFrom, modname: str) -> None:
"""Check the module import on the given import or import from node.""" """Check the module import on the given import or import from node."""
if not is_module_path(self.linter.current_file): if not is_module_path(self.linter.current_file):
return return

View File

@@ -1,7 +1,8 @@
# edit "black.requirements.in" and generate with: hacking/update-sanity-requirements.py --test black # edit "black.requirements.in" and generate with: hacking/update-sanity-requirements.py --test black
black==25.1.0 black==25.9.0
click==8.2.1 click==8.3.0
mypy_extensions==1.1.0 mypy_extensions==1.1.0
packaging==25.0 packaging==25.0
pathspec==0.12.1 pathspec==0.12.1
platformdirs==4.4.0 platformdirs==4.5.0
pytokens==0.1.10

View File

@@ -1,4 +1,4 @@
# edit "deprecated-config.requirements.in" and generate with: hacking/update-sanity-requirements.py --test deprecated-config # edit "deprecated-config.requirements.in" and generate with: hacking/update-sanity-requirements.py --test deprecated-config
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
PyYAML==6.0.2 PyYAML==6.0.3

View File

@@ -1,24 +1,24 @@
# edit "mypy.requirements.in" and generate with: hacking/update-sanity-requirements.py --test mypy # edit "mypy.requirements.in" and generate with: hacking/update-sanity-requirements.py --test mypy
cffi==2.0.0 cffi==2.0.0
cryptography==45.0.7 cryptography==46.0.2
iniconfig==2.1.0 iniconfig==2.1.0
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.2 MarkupSafe==3.0.3
mypy==1.17.1 mypy==1.18.2
mypy_extensions==1.1.0 mypy_extensions==1.1.0
packaging==25.0 packaging==25.0
pathspec==0.12.1 pathspec==0.12.1
pluggy==1.6.0 pluggy==1.6.0
pycparser==2.22 pycparser==2.23
Pygments==2.19.2 Pygments==2.19.2
pytest==8.4.2 pytest==8.4.2
pytest-mock==3.15.0 pytest-mock==3.15.1
resolvelib==1.2.0 resolvelib==1.2.1
tomli==2.2.1 tomli==2.3.0
types-backports==0.1.3 types-backports==0.1.3
types-paramiko==4.0.0.20250822 types-paramiko==4.0.0.20250822
types-PyYAML==6.0.12.20250822 types-PyYAML==6.0.12.20250915
types-requests==2.32.4.20250809 types-requests==2.32.4.20250913
types-setuptools==80.9.0.20250822 types-setuptools==80.9.0.20250822
types-toml==0.10.8.20240310 types-toml==0.10.8.20240310
typing_extensions==4.15.0 typing_extensions==4.15.0

View File

@@ -36,10 +36,19 @@ ignore_missing_imports = True
[mypy-astroid] [mypy-astroid]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-astroid.bases]
ignore_missing_imports = True
[mypy-astroid.exceptions]
ignore_missing_imports = True
[mypy-astroid.nodes]
ignore_missing_imports = True
[mypy-astroid.typing] [mypy-astroid.typing]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-astroid.context] [mypy-astroid.util]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-pylint] [mypy-pylint]

View File

@@ -1,10 +1,10 @@
# edit "pymarkdown.requirements.in" and generate with: hacking/update-sanity-requirements.py --test pymarkdown # edit "pymarkdown.requirements.in" and generate with: hacking/update-sanity-requirements.py --test pymarkdown
application_properties==0.9.0 application_properties==0.9.0
Columnar==1.4.1 Columnar==1.4.1
pyjson5==1.6.9 pyjson5==2.0.0
pymarkdownlnt==0.9.32 pymarkdownlnt==0.9.32
PyYAML==6.0.2 PyYAML==6.0.3
tomli==2.2.1 tomli==2.3.0
toolz==1.0.0 toolz==1.0.0
typing_extensions==4.15.0 typing_extensions==4.15.0
wcwidth==0.2.13 wcwidth==0.2.14

View File

@@ -65,7 +65,6 @@ test/integration/targets/ansible-doc/library/facts_one shebang
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-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-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-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-sanity/ansible_collections/ns/col/plugins/plugin_utils/check_pylint.py pylint:disallowed-name # 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-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/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/modules/test_hello.py pylint:relative-beyond-top-level
@@ -115,6 +114,11 @@ test/integration/targets/win_script/files/test_script_with_args.ps1 pslint:PSAvo
test/integration/targets/win_script/files/test_script_with_splatting.ps1 pslint:PSAvoidUsingWriteHost # Keep test/integration/targets/win_script/files/test_script_with_splatting.ps1 pslint:PSAvoidUsingWriteHost # Keep
test/integration/targets/ssh_agent/fake_agents/ssh-agent-bad-shebang shebang # required for test test/integration/targets/ssh_agent/fake_agents/ssh-agent-bad-shebang shebang # required for test
test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 pslint:PSCustomUseLiteralPath # Uses wildcards on purpose test/lib/ansible_test/_data/requirements/sanity.pslint.ps1 pslint:PSCustomUseLiteralPath # Uses wildcards on purpose
test/lib/ansible_test/_internal/compat/packaging.py pylint:invalid-name # pylint bug: https://github.com/pylint-dev/pylint/issues/10652
test/lib/ansible_test/_internal/compat/yaml.py pylint:invalid-name # pylint bug: https://github.com/pylint-dev/pylint/issues/10652
test/lib/ansible_test/_internal/init.py pylint:invalid-name # pylint bug: https://github.com/pylint-dev/pylint/issues/10652
test/lib/ansible_test/_internal/util.py pylint:invalid-name # pylint bug: https://github.com/pylint-dev/pylint/issues/10652
test/lib/ansible_test/_util/target/setup/requirements.py pylint:invalid-name # pylint bug: https://github.com/pylint-dev/pylint/issues/10652
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/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/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/async_status.ps1 pslint!skip