Patchwork [4,of,5] revset: use decorator to mark a function as revset predicate

login
register
mail settings
Submitter Katsunori FUJIWARA
Date Dec. 23, 2015, 4:46 p.m.
Message ID <81bd3d94ee96550155cc.1450889200@feefifofum>
Download mbox | patch
Permalink /patch/12300/
State Changes Requested
Delegated to: Yuya Nishihara
Headers show

Comments

Katsunori FUJIWARA - Dec. 23, 2015, 4:46 p.m.
# HG changeset patch
# User FUJIWARA Katsunori <foozy@lares.dti.ne.jp>
# Date 1450889156 -32400
#      Thu Dec 24 01:45:56 2015 +0900
# Node ID 81bd3d94ee96550155cc3e59d58ce28380484ddb
# Parent  1946805b8237bbfbfe8c83196c00d9e41751d4f3
revset: use decorator to mark a function as revset predicate

Using decorator can localize changes for adding (or removing) a revset
predicate function in source code.

It is also useful to pick predicates up for specific purpose. For
example, subsequent patch marks predicates as "safe" by decorator.

To avoid (1) redundancy between "predicate name" and (the beginning
of) help document, and (2) accidental typo of help document, this
patch also makes decorator put predicate declaration into the
beginning of help.

Patch

diff --git a/hgext/largefiles/overrides.py b/hgext/largefiles/overrides.py
--- a/hgext/largefiles/overrides.py
+++ b/hgext/largefiles/overrides.py
@@ -812,9 +812,9 @@  def overridepull(orig, ui, repo, source=
         ui.status(_("%d largefiles cached\n") % numcached)
     return result
 
+@revset.predicate('pulled()')
 def pulledrevsetsymbol(repo, subset, x):
-    """``pulled()``
-    Changesets that just has been pulled.
+    """Changesets that just has been pulled.
 
     Only available with largefiles from pull --lfrev expressions.
 
diff --git a/hgext/largefiles/uisetup.py b/hgext/largefiles/uisetup.py
--- a/hgext/largefiles/uisetup.py
+++ b/hgext/largefiles/uisetup.py
@@ -9,7 +9,7 @@ 
 '''setup for largefiles extension: uisetup'''
 
 from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \
-    httppeer, merge, scmutil, sshpeer, wireproto, revset, subrepo, copies
+    httppeer, merge, scmutil, sshpeer, wireproto, subrepo, copies
 from mercurial.i18n import _
 from mercurial.hgweb import hgweb_mod, webcommands
 
@@ -83,7 +83,6 @@  def uisetup(ui):
                ('', 'lfrev', [],
                 _('download largefiles for these revisions'), _('REV'))]
     entry[1].extend(pullopt)
-    revset.symbols['pulled'] = overrides.pulledrevsetsymbol
 
     entry = extensions.wrapcommand(commands.table, 'clone',
                                    overrides.overrideclone)
diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -3555,9 +3555,9 @@  def summaryhook(ui, repo):
         # i18n: column positioning for "hg summary"
         ui.note(_("mq:     (empty queue)\n"))
 
+@revset.predicate('mq()')
 def revsetmq(repo, subset, x):
-    """``mq()``
-    Changesets managed by MQ.
+    """Changesets managed by MQ.
     """
     revset.getargs(x, 0, 0, _("mq takes no arguments"))
     applied = set([repo[r.node].rev() for r in repo.mq.applied])
@@ -3593,8 +3593,6 @@  def extsetup(ui):
         if extmodule.__file__ != __file__:
             dotable(getattr(extmodule, 'cmdtable', {}))
 
-    revset.symbols['mq'] = revsetmq
-
 colortable = {'qguard.negative': 'red',
               'qguard.positive': 'yellow',
               'qguard.unguarded': 'green',
diff --git a/hgext/rebase.py b/hgext/rebase.py
--- a/hgext/rebase.py
+++ b/hgext/rebase.py
@@ -64,6 +64,7 @@  def _destrebase(repo):
     branch = repo[None].branch()
     return repo[branch].rev()
 
+@revset.predicate('_destrebase')
 def _revsetdestrebase(repo, subset, x):
     # ``_rebasedefaultdest()``
 
@@ -1231,4 +1232,3 @@  def uisetup(ui):
          _("use 'hg rebase --continue' or 'hg rebase --abort'")])
     # ensure rebased rev are not hidden
     extensions.wrapfunction(repoview, '_getdynamicblockers', _rebasedvisible)
-    revset.symbols['_destrebase'] = _revsetdestrebase
diff --git a/hgext/transplant.py b/hgext/transplant.py
--- a/hgext/transplant.py
+++ b/hgext/transplant.py
@@ -693,9 +693,9 @@  def _dotransplant(ui, repo, *revs, **opt
         if cleanupfn:
             cleanupfn()
 
+@revset.predicate('transplanted([set])')
 def revsettransplanted(repo, subset, x):
-    """``transplanted([set])``
-    Transplanted changesets in set, or all transplanted changesets.
+    """Transplanted changesets in set, or all transplanted changesets.
     """
     if x:
         s = revset.getset(repo, subset, x)
@@ -712,7 +712,6 @@  def kwtransplanted(repo, ctx, **args):
     return n and revlog.hex(n) or ''
 
 def extsetup(ui):
-    revset.symbols['transplanted'] = revsettransplanted
     cmdutil.unfinishedstates.append(
         ['series', True, False, _('transplant in progress'),
          _("use 'hg transplant --continue' or 'hg update' to abort")])
diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -464,19 +464,48 @@  def func(repo, subset, a, b):
 
 # functions
 
+# symbols are callables like:
+#   fn(repo, subset, x)
+# with:
+#   repo - current repository instance
+#   subset - of revisions to be examined
+#   x - argument in tree form
+symbols = {}
+
+def predicate(decl):
+    """Return a decorator for revset predicate function
+
+    'decl' argument is the declaration (including argument list like
+    'adds(pattern)') or the name (for internal use only) of a
+    predicate.
+    """
+    def decorator(func):
+        i = decl.find('(')
+        if i > 0:
+            name = decl[:i]
+        else:
+            name = decl # only for internal use
+        symbols[name] = func
+        if func.__doc__:
+            func.__doc__ = "``%s``\n    %s" % (decl, func.__doc__.strip())
+        return func
+    return decorator
+
+@predicate('_destupdate')
 def _destupdate(repo, subset, x):
     # experimental revset for update destination
     args = getargsdict(x, 'limit', 'clean check')
     return subset & baseset([destutil.destupdate(repo, **args)[0]])
 
+@predicate('_destmerge')
 def _destmerge(repo, subset, x):
     # experimental revset for merge destination
     getargs(x, 0, 0, _("_mergedefaultdest takes no arguments"))
     return subset & baseset([destutil.destmerge(repo)])
 
+@predicate('adds(pattern)')
 def adds(repo, subset, x):
-    """``adds(pattern)``
-    Changesets that add a file matching pattern.
+    """Changesets that add a file matching pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -486,9 +515,9 @@  def adds(repo, subset, x):
     pat = getstring(x, _("adds requires a pattern"))
     return checkstatus(repo, subset, pat, 1)
 
+@predicate('ancestor(*changeset)')
 def ancestor(repo, subset, x):
-    """``ancestor(*changeset)``
-    A greatest common ancestor of the changesets.
+    """A greatest common ancestor of the changesets.
 
     Accepts 0 or more changesets.
     Will return empty list when passed no args.
@@ -518,12 +547,13 @@  def _ancestors(repo, subset, x, followfi
     s = _revancestors(repo, heads, followfirst)
     return subset & s
 
+@predicate('ancestors(set)')
 def ancestors(repo, subset, x):
-    """``ancestors(set)``
-    Changesets that are ancestors of a changeset in set.
+    """Changesets that are ancestors of a changeset in set.
     """
     return _ancestors(repo, subset, x)
 
+@predicate('_firstancestors')
 def _firstancestors(repo, subset, x):
     # ``_firstancestors(set)``
     # Like ``ancestors(set)`` but follows only the first parents.
@@ -546,18 +576,18 @@  def ancestorspec(repo, subset, x, n):
         ps.add(r)
     return subset & ps
 
+@predicate('author(string)')
 def author(repo, subset, x):
-    """``author(string)``
-    Alias for ``user(string)``.
+    """Alias for ``user(string)``.
     """
     # i18n: "author" is a keyword
     n = encoding.lower(getstring(x, _("author requires a string")))
     kind, pattern, matcher = _substringmatcher(n)
     return subset.filter(lambda x: matcher(encoding.lower(repo[x].user())))
 
+@predicate('bisect(string)')
 def bisect(repo, subset, x):
-    """``bisect(string)``
-    Changesets marked in the specified bisect status:
+    """Changesets marked in the specified bisect status:
 
     - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
     - ``goods``, ``bads``      : csets topologically good/bad
@@ -574,12 +604,13 @@  def bisect(repo, subset, x):
 
 # Backward-compatibility
 # - no help entry so that we do not advertise it any more
+@predicate('bisected')
 def bisected(repo, subset, x):
     return bisect(repo, subset, x)
 
+@predicate('bookmark([name])')
 def bookmark(repo, subset, x):
-    """``bookmark([name])``
-    The named bookmark or all bookmarks.
+    """The named bookmark or all bookmarks.
 
     If `name` starts with `re:`, the remainder of the name is treated as
     a regular expression. To match a bookmark that actually starts with `re:`,
@@ -615,8 +646,9 @@  def bookmark(repo, subset, x):
     bms -= set([node.nullrev])
     return subset & bms
 
+@predicate('branch(string or set)')
 def branch(repo, subset, x):
-    """``branch(string or set)``
+    """
     All changesets belonging to the given branch or the branches of the given
     changesets.
 
@@ -651,9 +683,9 @@  def branch(repo, subset, x):
     c = s.__contains__
     return subset.filter(lambda r: c(r) or getbi(r)[0] in b)
 
+@predicate('bumped()')
 def bumped(repo, subset, x):
-    """``bumped()``
-    Mutable changesets marked as successors of public changesets.
+    """Mutable changesets marked as successors of public changesets.
 
     Only non-public and non-obsolete changesets can be `bumped`.
     """
@@ -662,9 +694,9 @@  def bumped(repo, subset, x):
     bumped = obsmod.getrevs(repo, 'bumped')
     return subset & bumped
 
+@predicate('bundle()')
 def bundle(repo, subset, x):
-    """``bundle()``
-    Changesets in the bundle.
+    """Changesets in the bundle.
 
     Bundle must be specified by the -R option."""
 
@@ -722,25 +754,25 @@  def _children(repo, narrow, parentset):
     # This does not break because of other fullreposet misbehavior.
     return baseset(cs)
 
+@predicate('children(set)')
 def children(repo, subset, x):
-    """``children(set)``
-    Child changesets of changesets in set.
+    """Child changesets of changesets in set.
     """
     s = getset(repo, fullreposet(repo), x)
     cs = _children(repo, subset, s)
     return subset & cs
 
+@predicate('closed()')
 def closed(repo, subset, x):
-    """``closed()``
-    Changeset is closed.
+    """Changeset is closed.
     """
     # i18n: "closed" is a keyword
     getargs(x, 0, 0, _("closed takes no arguments"))
     return subset.filter(lambda r: repo[r].closesbranch())
 
+@predicate('contains(pattern)')
 def contains(repo, subset, x):
-    """``contains(pattern)``
-    The revision's manifest contains a file matching pattern (but might not
+    """The revision's manifest contains a file matching pattern (but might not
     modify it). See :hg:`help patterns` for information about file patterns.
 
     The pattern without explicit kind like ``glob:`` is expected to be
@@ -765,9 +797,9 @@  def contains(repo, subset, x):
 
     return subset.filter(matches)
 
+@predicate('converted([id])')
 def converted(repo, subset, x):
-    """``converted([id])``
-    Changesets converted from the given identifier in the old repository if
+    """Changesets converted from the given identifier in the old repository if
     present, or all converted changesets if no identifier is specified.
     """
 
@@ -787,18 +819,18 @@  def converted(repo, subset, x):
 
     return subset.filter(lambda r: _matchvalue(r))
 
+@predicate('date(interval)')
 def date(repo, subset, x):
-    """``date(interval)``
-    Changesets within the interval, see :hg:`help dates`.
+    """Changesets within the interval, see :hg:`help dates`.
     """
     # i18n: "date" is a keyword
     ds = getstring(x, _("date requires a string"))
     dm = util.matchdate(ds)
     return subset.filter(lambda x: dm(repo[x].date()[0]))
 
+@predicate('desc(string)')
 def desc(repo, subset, x):
-    """``desc(string)``
-    Search commit message for string. The match is case-insensitive.
+    """Search commit message for string. The match is case-insensitive.
     """
     # i18n: "desc" is a keyword
     ds = encoding.lower(getstring(x, _("desc requires a string")))
@@ -828,20 +860,21 @@  def _descendants(repo, subset, x, follow
         result = subset & result
     return result
 
+@predicate('descendants(set)')
 def descendants(repo, subset, x):
-    """``descendants(set)``
-    Changesets which are descendants of changesets in set.
+    """Changesets which are descendants of changesets in set.
     """
     return _descendants(repo, subset, x)
 
+@predicate('_firstdescendants')
 def _firstdescendants(repo, subset, x):
     # ``_firstdescendants(set)``
     # Like ``descendants(set)`` but follows only the first parents.
     return _descendants(repo, subset, x, followfirst=True)
 
+@predicate('destination([set])')
 def destination(repo, subset, x):
-    """``destination([set])``
-    Changesets that were created by a graft, transplant or rebase operation,
+    """Changesets that were created by a graft, transplant or rebase operation,
     with the given revisions specified as the source.  Omitting the optional set
     is the same as passing all().
     """
@@ -883,8 +916,9 @@  def destination(repo, subset, x):
 
     return subset.filter(dests.__contains__)
 
+@predicate('divergent()')
 def divergent(repo, subset, x):
-    """``divergent()``
+    """
     Final successors of changesets with an alternative set of final successors.
     """
     # i18n: "divergent" is a keyword
@@ -892,18 +926,18 @@  def divergent(repo, subset, x):
     divergent = obsmod.getrevs(repo, 'divergent')
     return subset & divergent
 
+@predicate('extinct()')
 def extinct(repo, subset, x):
-    """``extinct()``
-    Obsolete changesets with obsolete descendants only.
+    """Obsolete changesets with obsolete descendants only.
     """
     # i18n: "extinct" is a keyword
     getargs(x, 0, 0, _("extinct takes no arguments"))
     extincts = obsmod.getrevs(repo, 'extinct')
     return subset & extincts
 
+@predicate('extra(label, [value])')
 def extra(repo, subset, x):
-    """``extra(label, [value])``
-    Changesets with the given label in the extra metadata, with the given
+    """Changesets with the given label in the extra metadata, with the given
     optional value.
 
     If `value` starts with `re:`, the remainder of the value is treated as
@@ -931,9 +965,9 @@  def extra(repo, subset, x):
 
     return subset.filter(lambda r: _matchvalue(r))
 
+@predicate('filelog(pattern)')
 def filelog(repo, subset, x):
-    """``filelog(pattern)``
-    Changesets connected to the specified filelog.
+    """Changesets connected to the specified filelog.
 
     For performance reasons, visits only revisions mentioned in the file-level
     filelog, rather than filtering through all changesets (much faster, but
@@ -1046,9 +1080,9 @@  def filelog(repo, subset, x):
 
     return subset & s
 
+@predicate('first(set, [n])')
 def first(repo, subset, x):
-    """``first(set, [n])``
-    An alias for limit().
+    """An alias for limit().
     """
     return limit(repo, subset, x)
 
@@ -1072,31 +1106,33 @@  def _follow(repo, subset, x, name, follo
 
     return subset & s
 
+@predicate('follow([pattern])')
 def follow(repo, subset, x):
-    """``follow([pattern])``
+    """
     An alias for ``::.`` (ancestors of the working directory's first parent).
     If pattern is specified, the histories of files matching given
     pattern is followed, including copies.
     """
     return _follow(repo, subset, x, 'follow')
 
+@predicate('_followfirst')
 def _followfirst(repo, subset, x):
     # ``followfirst([pattern])``
     # Like ``follow([pattern])`` but follows only the first parent of
     # every revisions or files revisions.
     return _follow(repo, subset, x, '_followfirst', followfirst=True)
 
+@predicate('all()')
 def getall(repo, subset, x):
-    """``all()``
-    All changesets, the same as ``0:tip``.
+    """All changesets, the same as ``0:tip``.
     """
     # i18n: "all" is a keyword
     getargs(x, 0, 0, _("all takes no arguments"))
     return subset & spanset(repo)  # drop "null" if any
 
+@predicate('grep(regex)')
 def grep(repo, subset, x):
-    """``grep(regex)``
-    Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
+    """Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
     to ensure special escape characters are handled correctly. Unlike
     ``keyword(string)``, the match is case-sensitive.
     """
@@ -1115,6 +1151,7 @@  def grep(repo, subset, x):
 
     return subset.filter(matches)
 
+@predicate('_matchfiles')
 def _matchfiles(repo, subset, x):
     # _matchfiles takes a revset list of prefixed arguments:
     #
@@ -1180,9 +1217,9 @@  def _matchfiles(repo, subset, x):
 
     return subset.filter(matches)
 
+@predicate('file(pattern)')
 def hasfile(repo, subset, x):
-    """``file(pattern)``
-    Changesets affecting files matched by pattern.
+    """Changesets affecting files matched by pattern.
 
     For a faster but less accurate result, consider using ``filelog()``
     instead.
@@ -1193,9 +1230,9 @@  def hasfile(repo, subset, x):
     pat = getstring(x, _("file requires a pattern"))
     return _matchfiles(repo, subset, ('string', 'p:' + pat))
 
+@predicate('head()')
 def head(repo, subset, x):
-    """``head()``
-    Changeset is a named branch head.
+    """Changeset is a named branch head.
     """
     # i18n: "head" is a keyword
     getargs(x, 0, 0, _("head takes no arguments"))
@@ -1209,26 +1246,26 @@  def head(repo, subset, x):
     # necessary to ensure we preserve the order in subset.
     return baseset(hs) & subset
 
+@predicate('heads(set)')
 def heads(repo, subset, x):
-    """``heads(set)``
-    Members of set with no children in set.
+    """Members of set with no children in set.
     """
     s = getset(repo, subset, x)
     ps = parents(repo, subset, x)
     return s - ps
 
+@predicate('hidden()')
 def hidden(repo, subset, x):
-    """``hidden()``
-    Hidden changesets.
+    """Hidden changesets.
     """
     # i18n: "hidden" is a keyword
     getargs(x, 0, 0, _("hidden takes no arguments"))
     hiddenrevs = repoview.filterrevs(repo, 'visible')
     return subset & hiddenrevs
 
+@predicate('keyword(string)')
 def keyword(repo, subset, x):
-    """``keyword(string)``
-    Search commit message, user name, and names of changed files for
+    """Search commit message, user name, and names of changed files for
     string. The match is case-insensitive.
     """
     # i18n: "keyword" is a keyword
@@ -1241,9 +1278,9 @@  def keyword(repo, subset, x):
 
     return subset.filter(matches)
 
+@predicate('limit(set[, n[, offset]])')
 def limit(repo, subset, x):
-    """``limit(set[, n[, offset]])``
-    First n members of set, defaulting to 1, starting from offset.
+    """First n members of set, defaulting to 1, starting from offset.
     """
     args = getargsdict(x, 'limit', 'set n offset')
     if 'set' not in args:
@@ -1277,9 +1314,9 @@  def limit(repo, subset, x):
             result.append(y)
     return baseset(result)
 
+@predicate('last(set, [n])')
 def last(repo, subset, x):
-    """``last(set, [n])``
-    Last n members of set, defaulting to 1.
+    """Last n members of set, defaulting to 1.
     """
     # i18n: "last" is a keyword
     l = getargs(x, 1, 2, _("last requires one or two arguments"))
@@ -1303,9 +1340,9 @@  def last(repo, subset, x):
             result.append(y)
     return baseset(result)
 
+@predicate('max(set)')
 def maxrev(repo, subset, x):
-    """``max(set)``
-    Changeset with highest revision number in set.
+    """Changeset with highest revision number in set.
     """
     os = getset(repo, fullreposet(repo), x)
     try:
@@ -1318,18 +1355,18 @@  def maxrev(repo, subset, x):
         pass
     return baseset()
 
+@predicate('merge()')
 def merge(repo, subset, x):
-    """``merge()``
-    Changeset is a merge changeset.
+    """Changeset is a merge changeset.
     """
     # i18n: "merge" is a keyword
     getargs(x, 0, 0, _("merge takes no arguments"))
     cl = repo.changelog
     return subset.filter(lambda r: cl.parentrevs(r)[1] != -1)
 
+@predicate('branchpoint()')
 def branchpoint(repo, subset, x):
-    """``branchpoint()``
-    Changesets with more than one child.
+    """Changesets with more than one child.
     """
     # i18n: "branchpoint" is a keyword
     getargs(x, 0, 0, _("branchpoint takes no arguments"))
@@ -1346,9 +1383,9 @@  def branchpoint(repo, subset, x):
                 parentscount[p - baserev] += 1
     return subset.filter(lambda r: parentscount[r - baserev] > 1)
 
+@predicate('min(set)')
 def minrev(repo, subset, x):
-    """``min(set)``
-    Changeset with lowest revision number in set.
+    """Changeset with lowest revision number in set.
     """
     os = getset(repo, fullreposet(repo), x)
     try:
@@ -1361,9 +1398,9 @@  def minrev(repo, subset, x):
         pass
     return baseset()
 
+@predicate('modifies(pattern)')
 def modifies(repo, subset, x):
-    """``modifies(pattern)``
-    Changesets modifying files matched by pattern.
+    """Changesets modifying files matched by pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -1373,9 +1410,9 @@  def modifies(repo, subset, x):
     pat = getstring(x, _("modifies requires a pattern"))
     return checkstatus(repo, subset, pat, 0)
 
+@predicate('named(namespace)')
 def named(repo, subset, x):
-    """``named(namespace)``
-    The changesets in a given namespace.
+    """The changesets in a given namespace.
 
     If `namespace` starts with `re:`, the remainder of the string is treated as
     a regular expression. To match a namespace that actually starts with `re:`,
@@ -1411,9 +1448,9 @@  def named(repo, subset, x):
     names -= set([node.nullrev])
     return subset & names
 
+@predicate('id(string)')
 def node_(repo, subset, x):
-    """``id(string)``
-    Revision non-ambiguously specified by the given hex string prefix.
+    """Revision non-ambiguously specified by the given hex string prefix.
     """
     # i18n: "id" is a keyword
     l = getargs(x, 1, 1, _("id requires one argument"))
@@ -1435,17 +1472,17 @@  def node_(repo, subset, x):
     result = baseset([rn])
     return result & subset
 
+@predicate('obsolete()')
 def obsolete(repo, subset, x):
-    """``obsolete()``
-    Mutable changeset with a newer version."""
+    """Mutable changeset with a newer version."""
     # i18n: "obsolete" is a keyword
     getargs(x, 0, 0, _("obsolete takes no arguments"))
     obsoletes = obsmod.getrevs(repo, 'obsolete')
     return subset & obsoletes
 
+@predicate('only(set, [set])')
 def only(repo, subset, x):
-    """``only(set, [set])``
-    Changesets that are ancestors of the first set that are not ancestors
+    """Changesets that are ancestors of the first set that are not ancestors
     of any other head in the repo. If a second set is specified, the result
     is ancestors of the first set that are not ancestors of the second set
     (i.e. ::<set1> - ::<set2>).
@@ -1469,8 +1506,9 @@  def only(repo, subset, x):
     # some optimisations from the fact this is a baseset.
     return subset & results
 
+@predicate('origin([set])')
 def origin(repo, subset, x):
-    """``origin([set])``
+    """
     Changesets that were specified as a source for the grafts, transplants or
     rebases that created the given revisions.  Omitting the optional set is the
     same as passing all().  If a changeset created by these operations is itself
@@ -1500,9 +1538,9 @@  def origin(repo, subset, x):
     # some optimisations from the fact this is a baseset.
     return subset & o
 
+@predicate('outgoing([path])')
 def outgoing(repo, subset, x):
-    """``outgoing([path])``
-    Changesets not found in the specified destination repository, or the
+    """Changesets not found in the specified destination repository, or the
     default push location.
     """
     # Avoid cycles.
@@ -1527,9 +1565,9 @@  def outgoing(repo, subset, x):
     o = set([cl.rev(r) for r in outgoing.missing])
     return subset & o
 
+@predicate('p1([set])')
 def p1(repo, subset, x):
-    """``p1([set])``
-    First parent of changesets in set, or the working directory.
+    """First parent of changesets in set, or the working directory.
     """
     if x is None:
         p = repo[x].p1().rev()
@@ -1546,9 +1584,9 @@  def p1(repo, subset, x):
     # some optimisations from the fact this is a baseset.
     return subset & ps
 
+@predicate('p2([set])')
 def p2(repo, subset, x):
-    """``p2([set])``
-    Second parent of changesets in set, or the working directory.
+    """Second parent of changesets in set, or the working directory.
     """
     if x is None:
         ps = repo[x].parents()
@@ -1569,8 +1607,9 @@  def p2(repo, subset, x):
     # some optimisations from the fact this is a baseset.
     return subset & ps
 
+@predicate('parents([set])')
 def parents(repo, subset, x):
-    """``parents([set])``
+    """
     The set of all parents for all changesets in set, or the working directory.
     """
     if x is None:
@@ -1601,17 +1640,17 @@  def _phase(repo, subset, target):
         condition = lambda r: phase(repo, r) == target
         return subset.filter(condition, cache=False)
 
+@predicate('draft()')
 def draft(repo, subset, x):
-    """``draft()``
-    Changeset in draft phase."""
+    """Changeset in draft phase."""
     # i18n: "draft" is a keyword
     getargs(x, 0, 0, _("draft takes no arguments"))
     target = phases.draft
     return _phase(repo, subset, target)
 
+@predicate('secret()')
 def secret(repo, subset, x):
-    """``secret()``
-    Changeset in secret phase."""
+    """Changeset in secret phase."""
     # i18n: "secret" is a keyword
     getargs(x, 0, 0, _("secret takes no arguments"))
     target = phases.secret
@@ -1642,9 +1681,9 @@  def parentspec(repo, subset, x, n):
                 ps.add(parents[1])
     return subset & ps
 
+@predicate('present(set)')
 def present(repo, subset, x):
-    """``present(set)``
-    An empty set, if any revision in set isn't found; otherwise,
+    """An empty set, if any revision in set isn't found; otherwise,
     all revisions in set.
 
     If any of specified revisions is not present in the local repository,
@@ -1657,6 +1696,7 @@  def present(repo, subset, x):
         return baseset()
 
 # for internal use
+@predicate('_notpublic')
 def _notpublic(repo, subset, x):
     getargs(x, 0, 0, "_notpublic takes no arguments")
     repo._phasecache.loadphaserevs(repo) # ensure phase's sets are loaded
@@ -1673,9 +1713,9 @@  def _notpublic(repo, subset, x):
         condition = lambda r: phase(repo, r) != target
         return subset.filter(condition, cache=False)
 
+@predicate('public()')
 def public(repo, subset, x):
-    """``public()``
-    Changeset in public phase."""
+    """Changeset in public phase."""
     # i18n: "public" is a keyword
     getargs(x, 0, 0, _("public takes no arguments"))
     phase = repo._phasecache.phase
@@ -1683,9 +1723,9 @@  def public(repo, subset, x):
     condition = lambda r: phase(repo, r) == target
     return subset.filter(condition, cache=False)
 
+@predicate('remote([id [,path]])')
 def remote(repo, subset, x):
-    """``remote([id [,path]])``
-    Local revision that corresponds to the given identifier in a
+    """Local revision that corresponds to the given identifier in a
     remote repository, if present. Here, the '.' identifier is a
     synonym for the current local branch.
     """
@@ -1718,9 +1758,9 @@  def remote(repo, subset, x):
             return baseset([r])
     return baseset()
 
+@predicate('removes(pattern)')
 def removes(repo, subset, x):
-    """``removes(pattern)``
-    Changesets which remove files matching pattern.
+    """Changesets which remove files matching pattern.
 
     The pattern without explicit kind like ``glob:`` is expected to be
     relative to the current directory and match against a file or a
@@ -1730,9 +1770,9 @@  def removes(repo, subset, x):
     pat = getstring(x, _("removes requires a pattern"))
     return checkstatus(repo, subset, pat, 2)
 
+@predicate('rev(number)')
 def rev(repo, subset, x):
-    """``rev(number)``
-    Revision with the given numeric identifier.
+    """Revision with the given numeric identifier.
     """
     # i18n: "rev" is a keyword
     l = getargs(x, 1, 1, _("rev requires one argument"))
@@ -1746,9 +1786,9 @@  def rev(repo, subset, x):
         return baseset()
     return subset & baseset([l])
 
+@predicate('matching(revision [, field])')
 def matching(repo, subset, x):
-    """``matching(revision [, field])``
-    Changesets in which a given set of fields match the set of fields in the
+    """Changesets in which a given set of fields match the set of fields in the
     selected revision or set.
 
     To match more than one field pass the list of fields to match separated
@@ -1858,17 +1898,17 @@  def matching(repo, subset, x):
 
     return subset.filter(matches)
 
+@predicate('reverse(set)')
 def reverse(repo, subset, x):
-    """``reverse(set)``
-    Reverse order of set.
+    """Reverse order of set.
     """
     l = getset(repo, subset, x)
     l.reverse()
     return l
 
+@predicate('roots(set)')
 def roots(repo, subset, x):
-    """``roots(set)``
-    Changesets in set with no parent changeset in set.
+    """Changesets in set with no parent changeset in set.
     """
     s = getset(repo, fullreposet(repo), x)
     parents = repo.changelog.parentrevs
@@ -1879,9 +1919,9 @@  def roots(repo, subset, x):
         return True
     return subset & s.filter(filter)
 
+@predicate('sort(set[, [-]key...])')
 def sort(repo, subset, x):
-    """``sort(set[, [-]key...])``
-    Sort set by keys. The default sort order is ascending, specify a key
+    """Sort set by keys. The default sort order is ascending, specify a key
     as ``-key`` to sort in descending order.
 
     The keys can be:
@@ -1942,9 +1982,9 @@  def sort(repo, subset, x):
     l.sort()
     return baseset([e[-1] for e in l])
 
+@predicate('subrepo([pattern])')
 def subrepo(repo, subset, x):
-    """``subrepo([pattern])``
-    Changesets that add, modify or remove the given subrepo.  If no subrepo
+    """Changesets that add, modify or remove the given subrepo.  If no subrepo
     pattern is named, any subrepo changes are returned.
     """
     # i18n: "subrepo" is a keyword
@@ -1991,9 +2031,9 @@  def _substringmatcher(pattern):
         matcher = lambda s: pattern in s
     return kind, pattern, matcher
 
+@predicate('tag([name])')
 def tag(repo, subset, x):
-    """``tag([name])``
-    The specified tag by name, or all tagged revisions if no name is given.
+    """The specified tag by name, or all tagged revisions if no name is given.
 
     If `name` starts with `re:`, the remainder of the name is treated as
     a regular expression. To match a tag that actually starts with `re:`,
@@ -2020,12 +2060,13 @@  def tag(repo, subset, x):
         s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
     return subset & s
 
+@predicate('tagged')
 def tagged(repo, subset, x):
     return tag(repo, subset, x)
 
+@predicate('unstable()')
 def unstable(repo, subset, x):
-    """``unstable()``
-    Non-obsolete changesets with obsolete ancestors.
+    """Non-obsolete changesets with obsolete ancestors.
     """
     # i18n: "unstable" is a keyword
     getargs(x, 0, 0, _("unstable takes no arguments"))
@@ -2033,9 +2074,9 @@  def unstable(repo, subset, x):
     return subset & unstables
 
 
+@predicate('user(string)')
 def user(repo, subset, x):
-    """``user(string)``
-    User name contains string. The match is case-insensitive.
+    """User name contains string. The match is case-insensitive.
 
     If `string` starts with `re:`, the remainder of the string is treated as
     a regular expression. To match a user that actually contains `re:`, use
@@ -2044,6 +2085,7 @@  def user(repo, subset, x):
     return author(repo, subset, x)
 
 # experimental
+@predicate('wdir')
 def wdir(repo, subset, x):
     # i18n: "wdir" is a keyword
     getargs(x, 0, 0, _("wdir takes no arguments"))
@@ -2052,6 +2094,7 @@  def wdir(repo, subset, x):
     return baseset()
 
 # for internal use
+@predicate('_list')
 def _list(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2081,6 +2124,7 @@  def _list(repo, subset, x):
     return baseset(ls)
 
 # for internal use
+@predicate('_intlist')
 def _intlist(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2090,6 +2134,7 @@  def _intlist(repo, subset, x):
     return baseset([r for r in ls if r in s])
 
 # for internal use
+@predicate('_hexlist')
 def _hexlist(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2099,83 +2144,6 @@  def _hexlist(repo, subset, x):
     s = subset
     return baseset([r for r in ls if r in s])
 
-symbols = {
-    "_destupdate": _destupdate,
-    "_destmerge": _destmerge,
-    "adds": adds,
-    "all": getall,
-    "ancestor": ancestor,
-    "ancestors": ancestors,
-    "_firstancestors": _firstancestors,
-    "author": author,
-    "bisect": bisect,
-    "bisected": bisected,
-    "bookmark": bookmark,
-    "branch": branch,
-    "branchpoint": branchpoint,
-    "bumped": bumped,
-    "bundle": bundle,
-    "children": children,
-    "closed": closed,
-    "contains": contains,
-    "converted": converted,
-    "date": date,
-    "desc": desc,
-    "descendants": descendants,
-    "_firstdescendants": _firstdescendants,
-    "destination": destination,
-    "divergent": divergent,
-    "draft": draft,
-    "extinct": extinct,
-    "extra": extra,
-    "file": hasfile,
-    "filelog": filelog,
-    "first": first,
-    "follow": follow,
-    "_followfirst": _followfirst,
-    "grep": grep,
-    "head": head,
-    "heads": heads,
-    "hidden": hidden,
-    "id": node_,
-    "keyword": keyword,
-    "last": last,
-    "limit": limit,
-    "_matchfiles": _matchfiles,
-    "max": maxrev,
-    "merge": merge,
-    "min": minrev,
-    "modifies": modifies,
-    "named": named,
-    "obsolete": obsolete,
-    "only": only,
-    "origin": origin,
-    "outgoing": outgoing,
-    "p1": p1,
-    "p2": p2,
-    "parents": parents,
-    "present": present,
-    "public": public,
-    "_notpublic": _notpublic,
-    "remote": remote,
-    "removes": removes,
-    "rev": rev,
-    "reverse": reverse,
-    "roots": roots,
-    "sort": sort,
-    "secret": secret,
-    "subrepo": subrepo,
-    "matching": matching,
-    "tag": tag,
-    "tagged": tagged,
-    "user": user,
-    "unstable": unstable,
-    "wdir": wdir,
-    "_list": _list,
-    "_intlist": _intlist,
-    "_hexlist": _hexlist,
-}
-
 # symbols which can't be used for a DoS attack for any given input
 # (e.g. those which accept regexes as plain strings shouldn't be included)
 # functions that just return a lot of changesets (like all) don't count here