Hide keyboard shortcuts

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 

2 

3import re 

4from pathlib import Path 

5import importlib 

6from types import FunctionType 

7from argparse import ArgumentParser 

8from llama.cli import RecursiveCli 

9from . import __doc__ 

10 

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 

25 

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} 

35 

36{toctxt} 

37""" 

38 

39 

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). 

46 

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. 

58 

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 

78 

79 

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 

90 

91 

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 

102 

103 

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)) 

108 

109 

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)