From patchwork Sun May 7 01:45:53 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: [12,of,12,RFC] policy: add helper to import cext/pure module From: Yuya Nishihara X-Patchwork-Id: 20510 Message-Id: <7d94765310f0b8fb0476.1494121553@mimosa> To: mercurial-devel@mercurial-scm.org Date: Sun, 07 May 2017 10:45:53 +0900 # HG changeset patch # User Yuya Nishihara # 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'' % (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)