Patchwork D7954: hgdemandimport: apply lazy module loading to sys.meta_path finders

login
register
mail settings
Submitter phabricator
Date Jan. 21, 2020, 4:26 p.m.
Message ID <49981cce47812e153c59b7112bf2287b@localhost.localdomain>
Download mbox | patch
Permalink /patch/44546/
State Not Applicable
Headers show

Comments

phabricator - Jan. 21, 2020, 4:26 p.m.
Closed by commit rHGf81c17ec303c: hgdemandimport: apply lazy module loading to sys.meta_path finders (authored by indygreg).
This revision was automatically updated to reflect the committed changes.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D7954?vs=19461&id=19477

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7954/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7954

AFFECTED FILES
  hgdemandimport/demandimportpy3.py
  tests/test-demandimport.py

CHANGE DETAILS




To: indygreg, #hg-reviewers, pulkit
Cc: mercurial-devel

Patch

diff --git a/tests/test-demandimport.py b/tests/test-demandimport.py
--- a/tests/test-demandimport.py
+++ b/tests/test-demandimport.py
@@ -137,7 +137,7 @@ 
 from mercurial import hgweb
 
 if ispy3:
-    assert not isinstance(hgweb, _LazyModule)
+    assert isinstance(hgweb, _LazyModule)
     assert f(hgweb) == "<module 'mercurial.hgweb' from '?'>", f(hgweb)
     assert isinstance(hgweb.hgweb_mod, _LazyModule)
     assert (
@@ -210,7 +210,7 @@ 
 import telnetlib
 
 if ispy3:
-    assert not isinstance(telnetlib, _LazyModule)
+    assert isinstance(telnetlib, _LazyModule)
     assert f(telnetlib) == "<module 'telnetlib' from '?'>"
 else:
     assert f(telnetlib) == "<unloaded module 'telnetlib'>", f(telnetlib)
diff --git a/hgdemandimport/demandimportpy3.py b/hgdemandimport/demandimportpy3.py
--- a/hgdemandimport/demandimportpy3.py
+++ b/hgdemandimport/demandimportpy3.py
@@ -27,8 +27,6 @@ 
 from __future__ import absolute_import
 
 import contextlib
-import importlib.abc
-import importlib.machinery
 import importlib.util
 import sys
 
@@ -57,23 +55,61 @@ 
                 super().exec_module(module)
 
 
-_extensions_loader = _lazyloaderex.factory(
-    importlib.machinery.ExtensionFileLoader
-)
-_bytecode_loader = _lazyloaderex.factory(
-    importlib.machinery.SourcelessFileLoader
-)
-_source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
+class LazyFinder(object):
+    """A wrapper around a ``MetaPathFinder`` that makes loaders lazy.
+
+    ``sys.meta_path`` finders have their ``find_spec()`` called to locate a
+    module. This returns a ``ModuleSpec`` if found or ``None``. The
+    ``ModuleSpec`` has a ``loader`` attribute, which is called to actually
+    load a module.
+
+    Our class wraps an existing finder and overloads its ``find_spec()`` to
+    replace the ``loader`` with our lazy loader proxy.
 
+    We have to use __getattribute__ to proxy the instance because some meta
+    path finders don't support monkeypatching.
+    """
+
+    __slots__ = ("_finder",)
+
+    def __init__(self, finder):
+        object.__setattr__(self, "_finder", finder)
+
+    def __repr__(self):
+        return "<LazyFinder for %r>" % object.__getattribute__(self, "_finder")
+
+    # __bool__ is canonical Python 3. But check-code insists on __nonzero__ being
+    # defined via `def`.
+    def __nonzero__(self):
+        return bool(object.__getattribute__(self, "_finder"))
 
-def _makefinder(path):
-    return importlib.machinery.FileFinder(
-        path,
-        # This is the order in which loaders are passed in in core Python.
-        (_extensions_loader, importlib.machinery.EXTENSION_SUFFIXES),
-        (_source_loader, importlib.machinery.SOURCE_SUFFIXES),
-        (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
-    )
+    __bool__ = __nonzero__
+
+    def __getattribute__(self, name):
+        if name in ("_finder", "find_spec"):
+            return object.__getattribute__(self, name)
+
+        return getattr(object.__getattribute__(self, "_finder"), name)
+
+    def __delattr__(self, name):
+        return delattr(object.__getattribute__(self, "_finder"))
+
+    def __setattr__(self, name, value):
+        return setattr(object.__getattribute__(self, "_finder"), name, value)
+
+    def find_spec(self, *args, **kwargs):
+        finder = object.__getattribute__(self, "_finder")
+        spec = finder.find_spec(*args, **kwargs)
+
+        # Lazy loader requires exec_module().
+        if (
+            spec is not None
+            and spec.loader is not None
+            and getattr(spec.loader, "exec_module")
+        ):
+            spec.loader = _lazyloaderex(spec.loader)
+
+        return spec
 
 
 ignores = set()
@@ -85,22 +121,30 @@ 
 
 
 def isenabled():
-    return _makefinder in sys.path_hooks and not _deactivated
+    return not _deactivated and any(
+        isinstance(finder, LazyFinder) for finder in sys.meta_path
+    )
 
 
 def disable():
-    try:
-        while True:
-            sys.path_hooks.remove(_makefinder)
-    except ValueError:
-        pass
+    new_finders = []
+    for finder in sys.meta_path:
+        new_finders.append(
+            finder._finder if isinstance(finder, LazyFinder) else finder
+        )
+    sys.meta_path[:] = new_finders
 
 
 def enable():
     if not _supported:
         return
 
-    sys.path_hooks.insert(0, _makefinder)
+    new_finders = []
+    for finder in sys.meta_path:
+        new_finders.append(
+            LazyFinder(finder) if not isinstance(finder, LazyFinder) else finder
+        )
+    sys.meta_path[:] = new_finders
 
 
 @contextlib.contextmanager