Coverage for pyquickhelper/sphinxext/sphinx_cmdref_extension.py: 80%

136 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Defines a :epkg:`sphinx` extension to keep track of *cmd*. 

5""" 

6from io import StringIO 

7from docutils import nodes 

8import sphinx 

9from sphinx.util import logging 

10from docutils.parsers.rst import directives 

11from ..loghelper import run_script, noLOG 

12from .sphinx_blocref_extension import BlocRef, process_blocrefs_generic, BlocRefList, process_blocref_nodes_generic 

13from .import_object_helper import import_object 

14 

15 

16class cmdref_node(nodes.admonition): 

17 """ 

18 defines ``cmdref`` node 

19 """ 

20 pass 

21 

22 

23class cmdreflist(nodes.General, nodes.Element): 

24 """ 

25 defines ``cmdreflist`` node 

26 """ 

27 pass 

28 

29 

30class CmdRef(BlocRef): 

31 """ 

32 A ``cmdref`` entry, displayed in the form of an admonition. 

33 It is used to reference a script a module is added as a command line. 

34 It takes the following options: 

35 

36 * *title*: a title for the bloc 

37 * *tag*: a tag to have several categories of blocs, if not specified, it will be equal to *cmd* 

38 * *lid* or *label*: a label to refer to 

39 * *index*: to add an additional entry to the index (comma separated) 

40 * *name*: command line name, if populated, the directive displays the output of 

41 ``name --help``. 

42 * *path*: used if the command line startswith ``-m`` 

43 

44 It works the same way as @see cl BlocRef. The command line can be 

45 something like ``-m <module> <command> ...``. The extension 

46 will call :epkg:`python` in a separate process. 

47 

48 .. todoext:: 

49 :title: cmdref does not display anything if the content is empty. 

50 :tag: bug 

51 :issue: 51 

52 """ 

53 

54 node_class = cmdref_node 

55 name_sphinx = "cmdref" 

56 

57 option_spec = dict(cmd=directives.unchanged, 

58 path=directives.unchanged, 

59 **BlocRef.option_spec) 

60 

61 def run(self): 

62 """ 

63 calls run from @see cl BlocRef and add index entries by default 

64 """ 

65 if 'title' not in self.options: 

66 lineno = self.lineno 

67 env = self.state.document.settings.env if hasattr( 

68 self.state.document.settings, "env") else None 

69 docname = None if env is None else env.docname 

70 raise KeyError( # pragma: no cover 

71 "unable to find 'title' in node {0}\n File \"{1}\", line {2}\nkeys: {3}".format( 

72 str(self.__class__), docname, lineno, list(self.options.keys()))) 

73 title = self.options['title'] 

74 if "tag" not in self.options: 

75 self.options["tag"] = "cmd" 

76 if "index" not in self.options: 

77 self.options["index"] = title 

78 else: 

79 self.options["index"] += "," + title 

80 path = self.options.get('path', None) 

81 

82 res, cont = BlocRef.private_run(self, add_container=True) 

83 name = self.options.get("cmd", None) 

84 

85 if name is not None and len(name) > 0: 

86 self.reporter = self.state.document.reporter 

87 try: 

88 source, lineno = self.reporter.get_source_and_line(self.lineno) 

89 except AttributeError: # pragma: no cover 

90 source = lineno = None 

91 

92 # object name 

93 if name.startswith("-m"): 

94 # example: -m pyquickhelper clean_files --help 

95 out, err = run_script( 

96 name, fLOG=noLOG, wait=True, change_path=path) 

97 if err: 

98 lines = err.split('\n') 

99 err = [] 

100 for line in lines: 

101 if 'is already registered, it will be overridden' in line: 

102 continue 

103 err.append(line) 

104 err = "\n".join(err).strip('\n\r\t ') 

105 if err: 

106 out = "--SCRIPT--{}\n--OUT--\n{}\n--ERR--\n{}\n--PATH--\n{}".format( 

107 name, out, err, path) 

108 logger = logging.getLogger("CmdRef") 

109 logger.warning("[CmdRef] cmd failed %r", name) 

110 elif out in (None, ''): 

111 out = f"--SCRIPT--{name}\n--EMPTY OUTPUT--\n--PATH--\n{path}" 

112 logger = logging.getLogger("CmdRef") 

113 logger.warning("[CmdRef] cmd empty %r", name) 

114 content = "python " + name 

115 cont += nodes.paragraph('<<<', '<<<') 

116 pout = nodes.literal_block(content, content) 

117 cont += pout 

118 cont += nodes.paragraph('>>>', '>>>') 

119 pout = nodes.literal_block(out, out) 

120 cont += pout 

121 else: 

122 if ":" not in name: 

123 logger = logging.getLogger("CmdRef") 

124 logger.warning( 

125 "[CmdRef] cmd %r should contain ':': " 

126 "<full_function_name>:<cmd_name> as specified in the setup.", 

127 name) 

128 if lineno is not None: 

129 logger.warning(' File "%s", line %r', source, lineno) 

130 

131 # example: pyquickhelper.cli.pyq_sync_cli:pyq_sync 

132 spl = name.strip("\r\n\t ").split(":") 

133 if len(spl) != 2: # pragma: no cover 

134 logger = logging.getLogger("CmdRef") 

135 logger.warning( 

136 "[CmdRef] cmd(*= %r should contain ':': " 

137 "<full_function_name>:<cmd_name> as specified in the setup.", 

138 name) 

139 if lineno is not None: 

140 logger.warning(' File "%s", line %r', source, lineno) 

141 

142 # rename the command line 

143 if "=" in spl[0]: 

144 name_cmd, fullname = spl[0].split('=') 

145 name_fct = spl[1] 

146 else: 

147 fullname, name_cmd = spl 

148 name_fct = name_cmd 

149 

150 name_fct = name_fct.strip() 

151 fullname = fullname.strip() 

152 name_cmd = name_cmd.strip() 

153 

154 fullname = f"{fullname}.{name_fct}" 

155 try: 

156 obj, name = import_object(fullname, kind="function") 

157 except ImportError: # pragma: no cover 

158 logger = logging.getLogger("CmdRef") 

159 logger.warning("[CmdRef] unable to import %r", fullname) 

160 if lineno is not None: 

161 logger.warning(' File "%s", line %r', source, lineno) 

162 obj = None 

163 

164 if obj is not None: 

165 stio = StringIO() 

166 

167 def local_print(*li): 

168 "local function" 

169 stio.write(" ".join(str(_) for _ in li) + "\n") 

170 obj(args=['--help'], fLOG=local_print) 

171 

172 content = f"{name_cmd} --help" 

173 pout = nodes.paragraph(content, content) 

174 cont += pout 

175 

176 content = stio.getvalue() 

177 if len(content) == 0: # pragma: no cover 

178 logger = logging.getLogger("CmdRef") 

179 logger.warning( 

180 "[CmdRef] empty output for %r", fullname) 

181 if lineno is not None: 

182 logger.warning( 

183 ' File "%s", line %r', source, lineno) 

184 out = f"--SCRIPT--{name}\n--EMPTY OUTPUT--\n--PATH--\n{path}" 

185 logger = logging.getLogger("CmdRef") 

186 logger.warning("[CmdRef] cmd empty %r", name) 

187 else: 

188 start = 'usage: ' + name_fct 

189 if content.startswith(start): 

190 content = f"usage: {name_cmd}{content[len(start):]}" 

191 pout = nodes.literal_block(content, content) 

192 cont += pout 

193 

194 return res 

195 

196 

197def process_cmdrefs(app, doctree): 

198 """ 

199 Collect all cmdrefs in the environment 

200 this is not done in the directive itself because it some transformations 

201 must have already been run, e.g. substitutions. 

202 """ 

203 process_blocrefs_generic( 

204 app, doctree, bloc_name="cmdref", class_node=cmdref_node) 

205 

206 

207class CmdRefList(BlocRefList): 

208 """ 

209 A list of all *cmdref* entries, for a specific tag. 

210 

211 * tag: a tag to have several categories of *cmdref* 

212 * contents: add a bullet list with links to added blocs 

213 """ 

214 name_sphinx = "cmdreflist" 

215 node_class = cmdreflist 

216 

217 def run(self): 

218 """ 

219 calls run from @see cl BlocRefList and add default tag if not present 

220 """ 

221 if "tag" not in self.options: 

222 self.options["tag"] = "cmd" 

223 return BlocRefList.run(self) 

224 

225 

226def process_cmdref_nodes(app, doctree, fromdocname): 

227 """ 

228 process_cmdref_nodes 

229 """ 

230 process_blocref_nodes_generic(app, doctree, fromdocname, class_name='cmdref', 

231 entry_name="cmdmes", class_node=cmdref_node, 

232 class_node_list=cmdreflist) 

233 

234 

235def purge_cmdrefs(app, env, docname): 

236 """ 

237 purge_cmdrefs 

238 """ 

239 if not hasattr(env, 'cmdref_all_cmdrefs'): 

240 return 

241 env.cmdref_all_cmdrefs = [cmdref for cmdref in env.cmdref_all_cmdrefs 

242 if cmdref['docname'] != docname] 

243 

244 

245def merge_cmdref(app, env, docnames, other): 

246 """ 

247 merge_cmdref 

248 """ 

249 if not hasattr(other, 'cmdref_all_cmdrefs'): 

250 return 

251 if not hasattr(env, 'cmdref_all_cmdrefs'): 

252 env.cmdref_all_cmdrefs = [] 

253 env.cmdref_all_cmdrefs.extend(other.cmdref_all_cmdrefs) 

254 

255 

256def visit_cmdref_node(self, node): 

257 """ 

258 visit_cmdref_node 

259 """ 

260 self.visit_admonition(node) 

261 

262 

263def depart_cmdref_node(self, node): 

264 """ 

265 *depart_cmdref_node*, 

266 see `sphinx/writers/html.py <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py>`_. 

267 """ 

268 self.depart_admonition(node) 

269 

270 

271def visit_cmdreflist_node(self, node): 

272 """ 

273 visit_cmdreflist_node 

274 see `sphinx/writers/html.py <https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py>`_. 

275 """ 

276 self.visit_admonition(node) 

277 

278 

279def depart_cmdreflist_node(self, node): 

280 """ 

281 *depart_cmdref_node* 

282 """ 

283 self.depart_admonition(node) 

284 

285 

286def setup(app): 

287 """ 

288 setup for ``cmdref`` (sphinx) 

289 """ 

290 if hasattr(app, "add_mapping"): 

291 app.add_mapping('cmdref', cmdref_node) 

292 app.add_mapping('cmdreflist', cmdreflist) 

293 

294 app.add_config_value('cmdref_include_cmdrefs', True, 'html') 

295 app.add_config_value('cmdref_link_only', False, 'html') 

296 

297 app.add_node(cmdreflist, 

298 html=(visit_cmdreflist_node, depart_cmdreflist_node), 

299 epub=(visit_cmdreflist_node, depart_cmdreflist_node), 

300 elatex=(visit_cmdreflist_node, depart_cmdreflist_node), 

301 latex=(visit_cmdreflist_node, depart_cmdreflist_node), 

302 text=(visit_cmdreflist_node, depart_cmdreflist_node), 

303 md=(visit_cmdreflist_node, depart_cmdreflist_node), 

304 rst=(visit_cmdreflist_node, depart_cmdreflist_node)) 

305 app.add_node(cmdref_node, 

306 html=(visit_cmdref_node, depart_cmdref_node), 

307 epub=(visit_cmdref_node, depart_cmdref_node), 

308 elatex=(visit_cmdref_node, depart_cmdref_node), 

309 latex=(visit_cmdref_node, depart_cmdref_node), 

310 text=(visit_cmdref_node, depart_cmdref_node), 

311 md=(visit_cmdref_node, depart_cmdref_node), 

312 rst=(visit_cmdref_node, depart_cmdref_node)) 

313 

314 app.add_directive('cmdref', CmdRef) 

315 app.add_directive('cmdreflist', CmdRefList) 

316 app.connect('doctree-read', process_cmdrefs) 

317 app.connect('doctree-resolved', process_cmdref_nodes) 

318 app.connect('env-purge-doc', purge_cmdrefs) 

319 app.connect('env-merge-info', merge_cmdref) 

320 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}