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

login
register
mail settings
Submitter Yuya Nishihara
Date Nov. 18, 2018, 2:19 p.m.
Message ID <1e1427e07940f57d4146.1542550760@mimosa>
Download mbox | patch
Permalink /patch/36638/
State Superseded
Headers show

Comments

Yuya Nishihara - Nov. 18, 2018, 2:19 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@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.
Yuya Nishihara - Nov. 21, 2018, 1:39 p.m.
On Sun, 18 Nov 2018 23:19:20 +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@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

> +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.

I'll send V2 which will simply call uipopulate(repo.ui) after repo/hgrc is
loaded. That seems less error-prone.

Patch

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)