Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman, 2019
3import re
4from pathlib import Path
5import importlib
6from types import FunctionType
7from argparse import ArgumentParser
8from llama.cli import RecursiveCli
9from . import __doc__
11TOC_HEAD = """
12*This section covers utilities available at the command line through the*
13``llama`` *command-line interface (CLI). All major commands are accessed
14recursively through* ``llama`` *and correspond to packages in the* ``llama``
15*library with the same names: for example,* ``llama run`` *is the command you
16would use to start up the main LLAMA analysis daemon, and it corresponds to the
17functionality defined in the* ``llama.run`` *module. Read through the
18subcommands (including* ``llama run`` *as well as others exposing different
19functionality) to learn how to use LLAMA to it's fullest from the command
20line.*
21"""
22TOC_FMT = """
23.. toctree::
24 :maxdepth: 6
26{}
27"""
28RST_ENTRY_FORMAT = """``{prog}``{cli_title}
29=={underline}=={cli_underline}
30{header}
31.. argparse::
32 :module: {modname}
33 :func: {funcname}
34 :prog: {prog}
36{toctxt}
37"""
40def walk_clis(cli: RecursiveCli, modname: str, test: FunctionType = None):
41 """Descend into this ``RecursiveCli`` looking for subcommands that are also
42 implemented using a ``RecursiveCli``. Infer this by seeing if
43 ``RecursiveCli`` has been imported and then finding further submodules (on
44 the assumption that a ``RecursiveCli`` import implies that
45 ``RecursiveCli.from_module`` is being used).
47 Parameters
48 ----------
49 cli : RecursiveCli
50 A starting ``RecursiveCli`` instance whose ``subcommands`` will be
51 searched for further instances thereof.
52 modname : str
53 The fully-qualified module name to which ``cli`` belongs (excluding the
54 trailing ``.__main__`` since it's not used by ``RecursiveCli``).
55 test : FunctionType, optional
56 An extra test to perform on submodules to decide whether to include
57 them in the output list. Takes a ``ModuleType`` as its only argument.
59 Returns
60 -------
61 modules : Dict[str, ModuleType]
62 A dict mapping fully-qualified package names to their ``__main__``
63 modules that implement their CLIs.
64 """
65 # import ipdb; ipdb.set_trace()
66 modules = {modname+'.__main__':
67 importlib.import_module(modname+'.__main__')}
68 test = test or (lambda module: True)
69 for subname, module in cli.subcommands.items():
70 if test(module):
71 fullname = f"{modname}.{subname}"
72 if hasattr(module, 'RecursiveCli'):
73 modules.update(walk_clis(RecursiveCli.from_module(fullname),
74 fullname, test))
75 else:
76 modules[fullname+'.__main__'] = module
77 return modules
80def get_parser():
81 """CLI parser."""
82 parser = ArgumentParser(description=__doc__)
83 parser.add_argument("module", help="""
84 Fully-qualified name of the module to parse.""")
85 parser.add_argument("outdir", help="""
86 Where to save the resulting .rst files, which will be named
87 ``cli.module.rst`` (where "module" is replaced the the module name in
88 which each CLI script is defined).""")
89 return parser
92def module_get_parser_name(module):
93 """Look in obvious locations for functions that return this module's
94 argument parser and return the first match (or ``None`` if none are
95 found)."""
96 if hasattr(module, 'get_parser'):
97 return 'get_parser'
98 if hasattr(module, 'cli'):
99 if hasattr(module.cli, 'get_parser'):
100 return 'cli.get_parser'
101 return None
104def issubmod(pkg: str, othermod: str):
105 """``True`` if the module named ``othermod`` seems to be a submodule of
106 the package named ``pkg``. Simple overly-conservative regex test."""
107 return bool(re.findall(f'^{pkg}\\.[a-zA-Z][a-zA-Z0-9]*$', othermod))
110def main():
111 """Run CLI."""
112 parser = get_parser()
113 args = parser.parse_args()
114 seed = RecursiveCli.from_module(args.module)
115 dest = Path(args.outdir)
116 modules = walk_clis(seed, args.module,
117 test=lambda m: module_get_parser_name(m) is not None)
118 subtoc = dest / f'cli.{args.module}.rst'
119 for modname, module in modules.items():
120 assert len(re.findall('.__main__', modname)) < 2
121 prog = modname.replace('.__main__', '').replace('.', ' ')
122 submods = [f" cli.{m}\n" for m in modules
123 if issubmod(modname.replace('.__main__', ''),
124 m.replace('.__main__', ''))]
125 toctxt = TOC_FMT.format(''.join(submods)) if submods else ''
126 if prog == args.module.replace('.', ' '):
127 cli_title = " Command Line Interface"
128 header = TOC_HEAD
129 with subtoc.open('w') as tocfile:
130 tocfile.write(toctxt)
131 toctxt = ""
132 else:
133 cli_title = ""
134 header = ""
135 txt = RST_ENTRY_FORMAT.format(
136 modname=modname,
137 prog=prog,
138 underline='='*len(prog),
139 funcname=module_get_parser_name(module),
140 cli_title=cli_title,
141 cli_underline='='*len(cli_title),
142 header=header,
143 toctxt=toctxt,
144 )
145 fname = f"cli.{modname}.rst"
146 with (dest / fname).open('w') as outfile:
147 outfile.write(txt)