view hgdemandimport/demandimportpy3.py @ 45755:8ed69bd42f10 stable

demandimport: don't raise AttributeError if `exec_module` is missing I assume this was meant to do the check gracefully. After shoveling a bunch of modules into the ignore list in order to get keyring to work out of the box on CentOS 8, I hit the following error accessing the password, which the change fixes. Now the SecretStorage backend works out of the box, without any edits to the ignore list. ** Unknown exception encountered with possibly-broken third-party extension mercurial_keyring ** which supports versions unknown of Mercurial. ** Please disable mercurial_keyring and try your action again. ** If that fixes the bug please report it to https://foss.heptapod.net/mercurial/mercurial_keyring/issues ** Python 3.6.8 (default, Apr 16 2020, 01:36:27) [GCC 8.3.1 20191121 (Red Hat 8.3.1-5)] ** Mercurial Distributed SCM (version 5.5.2) ** Extensions loaded: evolve, topic, rebase, absorb, mercurial_keyring Traceback (most recent call last): File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/mercurial_keyring.py", line 230, in _read_password_from_keyring password = keyring.get_password(KEYRING_SERVICE, pwdkey) File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/keyring/core.py", line 53, in get_password return _keyring_backend.get_password(service_name, username) File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/keyring/backends/chainer.py", line 51, in get_password password = keyring.get_password(service, username) File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/keyring/backends/SecretService.py", line 79, in get_password return item.get_secret().decode('utf-8') File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/secretstorage/item.py", line 105, in get_secret decryptor = Cipher(aes, modes.CBC(aes_iv), default_backend()).decryptor() File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/cryptography/hazmat/backends/__init__.py", line 15, in default_backend from cryptography.hazmat.backends.openssl.backend import backend File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 665, in _load_unlocked File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/hgdemandimport/demandimportpy3.py", line 53, in exec_module self.loader.exec_module(module) File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/cryptography/hazmat/backends/openssl/__init__.py", line 7, in <module> from cryptography.hazmat.backends.openssl.backend import backend File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 955, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 665, in _load_unlocked File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/hgdemandimport/demandimportpy3.py", line 53, in exec_module self.loader.exec_module(module) File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 14, in <module> from six.moves import range File "<frozen importlib._bootstrap>", line 971, in _find_and_load File "<frozen importlib._bootstrap>", line 951, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 894, in _find_spec File "/home/mharbison/hg_py3.6.8_venv/lib64/python3.6/site-packages/hgdemandimport/demandimportpy3.py", line 117, in find_spec and getattr(spec.loader, "exec_module") AttributeError: '_SixMetaPathImporter' object has no attribute 'exec_module' Differential Revision: https://phab.mercurial-scm.org/D9243
author Matt Harbison <matt_harbison@yahoo.com>
date Thu, 22 Oct 2020 18:38:41 -0400
parents a6e12d477595
children 6000f5b25c9b
line wrap: on
line source

# 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 importlib.util
import sys

from . import tracing

_deactivated = False

# Python 3.5's LazyLoader doesn't work for some reason.
# https://bugs.python.org/issue26186 is a known issue with extension
# importing. But it appears to not have a meaningful effect with
# Mercurial.
_supported = sys.version_info[0:2] >= (3, 6)


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."""
        with tracing.log('demandimport %s', module):
            if _deactivated or module.__name__ in ignores:
                self.loader.exec_module(module)
            else:
                super().exec_module(module)


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"))

    __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, fullname, path, target=None):
        finder = object.__getattribute__(self, "_finder")
        try:
            find_spec = finder.find_spec
        except AttributeError:
            loader = finder.find_module(fullname, path)
            if loader is None:
                spec = None
            else:
                spec = importlib.util.spec_from_loader(fullname, loader)
        else:
            spec = find_spec(fullname, path, target)

        # Lazy loader requires exec_module().
        if (
            spec is not None
            and spec.loader is not None
            and getattr(spec.loader, "exec_module", None)
        ):
            spec.loader = _lazyloaderex(spec.loader)

        return spec


ignores = set()


def init(ignoreset):
    global ignores
    ignores = ignoreset


def isenabled():
    return not _deactivated and any(
        isinstance(finder, LazyFinder) for finder in sys.meta_path
    )


def disable():
    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

    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
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