Patchwork [2,of,5] registrar: add classes to wrap existing function

login
register
mail settings
Submitter Katsunori FUJIWARA
Date Aug. 22, 2018, 2:21 a.m.
Message ID <eafbb78561cfd9d299f7.1534904513@blacknile>
Download mbox | patch
Permalink /patch/33956/
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 1534896968 -32400
#      Wed Aug 22 09:16:08 2018 +0900
# Node ID eafbb78561cfd9d299f72fca659bf4747c4808c6
# Parent  f52f112594f5d27067446280a685fba3a9053a13
# Available At https://bitbucket.org/foozy/mercurial-wip
#              hg pull https://bitbucket.org/foozy/mercurial-wip -r eafbb78561cf
# EXP-Topic use-decorator-for-webcommand
registrar: add classes to wrap existing function

This allows extensions to wrap existing functions at loading
automatically by annotation.

Wrapping functions in core modules of Mercurial is tested in
subsequent patch by testing actual wrapping.

BTW, this patch does not encapsulate core procedure of wrapping into
_wrapfuncentry class, but implements it in extensions.loadwrapping(),
because wrapfunction(), which is required to wrap existing function,
is defined in extensions.py.

Just factoring wrapfunction() and its utilities out can solve this
cyclic dependency problem. But classes for wrapping existing command
requires invocation of wrapcommand(), which implies cmdutil.findcmd().

Smartly solving these dependency problems requires a little more
investigation/analysis, but it can be separated from the series of
this patch by avoiding encapsulation.

Patch

diff --git a/mercurial/extensions.py b/mercurial/extensions.py
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -246,7 +246,16 @@  def loadwrapping(wrapperlist):
         name = wrapentry.name
         wrapper = wrapentry.wrapper
 
-        raise NotImplemented(wrapentry.target)
+        # Here, importing registrar is cheap enough, because
+        # the loaded extension should already import it
+        from . import registrar
+
+        # TODO: encapsulate wrapping logic into _wrapentrybase instance
+
+        if wrapentry.target == registrar._wrapentrybase.TARGET_FUNCTION:
+            wrapfunction(container, name, wrapper)
+        else:
+            raise NotImplemented(wrapentry.target)
 
 def _runwrapping(name, ui):
     """Execute wrapping procedures in 'name' extension
diff --git a/mercurial/registrar.py b/mercurial/registrar.py
--- a/mercurial/registrar.py
+++ b/mercurial/registrar.py
@@ -120,6 +120,8 @@  class _wrapentrybase(object):
     """
     target = None
 
+    TARGET_FUNCTION = 'function'
+
     def __init__(self, name, extname):
         self.name = name
         self.extname = extname
@@ -257,6 +259,66 @@  class command(_funcregistrarbase):
 
 INTENT_READONLY = b'readonly'
 
+class _wrapfuncentry(_wrapentrybase):
+    target = _wrapentrybase.TARGET_FUNCTION
+
+    def __init__(self, name, modorextname, path=None):
+        if isinstance(modorextname, (bytes, unicode)):
+            # wrap function in another extension
+            extname = modorextname
+            container = None
+        else:
+            # wrap function in specified container object
+            extname = None
+            container = modorextname
+        super(_wrapfuncentry, self).__init__(name, extname)
+        self.path = path
+        self.container = container
+
+    def getcontainer(self, extmod):
+        return self.lookup(extmod or self.container, self.path)
+
+class wrapfunc(_wrapdecoratorbase):
+    """Decorator to register function wrapping
+
+    Usage::
+
+        hgwrapperlist = []
+        wrapfunc = registrar.wrapfunc(hgwrapperlist)
+
+        # wrapping the function foo() in mercurial.cmdutil
+        # (use module to specify base "container")
+        from mercurial import cmdutil
+        @wrapfunc('foo', cmdutil)
+        def foowrapper(origfn, *args, **kwargs):
+            pass
+
+        # wrapping the function bar() in another extension 'barbar'
+        # (use string to specify another extension as base "container")
+        @wrapfunc('bar', 'barbar')
+        def barwrapper(origfn, *args, **kwargs):
+            pass
+
+    In addition, 'path' argument can be used to lookup the target
+    function relative to the specified base "container". This can
+    delay getting actual "container" of the target function until
+    processing function wrapping. For example::
+
+        # bar() of x.y.z object in barbar extension
+        @wrapfunc('bar', 'barbar', path='x.y.z')
+        def barwrapper(origfn, *args, **kwargs):
+            pass
+
+    Decorated functions are processed to wrap a corresponded function
+    automatically at loading extension, if a list named as
+    'hgwrapperlist' is used to create 'wrapfunc' instance.
+
+    Otherwise, explicit 'extensions.loadwrapping()' is needed.
+
+    'hgwrapperlist' can be shared with other decorators for wrapping.
+    """
+    _entrycls = _wrapfuncentry
+
 class revsetpredicate(_funcregistrarbase):
     """Decorator to register revset predicate
 
diff --git a/tests/test-extension.t b/tests/test-extension.t
--- a/tests/test-extension.t
+++ b/tests/test-extension.t
@@ -1753,3 +1753,88 @@  Prohibit the use of unicode strings as t
   hg: unknown command 'dummy'
   (did you mean summary?)
   [255]
+
+Use decorator registrar.wrapfunc to wrap function of another extension
+automatically at loading
+
+  $ hg init wrapfunc
+  $ cd wrapfunc
+
+  $ mkdir $TESTTMP/wrappee
+
+  $ cat > $TESTTMP/wrappee/__init__.py <<EOF
+  > from . import submodule
+  > from mercurial import commands, registrar
+  > cmdtable = {}
+  > command = registrar.command(cmdtable)
+  > def say(ui, message):
+  >     ui.write('say: %s\n' % message)
+  > class foo(object):
+  >     def doit(self, ui):
+  >         ui.write('foo, doit\n')
+  > @command(b'wrappee', [], b'hg wrappee')
+  > def wrappee(ui, repo, *args, **kwargs):
+  >     say(ui, 'hello #1')
+  >     foo().doit(ui)
+  >     submodule.say(ui, 'hello #2')
+  >     submodule.bar().doit(ui)
+  > EOF
+
+  $ cat > $TESTTMP/wrappee/submodule.py <<EOF
+  > def say(ui, message):
+  >     ui.write('submodule.say: %s\n' % message)
+  > class bar(object):
+  >     def doit(self, ui):
+  >         ui.write('bar, doit\n')
+  > EOF
+
+  $ cat > $TESTTMP/wrapper.py <<EOF
+  > from mercurial import commands, registrar
+  > hgwrapperlist = []
+  > wrapfunc = registrar.wrapfunc(hgwrapperlist)
+  > # wrap method without path
+  > @wrapfunc('say', 'wrappee')
+  > def wrapsay(orig, ui, message):
+  >     ui.write('== substitute %r\n' % message)
+  >     orig(ui, 'goodby #1')
+  > # wrap method with path
+  > @wrapfunc('doit', 'wrappee', path='foo')
+  > def wrapfoodoit(orig, self, ui):
+  >     ui.write('== foo.doit is wrapped\n')
+  >     orig(self, ui)
+  > # wrap normal function with path
+  > @wrapfunc('say', 'wrappee', path='submodule')
+  > def wrapsubmodulesay(orig, ui, message):
+  >     ui.write('== substitute %r\n' % message)
+  >     orig(ui, 'goodby #2')
+  > # wrap method with nested path
+  > @wrapfunc('doit', 'wrappee', path='submodule.bar')
+  > def wrapbardoit(orig, self, ui):
+  >     ui.write('== submodule.bar.doit is wrapped\n')
+  >     orig(self, ui)
+  > EOF
+
+  $ cat >> .hg/hgrc <<EOF
+  > [extensions]
+  > wrappee = $TESTTMP/wrappee
+  > EOF
+  $ hg wrappee
+  say: hello #1
+  foo, doit
+  submodule.say: hello #2
+  bar, doit
+
+  $ cat >> .hg/hgrc <<EOF
+  > wrapper = $TESTTMP/wrapper.py
+  > EOF
+  $ hg wrappee
+  == substitute 'hello #1'
+  say: goodby #1
+  == foo.doit is wrapped
+  foo, doit
+  == substitute 'hello #2'
+  submodule.say: goodby #2
+  == submodule.bar.doit is wrapped
+  bar, doit
+
+  $ cd ..