From patchwork Sat Jul 12 20:12:22 2014 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: [2,of,6] localrepo: add internal "pyhooks" hooks mechanism From: Gregory Szorc X-Patchwork-Id: 5148 Message-Id: <8fa7b8de6cf5c6f809ca.1405195942@vm-ubuntu-main.gateway.sonic.net> To: mercurial-devel@selenic.com Date: Sat, 12 Jul 2014 13:12:22 -0700 # HG changeset patch # User Gregory Szorc # Date 1405188833 25200 # Sat Jul 12 11:13:53 2014 -0700 # Node ID 8fa7b8de6cf5c6f809cabfd530d7f14383a18c2c # Parent ab912eb22894240c60ff757db60e06e343fef6a7 localrepo: add internal "pyhooks" hooks mechanism The intent of this feature addition is to give extensions (and possibly even core code) a better way than monkeypatching/wrapping to modify/supplement run-time behavior. Hooks have a few significant advantages over monkeypatching/wrapping: * Extensibility points are well defined. If Mercurial maintainers wish to clearly mark an activity as extensible, they can add a pyhook for it. Contrast with monkeypatching/wrapping, where it isn't always clear what the risks with each function are. Hooks give extension authors a clear set of points from which to start extending. * They are instance-specific. Monkeypatching often results in changing symbols on modules as opposed to a per-instance basis. Extensions may wish to modify behavior for classes of a certain type or perhaps even specific instances of a specific class. Globally modifying functions and then filtering for applicability at run-time can be difficult and dangerous. Beginning extension authors may not realize the full impact of global changes, especially in "shared" process spaces, such as hgweb or the command server. Per-instance hooks are much safer. The patch author considered an alternative implementation that introduced hooks.addrepohook() or extensions.addrepohook() and hooks.runrepohook() or extensions.runrepohook(). In the mind of the patch author, the choice of where the API should live and the names of the APIs (the author concedes "pyhook" isn't a great name but can think of nothing better) are better decided by someone with more experience than him. The author anticipates much bikeshedding on this patch. diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -300,8 +300,11 @@ class localrepository(object): # - working directory parent change, # - bookmark changes self.filteredrevcache = {} + # Maps names to list of callables. + self._hooks = {} + def close(self): pass def _restrictcapabilities(self, caps): @@ -485,8 +488,50 @@ class localrepository(object): replacing code that is expected to call a hook. """ return hook.hook(self.ui, self, name, throw, **args) + def addpyhook(self, name, fn): + """Register a Python hook against this repo instance. + + It is common to want to execute some Python code when certain events + occur. This is common in extensions. This method provides a + registration mechanism to do that. + + This method receives the name of a hook, name, and a callable, fn. + + If the hook name is not known, KeyError will be raised. This means + that if a hook is deleted, extensions will fail fast unless they catch + KeyError. + + The Mercurial API does not make any guarantees about the stability + of arguments passed to any called hook. However, an effort is made + to avoid unnecessary churn. + + "pyhooks" are an internal-oriented variation of the external-facing + hooks mechanism. The latter has strong API guarantees and hooks can + be added via hgrc files. pyhooks are strictly internal and have + weaker API guarantees. + """ + if not callable(fn): + raise ValueError('Argument is not callable') + self._hooks[name].append(fn) + + def runpyhook(self, name, **args): + """Run a Python hook. + + All callables registered via addpyhook() will be executed in the order + they were registered. + + Each hook will receive as arguments this repo instance as its single + positional argument and the named arguments passed into this method, if + any. All custom arguments are named because the API contract is not + guaranteed and this gives extensions yet another to point to fail + fast (unknown arguments will result in call failures and will require + extensions to adapt to changes in the API). + """ + for fn in self._hooks[name]: + fn(self, **args) + @unfilteredmethod def _tag(self, names, node, message, local, user, date, extra={}, editor=False): if isinstance(names, str):