ansible-test - Improve deprecated checking type inference (#85159)

* ansible-test - Improve deprecated checking type inference

Also disabled the ``bad-super-call`` pylint rule due to false positives.

* Add type comment support

* Try without using register_transform
This commit is contained in:
Matt Clay
2025-05-20 11:23:06 -07:00
committed by GitHub
parent feda0a5c6e
commit e82be177cd
9 changed files with 114 additions and 6 deletions

View File

@@ -0,0 +1,3 @@
bugfixes:
- ansible-test - Improve type inference for pylint deprecated checks to accommodate some type annotations.
- ansible-test - Disabled the ``bad-super-call`` pylint rule due to false positives.

View File

@@ -19,6 +19,7 @@ import ansible.module_utils.common.warnings
from ansible.module_utils import datatag
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import AnsibleModule as AliasedAnsibleModule
from ansible.module_utils.basic import deprecate
from ansible.module_utils.common import warnings
from ansible.module_utils.common.warnings import deprecate as basic_deprecate
@@ -39,7 +40,6 @@ foreign_global_display = x_display._display
#
#
#
#
class LookupModule(LookupBase):
@@ -90,3 +90,43 @@ def do_stuff() -> None:
a_version = 'version not checked'
a_collection_name = 'mismatched'
_display.deprecated(msg=a_msg, date=a_date, version=a_version, collection_name=a_collection_name)
wrapper = MyWrapper(AnsibleModule({}))
wrapper.module.deprecate('', version='2.0.0', collection_name='ns.col')
wrapper = MyAliasedWrapper(AnsibleModule({}))
wrapper.module.deprecate('', version='2.0.0', collection_name='ns.col')
wrapper = MyOtherWrapper(AnsibleModule({}))
wrapper.module.deprecate('', version='2.0.0', collection_name='ns.col')
wrapper = MyOtherAliasedWrapper(AnsibleModule({}))
wrapper.module.deprecate('', version='2.0.0', collection_name='ns.col')
wrapper = MyTypeCommentWrapper(AnsibleModule({}))
wrapper.module.deprecate('', version='2.0.0', collection_name='ns.col')
class MyWrapper:
def __init__(self, thing) -> None:
self.module: AnsibleModule = thing
class MyAliasedWrapper:
def __init__(self, thing) -> None:
self.module: AliasedAnsibleModule = thing
class MyOtherWrapper:
def __init__(self, thing: AnsibleModule) -> None:
self.module = thing
class MyOtherAliasedWrapper:
def __init__(self, thing: AliasedAnsibleModule) -> None:
self.module = thing
class MyTypeCommentWrapper:
def __init__(self, thing) -> None:
self.module = thing # type: AnsibleModule

View File

@@ -25,6 +25,11 @@ plugins/lookup/deprecated.py:81:4: collection-deprecated-version: Deprecated ver
plugins/lookup/deprecated.py:82:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.utils.display.Display.deprecated'
plugins/lookup/deprecated.py:83:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.utils.display.Display.deprecated'
plugins/lookup/deprecated.py:84:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.utils.display.Display.deprecated'
plugins/lookup/deprecated.py:95:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.module_utils.basic.AnsibleModule.deprecate'
plugins/lookup/deprecated.py:98:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.module_utils.basic.AnsibleModule.deprecate'
plugins/lookup/deprecated.py:101:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.module_utils.basic.AnsibleModule.deprecate'
plugins/lookup/deprecated.py:104:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.module_utils.basic.AnsibleModule.deprecate'
plugins/lookup/deprecated.py:107:4: collection-deprecated-version: Deprecated version '2.0.0' found in call to 'ansible.module_utils.basic.AnsibleModule.deprecate'
plugins/module_utils/deprecated_utils.py:21:4: ansible-deprecated-no-version: Found 'ansible.module_utils.common.warnings.deprecate' call without a version or date
plugins/module_utils/deprecated_utils.py:23:4: collection-deprecated-version: Deprecated version '1.0.0' found in call to 'ansible.module_utils.common.warnings.deprecate'
plugins/module_utils/deprecated_utils.py:24:4: collection-invalid-deprecated-version: Invalid deprecated version 'not-a-version' found in call to 'ansible.module_utils.common.warnings.deprecate'

View File

@@ -6,6 +6,7 @@ load-plugins=
pylint.extensions.docstyle,
disable=
bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
docstring-first-line-empty,
consider-using-f-string, # Python 2.x support still required
cyclic-import, # consistent results require running with --jobs 1 and testing all files

View File

@@ -6,6 +6,7 @@ load-plugins=
pylint.extensions.docstyle,
disable=
bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
docstring-first-line-empty,
consider-using-f-string, # many occurrences
cyclic-import, # consistent results require running with --jobs 1 and testing all files

View File

@@ -6,6 +6,7 @@ load-plugins=
pylint.extensions.docstyle,
disable=
bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
docstring-first-line-empty,
consider-using-f-string, # many occurrences
cyclic-import, # consistent results require running with --jobs 1 and testing all files

View File

@@ -11,6 +11,7 @@ disable=
attribute-defined-outside-init,
bad-indentation,
bad-mcs-classmethod-argument,
bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
broad-exception-caught,
broad-exception-raised,
c-extension-no-member,

View File

@@ -16,6 +16,7 @@ disable=
attribute-defined-outside-init,
bad-indentation,
bad-mcs-classmethod-argument,
bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
broad-exception-caught,
broad-exception-raised,
c-extension-no-member,

View File

@@ -263,6 +263,20 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
break
for name in reversed(names):
if isinstance(inferred, astroid.Instance):
try:
attr = next(iter(inferred.getattr(name)), None)
except astroid.AttributeInferenceError:
break
if isinstance(attr, astroid.AssignAttr):
inferred = self.get_ansible_module(attr)
continue
if isinstance(attr, astroid.FunctionDef):
inferred = attr
continue
if not isinstance(inferred, (astroid.Module, astroid.ClassDef)):
inferred = None
break
@@ -282,25 +296,46 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
def infer_name(self, node: astroid.Name) -> astroid.NodeNG | None:
"""Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred."""
scope = node.scope()
name = None
inferred: astroid.NodeNG | None = None
name = node.name
while scope:
try:
assignment = scope[node.name]
assignment = scope[name]
except KeyError:
scope = scope.parent.scope() if scope.parent else None
continue
if isinstance(assignment, astroid.AssignName) and isinstance(assignment.parent, astroid.Assign):
name = assignment.parent.value
inferred = assignment.parent.value
elif (
isinstance(scope, astroid.FunctionDef)
and isinstance(assignment, astroid.AssignName)
and isinstance(assignment.parent, astroid.Arguments)
and assignment.parent.annotations
):
idx, _node = assignment.parent.find_argname(name)
if idx is not None:
try:
annotation = assignment.parent.annotations[idx]
except IndexError:
pass
else:
if isinstance(annotation, astroid.Name):
name = annotation.name
continue
elif isinstance(assignment, astroid.ClassDef):
inferred = assignment
elif isinstance(assignment, astroid.ImportFrom):
if module := self.get_module(assignment):
name = assignment.real_name(name)
scope = module.scope()
continue
break
return name
return inferred
def get_module(self, node: astroid.ImportFrom) -> astroid.Module | None:
"""Import the requested module if possible and cache the result."""
@@ -480,7 +515,27 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
raise TypeError(type(value))
def get_ansible_module(self, node: astroid.AssignAttr) -> astroid.Instance | None:
"""Infer an AnsibleModule instance node from the given assignment."""
if isinstance(node.parent, astroid.Assign) and isinstance(node.parent.type_annotation, astroid.Name):
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):
inferred = self.infer_name(node.parent.value)
elif isinstance(node.parent, astroid.AnnAssign) and isinstance(node.parent.annotation, astroid.Name):
inferred = self.infer_name(node.parent.annotation)
else:
inferred = None
if isinstance(inferred, astroid.ClassDef) and inferred.name == 'AnsibleModule':
return inferred.instantiate_class()
return None
def register(self) -> None:
"""Register this plugin."""
self.linter.register_checker(self)
def register(linter: pylint.lint.PyLinter) -> None:
"""Required method to auto-register this checker."""
linter.register_checker(AnsibleDeprecatedChecker(linter))
AnsibleDeprecatedChecker(linter).register()