[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1111486: marked as done (bookworm-pu: package python-mitogen/0.3.3-9+deb12u1)



Your message dated Sat, 06 Sep 2025 12:14:50 +0100
with message-id <ee4c0876608d99eb3f8b333b556fbd92e7a652eb.camel@adam-barratt.org.uk>
and subject line Closing p-u requests for fixes included in 12.12
has caused the Debian Bug report #1111486,
regarding bookworm-pu: package python-mitogen/0.3.3-9+deb12u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact owner@bugs.debian.org
immediately.)


-- 
1111486: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1111486
Debian Bug Tracking System
Contact owner@bugs.debian.org with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: python-mitogen@packages.debian.org
Control: affects -1 + src:python-mitogen
User: release.debian.org@packages.debian.org
Usertags: pu

[ Reason ]
bookworm systems were unable to use mitogen to operate on trixie systems 
that have Python 3.13. The payload code expected the "imp" module to 
exist.

[ Impact ]
Users are unable to use mitogen (e.g. with ansible) to operate on trixie 
systems.

[ Tests ]
There is an automated test suite. We run as much of it as we can.

danjean manually tested the proposed package and confirmed that it 
works.

[ Risks ]
Changes are from a single upstream commit, with minimal backporting 
required. They are relatively self-contained. I don't recall any issues 
with them, when this change first landed upstream.

mitogen is virtually a leaf package.

[ Checklist ]
  [x] *all* changes are documented in the d/changelog
  [x] I reviewed all changes and I approve them
  [x] attach debdiff against the package in oldstable
  [x] the issue is verified as fixed in unstable

[ Changes ]

 python-mitogen (0.3.3-9+deb12u1) bookworm; urgency=medium
 .
   * Patch: Support targets with Python >= 3.12. (Closes: #1111363)
   * Patch test module paths in testlib.py in the ansible-tests autopkgtest.

The patch adds new mechanisms (using importlib) to find Python modules 
to import. It keeps the old imp-based methods for older Python versions.
diff -Nru python-mitogen-0.3.3/debian/changelog python-mitogen-0.3.3/debian/changelog
--- python-mitogen-0.3.3/debian/changelog	2023-05-13 15:45:14.000000000 +0200
+++ python-mitogen-0.3.3/debian/changelog	2025-08-18 16:33:11.000000000 +0200
@@ -1,3 +1,10 @@
+python-mitogen (0.3.3-9+deb12u1) bookworm; urgency=medium
+
+  * Patch: Support targets with Python >= 3.12. (Closes: #1111363)
+  * Patch test module paths in testlib.py in the ansible-tests autopkgtest.
+
+ -- Stefano Rivera <stefanor@debian.org>  Mon, 18 Aug 2025 16:33:11 +0200
+
 python-mitogen (0.3.3-9) unstable; urgency=medium
 
   * Patch: Use poll() in the broker to handle more file descriptors.
diff -Nru python-mitogen-0.3.3/debian/patches/ansible-6 python-mitogen-0.3.3/debian/patches/ansible-6
--- python-mitogen-0.3.3/debian/patches/ansible-6	2023-05-13 15:45:14.000000000 +0200
+++ python-mitogen-0.3.3/debian/patches/ansible-6	2025-08-18 16:33:11.000000000 +0200
@@ -8,16 +8,18 @@
 Bug-Debian: https://bugs.debian.org/1019501
 Origin: upstream, https://github.com/mitogen-hq/mitogen/pull/933
 ---
- .ci/azure-pipelines.yml    |   60 ++-------------------------------------------
- ansible_mitogen/loaders.py |    2 -
- docs/ansible_detailed.rst  |    2 -
- mitogen/master.py          |    2 -
- tox.ini                    |   18 ++++++-------
+ .ci/azure-pipelines.yml    | 60 +++-------------------------------------------
+ ansible_mitogen/loaders.py |  2 +-
+ docs/ansible_detailed.rst  |  2 +-
+ mitogen/master.py          |  2 +-
+ tox.ini                    | 18 +++++++-------
  5 files changed, 14 insertions(+), 70 deletions(-)
 
---- a/.ci/azure-pipelines.yml	2022-09-17 01:48:54.444253545 -0400
-+++ b/.ci/azure-pipelines.yml	2022-09-17 01:48:54.436253501 -0400
-@@ -33,9 +33,6 @@
+diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
+index 6f45397..98f4805 100644
+--- a/.ci/azure-pipelines.yml
++++ b/.ci/azure-pipelines.yml
+@@ -33,9 +33,6 @@ jobs:
        Loc_27_210:
          python.version: '2.7'
          tox.env: py27-mode_localhost-ansible2.10
@@ -119,16 +121,18 @@
        Ans_36_4:
          python.version: '3.6'
          tox.env: py36-mode_ansible-ansible4
-@@ -259,3 +202,6 @@
+@@ -259,3 +202,6 @@ jobs:
        Ans_310_5:
          python.version: '3.10'
          tox.env: py310-mode_ansible-ansible5
 +      Ans_310_6:
 +        python.version: '3.10'
 +        tox.env: py310-mode_ansible-ansible6
---- a/ansible_mitogen/loaders.py	2022-09-17 01:48:54.444253545 -0400
-+++ b/ansible_mitogen/loaders.py	2022-09-17 01:48:54.436253501 -0400
-@@ -48,7 +48,7 @@
+diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py
+index cd05fea..20aa499 100644
+--- a/ansible_mitogen/loaders.py
++++ b/ansible_mitogen/loaders.py
+@@ -48,7 +48,7 @@ __all__ = [
  
  
  ANSIBLE_VERSION_MIN = (2, 10)
@@ -137,9 +141,11 @@
  
  NEW_VERSION_MSG = (
      "Your Ansible version (%s) is too recent. The most recent version\n"
---- a/docs/ansible_detailed.rst	2022-09-17 01:48:54.444253545 -0400
-+++ b/docs/ansible_detailed.rst	2022-09-17 01:48:54.436253501 -0400
-@@ -148,7 +148,7 @@
+diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst
+index d329807..dd569a7 100644
+--- a/docs/ansible_detailed.rst
++++ b/docs/ansible_detailed.rst
+@@ -148,7 +148,7 @@ Noteworthy Differences
  * Mitogen 0.2.x supports Ansible 2.3-2.9; with Python 2.6, 2.7, or 3.6.
    Mitogen 0.3.1+ supports
      - Ansible 2.10, 3, and 4; with Python 2.7, or 3.6-3.10
@@ -161,8 +167,10 @@
              # - get_filename() may throw ImportError if pkgutil.find_loader()
              #   picks a "parent" package's loader for some crap that's been
              #   stuffed in sys.modules, for example in the case of urllib3:
---- a/tox.ini	2022-09-17 01:48:54.444253545 -0400
-+++ b/tox.ini	2022-09-17 01:48:54.436253501 -0400
+diff --git a/tox.ini b/tox.ini
+index 6b2addc..5b01516 100644
+--- a/tox.ini
++++ b/tox.ini
 @@ -26,6 +26,7 @@
  # ansible == 3.*     ansible-base ~= 2.10.0
  # ansible == 4.*     ansible-core ~= 2.11.0
diff -Nru python-mitogen-0.3.3/debian/patches/python3.12-targets python-mitogen-0.3.3/debian/patches/python3.12-targets
--- python-mitogen-0.3.3/debian/patches/python3.12-targets	1970-01-01 01:00:00.000000000 +0100
+++ python-mitogen-0.3.3/debian/patches/python3.12-targets	2025-08-18 16:33:11.000000000 +0200
@@ -0,0 +1,1121 @@
+From: Alex Willmer <alex@moreati.org.uk>
+Date: Sun, 17 Mar 2024 14:55:15 +0000
+Subject: mitogen: Support PEP 451 ModuleSpec API, required for Python 3.12
+
+importlib.machinery.ModuleSpec and find_spec() were introduced in Python 3.4
+under PEP 451. They replace the find_module() API of PEP 302, which was
+deprecated from Python 3.4. They were removed in Python 3.12 along with the
+imp module.
+
+This change adds support for the PEP 451 APIs. Mitogen should no longer import
+imp on Python versions that support ModuleSpec. Tests have been added to cover
+the new APIs.
+
+CI jobs have been added to cover Python 3.x on macOS.
+
+Refs #1033
+Co-authored-by: Witold Baryluk <witold.baryluk@gmail.com>
+Bug-Debain: https://bugs.debian.org/1111363
+---
+ ansible_mitogen/module_finder.py                   | 122 +++++++++++++++-
+ ansible_mitogen/runner.py                          |  91 +++++++++++-
+ docs/internals.rst                                 |   2 +-
+ mitogen/core.py                                    | 153 +++++++++++++++++++--
+ mitogen/master.py                                  | 122 +++++++++++++++-
+ .../connection_delegation/delegate_to_template.yml |   4 +-
+ .../interpreter_discovery/ansible_2_8_tests.yml    |   7 -
+ tests/ansible/lib/modules/module_finder_test.py    |  12 ++
+ tests/ansible/tests/module_finder_test.py          |  80 +++++++++++
+ tests/importer_test.py                             | 126 +++++++++++++++--
+ tests/module_finder_test.py                        |  14 +-
+ tests/testlib.py                                   |   9 +-
+ 12 files changed, 697 insertions(+), 45 deletions(-)
+ create mode 100644 tests/ansible/lib/modules/module_finder_test.py
+ create mode 100644 tests/ansible/tests/module_finder_test.py
+
+diff --git a/ansible_mitogen/module_finder.py b/ansible_mitogen/module_finder.py
+index cec465c..f227c7b 100644
+--- a/ansible_mitogen/module_finder.py
++++ b/ansible_mitogen/module_finder.py
+@@ -31,12 +31,23 @@ from __future__ import unicode_literals
+ __metaclass__ = type
+ 
+ import collections
+-import imp
++import logging
+ import os
++import re
++import sys
++
++try:
++    # Python >= 3.4, PEP 451 ModuleSpec API
++    import importlib.machinery
++    import importlib.util
++except ImportError:
++    # Python < 3.4, PEP 302 Import Hooks
++    import imp
+ 
+ import mitogen.master
+ 
+ 
++LOG = logging.getLogger(__name__)
+ PREFIX = 'ansible.module_utils.'
+ 
+ 
+@@ -119,14 +130,121 @@ def find_relative(parent, name, path=()):
+ 
+ 
+ def scan_fromlist(code):
++    """Return an iterator of (level, name) for explicit imports in a code
++    object.
++
++    Not all names identify a module. `from os import name, path` generates
++    `(0, 'os.name'), (0, 'os.path')`, but `os.name` is usually a string.
++
++    >>> src = 'import a; import b.c; from d.e import f; from g import h, i\\n'
++    >>> code = compile(src, '<str>', 'exec')
++    >>> list(scan_fromlist(code))
++    [(0, 'a'), (0, 'b.c'), (0, 'd.e.f'), (0, 'g.h'), (0, 'g.i')]
++    """
+     for level, modname_s, fromlist in mitogen.master.scan_code_imports(code):
+         for name in fromlist:
+-            yield level, '%s.%s' % (modname_s, name)
++            yield level, str('%s.%s' % (modname_s, name))
+         if not fromlist:
+             yield level, modname_s
+ 
+ 
++def walk_imports(code, prefix=None):
++    """Return an iterator of names for implicit parent imports & explicit
++    imports in a code object.
++
++    If a prefix is provided, then only children of that prefix are included.
++    Not all names identify a module. `from os import name, path` generates
++    `'os', 'os.name', 'os.path'`, but `os.name` is usually a string.
++
++    >>> source = 'import a; import b; import b.c; from b.d import e, f\\n'
++    >>> code = compile(source, '<str>', 'exec')
++    >>> list(walk_imports(code))
++    ['a', 'b', 'b', 'b.c', 'b', 'b.d', 'b.d.e', 'b.d.f']
++    >>> list(walk_imports(code, prefix='b'))
++    ['b.c', 'b.d', 'b.d.e', 'b.d.f']
++    """
++    if prefix is None:
++        prefix = ''
++    pattern = re.compile(r'(^|\.)(\w+)')
++    start = len(prefix)
++    for _, name, fromlist in mitogen.master.scan_code_imports(code):
++        if not name.startswith(prefix):
++            continue
++        for match in pattern.finditer(name, start):
++            yield name[:match.end()]
++        for leaf in fromlist:
++            yield str('%s.%s' % (name, leaf))
++
++
+ def scan(module_name, module_path, search_path):
++    # type: (str, str, list[str]) -> list[(str, str, bool)]
++    """Return a list of (name, path, is_package) for ansible.module_utils
++    imports used by an Ansible module.
++    """
++    log = LOG.getChild('scan')
++    log.debug('%r, %r, %r', module_name, module_path, search_path)
++
++    if sys.version_info >= (3, 4):
++        result = _scan_importlib_find_spec(
++            module_name, module_path, search_path,
++        )
++        log.debug('_scan_importlib_find_spec %r', result)
++    else:
++        result = _scan_imp_find_module(module_name, module_path, search_path)
++        log.debug('_scan_imp_find_module %r', result)
++    return result
++
++
++def _scan_importlib_find_spec(module_name, module_path, search_path):
++    # type: (str, str, list[str]) -> list[(str, str, bool)]
++    module = importlib.machinery.ModuleSpec(
++        module_name, loader=None, origin=module_path,
++    )
++    prefix = importlib.machinery.ModuleSpec(
++        PREFIX.rstrip('.'), loader=None,
++    )
++    prefix.submodule_search_locations = search_path
++    queue = collections.deque([module])
++    specs = {prefix.name: prefix}
++    while queue:
++        spec = queue.popleft()
++        if spec.origin is None:
++            continue
++        try:
++            with open(spec.origin, 'rb') as f:
++                code = compile(f.read(), spec.name, 'exec')
++        except Exception as exc:
++            raise ValueError((exc, module, spec, specs))
++
++        for name in walk_imports(code, prefix.name):
++            if name in specs:
++                continue
++
++            parent_name = name.rpartition('.')[0]
++            parent = specs[parent_name]
++            if parent is None or not parent.submodule_search_locations:
++                specs[name] = None
++                continue
++
++            child = importlib.util._find_spec(
++                name, parent.submodule_search_locations,
++            )
++            if child is None or child.origin is None:
++                specs[name] = None
++                continue
++
++            specs[name] = child
++            queue.append(child)
++
++    del specs[prefix.name]
++    return sorted(
++        (spec.name, spec.origin, spec.submodule_search_locations is not None)
++        for spec in specs.values() if spec is not None
++    )
++
++
++def _scan_imp_find_module(module_name, module_path, search_path):
++    # type: (str, str, list[str]) -> list[(str, str, bool)]
+     module = Module(module_name, module_path, imp.PY_SOURCE, None)
+     stack = [module]
+     seen = set()
+diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py
+index 31ccf1c..4306b0b 100644
+--- a/ansible_mitogen/runner.py
++++ b/ansible_mitogen/runner.py
+@@ -40,7 +40,6 @@ from __future__ import absolute_import, division, print_function
+ __metaclass__ = type
+ 
+ import atexit
+-import imp
+ import os
+ import re
+ import shlex
+@@ -63,6 +62,14 @@ except ImportError:
+     # Python 2.4
+     ctypes = None
+ 
++try:
++    # Python >= 3.4, PEP 451 ModuleSpec API
++    import importlib.machinery
++    import importlib.util
++except ImportError:
++    # Python < 3.4, PEP 302 Import Hooks
++    import imp
++
+ try:
+     import json
+ except ImportError:
+@@ -519,10 +526,71 @@ class ModuleUtilsImporter(object):
+             sys.modules.pop(fullname, None)
+ 
+     def find_module(self, fullname, path=None):
++        """
++        Return a loader for the module with fullname, if we will load it.
++
++        Implements importlib.abc.MetaPathFinder.find_module().
++        Deprecrated in Python 3.4+, replaced by find_spec().
++        Raises ImportWarning in Python 3.10+. Removed in Python 3.12.
++        """
+         if fullname in self._by_fullname:
+             return self
+ 
++    def find_spec(self, fullname, path, target=None):
++        """
++        Return a `ModuleSpec` for module with `fullname` if we will load it.
++        Otherwise return `None`.
++
++        Implements importlib.abc.MetaPathFinder.find_spec(). Python 3.4+.
++        """
++        if fullname.endswith('.'):
++            return None
++
++        try:
++            module_path, is_package = self._by_fullname[fullname]
++        except KeyError:
++            LOG.debug('Skipping %s: not present', fullname)
++            return None
++
++        LOG.debug('Handling %s', fullname)
++        origin = 'master:%s' % (module_path,)
++        return importlib.machinery.ModuleSpec(
++            fullname, loader=self, origin=origin, is_package=is_package,
++        )
++
++    def create_module(self, spec):
++        """
++        Return a module object for the given ModuleSpec.
++
++        Implements PEP-451 importlib.abc.Loader API introduced in Python 3.4.
++        Unlike Loader.load_module() this shouldn't populate sys.modules or
++        set module attributes. Both are done by Python.
++        """
++        module = types.ModuleType(spec.name)
++        # FIXME create_module() shouldn't initialise module attributes
++        module.__file__ = spec.origin
++        return module
++
++    def exec_module(self, module):
++        """
++        Execute the module to initialise it. Don't return anything.
++
++        Implements PEP-451 importlib.abc.Loader API, introduced in Python 3.4.
++        """
++        spec = module.__spec__
++        path, _ = self._by_fullname[spec.name]
++        source = ansible_mitogen.target.get_small_file(self._context, path)
++        code = compile(source, path, 'exec', 0, 1)
++        exec(code, module.__dict__)
++        self._loaded.add(spec.name)
++
+     def load_module(self, fullname):
++        """
++        Return the loaded module specified by fullname.
++
++        Implements PEP 302 importlib.abc.Loader.load_module().
++        Deprecated in Python 3.4+, replaced by create_module() & exec_module().
++        """
+         path, is_pkg = self._by_fullname[fullname]
+         source = ansible_mitogen.target.get_small_file(self._context, path)
+         code = compile(source, path, 'exec', 0, 1)
+@@ -823,12 +891,17 @@ class NewStyleRunner(ScriptRunner):
+         synchronization mechanism by importing everything the module will need
+         prior to detaching.
+         """
++        # I think "custom" means "found in custom module_utils search path",
++        # e.g. playbook relative dir, ~/.ansible/..., Ansible collection.
+         for fullname, _, _ in self.module_map['custom']:
+             mitogen.core.import_module(fullname)
++
++        # I think "builtin" means "part of ansible/ansible-base/ansible-core",
++        # as opposed to Python builtin modules such as sys.
+         for fullname in self.module_map['builtin']:
+             try:
+                 mitogen.core.import_module(fullname)
+-            except ImportError:
++            except ImportError as exc:
+                 # #590: Ansible 2.8 module_utils.distro is a package that
+                 # replaces itself in sys.modules with a non-package during
+                 # import. Prior to replacement, it is a real package containing
+@@ -839,8 +912,18 @@ class NewStyleRunner(ScriptRunner):
+                 # loop progresses to the next entry and attempts to preload
+                 # 'distro._distro', the import mechanism will fail. So here we
+                 # silently ignore any failure for it.
+-                if fullname != 'ansible.module_utils.distro._distro':
+-                    raise
++                if fullname == 'ansible.module_utils.distro._distro':
++                    continue
++
++                # ansible.module_utils.compat.selinux raises ImportError if it
++                # can't load libselinux.so. The importer would usually catch
++                # this & skip selinux operations. We don't care about selinux,
++                # we're using import to get a copy of the module.
++                if (fullname == 'ansible.module_utils.compat.selinux'
++                    and exc.msg == 'unable to load libselinux.so'):
++                    continue
++
++                raise
+ 
+     def _setup_excepthook(self):
+         """
+diff --git a/docs/internals.rst b/docs/internals.rst
+index 7f44d7b..434a74f 100644
+--- a/docs/internals.rst
++++ b/docs/internals.rst
+@@ -174,7 +174,7 @@ Module Finders
+    :members:
+ 
+ .. currentmodule:: mitogen.master
+-.. autoclass:: ParentEnumerationMethod
++.. autoclass:: ParentImpEnumerationMethod
+    :members:
+ 
+ 
+diff --git a/mitogen/core.py b/mitogen/core.py
+index bee722e..253fb07 100644
+--- a/mitogen/core.py
++++ b/mitogen/core.py
+@@ -54,13 +54,18 @@ import syslog
+ import threading
+ import time
+ import traceback
++import types
+ import warnings
+ import weakref
+ import zlib
+ 
+-# Python >3.7 deprecated the imp module.
+-warnings.filterwarnings('ignore', message='the imp module is deprecated')
+-import imp
++try:
++    # Python >= 3.4, PEP 451 ModuleSpec API
++    import importlib.machinery
++    import importlib.util
++except ImportError:
++    # Python < 3.4, PEP 302 Import Hooks
++    import imp
+ 
+ # Absolute imports for <2.5.
+ select = __import__('select')
+@@ -1353,6 +1358,19 @@ class Importer(object):
+     def __repr__(self):
+         return 'Importer'
+ 
++    @staticmethod
++    def _loader_from_module(module, default=None):
++        """Return the loader for a module object."""
++        try:
++            return module.__spec__.loader
++        except AttributeError:
++            pass
++        try:
++            return module.__loader__
++        except AttributeError:
++            pass
++        return default
++
+     def builtin_find_module(self, fullname):
+         # imp.find_module() will always succeed for __main__, because it is a
+         # built-in module. That means it exists on a special linked list deep
+@@ -1388,14 +1406,13 @@ class Importer(object):
+         try:
+             #_v and self._log.debug('Python requested %r', fullname)
+             fullname = to_text(fullname)
+-            pkgname, dot, _ = str_rpartition(fullname, '.')
++            pkgname, _, suffix = str_rpartition(fullname, '.')
+             pkg = sys.modules.get(pkgname)
+             if pkgname and getattr(pkg, '__loader__', None) is not self:
+                 self._log.debug('%s is submodule of a locally loaded package',
+                                 fullname)
+                 return None
+ 
+-            suffix = fullname[len(pkgname+dot):]
+             if pkgname and suffix not in self._present.get(pkgname, ()):
+                 self._log.debug('%s has no submodule %s', pkgname, suffix)
+                 return None
+@@ -1415,6 +1432,66 @@ class Importer(object):
+         finally:
+             del _tls.running
+ 
++    def find_spec(self, fullname, path, target=None):
++        """
++        Return a `ModuleSpec` for module with `fullname` if we will load it.
++        Otherwise return `None`, allowing other finders to try.
++
++        fullname    Fully qualified name of the module (e.g. foo.bar.baz)
++        path        Path entries to search. None for a top-level module.
++        target      Existing module to be reloaded (if any).
++
++        Implements importlib.abc.MetaPathFinder.find_spec()
++        Python 3.4+.
++        """
++        # Presence of _tls.running indicates we've re-invoked importlib.
++        # Abort early to prevent infinite recursion. See below.
++        if hasattr(_tls, 'running'):
++            return None
++
++        log = self._log.getChild('find_spec')
++
++        if fullname.endswith('.'):
++            return None
++
++        pkgname, _, modname = fullname.rpartition('.')
++        if pkgname and modname not in self._present.get(pkgname, ()):
++            log.debug('Skipping %s. Parent %s has no submodule %s',
++                      fullname, pkgname, modname)
++            return None
++
++        pkg = sys.modules.get(pkgname)
++        pkg_loader = self._loader_from_module(pkg)
++        if pkgname and pkg_loader is not self:
++            log.debug('Skipping %s. Parent %s was loaded by %r',
++                      fullname, pkgname, pkg_loader)
++            return None
++
++        # #114: whitelisted prefixes override any system-installed package.
++        if self.whitelist != ['']:
++            if any(s and fullname.startswith(s) for s in self.whitelist):
++                log.debug('Handling %s. It is whitelisted', fullname)
++                return importlib.machinery.ModuleSpec(fullname, loader=self)
++
++        if fullname == '__main__':
++            log.debug('Handling %s. A special case', fullname)
++            return importlib.machinery.ModuleSpec(fullname, loader=self)
++
++        # Re-invoke the import machinery to allow other finders to try.
++        # Set a guard, so we don't infinitely recurse. See top of this method.
++        _tls.running = True
++        try:
++            spec = importlib.util._find_spec(fullname, path, target)
++        finally:
++            del _tls.running
++
++        if spec:
++            log.debug('Skipping %s. Available as %r', fullname, spec)
++            return spec
++
++        log.debug('Handling %s. Unavailable locally', fullname)
++        return importlib.machinery.ModuleSpec(fullname, loader=self)
++
+     blacklisted_msg = (
+         '%r is present in the Mitogen importer blacklist, therefore this '
+         'context will not attempt to request it from the master, as the '
+@@ -1501,6 +1578,64 @@ class Importer(object):
+         if present:
+             callback()
+ 
++    def create_module(self, spec):
++        """
++        Return a module object for the given ModuleSpec.
++
++        Implements PEP-451 importlib.abc.Loader API introduced in Python 3.4.
++        Unlike Loader.load_module() this shouldn't populate sys.modules or
++        set module attributes. Both are done by Python.
++        """
++        self._log.debug('Creating module for %r', spec)
++
++        # FIXME Should this be done in find_spec()? Can it?
++        self._refuse_imports(spec.name)
++
++        # FIXME "create_module() should properly handle the case where it is
++        #       called more than once for the same spec/module." -- PEP-451
++        event = threading.Event()
++        self._request_module(spec.name, callback=event.set)
++        event.wait()
++
++        # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++        _, pkg_present, path, _, _ = self._cache[spec.name]
++
++        if path is None:
++            raise ImportError(self.absent_msg % (spec.name))
++
++        spec.origin = self.get_filename(spec.name)
++        if pkg_present is not None:
++            # TODO Namespace packages
++            spec.submodule_search_locations = []
++            self._present[spec.name] = pkg_present
++
++        module = types.ModuleType(spec.name)
++        # FIXME create_module() shouldn't initialise module attributes
++        module.__file__ = spec.origin
++        return module
++
++    def exec_module(self, module):
++        """
++        Execute the module to initialise it. Don't return anything.
++
++        Implements PEP-451 importlib.abc.Loader API, introduced in Python 3.4.
++        """
++        name = module.__spec__.name
++        origin = module.__spec__.origin
++        self._log.debug('Executing %s from %s', name, origin)
++        source = self.get_source(name)
++        try:
++            # Compile the source into a code object. Don't add any __future__
++            # flags and don't inherit any from this module.
++            # FIXME Should probably be exposed as get_code()
++            code = compile(source, origin, 'exec', flags=0, dont_inherit=True)
++        except SyntaxError:
++            # FIXME Why is this LOG, rather than self._log?
++            LOG.exception('while importing %r', name)
++            raise
++
++        exec(code, module.__dict__)
++
+     def load_module(self, fullname):
+         """
+         Return the loaded module specified by fullname.
+@@ -1516,11 +1651,11 @@ class Importer(object):
+         self._request_module(fullname, event.set)
+         event.wait()
+ 
+-        ret = self._cache[fullname]
+-        if ret[2] is None:
++        # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++        _, pkg_present, path, _, _ = self._cache[fullname]
++        if path is None:
+             raise ModuleNotFoundError(self.absent_msg % (fullname,))
+ 
+-        pkg_present = ret[1]
+         mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
+         mod.__file__ = self.get_filename(fullname)
+         mod.__loader__ = self
+@@ -3921,7 +4056,7 @@ class ExternalContext(object):
+ 
+     def _setup_package(self):
+         global mitogen
+-        mitogen = imp.new_module('mitogen')
++        mitogen = types.ModuleType('mitogen')
+         mitogen.__package__ = 'mitogen'
+         mitogen.__path__ = []
+         mitogen.__loader__ = self.importer
+diff --git a/mitogen/master.py b/mitogen/master.py
+index 4fb535f..9b5c2ff 100644
+--- a/mitogen/master.py
++++ b/mitogen/master.py
+@@ -37,7 +37,6 @@ contexts.
+ 
+ import dis
+ import errno
+-import imp
+ import inspect
+ import itertools
+ import logging
+@@ -50,6 +49,16 @@ import threading
+ import types
+ import zlib
+ 
++try:
++    # Python >= 3.4, PEP 451 ModuleSpec API
++    import importlib.machinery
++    import importlib.util
++    from _imp import is_builtin as _is_builtin
++except ImportError:
++    # Python < 3.4, PEP 302 Import Hooks
++    import imp
++    from imp import is_builtin as _is_builtin
++
+ try:
+     import sysconfig
+ except ImportError:
+@@ -122,14 +131,14 @@ def is_stdlib_name(modname):
+     """
+     Return :data:`True` if `modname` appears to come from the standard library.
+     """
+-    # `imp.is_builtin()` isn't a documented as part of Python's stdlib API.
++    # `(_imp|imp).is_builtin()` isn't a documented part of Python's stdlib.
+     #
+     # """
+     # Main is a little special - imp.is_builtin("__main__") will return False,
+     # but BuiltinImporter is still the most appropriate initial setting for
+     # its __loader__ attribute.
+     # """ -- comment in CPython pylifecycle.c:add_main_module()
+-    if imp.is_builtin(modname) != 0:
++    if _is_builtin(modname) != 0:
+         return True
+ 
+     module = sys.modules.get(modname)
+@@ -460,6 +469,9 @@ class FinderMethod(object):
+     name according to the running Python interpreter. You'd think this was a
+     simple task, right? Naive young fellow, welcome to the real world.
+     """
++    def __init__(self):
++        self.log = LOG.getChild(self.__class__.__name__)
++
+     def __repr__(self):
+         return '%s()' % (type(self).__name__,)
+ 
+@@ -641,7 +653,7 @@ class SysModulesMethod(FinderMethod):
+         return path, source, is_pkg
+ 
+ 
+-class ParentEnumerationMethod(FinderMethod):
++class ParentImpEnumerationMethod(FinderMethod):
+     """
+     Attempt to fetch source code by examining the module's (hopefully less
+     insane) parent package, and if no insane parents exist, simply use
+@@ -668,9 +680,9 @@ class ParentEnumerationMethod(FinderMethod):
+     @staticmethod
+     def _iter_parents(fullname):
+         """
+-        >>> list(ParentEnumerationMethod._iter_parents('a'))
++        >>> list(ParentImpEnumerationMethod._iter_parents('a'))
+         [('', 'a')]
+-        >>> list(ParentEnumerationMethod._iter_parents('a.b.c'))
++        >>> list(ParentImpEnumerationMethod._iter_parents('a.b.c'))
+         [('a.b', 'c'), ('a', 'b'), ('', 'a')]
+         """
+         while fullname:
+@@ -770,6 +782,9 @@ class ParentEnumerationMethod(FinderMethod):
+         """
+         See implementation for a description of how this works.
+         """
++        if sys.version_info >= (3, 4):
++            return None
++
+         #if fullname not in sys.modules:
+             # Don't attempt this unless a module really exists in sys.modules,
+             # else we could return junk.
+@@ -798,6 +813,98 @@ class ParentEnumerationMethod(FinderMethod):
+                 return self._found_module(fullname, path, fp)
+ 
+ 
++class ParentSpecEnumerationMethod(ParentImpEnumerationMethod):
++    def _find_parent_spec(self, fullname):
++        #history = []
++        debug = self.log.debug
++        children = []
++        for parent_name, child_name in self._iter_parents(fullname):
++            children.insert(0, child_name)
++            if not parent_name:
++                debug('abandoning %r, reached top-level', fullname)
++                return None, children
++
++            try:
++                parent = sys.modules[parent_name]
++            except KeyError:
++                debug('skipping %r, not in sys.modules', parent_name)
++                continue
++
++            try:
++                spec = parent.__spec__
++            except AttributeError:
++                debug('skipping %r: %r.__spec__ is absent',
++                      parent_name, parent)
++                continue
++
++            if not spec:
++                debug('skipping %r: %r.__spec__=%r',
++                      parent_name, parent, spec)
++                continue
++
++            if spec.name != parent_name:
++                debug('skipping %r: %r.__spec__.name=%r does not match',
++                      parent_name, parent, spec.name)
++                continue
++
++            if not spec.submodule_search_locations:
++                debug('skipping %r: %r.__spec__.submodule_search_locations=%r',
++                      parent_name, parent, spec.submodule_search_locations)
++                continue
++
++            return spec, children
++
++        raise ValueError('%s._find_parent_spec(%r) unexpectedly reached bottom'
++                         % (self.__class__.__name__, fullname))
++
++    def find(self, fullname):
++        # Returns absolute path, ParentImpEnumerationMethod returns relative
++        # >>> spec_pem.find('six_brokenpkg._six')[::2]
++        # ('/Users/alex/src/mitogen/tests/data/importer/six_brokenpkg/_six.py', False)
++
++        if sys.version_info < (3, 4):
++            return None
++
++        fullname = to_text(fullname)
++        spec, children = self._find_parent_spec(fullname)
++        for child_name in children:
++            if spec:
++                name = '%s.%s' % (spec.name, child_name)
++                submodule_search_locations = spec.submodule_search_locations
++            else:
++                name = child_name
++                submodule_search_locations = None
++            spec = importlib.util._find_spec(name, submodule_search_locations)
++            if spec is None:
++                self.log.debug('%r spec unavailable from %s', fullname, spec)
++                return None
++
++            is_package = spec.submodule_search_locations is not None
++            if name != fullname:
++                if not is_package:
++                    self.log.debug('%r appears to be child of non-package %r',
++                                   fullname, spec)
++                    return None
++                continue
++
++            if not spec.has_location:
++                self.log.debug('%r.origin cannot be read as a file', spec)
++                return None
++
++            if os.path.splitext(spec.origin)[1] != '.py':
++                self.log.debug('%r.origin does not contain Python source code',
++                               spec)
++                return None
++
++            # FIXME This should use loader.get_source()
++            with open(spec.origin, 'rb') as f:
++                source = f.read()
++
++            return spec.origin, source, is_package
++
++        raise ValueError('%s.find(%r) unexpectedly reached bottom'
++                         % (self.__class__.__name__, fullname))
++
+ class ModuleFinder(object):
+     """
+     Given the name of a loaded module, make a best-effort attempt at finding
+@@ -838,7 +945,8 @@ class ModuleFinder(object):
+         DefectivePython3xMainMethod(),
+         PkgutilMethod(),
+         SysModulesMethod(),
+-        ParentEnumerationMethod(),
++        ParentSpecEnumerationMethod(),
++        ParentImpEnumerationMethod(),
+     ]
+ 
+     def get_module_source(self, fullname):
+diff --git a/tests/ansible/integration/connection_delegation/delegate_to_template.yml b/tests/ansible/integration/connection_delegation/delegate_to_template.yml
+index be083ff..357c485 100644
+--- a/tests/ansible/integration/connection_delegation/delegate_to_template.yml
++++ b/tests/ansible/integration/connection_delegation/delegate_to_template.yml
+@@ -42,7 +42,7 @@
+               'keepalive_count': 10,
+               'password': null,
+               'port': null,
+-              'python_path': ["/usr/bin/python"],
++              'python_path': ["{{ ansible_facts.discovered_interpreter_python | default('/usr/bin/python') }}"],
+               'remote_name': null,
+               'ssh_args': [
+                 '-o',
+@@ -72,7 +72,7 @@
+               'keepalive_count': 10,
+               'password': null,
+               'port': null,
+-              'python_path': ["/usr/bin/python"],
++              'python_path': ["{{ ansible_facts.discovered_interpreter_python | default('/usr/bin/python') }}"],
+               'remote_name': null,
+               'ssh_args': [
+                 '-o',
+diff --git a/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml b/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml
+index 201ef8b..fa9baba 100644
+--- a/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml
++++ b/tests/ansible/integration/interpreter_discovery/ansible_2_8_tests.yml
+@@ -190,13 +190,6 @@
+               - distro == 'ubuntu'
+               - distro_version is version('16.04', '>=', strict=True)
+ 
+-          - name: mac assertions
+-            assert:
+-              that:
+-              - auto_out.ansible_facts.discovered_interpreter_python == '/usr/bin/python'
+-              fail_msg: auto_out={{auto_out}}
+-            when: os_family == 'Darwin'
+-
+           always:
+           - meta: clear_facts
+       when:
+diff --git a/tests/ansible/lib/modules/module_finder_test.py b/tests/ansible/lib/modules/module_finder_test.py
+new file mode 100644
+index 0000000..41cf1c1
+--- /dev/null
++++ b/tests/ansible/lib/modules/module_finder_test.py
+@@ -0,0 +1,12 @@
++from __future__ import absolute_import, division, print_function
++__metaclass__ = type
++
++import os
++import sys
++
++import ansible.module_utils.external1
++
++from ansible.module_utils.externalpkg.extmod import path as epem_path
++
++def main():
++    pass
+diff --git a/tests/ansible/tests/module_finder_test.py b/tests/ansible/tests/module_finder_test.py
+new file mode 100644
+index 0000000..79e8fdb
+--- /dev/null
++++ b/tests/ansible/tests/module_finder_test.py
+@@ -0,0 +1,80 @@
++import os.path
++import sys
++import textwrap
++import unittest
++
++import ansible_mitogen.module_finder
++
++import testlib
++
++
++class ScanFromListTest(testlib.TestCase):
++    def test_absolute_imports(self):
++        source = textwrap.dedent('''\
++            from __future__ import absolute_import
++            import a; import b.c; from d.e import f; from g import h, i
++        ''')
++        code = compile(source, '<str>', 'exec')
++        self.assertEqual(
++            list(ansible_mitogen.module_finder.scan_fromlist(code)),
++            [(0, '__future__.absolute_import'), (0, 'a'), (0, 'b.c'), (0, 'd.e.f'), (0, 'g.h'), (0, 'g.i')],
++        )
++
++
++class WalkImportsTest(testlib.TestCase):
++    def test_absolute_imports(self):
++        source = textwrap.dedent('''\
++            from __future__ import absolute_import
++            import a; import b; import b.c; from b.d import e, f
++        ''')
++        code = compile(source, '<str>', 'exec')
++
++        self.assertEqual(
++            list(ansible_mitogen.module_finder.walk_imports(code)),
++            ['__future__', '__future__.absolute_import', 'a', 'b', 'b', 'b.c', 'b', 'b.d', 'b.d.e', 'b.d.f'],
++        )
++        self.assertEqual(
++            list(ansible_mitogen.module_finder.walk_imports(code, prefix='b')),
++            ['b.c', 'b.d', 'b.d.e', 'b.d.f'],
++        )
++
++
++class ScanTest(testlib.TestCase):
++    module_name = 'ansible_module_module_finder_test__this_should_not_matter'
++    module_path = os.path.join(testlib.ANSIBLE_MODULES_DIR, 'module_finder_test.py')
++    search_path = (
++        'does_not_exist/module_utils',
++        testlib.ANSIBLE_MODULE_UTILS_DIR,
++    )
++
++    @staticmethod
++    def relpath(path):
++        return os.path.relpath(path, testlib.ANSIBLE_MODULE_UTILS_DIR)
++
++    @unittest.skipIf(sys.version_info < (3, 4), 'find spec() unavailable')
++    def test_importlib_find_spec(self):
++        scan = ansible_mitogen.module_finder._scan_importlib_find_spec
++        actual = scan(self.module_name, self.module_path, self.search_path)
++        self.assertEqual(
++            [(name, self.relpath(path), is_pkg) for name, path, is_pkg in actual],
++            [
++                ('ansible.module_utils.external1', 'external1.py', False),
++                ('ansible.module_utils.external2', 'external2.py', False),
++                ('ansible.module_utils.externalpkg', 'externalpkg/__init__.py', True),
++                ('ansible.module_utils.externalpkg.extmod', 'externalpkg/extmod.py',False),
++            ],
++        )
++
++    @unittest.skipIf(sys.version_info >= (3, 4), 'find spec() preferred')
++    def test_imp_find_module(self):
++        scan = ansible_mitogen.module_finder._scan_imp_find_module
++        actual = scan(self.module_name, self.module_path, self.search_path)
++        self.assertEqual(
++            [(name, self.relpath(path), is_pkg) for name, path, is_pkg in actual],
++            [
++                ('ansible.module_utils.external1', 'external1.py', False),
++                ('ansible.module_utils.external2', 'external2.py', False),
++                ('ansible.module_utils.externalpkg', 'externalpkg/__init__.py', True),
++                ('ansible.module_utils.externalpkg.extmod', 'externalpkg/extmod.py',False),
++            ],
++        )
+diff --git a/tests/importer_test.py b/tests/importer_test.py
+index e48c02a..e86af8a 100644
+--- a/tests/importer_test.py
++++ b/tests/importer_test.py
+@@ -2,6 +2,7 @@ import sys
+ import threading
+ import types
+ import zlib
++import unittest
+ 
+ import mock
+ 
+@@ -42,6 +43,49 @@ class ImporterMixin(testlib.RouterMixin):
+         super(ImporterMixin, self).tearDown()
+ 
+ 
++class InvalidNameTest(ImporterMixin, testlib.TestCase):
++    modname = 'trailingdot.'
++    # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++    response = (modname, None, None, None, None)
++
++    @unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++    def test_find_spec_invalid(self):
++        self.set_get_module_response(self.response)
++        self.assertEqual(self.importer.find_spec(self.modname, path=None), None)
++
++
++class MissingModuleTest(ImporterMixin, testlib.TestCase):
++    modname = 'missing'
++    # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++    response = (modname, None, None, None, None)
++
++    @unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python 3.4+')
++    def test_load_module_missing(self):
++        self.set_get_module_response(self.response)
++        self.assertRaises(ImportError, self.importer.load_module, self.modname)
++
++    @unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++    def test_find_spec_missing(self):
++        """
++        Importer should optimistically offer itself as a module loader
++        when there are no disqualifying criteria.
++        """
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = self.importer.find_spec(self.modname, path=None)
++        self.assertIsInstance(spec, importlib.machinery.ModuleSpec)
++        self.assertEqual(spec.name, self.modname)
++        self.assertEqual(spec.loader, self.importer)
++
++    @unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++    def test_create_module_missing(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        self.assertRaises(ImportError, self.importer.create_module, spec)
++
++
++@unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python 3.4+')
+ class LoadModuleTest(ImporterMixin, testlib.TestCase):
+     data = zlib.compress(b("data = 1\n\n"))
+     path = 'fake_module.py'
+@@ -50,14 +94,6 @@ class LoadModuleTest(ImporterMixin, testlib.TestCase):
+     # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
+     response = (modname, None, path, data, [])
+ 
+-    def test_no_such_module(self):
+-        self.set_get_module_response(
+-            # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
+-            (self.modname, None, None, None, None)
+-        )
+-        self.assertRaises(ImportError,
+-            lambda: self.importer.load_module(self.modname))
+-
+     def test_module_added_to_sys_modules(self):
+         self.set_get_module_response(self.response)
+         mod = self.importer.load_module(self.modname)
+@@ -80,6 +116,26 @@ class LoadModuleTest(ImporterMixin, testlib.TestCase):
+         self.assertIsNone(mod.__package__)
+ 
+ 
++@unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++class ModuleSpecTest(ImporterMixin, testlib.TestCase):
++    data = zlib.compress(b("data = 1\n\n"))
++    path = 'fake_module.py'
++    modname = 'fake_module'
++
++    # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++    response = (modname, None, path, data, [])
++
++    def test_module_attributes(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        mod = self.importer.create_module(spec)
++        self.assertIsInstance(mod, types.ModuleType)
++        self.assertEqual(mod.__name__, 'fake_module')
++        #self.assertFalse(hasattr(mod, '__file__'))
++
++
++@unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python 3.4+')
+ class LoadSubmoduleTest(ImporterMixin, testlib.TestCase):
+     data = zlib.compress(b("data = 1\n\n"))
+     path = 'fake_module.py'
+@@ -93,6 +149,25 @@ class LoadSubmoduleTest(ImporterMixin, testlib.TestCase):
+         self.assertEqual(mod.__package__, 'mypkg')
+ 
+ 
++@unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++class SubmoduleSpecTest(ImporterMixin, testlib.TestCase):
++    data = zlib.compress(b("data = 1\n\n"))
++    path = 'fake_module.py'
++    modname = 'mypkg.fake_module'
++    # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++    response = (modname, None, path, data, [])
++
++    def test_module_attributes(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        mod = self.importer.create_module(spec)
++        self.assertIsInstance(mod, types.ModuleType)
++        self.assertEqual(mod.__name__, 'mypkg.fake_module')
++        #self.assertFalse(hasattr(mod, '__file__'))
++
++
++@unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python 3.4+')
+ class LoadModulePackageTest(ImporterMixin, testlib.TestCase):
+     data = zlib.compress(b("func = lambda: 1\n\n"))
+     path = 'fake_pkg/__init__.py'
+@@ -140,6 +215,41 @@ class LoadModulePackageTest(ImporterMixin, testlib.TestCase):
+         self.assertEqual(mod.func.__module__, self.modname)
+ 
+ 
++@unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++class PackageSpecTest(ImporterMixin, testlib.TestCase):
++    data = zlib.compress(b("func = lambda: 1\n\n"))
++    path = 'fake_pkg/__init__.py'
++    modname = 'fake_pkg'
++    # 0:fullname 1:pkg_present 2:path 3:compressed 4:related
++    response = (modname, [], path, data, [])
++
++    def test_module_attributes(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        mod = self.importer.create_module(spec)
++        self.assertIsInstance(mod, types.ModuleType)
++        self.assertEqual(mod.__name__, 'fake_pkg')
++        #self.assertFalse(hasattr(mod, '__file__'))
++
++    def test_get_filename(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        _ = self.importer.create_module(spec)
++        filename = self.importer.get_filename(self.modname)
++        self.assertEqual('master:fake_pkg/__init__.py', filename)
++
++    def test_get_source(self):
++        import importlib.machinery
++        self.set_get_module_response(self.response)
++        spec = importlib.machinery.ModuleSpec(self.modname, self.importer)
++        _ = self.importer.create_module(spec)
++        source = self.importer.get_source(self.modname)
++        self.assertEqual(source,
++            mitogen.core.to_text(zlib.decompress(self.data)))
++
++
+ class EmailParseAddrSysTest(testlib.RouterMixin, testlib.TestCase):
+     def initdir(self, caplog):
+         self.caplog = caplog
+diff --git a/tests/module_finder_test.py b/tests/module_finder_test.py
+index 401a607..8ccbd88 100644
+--- a/tests/module_finder_test.py
++++ b/tests/module_finder_test.py
+@@ -139,9 +139,7 @@ class SysModulesMethodTest(testlib.TestCase):
+         self.assertIsNone(tup)
+ 
+ 
+-class GetModuleViaParentEnumerationTest(testlib.TestCase):
+-    klass = mitogen.master.ParentEnumerationMethod
+-
++class ParentEnumerationMixin(object):
+     def call(self, fullname):
+         return self.klass().find(fullname)
+ 
+@@ -231,6 +229,16 @@ class GetModuleViaParentEnumerationTest(testlib.TestCase):
+         self.assertEqual(is_pkg, False)
+ 
+ 
++@unittest.skipIf(sys.version_info >= (3, 4), 'Superceded in Python >= 3.4')
++class ParentImpEnumerationMethodTest(ParentEnumerationMixin, testlib.TestCase):
++    klass = mitogen.master.ParentImpEnumerationMethod
++
++
++@unittest.skipIf(sys.version_info < (3, 4), 'Requires ModuleSpec, Python 3.4+')
++class ParentSpecEnumerationMethodTest(ParentEnumerationMixin, testlib.TestCase):
++    klass = mitogen.master.ParentSpecEnumerationMethod
++
++
+ class ResolveRelPathTest(testlib.TestCase):
+     klass = mitogen.master.ModuleFinder
+ 
+diff --git a/tests/testlib.py b/tests/testlib.py
+index 8ab895c..1146a92 100644
+--- a/tests/testlib.py
++++ b/tests/testlib.py
+@@ -41,8 +41,13 @@ except NameError:
+ 
+ 
+ LOG = logging.getLogger(__name__)
+-DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
+-MODS_DIR = os.path.join(DATA_DIR, 'importer')
++
++TESTS_DIR =                     os.path.join(os.path.dirname(__file__))
++ANSIBLE_LIB_DIR =               os.path.join(TESTS_DIR, 'ansible', 'lib')
++ANSIBLE_MODULE_UTILS_DIR =      os.path.join(TESTS_DIR, 'ansible', 'lib', 'module_utils')
++ANSIBLE_MODULES_DIR =           os.path.join(TESTS_DIR, 'ansible', 'lib', 'modules')
++DATA_DIR =                      os.path.join(TESTS_DIR, 'data')
++MODS_DIR =                      os.path.join(TESTS_DIR, 'data', 'importer')
+ 
+ sys.path.append(DATA_DIR)
+ sys.path.append(MODS_DIR)
diff -Nru python-mitogen-0.3.3/debian/patches/series python-mitogen-0.3.3/debian/patches/series
--- python-mitogen-0.3.3/debian/patches/series	2023-05-13 15:45:14.000000000 +0200
+++ python-mitogen-0.3.3/debian/patches/series	2025-08-18 16:33:11.000000000 +0200
@@ -7,3 +7,4 @@
 ansible-6
 hack-remove-cleanup
 poll-poller
+python3.12-targets
diff -Nru python-mitogen-0.3.3/debian/tests/ansible-tests python-mitogen-0.3.3/debian/tests/ansible-tests
--- python-mitogen-0.3.3/debian/tests/ansible-tests	2023-05-13 15:45:14.000000000 +0200
+++ python-mitogen-0.3.3/debian/tests/ansible-tests	2025-08-18 16:33:11.000000000 +0200
@@ -2,7 +2,8 @@
 set -eufx
 
 cp -a tests/ansible "$AUTOPKGTEST_TMP/ansible-tests"
-cp tests/testlib.py "$AUTOPKGTEST_TMP/ansible-tests/"
+sed -e "s/TESTS_DIR, 'ansible', /TESTS_DIR, /" tests/testlib.py \
+    > "$AUTOPKGTEST_TMP/ansible-tests/testlib.py"
 cd "$AUTOPKGTEST_TMP"
 
 python3 -m unittest discover -v \

--- End Message ---
--- Begin Message ---
Package: release.debian.org
Version: 12.12

Hi,

Each of the updates referenced by these requests was included in
today's 12.12 point release for bookworm.

Regards,

Adam

--- End Message ---

Reply to: