From d2ca8a94239958394363c9faab70c5db99a82cae Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Mon, 28 Mar 2022 16:08:15 +0100 Subject: [PATCH] master.ParentEnumerationMethod: Require matching pkg.__name__ Co-authored-by: Stefano Rivera 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 --- docs/changelog.rst | 2 ++ mitogen/master.py | 53 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e459842..abb848f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,8 @@ To avail of fixes in an unreleased version, please download a ZIP file 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) ------------------- diff --git a/mitogen/master.py b/mitogen/master.py index 61114f90..31dd4e3d 100644 --- a/mitogen/master.py +++ b/mitogen/master.py @@ -664,10 +664,24 @@ class ParentEnumerationMethod(FinderMethod): module object or any parent package's :data:`__path__`, since they have all 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): """ 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: `(parent_name, path, modpath)` tuple, where: @@ -680,21 +694,40 @@ class ParentEnumerationMethod(FinderMethod): * `modpath`: list of module name components leading from `path` to the target module. """ - path = None modpath = [] - while True: - pkgname, _, modname = str_rpartition(to_text(fullname), u'.') + for pkgname, modname in self._iter_parents(fullname): modpath.insert(0, modname) if not pkgname: return [], None, modpath - pkg = sys.modules.get(pkgname) - path = getattr(pkg, '__path__', None) - if pkg and path: - return pkgname.split('.'), path, modpath + try: + pkg = sys.modules[pkgname] + except KeyError: + 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) - fullname = pkgname + return pkgname.split('.'), path, modpath def _found_package(self, fullname, path): path = os.path.join(path, '__init__.py')