[PATCH 12 of 12 RFC] policy: add helper to import cext/pure module

Yuya Nishihara yuya at tcha.org
Sat May 6 21:45:53 EDT 2017


# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1470969017 -32400
#      Fri Aug 12 11:30:17 2016 +0900
# Node ID 7d94765310f0b8fb04765ce0bc958edfc3f12960
# Parent  89045bec86dcb51ac0641e06abd59e82d3c22172
policy: add helper to import cext/pure module

These functions are sysstr API since __import__() and getattr() hate byte
strings on Python 3.

There's a minor BC, which is ImportError will be raised if invalid
HGMODULEPOLICY is specified. I think this is more desirable behavior.

THIS PATCH IS RFC because the current implementation uses a proxy module
to dispatch attribute accesses to cext or pure. This appears the most
controversial part in this series. We could instead add version checks
if we plan to switch to per-module versioning (proposed by Jun Wu):

  def _importversioned(pkgname, modname, locals):
      mod = _importfrom(pkgname, modname, locals)
      ver = getattr(mod, r'version', 0)  # TODO: select name of constant
      if ver != 0:
          raise ImportError(r'module version mismatch')
      return mod

Where a mod is loaded before assigned to locals, so we should disable
demandimport in policy.importmod(). This means we won't need locals dict.

diff --git a/mercurial/policy.py b/mercurial/policy.py
--- a/mercurial/policy.py
+++ b/mercurial/policy.py
@@ -24,6 +24,13 @@ import sys
 policy = b'allow'
 policynoc = (b'cffi', b'cffi-allow', b'py')
 policynocffi = (b'c', b'py')
+_packageprefs = {
+    b'c': [r'cext'],
+    b'allow': [r'cext', r'pure'],
+    b'cffi': [r'pure'],  # TODO: [r'cffi']
+    b'cffi-allow': [r'pure'],  # TODO: [r'cffi', r'pure']
+    b'py': [r'pure'],
+}
 
 try:
     from . import __modulepolicy__
@@ -49,3 +56,53 @@ if sys.version_info[0] >= 3:
         policy = os.environ[r'HGMODULEPOLICY'].encode(r'utf-8')
 else:
     policy = os.environ.get(r'HGMODULEPOLICY', policy)
+
+class _dualmod(object):
+    """Proxy loading named attribute from mod1 or mod2
+
+    mod2 is kept unloaded as long as the given name can be found in mod1.
+    """
+
+    def __init__(self, mod1, mod2):
+        self._mod1 = mod1
+        self._mod2 = mod2
+
+    def __getattr__(self, name):
+        try:
+            obj = getattr(self._mod1, name)  # may be unloaded module
+        except (AttributeError, ImportError):
+            obj = getattr(self._mod2, name)
+        self.__dict__[name] = obj
+        return obj
+
+    @property
+    def __doc__(self):
+        return self.__getattr__(r'__doc__')
+
+    def __repr__(self):
+        return r'<dual module %r, %r>' % (self._mod1, self._mod2)
+
+def _importfrom(pkgname, modname, locals):
+    # pass globals() to resolve name relative to this module
+    pkg = __import__(pkgname, globals(), locals, [modname], level=1)
+    return getattr(pkg, modname)
+
+def importmod(modname, locals):
+    """Import module according to policy
+
+    locals dict must be specified so demandimport can update the module
+    reference in place.
+    """
+    try:
+        prefs = _packageprefs[policy]
+    except KeyError:
+        raise ImportError(r'invalid HGMODULEPOLICY %r' % policy)
+
+    if len(prefs) == 1:
+        return _importfrom(prefs[0], modname, locals)
+    try:
+        mods = [_importfrom(p, modname, locals) for p in prefs]
+        return _dualmod(*mods)
+    except ImportError:
+        # fallback module must exist
+        return _importfrom(prefs[-1], modname, locals)


More information about the Mercurial-devel mailing list