Patchwork [6,of,8,demandimport-py3] demandimport: add python 3 implementation

login
register
mail settings
Submitter Siddharth Agarwal
Date May 21, 2017, 8:48 p.m.
Message ID <0c781be127ded0349e09.1495399681@devvm31800.prn1.facebook.com>
Download mbox | patch
Permalink /patch/20799/
State Accepted
Headers show

Comments

Siddharth Agarwal - May 21, 2017, 8:48 p.m.
# HG changeset patch
# User Siddharth Agarwal <sid0@fb.com>
# Date 1495394584 25200
#      Sun May 21 12:23:04 2017 -0700
# Node ID 0c781be127ded0349e093e492b507e343bf78676
# Parent  24d81489a7dcac5a68b846dca4928c0e22a312a1
demandimport: add python 3 implementation

This implementation uses the new importlib finder/loader functionality
available in Python 3.5 and up.

Patch

diff --git a/hgdemandimport/__init__.py b/hgdemandimport/__init__.py
--- a/hgdemandimport/__init__.py
+++ b/hgdemandimport/__init__.py
@@ -15,7 +15,10 @@  from __future__ import absolute_import
 
 import sys
 
-from . import demandimportpy2 as demandimport
+if sys.version_info[0] >= 3:
+    from . import demandimportpy3 as demandimport
+else:
+    from . import demandimportpy2 as demandimport
 
 # Extensions can add to this list if necessary.
 ignore = [
diff --git a/hgdemandimport/demandimportpy3.py b/hgdemandimport/demandimportpy3.py
new file mode 100644
--- /dev/null
+++ b/hgdemandimport/demandimportpy3.py
@@ -0,0 +1,112 @@ 
+# demandimportpy3 - global demand-loading of modules for Mercurial
+#
+# Copyright 2017 Facebook Inc.
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""Lazy loading for Python 3.6 and above.
+
+This uses the new importlib finder/loader functionality available in Python 3.5
+and up. The code reuses most of the mechanics implemented inside importlib.util,
+but with a few additions:
+
+* Allow excluding certain modules from lazy imports.
+* Expose an interface that's substantially the same as demandimport for
+  Python 2.
+
+This also has some limitations compared to the Python 2 implementation:
+
+* Much of the logic is per-package, not per-module, so any packages loaded
+  before demandimport is enabled will not be lazily imported in the future. In
+  practice, we only expect builtins to be loaded before demandimport is
+  enabled.
+"""
+
+# This line is unnecessary, but it satisfies test-check-py3-compat.t.
+from __future__ import absolute_import
+
+import contextlib
+import os
+import sys
+
+import importlib.abc
+import importlib.machinery
+import importlib.util
+
+_deactivated = False
+
+class _lazyloaderex(importlib.util.LazyLoader):
+    """This is a LazyLoader except it also follows the _deactivated global and
+    the ignore list.
+    """
+    def exec_module(self, module):
+        """Make the module load lazily."""
+        if _deactivated or module.__name__ in ignore:
+            self.loader.exec_module(module)
+        else:
+            super().exec_module(module)
+
+# This is 3.6+ because with Python 3.5 it isn't possible to lazily load
+# extensions. See the discussion in https://python.org/sf/26186 for more.
+_extensions_loader = _lazyloaderex.factory(
+    importlib.machinery.ExtensionFileLoader)
+_bytecode_loader = _lazyloaderex.factory(
+    importlib.machinery.SourcelessFileLoader)
+_source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
+
+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),
+    )
+
+ignore = []
+
+def init(ignorelist):
+    global ignore
+    ignore = ignorelist
+
+def isenabled():
+    return _makefinder in sys.path_hooks and not _deactivated
+
+def disable():
+    try:
+        while True:
+            sys.path_hooks.remove(_makefinder)
+    except ValueError:
+        pass
+
+def enable():
+    if os.environ.get('HGDEMANDIMPORT') != 'disable':
+        sys.path_hooks.insert(0, _makefinder)
+
+@contextlib.contextmanager
+def deactivated():
+    # This implementation is a bit different from Python 2's. Python 3
+    # maintains a per-package finder cache in sys.path_importer_cache (see
+    # PEP 302). This means that we can't just call disable + enable.
+    # If we do that, in situations like:
+    #
+    #   demandimport.enable()
+    #   ...
+    #   from foo.bar import mod1
+    #   with demandimport.deactivated():
+    #       from foo.bar import mod2
+    #
+    # mod2 will be imported lazily. (The converse also holds -- whatever finder
+    # first gets cached will be used.)
+    #
+    # Instead, have a global flag the LazyLoader can use.
+    global _deactivated
+    demandenabled = isenabled()
+    if demandenabled:
+        _deactivated = True
+    try:
+        yield
+    finally:
+        if demandenabled:
+            _deactivated = False