Patchwork [1,of,5] registrar: add base classes to wrap functions, commands, and so on (API)

login
register
mail settings
Submitter Katsunori FUJIWARA
Date Aug. 22, 2018, 2:21 a.m.
Message ID <f52f112594f5d2706744.1534904512@blacknile>
Download mbox | patch
Permalink /patch/33955/
State New
Headers show

Comments

Katsunori FUJIWARA - Aug. 22, 2018, 2:21 a.m.
# HG changeset patch
# User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
# Date 1534511513 -32400
#      Fri Aug 17 22:11:53 2018 +0900
# Node ID f52f112594f5d27067446280a685fba3a9053a13
# Parent  7a759ad2d06dcf6548a8cc19fcd773eaa943f7ac
# Available At https://bitbucket.org/foozy/mercurial-wip
#              hg pull https://bitbucket.org/foozy/mercurial-wip -r f52f112594f5
# EXP-Topic use-decorator-for-webcommand
registrar: add base classes to wrap functions, commands, and so on (API)

This patch adds base classes to wrap functions, commands, and so on,
and adds functions to execute wrapping procedures. The latter is
useful to understand how added base classes are processed.

This change requires that 'hgwrapperlist' attribute of (3rd party)
extension is a list of registrar._wrapentrybase instances. This is
reason why this patch is marked with "(API)".

This patch chooses 'hgwrapperlist' instead of 'wrapperlist', because
the latter seems too generic to treat it as reserved.

  .. api::

     attribute name 'hgwrapperlist' of an extension is reserved to
     load wrapping automatically
Yuya Nishihara - Aug. 23, 2018, 12:27 p.m.
On Wed, 22 Aug 2018 11:21:52 +0900, FUJIWARA Katsunori wrote:
> # HG changeset patch
> # User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
> # Date 1534511513 -32400
> #      Fri Aug 17 22:11:53 2018 +0900
> # Node ID f52f112594f5d27067446280a685fba3a9053a13
> # Parent  7a759ad2d06dcf6548a8cc19fcd773eaa943f7ac
> # Available At https://bitbucket.org/foozy/mercurial-wip
> #              hg pull https://bitbucket.org/foozy/mercurial-wip -r f52f112594f5
> # EXP-Topic use-decorator-for-webcommand
> registrar: add base classes to wrap functions, commands, and so on (API)

Just scanned the series quickly, but can't we simply add registrar.webcommand
and extensions.wraptable()?

I see the decorators defined in the registrar module as a sort of DSL or
macro, which is nice if the concept is clear and we have lots of boring
repetition. But monkey-patching isn't that simple. We sometimes have to
conditionalize it for example, and we'll lose the readability provided by
decorator syntax.

Patch

diff --git a/mercurial/extensions.py b/mercurial/extensions.py
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -231,6 +231,38 @@  def _runextsetup(name, ui):
             return False
     return True
 
+def loadwrapping(wrapperlist):
+    """Execute wrapping procedures registered in wrapperlist
+    """
+    for wrapentry in wrapperlist:
+        extmod = None
+        if wrapentry.extname:
+            if wrapentry.extname not in _extensions:
+                # target extension is not loaded
+                continue
+            extmod = _extensions[wrapentry.extname]
+
+        container = wrapentry.getcontainer(extmod)
+        name = wrapentry.name
+        wrapper = wrapentry.wrapper
+
+        raise NotImplemented(wrapentry.target)
+
+def _runwrapping(name, ui):
+    """Execute wrapping procedures in 'name' extension
+    """
+    wrapperlist = getattr(_extensions[name], 'hgwrapperlist', None)
+    if wrapperlist:
+        try:
+            loadwrapping(wrapperlist)
+        except Exception as inst:
+            ui.traceback(force=True)
+            msg = stringutil.forcebytestr(inst)
+            ui.warn(_("*** failed to wrap functions by extension %s: %s\n")
+                    % (name, msg))
+            return False
+    return True
+
 def loadall(ui, whitelist=None):
     if ui.configbool('devel', 'debug.extensions'):
         log = lambda msg, *values: ui.debug('debug.extensions: ',
@@ -303,6 +335,18 @@  def loadall(ui, whitelist=None):
                 broken.add(name)
         log('  > extsetup for %r took %s\n', name, stats)
 
+    log('- executing wrapping functions\n')
+    for name in _order[newindex:]:
+        if name in broken:
+            continue
+        log('  - running wrapping functions for %r\n', name)
+        with util.timedcm() as stats:
+            if not _runwrapping(name, ui):
+                log('    - wrapping functions for the %r extension failed\n',
+                    name)
+                broken.add(name)
+        log('  > wrapping functions for %r took %s\n', name, stats)
+
     for name in broken:
         log('    - disabling broken %r extension\n', name)
         _extensions[name] = None
diff --git a/mercurial/registrar.py b/mercurial/registrar.py
--- a/mercurial/registrar.py
+++ b/mercurial/registrar.py
@@ -103,6 +103,83 @@  class _funcregistrarbase(object):
         """Execute exra setup for registered function, if needed
         """
 
+class _wrapentrybase(object):
+    """Base of the object to encapsulate information for wrapping
+
+    Targets of wrapping are:
+
+    - a normal function
+    - a filecache property of a class
+    - a command function in a command table
+    - a function in a dispatch table (e.g. fileset, revset, and so on)
+
+    These exist in:
+
+    - core modules of Mercurial
+    - extensions loaded at runtime
+    """
+    target = None
+
+    def __init__(self, name, extname):
+        self.name = name
+        self.extname = extname
+
+    def __call__(self, wrapper):
+        self.wrapper = wrapper
+        return wrapper
+
+    def getcontainer(self, extmod):
+        """Get a container of the function to be wrapped
+
+        If `self.extname` is not None, `extmod` should be the
+        extension module object corresponded to it.
+        """
+        raise NotImplemented()
+
+    def extrasetup(self, funcentry, wrap):
+        """Do extra setup for wrapping
+
+        `funcentry` is the original object, which is looked up with
+        `self.name` from the object returned by `getcontainer()`.
+        Usually, it is a function to be wrapped itself (aka "origfn").
+
+        `wrap` is the result of `bind(wrapper, origfn)` or so.
+
+        Typically, this method is used to copy some specific
+        information from `funcentry` to `wrap`. For example, `norepo`,
+        `optionalrepo`, and such attributes of original "command"
+        function should be visible via `wrap` at
+        commands.loadcmdtable().
+
+        The result of this method is stored into the object returned
+        by `getcontainer()` with `self.name`.
+
+        """
+        return wrap
+
+    @staticmethod
+    def lookup(basecontainer, path):
+        components = []
+        if path:
+            components.extend(path.split('.'))
+        container = basecontainer
+        while components:
+            container = getattr(container, components.pop(0))
+        return container
+
+class _wrapdecoratorbase(object):
+    """Base of decorator to store wrapping information into the list
+    """
+    _entrycls = None
+
+    def __init__(self, entrylist):
+        self.entrylist = entrylist
+
+    def __call__(self, name, *args, **kwargs):
+        entry = self._entrycls(name, *args, **kwargs)
+        self.entrylist.append(entry)
+        return entry
+
 class command(_funcregistrarbase):
     """Decorator to register a command function to table
 
diff --git a/tests/test-bad-extension.t b/tests/test-bad-extension.t
--- a/tests/test-bad-extension.t
+++ b/tests/test-bad-extension.t
@@ -116,6 +116,11 @@  show traceback for ImportError of hgext.
   debug.extensions:   > extsetup for 'gpg' took * (glob)
   debug.extensions:   - running extsetup for 'baddocext'
   debug.extensions:   > extsetup for 'baddocext' took * (glob)
+  debug.extensions: - executing wrapping functions
+  debug.extensions:   - running wrapping functions for 'gpg'
+  debug.extensions:   > wrapping functions for 'gpg' took * us (glob)
+  debug.extensions:   - running wrapping functions for 'baddocext'
+  debug.extensions:   > wrapping functions for 'baddocext' took * us (glob)
   debug.extensions: - executing remaining aftercallbacks
   debug.extensions: > remaining aftercallbacks completed in * (glob)
   debug.extensions: - loading extension registration objects