Mercurial > public > mercurial-scm > hg
view doc/gendoc.py @ 52016:1f5974f8f730
doc: refactor gendoc for better reusability
This change separates the gathering of commands/topics/etc from the logic of
printing their documentation out.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Mon, 09 Oct 2023 22:11:21 -0700 |
parents | 76387080f238 |
children | 2a875530a023 |
line wrap: on
line source
#!/usr/bin/env python3 """usage: %s DOC ... where DOC is the name of a document """ import os import sys import textwrap import argparse try: import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) except ImportError: pass # This script is executed during installs and may not have C extensions # available. Relax C module requirements. os.environ['HGMODULEPOLICY'] = 'allow' # import from the live mercurial repo sys.path.insert(0, os.path.abspath("..")) from mercurial import demandimport demandimport.enable() from mercurial import ( commands, encoding, extensions, fancyopts, help, minirst, pycompat, ui as uimod, ) from mercurial.i18n import ( gettext, _, ) from mercurial.utils import stringutil table = commands.table globalopts = commands.globalopts helptable = help.helptable loaddoc = help.loaddoc def get_desc(docstr): if not docstr: return b"", b"" # sanitize docstr = docstr.strip(b"\n") docstr = docstr.rstrip() shortdesc = docstr.splitlines()[0].strip() i = docstr.find(b"\n") if i != -1: desc = docstr[i + 2 :] else: desc = shortdesc desc = textwrap.dedent(desc.decode('latin1')).encode('latin1') return (shortdesc, desc) def get_opts(opts): for opt in opts: if len(opt) == 5: shortopt, longopt, default, desc, optlabel = opt else: shortopt, longopt, default, desc = opt optlabel = _(b"VALUE") allopts = [] if shortopt: allopts.append(b"-%s" % shortopt) if longopt: allopts.append(b"--%s" % longopt) if isinstance(default, list): allopts[-1] += b" <%s[+]>" % optlabel elif (default is not None) and not isinstance(default, bool): allopts[-1] += b" <%s>" % optlabel if b'\n' in desc: # only remove line breaks and indentation desc = b' '.join(l.lstrip() for l in desc.split(b'\n')) if isinstance(default, fancyopts.customopt): default = default.getdefaultvalue() if default: default = stringutil.forcebytestr(default) desc += _(b" (default: %s)") % default yield (b", ".join(allopts), desc) def get_cmd(cmd, cmdtable): d = {} attr = cmdtable[cmd] cmds = cmd.lstrip(b"^").split(b"|") d[b'cmd'] = cmds[0] d[b'aliases'] = cmd.split(b"|")[1:] d[b'desc'] = get_desc(gettext(pycompat.getdoc(attr[0]))) d[b'opts'] = list(get_opts(attr[1])) s = b'hg ' + cmds[0] if len(attr) > 2: if not attr[2].startswith(b'hg'): s += b' ' + attr[2] else: s = attr[2] d[b'synopsis'] = s.strip() return d def showdoc(ui, debugcmds=False): # print options ui.write(minirst.section(_(b"Options"))) multioccur = False for optstr, desc in get_opts(globalopts): ui.write(b"%s\n %s\n\n" % (optstr, desc)) if optstr.endswith(b"[+]>"): multioccur = True if multioccur: ui.write(_(b"\n[+] marked option can be specified multiple times\n")) ui.write(b"\n") # print cmds ui.write(minirst.section(_(b"Commands"))) commandprinter( ui, table, minirst.subsection, minirst.subsubsection, debugcmds=debugcmds, ) # print help topics # The config help topic is included in the hgrc.5 man page. topics = findtopics(helptable, exclude=[b'config']) helpprinter(ui, topics, minirst.section) ui.write(minirst.section(_(b"Extensions"))) ui.write( _( b"This section contains help for extensions that are " b"distributed together with Mercurial. Help for other " b"extensions is available in the help system." ) ) ui.write( ( b"\n\n" b".. contents::\n" b" :class: htmlonly\n" b" :local:\n" b" :depth: 1\n\n" ) ) for extensionname in sorted(allextensionnames()): mod = extensions.load(ui, extensionname, None) ui.write(minirst.subsection(extensionname)) ext_doc = help.ext_help(ui, mod) ui.write(b"%s\n\n" % ext_doc) cmdtable = getattr(mod, 'cmdtable', None) if cmdtable: ui.write(minirst.subsubsection(_(b'Commands'))) commandprinter( ui, cmdtable, minirst.subsubsubsection, minirst.subsubsubsubsection, debugcmds=debugcmds, ) def gettopicstable(): extrahelptable = [ ([b"common"], b'', loaddoc(b'common'), help.TOPIC_CATEGORY_MISC), ([b"hg.1"], b'', loaddoc(b'hg.1'), help.TOPIC_CATEGORY_CONFIG), ([b"hg-ssh.8"], b'', loaddoc(b'hg-ssh.8'), help.TOPIC_CATEGORY_CONFIG), ( [b"hgignore.5"], b'', loaddoc(b'hgignore.5'), help.TOPIC_CATEGORY_CONFIG, ), ([b"hgrc.5"], b'', loaddoc(b'hgrc.5'), help.TOPIC_CATEGORY_CONFIG), ([b"hg-ssh.8.gendoc"], b'', b'', help.TOPIC_CATEGORY_CONFIG), ( [b"hgignore.5.gendoc"], b'', loaddoc(b'hgignore'), help.TOPIC_CATEGORY_CONFIG, ), ( [b"hgrc.5.gendoc"], b'', loaddoc(b'config'), help.TOPIC_CATEGORY_CONFIG, ), ] return helptable + extrahelptable def findtopics(helptable, include=[], exclude=[]): """Find topics whose names match the given include/exclude rules Note that exclude rules take precedence over include rules. """ found = [] for h in helptable: names, sec, doc = h[0:3] if exclude and names[0] in exclude: continue if include and names[0] not in include: continue found.append((names, sec, doc)) return found def showtopic(ui, topic, wraptpl=False): """Render a help topic Args: ui: the UI object to output to topic: the topic name to output wraptpl: whether to wrap the output in the individual help topic pages' header/footer """ found = findtopics(gettopicstable(), include=[topic]) if not found: ui.write_err(_(b"ERROR: no such topic: %s\n") % topic) sys.exit(1) if wraptpl: header = _rendertpl( 'topicheader.txt', {'topicname': topic, 'topictitle': minirst.section(found[0][1])}, ) ui.write(header.encode()) helpprinter(ui, found, None) return True def helpprinter(ui, topics, sectionfunc): """Print a help topic Args: ui: the UI object to output to topics: a list of help topics to output sectionfunc: a callback to write the section title """ for h in topics: names, sec, doc = h[0:3] for name in names: ui.write(b".. _%s:\n" % name) ui.write(b"\n") if sectionfunc: ui.write(sectionfunc(sec)) if callable(doc): doc = doc(ui) ui.write(doc) ui.write(b"\n") def commandprinter(ui, cmdtable, sectionfunc, subsectionfunc, debugcmds=False): """Render restructuredtext describing a list of commands and their documentations, grouped by command category. Args: ui: UI object to write the output to cmdtable: a dict that maps a string of the command name plus its aliases (separated with pipes) to a 3-tuple of (the command's function, a list of its option descriptions, and a string summarizing available options). Example, with aliases added for demonstration purposes: 'phase|alias1|alias2': ( <function phase at 0x7f0816b05e60>, [ ('p', 'public', False, 'set changeset phase to public'), ..., ('r', 'rev', [], 'target revision', 'REV')], '[-p|-d|-s] [-f] [-r] [REV...]' ) sectionfunc: minirst function to format command category headers subsectionfunc: minirst function to format command headers """ h = allcommandnames(cmdtable, debugcmds=debugcmds) cmds = h.keys() def helpcategory(cmd): """Given a canonical command name from `cmds` (above), retrieve its help category. If helpcategory is None, default to CATEGORY_NONE. """ fullname = h[cmd] details = cmdtable[fullname] helpcategory = details[0].helpcategory return helpcategory or help.registrar.command.CATEGORY_NONE cmdsbycategory = {category: [] for category in help.CATEGORY_ORDER} for cmd in cmds: # If a command category wasn't registered, the command won't get # rendered below, so we raise an AssertionError. if helpcategory(cmd) not in cmdsbycategory: raise AssertionError( "The following command did not register its (category) in " "help.CATEGORY_ORDER: %s (%s)" % (cmd, helpcategory(cmd)) ) cmdsbycategory[helpcategory(cmd)].append(cmd) # Print the help for each command. We present the commands grouped by # category, and we use help.CATEGORY_ORDER as a guide for a helpful order # in which to present the categories. for category in help.CATEGORY_ORDER: categorycmds = cmdsbycategory[category] if not categorycmds: # Skip empty categories continue # Print a section header for the category. # For now, the category header is at the same level as the headers for # the commands in the category; this is fixed in the next commit. ui.write(sectionfunc(help.CATEGORY_NAMES[category])) # Print each command in the category for f in sorted(categorycmds): d = get_cmd(h[f], cmdtable) ui.write(subsectionfunc(d[b'cmd'])) # short description ui.write(d[b'desc'][0]) # synopsis ui.write(b"::\n\n") synopsislines = d[b'synopsis'].splitlines() for line in synopsislines: # some commands (such as rebase) have a multi-line # synopsis ui.write(b" %s\n" % line) ui.write(b'\n') # description ui.write(b"%s\n\n" % d[b'desc'][1]) # options def _optsection(s): return b"%s:\n\n" % s _optionsprinter(ui, d, _optsection) # aliases if d[b'aliases']: # Note the empty comment, this is required to separate this # (which should be a blockquote) from any preceding things (such # as a definition list). ui.write( _(b"..\n\n aliases: %s\n\n") % b" ".join(d[b'aliases']) ) def _optionsprinter(ui, cmd, sectionfunc): """Outputs the list of options for a given command object""" opt_output = list(cmd[b'opts']) if opt_output: opts_len = max([len(line[0]) for line in opt_output]) ui.write(sectionfunc(_(b"Options"))) multioccur = False for optstr, desc in opt_output: if desc: s = b"%-*s %s" % (opts_len, optstr, desc) else: s = optstr ui.write(b"%s\n" % s) if optstr.endswith(b"[+]>"): multioccur = True if multioccur: ui.write( _(b"\n[+] marked option can be specified multiple times\n") ) ui.write(b"\n") def allcommandnames(cmdtable, debugcmds=False): """Get a collection of all command names in the given command table Args: cmdtable: the command table to get the names from debugcmds: whether to include debug commands Returns a dictionary where the keys are the main command names, and the values are the "raw" names (in the form of `name|alias1|alias2`). """ allcmdnames = {} for rawnames, attr in cmdtable.items(): mainname = rawnames.split(b"|")[0].lstrip(b"^") if not debugcmds and mainname.startswith(b"debug"): continue allcmdnames[mainname] = rawnames return allcmdnames def allextensionnames(): """Get a set of all known extension names""" return set(extensions.enabled().keys()) | set(extensions.disabled().keys()) if __name__ == "__main__": parser = argparse.ArgumentParser( prog='gendoc', description="Generate mercurial documentation files" ) parser.add_argument('doc', default='hg.1.gendoc', nargs='?') parser.add_argument( '-d', '--debug-cmds', action='store_true', help="Show debug commands in help pages", ) args = parser.parse_args() doc = encoding.strtolocal(args.doc) debugcmds = args.debug_cmds ui = uimod.ui.load() # Trigger extensions to load. This is disabled by default because it uses # the current user's configuration, which is often not what is wanted. if encoding.environ.get(b'GENDOC_LOAD_CONFIGURED_EXTENSIONS', b'0') != b'0': extensions.loadall(ui) # ui.debugflag determines if the help module returns debug commands to us. ui.debugflag = debugcmds if doc == b'hg.1.gendoc': showdoc(ui) else: showtopic(ui, doc)