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 "" % 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 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) == "", 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) == "" else: assert f(telnetlib) == "", f(telnetlib)