master.ParentEnumerationMethod: Require matching pkg.__name__

Co-authored-by: Stefano Rivera <stefano@rivera.za.net>

When the requested module (e.g. ansible.module_utils.distro)
- is provided by another module *e.g. distro)
- that itself was a package (e.g. distro 1.7.0)

At runtime
- ansible/module_utils/distro/__init__.py executes
- if https://pypi.org/project/distro/ is present, it's loaded as
ansible.module_utils.distro
- otherwise ansible/module_utils/distro/_distro.py is loaded

ParentEnumerationMethod would wrongly use whatever was in
sys.modules['ansible.module_utils.distro]. Instead we should ascend to
the first parent that has fullname == sys.modules[fullname].__name__.
Then descend to the appropriate .py file on disk.

This bug didn't show up before because until distro 1.7.0 (Feb 2022) the
top-level distro module was a module (distro.py) not a package
(distro/__init__.py)

fixes #906
pull/913/head
Alex Willmer 3 years ago
parent 47699e15aa
commit d2ca8a9423

@ -21,6 +21,8 @@ To avail of fixes in an unreleased version, please download a ZIP file
v0.3.3.dev0 v0.3.3.dev0
------------------- -------------------
* :gh:issue:`906` Support packages dynamically inserted into sys.modules, e.g. `distro` >= 1.7.0 as `ansible.module_utils.distro`.
v0.3.2 (2022-01-12) v0.3.2 (2022-01-12)
------------------- -------------------

@ -664,10 +664,24 @@ class ParentEnumerationMethod(FinderMethod):
module object or any parent package's :data:`__path__`, since they have all module object or any parent package's :data:`__path__`, since they have all
been overwritten. Some men just want to watch the world burn. been overwritten. Some men just want to watch the world burn.
""" """
@staticmethod
def _iter_parents(fullname):
"""
>>> list(ParentEnumerationMethod._iter_parents('a'))
[('', 'a')]
>>> list(ParentEnumerationMethod._iter_parents('a.b.c'))
[('a.b', 'c'), ('a', 'b'), ('', 'a')]
"""
while fullname:
fullname, _, modname = str_rpartition(fullname, u'.')
yield fullname, modname
def _find_sane_parent(self, fullname): def _find_sane_parent(self, fullname):
""" """
Iteratively search :data:`sys.modules` for the least indirect parent of Iteratively search :data:`sys.modules` for the least indirect parent of
`fullname` that is loaded and contains a :data:`__path__` attribute. `fullname` that's from the same package and has a :data:`__path__`
attribute.
:return: :return:
`(parent_name, path, modpath)` tuple, where: `(parent_name, path, modpath)` tuple, where:
@ -680,21 +694,40 @@ class ParentEnumerationMethod(FinderMethod):
* `modpath`: list of module name components leading from `path` * `modpath`: list of module name components leading from `path`
to the target module. to the target module.
""" """
path = None
modpath = [] modpath = []
while True: for pkgname, modname in self._iter_parents(fullname):
pkgname, _, modname = str_rpartition(to_text(fullname), u'.')
modpath.insert(0, modname) modpath.insert(0, modname)
if not pkgname: if not pkgname:
return [], None, modpath return [], None, modpath
pkg = sys.modules.get(pkgname) try:
path = getattr(pkg, '__path__', None) pkg = sys.modules[pkgname]
if pkg and path: except KeyError:
return pkgname.split('.'), path, modpath LOG.debug('%r: sys.modules[%r] absent, skipping', self, pkgname)
continue
try:
resolved_pkgname = pkg.__name__
except AttributeError:
LOG.debug('%r: %r has no __name__, skipping', self, pkg)
continue
if resolved_pkgname != pkgname:
LOG.debug('%r: %r.__name__ is %r, skipping',
self, pkg, resolved_pkgname)
continue
try:
path = pkg.__path__
except AttributeError:
LOG.debug('%r: %r has no __path__, skipping', self, pkg)
continue
if not path:
LOG.debug('%r: %r.__path__ is %r, skipping', self, pkg, path)
continue
LOG.debug('%r: %r lacks __path__ attribute', self, pkgname) return pkgname.split('.'), path, modpath
fullname = pkgname
def _found_package(self, fullname, path): def _found_package(self, fullname, path):
path = os.path.join(path, '__init__.py') path = os.path.join(path, '__init__.py')

Loading…
Cancel
Save