Skip to content

Instantly share code, notes, and snippets.

@sunshowers
Last active July 25, 2016 22:37
Show Gist options
  • Save sunshowers/49513f0adc0c2ae2c00589dd99033819 to your computer and use it in GitHub Desktop.
Save sunshowers/49513f0adc0c2ae2c00589dd99033819 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# Copyright 2004-present Facebook. Licensed as GPLv2+.
"""Lazy loading for Python 3.
Requires Python 3.5.2 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 the open source
demandimport, including a 'deactivated' context manager.
This also has some limitations compared to the Python 2 implementation:
* zipimports are not supported.
* 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.
* Extension modules cannot be lazily imported. This is a limitation in Python
3.5 and has been lifted in Python 3.6 (see https://python.org/sf/26186 for
more).
"""
import contextlib
import importlib.abc
import importlib.machinery
import importlib.util
import os
import sys
import _demandimport
ignore = _demandimport.ignore
_deactivated = False
class _LazyLoaderWithExclusions(importlib.util.LazyLoader):
"""This is a LazyLoader except it also ignores 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)
# With Python 3.5 it isn't possible to lazily load extensions. With Python 3.6
# it will be, so this code will have to be updated to reflect that. See the
# discussion in https://python.org/sf/26186 for more.
_extensions_loader = importlib.machinery.ExtensionFileLoader
_bytecode_loader = _LazyLoaderWithExclusions.factory(
importlib.machinery.SourcelessFileLoader)
_source_loader = _LazyLoaderWithExclusions.factory(
importlib.machinery.SourceFileLoader)
def _make_finder(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),
)
def isenabled():
return _make_finder in sys.path_hooks and not _deactivated
def disable():
try:
while True:
sys.path_hooks.remove(_make_finder)
except ValueError:
pass
def enable():
if os.environ.get('HGDEMANDIMPORT') != 'disable':
sys.path_hooks.insert(0, _make_finder)
@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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment