[stable-2.10] Fix ansible-test relative import analysis. (#70993)

(cherry picked from commit 2e0097ada3)

Co-authored-by: Matt Clay <matt@mystile.com>
This commit is contained in:
Matt Clay 2020-07-30 13:36:49 -07:00 committed by GitHub
parent abfedb06c3
commit b764d381f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 63 additions and 2 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- ansible-test - Change detection now properly resolves relative imports instead of treating them as absolute imports.

View file

@ -4,6 +4,7 @@ __metaclass__ = type
import ast import ast
import os import os
import re
from . import types as t from . import types as t
@ -207,6 +208,35 @@ def get_import_path(name, package=False): # type: (str, bool) -> str
return path return path
def path_to_module(path): # type: (str) -> str
"""Convert the given path to a module name."""
module = os.path.splitext(path)[0].replace(os.path.sep, '.')
if module.endswith('.__init__'):
module = module[:-9]
return module
def relative_to_absolute(name, level, module, path, lineno): # type: (str, int, str, str, int) -> str
"""Convert a relative import to an absolute import."""
if level <= 0:
absolute_name = name
elif not module:
display.warning('Cannot resolve relative import "%s%s" in unknown module at %s:%d' % ('.' * level, name, path, lineno))
absolute_name = 'relative.nomodule'
else:
parts = module.split('.')
if level >= len(parts):
display.warning('Cannot resolve relative import "%s%s" above module "%s" at %s:%d' % ('.' * level, name, module, path, lineno))
absolute_name = 'relative.abovelevel'
else:
absolute_name = '.'.join(parts[:-level] + [name])
return absolute_name
class ModuleUtilFinder(ast.NodeVisitor): class ModuleUtilFinder(ast.NodeVisitor):
"""AST visitor to find valid module_utils imports.""" """AST visitor to find valid module_utils imports."""
def __init__(self, path, module_utils): def __init__(self, path, module_utils):
@ -229,6 +259,33 @@ class ModuleUtilFinder(ast.NodeVisitor):
if package != 'ansible.module_utils' and package not in VIRTUAL_PACKAGES: if package != 'ansible.module_utils' and package not in VIRTUAL_PACKAGES:
self.add_import(package, 0) self.add_import(package, 0)
self.module = None
if data_context().content.is_ansible:
# Various parts of the Ansible source tree execute within diffent modules.
# To support import analysis, each file which uses relative imports must reside under a path defined here.
# The mapping is a tuple consisting of a path pattern to match and a replacement path.
# During analyis, any relative imports not covered here will result in warnings, which can be fixed by adding the appropriate entry.
path_map = (
('^hacking/build_library/build_ansible/', 'build_ansible/'),
('^lib/ansible/', 'ansible/'),
('^test/lib/ansible_test/_data/sanity/validate-modules/', 'validate_modules/'),
('^test/units/', 'test/units/'),
('^test/lib/ansible_test/_internal/', 'ansible_test/_internal/'),
('^test/integration/targets/.*/ansible_collections/(?P<ns>[^/]*)/(?P<col>[^/]*)/', r'ansible_collections/\g<ns>/\g<col>/'),
('^test/integration/targets/.*/library/', 'ansible/modules/'),
)
for pattern, replacement in path_map:
if re.search(pattern, self.path):
revised_path = re.sub(pattern, replacement, self.path)
self.module = path_to_module(revised_path)
break
else:
# This assumes that all files within the collection are executed by Ansible as part of the collection.
# While that will usually be true, there are exceptions which will result in this resolution being incorrect.
self.module = path_to_module(os.path.join(data_context().content.collection.directory, self.path))
# noinspection PyPep8Naming # noinspection PyPep8Naming
# pylint: disable=locally-disabled, invalid-name # pylint: disable=locally-disabled, invalid-name
def visit_Import(self, node): def visit_Import(self, node):
@ -252,14 +309,16 @@ class ModuleUtilFinder(ast.NodeVisitor):
if not node.module: if not node.module:
return return
if not node.module.startswith('ansible'): module = relative_to_absolute(node.module, node.level, self.module, self.path, node.lineno)
if not module.startswith('ansible'):
return return
# from ansible.module_utils import MODULE[, MODULE] # from ansible.module_utils import MODULE[, MODULE]
# from ansible.module_utils.MODULE[.MODULE] import MODULE[, MODULE] # from ansible.module_utils.MODULE[.MODULE] import MODULE[, MODULE]
# from ansible_collections.{ns}.{col}.plugins.module_utils import MODULE[, MODULE] # from ansible_collections.{ns}.{col}.plugins.module_utils import MODULE[, MODULE]
# from ansible_collections.{ns}.{col}.plugins.module_utils.MODULE[.MODULE] import MODULE[, MODULE] # from ansible_collections.{ns}.{col}.plugins.module_utils.MODULE[.MODULE] import MODULE[, MODULE]
self.add_imports(['%s.%s' % (node.module, alias.name) for alias in node.names], node.lineno) self.add_imports(['%s.%s' % (module, alias.name) for alias in node.names], node.lineno)
def add_import(self, name, line_number): def add_import(self, name, line_number):
""" """