extensions: add "uipopulate" hook, called per instance, not per process
authorYuya Nishihara <yuya@tcha.org>
Mon, 12 Nov 2018 21:10:51 +0900
changeset 40729 c93d046d4300
parent 40728 2cd5f1fac788
child 40730 55b053af7196
extensions: add "uipopulate" hook, called per instance, not per process In short, this is the "reposetup" function for ui. It allows us to modify ui attributes without extending ui.__class__. Before, the only way to do that was to abuse the config dictionary, which is copied across ui instances. See the next patch for usage example.
mercurial/chgserver.py
mercurial/dispatch.py
mercurial/extensions.py
mercurial/help/internals/extensions.txt
mercurial/hgweb/hgweb_mod.py
mercurial/hgweb/hgwebdir_mod.py
mercurial/localrepo.py
tests/blackbox-readonly-dispatch.py
tests/test-clone.t
tests/test-duplicateoptions.py
tests/test-extension.t
--- a/mercurial/chgserver.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/chgserver.py	Mon Nov 12 21:10:51 2018 +0900
@@ -246,6 +246,10 @@
     rpath = options['repository']
     path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
 
+    extensions.populateui(newui)
+    if newui is not newlui:
+        extensions.populateui(newlui)
+
     return (newui, newlui)
 
 class channeledsystem(object):
--- a/mercurial/dispatch.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/dispatch.py	Mon Nov 12 21:10:51 2018 +0900
@@ -866,6 +866,9 @@
         # Check abbreviation/ambiguity of shell alias.
         shellaliasfn = _checkshellalias(lui, ui, args)
         if shellaliasfn:
+            # no additional configs will be set, set up the ui instances
+            for ui_ in uis:
+                extensions.populateui(ui_)
             return shellaliasfn()
 
         # check for fallback encoding
@@ -948,6 +951,10 @@
             for ui_ in uis:
                 ui_.disablepager()
 
+        # configs are fully loaded, set up the ui instances
+        for ui_ in uis:
+            extensions.populateui(ui_)
+
         if options['version']:
             return commands.version_(ui)
         if options['help']:
--- a/mercurial/extensions.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/extensions.py	Mon Nov 12 21:10:51 2018 +0900
@@ -405,6 +405,25 @@
     else:
         _aftercallbacks.setdefault(extension, []).append(callback)
 
+def populateui(ui):
+    """Run extension hooks on the given ui to populate additional members,
+    extend the class dynamically, etc.
+
+    This will be called after the configuration is loaded, and/or extensions
+    are loaded. In general, it's once per ui instance, but in command-server
+    and hgweb, this may be called more than once with the same ui.
+    """
+    for name, mod in extensions(ui):
+        hook = getattr(mod, 'uipopulate', None)
+        if not hook:
+            continue
+        try:
+            hook(ui)
+        except Exception as inst:
+            ui.traceback(force=True)
+            ui.warn(_('*** failed to populate ui by extension %s: %s\n')
+                    % (name, stringutil.forcebytestr(inst)))
+
 def bind(func, *args):
     '''Partial function application
 
--- a/mercurial/help/internals/extensions.txt	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/help/internals/extensions.txt	Mon Nov 12 21:10:51 2018 +0900
@@ -183,6 +183,29 @@
 After ``extsetup``, the ``cmdtable`` is copied into the global command table
 in Mercurial.
 
+Ui instance setup
+-----------------
+
+The optional ``uipopulate`` is called for each ``ui`` instance after
+configuration is loaded, where extensions can set up additional ui members,
+update configuration by ``ui.setconfig()``, and extend the class dynamically.
+
+Typically there are three ``ui`` instances involved in command execution:
+
+``req.ui`` (or ``repo.baseui``)
+    Only system and user configurations are loaded into it.
+``lui``
+    Local repository configuration is loaded as well. This will be used at
+    early dispatching stage where a repository isn't available.
+``repo.ui``
+    The fully-loaded ``ui`` used after a repository is instantiated. This
+    will be created from the ``req.ui`` per repository.
+
+In command server and hgweb, this may be called more than once for the same
+``ui`` instance.
+
+(New in Mercurial 4.9)
+
 Repository setup
 ----------------
 
@@ -304,7 +327,8 @@
   a change made here will be visible by other extensions during ``extsetup``.
 * Monkeypatches or function wraps (``extensions.wrapfunction``) of ``dispatch``
   module members
-* Setup of ``pre-*`` and ``post-*`` hooks
+* Set up ``pre-*`` and ``post-*`` hooks. (DEPRECATED. ``uipopulate`` is
+  preferred on Mercurial 4.9 and later.)
 * ``pushkey`` setup
 
 extsetup
@@ -314,9 +338,17 @@
 * Add a global option to all commands
 * Extend revsets
 
+uipopulate
+----------
+
+* Modify ``ui`` instance attributes and configuration variables.
+* Changes to ``ui.__class__`` per instance.
+* Set up all hooks per scoped configuration.
+
 reposetup
 ---------
 
-* All hooks but ``pre-*`` and ``post-*``
+* Set up all hooks but ``pre-*`` and ``post-*``. (DEPRECATED. ``uipopulate`` is
+  preferred on Mercurial 4.9 and later.)
 * Modify configuration variables
 * Changes to ``repo.__class__``, ``repo.dirstate.__class__``
--- a/mercurial/hgweb/hgweb_mod.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/hgweb/hgweb_mod.py	Mon Nov 12 21:10:51 2018 +0900
@@ -214,6 +214,7 @@
             else:
                 u = uimod.ui.load()
                 extensions.loadall(u)
+                extensions.populateui(u)
             r = hg.repository(u, repo)
         else:
             # we trust caller to give us a private copy
--- a/mercurial/hgweb/hgwebdir_mod.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/hgweb/hgwebdir_mod.py	Mon Nov 12 21:10:51 2018 +0900
@@ -272,6 +272,7 @@
         if not baseui:
             # set up environment for new ui
             extensions.loadall(self.ui)
+            extensions.populateui(self.ui)
 
     def refresh(self):
         if self.ui:
@@ -308,6 +309,7 @@
             paths = self.conf
         elif isinstance(self.conf, dict):
             paths = self.conf.items()
+        extensions.populateui(u)
 
         repos = findrepos(paths)
         for prefix, root in u.configitems('collections'):
--- a/mercurial/localrepo.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/mercurial/localrepo.py	Mon Nov 12 21:10:51 2018 +0900
@@ -454,6 +454,7 @@
     if loadhgrc(ui, wdirvfs, hgvfs, requirements):
         afterhgrcload(ui, wdirvfs, hgvfs, requirements)
         extensions.loadall(ui)
+        extensions.populateui(ui)
 
     # Set of module names of extensions loaded for this repository.
     extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
--- a/tests/blackbox-readonly-dispatch.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/tests/blackbox-readonly-dispatch.py	Mon Nov 12 21:10:51 2018 +0900
@@ -2,6 +2,7 @@
 import os
 from mercurial import (
     dispatch,
+    extensions,
     ui as uimod,
 )
 
@@ -11,6 +12,7 @@
     Prints command and result value, but does not handle quoting.
     """
     ui = uimod.ui.load()
+    extensions.populateui(ui)
     ui.status(b"running: %s\n" % cmd)
     req = dispatch.request(cmd.split(), ui)
     result = dispatch.dispatch(req)
--- a/tests/test-clone.t	Sat Nov 17 19:11:45 2018 +0900
+++ b/tests/test-clone.t	Mon Nov 12 21:10:51 2018 +0900
@@ -574,6 +574,7 @@
   > from mercurial import extensions, hg, ui as uimod
   > myui = uimod.ui.load()
   > extensions.loadall(myui)
+  > extensions.populateui(myui)
   > repo = hg.repository(myui, b'a')
   > hg.clone(myui, {}, repo, dest=b"ua", branch=[b"stable",])
   > EOF
--- a/tests/test-duplicateoptions.py	Sat Nov 17 19:11:45 2018 +0900
+++ b/tests/test-duplicateoptions.py	Mon Nov 12 21:10:51 2018 +0900
@@ -29,6 +29,7 @@
 
 u = uimod.ui.load()
 extensions.loadall(u)
+extensions.populateui(u)
 
 globalshort = set()
 globallong = set()
--- a/tests/test-extension.t	Sat Nov 17 19:11:45 2018 +0900
+++ b/tests/test-extension.t	Mon Nov 12 21:10:51 2018 +0900
@@ -24,6 +24,9 @@
   >     ui.write(b"uisetup called\\n")
   >     ui.status(b"uisetup called [status]\\n")
   >     ui.flush()
+  > def uipopulate(ui):
+  >     ui._populatecnt = getattr(ui, "_populatecnt", 0) + 1
+  >     ui.write(b"uipopulate called (%d times)\n" % ui._populatecnt)
   > def reposetup(ui, repo):
   >     ui.write(b"reposetup called for %s\\n" % os.path.basename(repo.root))
   >     ui.write(b"ui %s= repo.ui\\n" % (ui == repo.ui and b"=" or b"!"))
@@ -54,13 +57,26 @@
   $ hg foo
   uisetup called
   uisetup called [status]
+  uipopulate called (1 times)
+  uipopulate called (1 times)
+  uipopulate called (1 times)
   reposetup called for a
   ui == repo.ui
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
   reposetup called for a (chg !)
   ui == repo.ui (chg !)
   Foo
   $ hg foo --quiet
   uisetup called (no-chg !)
+  uipopulate called (1 times)
+  uipopulate called (1 times)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
   reposetup called for a (chg !)
   ui == repo.ui
   Foo
@@ -68,6 +84,11 @@
   uisetup called [debug] (no-chg !)
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
+  uipopulate called (1 times)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
   reposetup called for a (chg !)
   ui == repo.ui
   Foo
@@ -76,8 +97,12 @@
   $ hg clone a b
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
   reposetup called for a
   ui == repo.ui
+  uipopulate called (1 times)
   reposetup called for b
   ui == repo.ui
   updating to branch default
@@ -86,6 +111,8 @@
   $ hg bar
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
+  uipopulate called (1 times) (chg !)
   Bar
   $ echo 'foobar = !' >> $HGRCPATH
 
@@ -96,8 +123,16 @@
   $ hg foo
   uisetup called
   uisetup called [status]
+  uipopulate called (1 times)
+  uipopulate called (1 times)
+  uipopulate called (1 times)
   reposetup called for a
   ui == repo.ui
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
+  uipopulate called (1 times) (chg !)
   reposetup called for a (chg !)
   ui == repo.ui (chg !)
   Foo
@@ -114,8 +149,10 @@
   >     print("2) %s uisetup" % name, flush=True)
   > def extsetup():
   >     print("3) %s extsetup" % name, flush=True)
+  > def uipopulate(ui):
+  >     print("4) %s uipopulate" % name, flush=True)
   > def reposetup(ui, repo):
-  >    print("4) %s reposetup" % name, flush=True)
+  >     print("5) %s reposetup" % name, flush=True)
   > 
   > bytesname = name.encode('utf-8')
   > # custom predicate to check registration of functions at loading
@@ -143,8 +180,14 @@
   2) bar uisetup
   3) foo extsetup
   3) bar extsetup
-  4) foo reposetup
-  4) bar reposetup
+  4) foo uipopulate
+  4) bar uipopulate
+  4) foo uipopulate
+  4) bar uipopulate
+  4) foo uipopulate
+  4) bar uipopulate
+  5) foo reposetup
+  5) bar reposetup
   0:c24b9ac61126
 
 Check hgweb's load order of extensions and registration of functions
@@ -167,8 +210,12 @@
   2) bar uisetup
   3) foo extsetup
   3) bar extsetup
-  4) foo reposetup
-  4) bar reposetup
+  4) foo uipopulate
+  4) bar uipopulate
+  4) foo uipopulate
+  4) bar uipopulate
+  5) foo reposetup
+  5) bar reposetup
 
 (check that revset predicate foo() and bar() are available)