[PATCH 4 of 5] extensions: add "uipopulate" hook, called per instance, not per process

Yuya Nishihara yuya at tcha.org
Sun Nov 18 09:19:20 EST 2018


# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1542024651 -32400
#      Mon Nov 12 21:10:51 2018 +0900
# Node ID 1e1427e07940f57d4146c38a0757e5104a88985c
# Parent  7b0e4efc0a7140a3d6f0814214e0c2bfa6c60acf
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.

diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py
--- a/mercurial/dispatch.py
+++ b/mercurial/dispatch.py
@@ -866,6 +866,9 @@ def _dispatch(req):
         # 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 @@ def _dispatch(req):
             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']:
diff --git a/mercurial/extensions.py b/mercurial/extensions.py
--- a/mercurial/extensions.py
+++ b/mercurial/extensions.py
@@ -405,6 +405,25 @@ def afterloaded(extension, callback):
     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
 
diff --git a/mercurial/help/internals/extensions.txt b/mercurial/help/internals/extensions.txt
--- a/mercurial/help/internals/extensions.txt
+++ b/mercurial/help/internals/extensions.txt
@@ -183,6 +183,34 @@ Command table setup
 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.
+
+Be aware that the ``req.ui`` wouldn't see local repository configuration,
+and the ``repo.ui`` will be copied from the base ``req.ui`` *after*
+``uipopulate`` is called. Use ``reposetup`` to set up ``repo.ui`` instance
+with repository-local configuration.
+
+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 +332,8 @@ uisetup
   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 +343,16 @@ extsetup
 * 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-*`` for repository
 * Modify configuration variables
 * Changes to ``repo.__class__``, ``repo.dirstate.__class__``
diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py
--- a/mercurial/hgweb/hgweb_mod.py
+++ b/mercurial/hgweb/hgweb_mod.py
@@ -214,6 +214,7 @@ class hgweb(object):
             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
diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py
--- a/mercurial/hgweb/hgwebdir_mod.py
+++ b/mercurial/hgweb/hgwebdir_mod.py
@@ -272,6 +272,7 @@ class hgwebdir(object):
         if not baseui:
             # set up environment for new ui
             extensions.loadall(self.ui)
+            extensions.populateui(self.ui)
 
     def refresh(self):
         if self.ui:
diff --git a/mercurial/ui.py b/mercurial/ui.py
--- a/mercurial/ui.py
+++ b/mercurial/ui.py
@@ -29,6 +29,7 @@ from . import (
     configitems,
     encoding,
     error,
+    extensions,
     formatter,
     progress,
     pycompat,
@@ -303,6 +304,9 @@ class ui(object):
             else:
                 raise error.ProgrammingError('unknown rctype: %s' % t)
         u._maybetweakdefaults()
+        # at the very first ui.load() of the process, this would be noop since
+        # no extension is loaded yet. _dispatch() needs to handle that case.
+        extensions.populateui(u)
         return u
 
     def _maybetweakdefaults(self):
diff --git a/tests/test-extension.t b/tests/test-extension.t
--- a/tests/test-extension.t
+++ b/tests/test-extension.t
@@ -24,6 +24,9 @@ Test basic extension support
   >     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,19 @@ Test basic extension support
   $ hg foo
   uisetup called
   uisetup called [status]
+  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 !)
   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)
   reposetup called for a (chg !)
   ui == repo.ui
   Foo
@@ -68,6 +77,8 @@ Test basic extension support
   uisetup called [debug] (no-chg !)
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
+  uipopulate called (1 times)
   reposetup called for a (chg !)
   ui == repo.ui
   Foo
@@ -76,6 +87,7 @@ Test basic extension support
   $ hg clone a b
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
   reposetup called for a
   ui == repo.ui
   reposetup called for b
@@ -86,6 +98,7 @@ Test basic extension support
   $ hg bar
   uisetup called (no-chg !)
   uisetup called [status] (no-chg !)
+  uipopulate called (1 times)
   Bar
   $ echo 'foobar = !' >> $HGRCPATH
 
@@ -96,8 +109,12 @@ module/__init__.py-style
   $ hg foo
   uisetup called
   uisetup called [status]
+  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 !)
   reposetup called for a (chg !)
   ui == repo.ui (chg !)
   Foo
@@ -114,8 +131,10 @@ Check that extensions are loaded in phas
   >     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 +162,12 @@ Check normal command's load order of ext
   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
   0:c24b9ac61126
 
 Check hgweb's load order of extensions and registration of functions
@@ -167,8 +190,10 @@ Check hgweb's load order of extensions a
   2) bar uisetup
   3) foo extsetup
   3) bar extsetup
-  4) foo reposetup
-  4) bar reposetup
+  4) foo uipopulate
+  4) bar uipopulate
+  5) foo reposetup
+  5) bar reposetup
 
 (check that revset predicate foo() and bar() are available)
 


More information about the Mercurial-devel mailing list