revset: use decorator to register a function as revset predicate
authorFUJIWARA Katsunori <foozy@lares.dti.ne.jp>
Tue, 29 Dec 2015 23:58:30 +0900
changeset 27584 fc7c8cac6a4b
parent 27583 37d50250b696
child 27585 60bf90eb8bf8
revset: use decorator to register 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. This patch defines 'parsefuncdecl()' in 'funcregistrar' class, because this implementation can be uesd by other decorator class for fileset predicate and template function.
mercurial/registrar.py
mercurial/revset.py
--- a/mercurial/registrar.py	Tue Dec 29 23:58:30 2015 +0900
+++ b/mercurial/registrar.py	Tue Dec 29 23:58:30 2015 +0900
@@ -70,6 +70,15 @@
         """
         return self.decl
 
+    def parsefuncdecl(self):
+        """Parse function declaration and return the name of function in it
+        """
+        i = self.decl.find('(')
+        if i > 0:
+            return self.decl[:i]
+        else:
+            return self.decl
+
     def formatdoc(self, doc):
         """Return formatted document of the registered function for help
 
--- a/mercurial/revset.py	Tue Dec 29 23:58:30 2015 +0900
+++ b/mercurial/revset.py	Tue Dec 29 23:58:30 2015 +0900
@@ -22,6 +22,7 @@
     parser,
     pathutil,
     phases,
+    registrar,
     repoview,
     util,
 )
@@ -465,19 +466,47 @@
 
 # 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 = {}
+
+class predicate(registrar.funcregistrar):
+    """Decorator to register revset predicate
+
+    Usage::
+
+        @predicate('mypredicate(arg1, arg2[, arg3])')
+        def mypredicatefunc(repo, subset, x):
+            '''Explanation of this revset predicate ....
+            '''
+            pass
+
+    The first string argument of the constructor is used also in
+    online help.
+    """
+    table = symbols
+    formatdoc = "``%s``\n    %s"
+    getname = registrar.funcregistrar.parsefuncdecl
+
+@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
@@ -487,9 +516,9 @@
     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.
@@ -519,12 +548,13 @@
     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.
@@ -547,18 +577,18 @@
         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
@@ -575,12 +605,13 @@
 
 # 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:`,
@@ -616,8 +647,9 @@
     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.
 
@@ -652,9 +684,9 @@
     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`.
     """
@@ -663,9 +695,9 @@
     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."""
 
@@ -723,25 +755,25 @@
     # 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
@@ -766,9 +798,9 @@
 
     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.
     """
 
@@ -788,18 +820,18 @@
 
     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")))
@@ -829,20 +861,21 @@
         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().
     """
@@ -884,8 +917,9 @@
 
     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
@@ -893,18 +927,18 @@
     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
@@ -932,9 +966,9 @@
 
     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
@@ -1047,9 +1081,9 @@
 
     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)
 
@@ -1073,31 +1107,33 @@
 
     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.
     """
@@ -1116,6 +1152,7 @@
 
     return subset.filter(matches)
 
+@predicate('_matchfiles')
 def _matchfiles(repo, subset, x):
     # _matchfiles takes a revset list of prefixed arguments:
     #
@@ -1181,9 +1218,9 @@
 
     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.
@@ -1194,9 +1231,9 @@
     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"))
@@ -1210,26 +1247,26 @@
     # 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
@@ -1242,9 +1279,9 @@
 
     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:
@@ -1278,9 +1315,9 @@
             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"))
@@ -1304,9 +1341,9 @@
             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:
@@ -1319,18 +1356,18 @@
         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"))
@@ -1347,9 +1384,9 @@
                 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:
@@ -1362,9 +1399,9 @@
         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
@@ -1374,9 +1411,9 @@
     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:`,
@@ -1412,9 +1449,9 @@
     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"))
@@ -1436,17 +1473,17 @@
     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>).
@@ -1470,8 +1507,9 @@
     # 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
@@ -1501,9 +1539,9 @@
     # 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.
@@ -1528,9 +1566,9 @@
     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()
@@ -1547,9 +1585,9 @@
     # 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()
@@ -1570,8 +1608,9 @@
     # 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:
@@ -1602,17 +1641,17 @@
         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
@@ -1643,9 +1682,9 @@
                 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,
@@ -1658,6 +1697,7 @@
         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
@@ -1674,9 +1714,9 @@
         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
@@ -1684,9 +1724,9 @@
     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.
     """
@@ -1719,9 +1759,9 @@
             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
@@ -1731,9 +1771,9 @@
     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"))
@@ -1747,9 +1787,9 @@
         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
@@ -1859,17 +1899,17 @@
 
     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
@@ -1880,9 +1920,9 @@
         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:
@@ -1943,9 +1983,9 @@
     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
@@ -1992,9 +2032,9 @@
         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:`,
@@ -2021,12 +2061,13 @@
         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"))
@@ -2034,9 +2075,9 @@
     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
@@ -2045,6 +2086,7 @@
     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"))
@@ -2053,6 +2095,7 @@
     return baseset()
 
 # for internal use
+@predicate('_list')
 def _list(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2082,6 +2125,7 @@
     return baseset(ls)
 
 # for internal use
+@predicate('_intlist')
 def _intlist(repo, subset, x):
     s = getstring(x, "internal error")
     if not s:
@@ -2091,6 +2135,7 @@
     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:
@@ -2100,83 +2145,6 @@
     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