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# -*- 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("unable to find 'title' in node {0}\n File \"{1}\", line {2}\nkeys: {3}".format( 

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

72 title = self.options['title'] 

73 if "tag" not in self.options: 

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

75 if "index" not in self.options: 

76 self.options["index"] = title 

77 else: 

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

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

80 

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

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

83 

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

85 self.reporter = self.state.document.reporter 

86 try: 

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

88 except AttributeError: # pragma: no cover 

89 source = lineno = None 

90 

91 # object name 

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

93 # example: -m pyquickhelper clean_files --help 

94 out, err = run_script( 

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

96 if err: 

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

98 err = [] 

99 for line in lines: 

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

101 continue 

102 err.append(line) 

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

104 if err: 

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

106 name, out, err, path) 

107 logger = logging.getLogger("CmdRef") 

108 logger.warning("[CmdRef] cmd failed '{0}'".format(name)) 

109 elif out in (None, ''): 

110 out = "--SCRIPT--{}\n--EMPTY OUTPUT--\n--PATH--\n{}".format( 

111 name, path) 

112 logger = logging.getLogger("CmdRef") 

113 logger.warning("[CmdRef] cmd empty '{0}'".format(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 '{0}' should contain ':': <full_function_name>:<cmd_name> as specified in the setup.".format(name)) 

126 if lineno is not None: 

127 logger.warning( 

128 ' File "{0}", line {1}'.format(source, lineno)) 

129 

130 # example: pyquickhelper.cli.pyq_sync_cli:pyq_sync 

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

132 if len(spl) != 2: 

133 logger = logging.getLogger("CmdRef") 

134 logger.warning( 

135 "[CmdRef] cmd(*= '{0}' should contain ':': <full_function_name>:<cmd_name> as specified in the setup.".format(name)) 

136 if lineno is not None: 

137 logger.warning( 

138 ' File "{0}", line {1}'.format(source, lineno)) 

139 

140 # rename the command line 

141 if "=" in spl[0]: 

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

143 name_fct = spl[1] 

144 else: 

145 fullname, name_cmd = spl 

146 name_fct = name_cmd 

147 

148 name_fct = name_fct.strip() 

149 fullname = fullname.strip() 

150 name_cmd = name_cmd.strip() 

151 

152 fullname = "{0}.{1}".format(fullname, name_fct) 

153 try: 

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

155 except ImportError: # pragma: no cover 

156 logger = logging.getLogger("CmdRef") 

157 logger.warning( 

158 "[CmdRef] unable to import '{0}'".format(fullname)) 

159 if lineno is not None: 

160 logger.warning( 

161 ' File "{0}", line {1}'.format(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 = "{0} --help".format(name_cmd) 

173 pout = nodes.paragraph(content, content) 

174 cont += pout 

175 

176 content = stio.getvalue() 

177 if len(content) == 0: 

178 logger = logging.getLogger("CmdRef") 

179 logger.warning( 

180 "[CmdRef] empty output for '{0}'".format(fullname)) 

181 if lineno is not None: 

182 logger.warning( 

183 ' File "{0}", line {1}'.format(source, lineno)) 

184 out = "--SCRIPT--{}\n--EMPTY OUTPUT--\n--PATH--\n{}".format( 

185 name, path) 

186 logger = logging.getLogger("CmdRef") 

187 logger.warning("[CmdRef] cmd empty '{0}'".format(name)) 

188 else: 

189 start = 'usage: ' + name_fct 

190 if content.startswith(start): 

191 content = "usage: {0}{1}".format( 

192 name_cmd, content[len(start):]) 

193 pout = nodes.literal_block(content, content) 

194 cont += pout 

195 

196 return res 

197 

198 

199def process_cmdrefs(app, doctree): 

200 """ 

201 Collect all cmdrefs in the environment 

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

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

204 """ 

205 process_blocrefs_generic( 

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

207 

208 

209class CmdRefList(BlocRefList): 

210 """ 

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

212 

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

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

215 """ 

216 name_sphinx = "cmdreflist" 

217 node_class = cmdreflist 

218 

219 def run(self): 

220 """ 

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

222 """ 

223 if "tag" not in self.options: 

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

225 return BlocRefList.run(self) 

226 

227 

228def process_cmdref_nodes(app, doctree, fromdocname): 

229 """ 

230 process_cmdref_nodes 

231 """ 

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

233 entry_name="cmdmes", class_node=cmdref_node, 

234 class_node_list=cmdreflist) 

235 

236 

237def purge_cmdrefs(app, env, docname): 

238 """ 

239 purge_cmdrefs 

240 """ 

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

242 return 

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

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

245 

246 

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

248 """ 

249 merge_cmdref 

250 """ 

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

252 return 

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

254 env.cmdref_all_cmdrefs = [] 

255 env.cmdref_all_cmdrefs.extend(other.cmdref_all_cmdrefs) 

256 

257 

258def visit_cmdref_node(self, node): 

259 """ 

260 visit_cmdref_node 

261 """ 

262 self.visit_admonition(node) 

263 

264 

265def depart_cmdref_node(self, node): 

266 """ 

267 *depart_cmdref_node*, 

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

269 """ 

270 self.depart_admonition(node) 

271 

272 

273def visit_cmdreflist_node(self, node): 

274 """ 

275 visit_cmdreflist_node 

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

277 """ 

278 self.visit_admonition(node) 

279 

280 

281def depart_cmdreflist_node(self, node): 

282 """ 

283 *depart_cmdref_node* 

284 """ 

285 self.depart_admonition(node) 

286 

287 

288def setup(app): 

289 """ 

290 setup for ``cmdref`` (sphinx) 

291 """ 

292 if hasattr(app, "add_mapping"): 

293 app.add_mapping('cmdref', cmdref_node) 

294 app.add_mapping('cmdreflist', cmdreflist) 

295 

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

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

298 

299 app.add_node(cmdreflist, 

300 html=(visit_cmdreflist_node, depart_cmdreflist_node), 

301 epub=(visit_cmdreflist_node, depart_cmdreflist_node), 

302 elatex=(visit_cmdreflist_node, depart_cmdreflist_node), 

303 latex=(visit_cmdreflist_node, depart_cmdreflist_node), 

304 text=(visit_cmdreflist_node, depart_cmdreflist_node), 

305 md=(visit_cmdreflist_node, depart_cmdreflist_node), 

306 rst=(visit_cmdreflist_node, depart_cmdreflist_node)) 

307 app.add_node(cmdref_node, 

308 html=(visit_cmdref_node, depart_cmdref_node), 

309 epub=(visit_cmdref_node, depart_cmdref_node), 

310 elatex=(visit_cmdref_node, depart_cmdref_node), 

311 latex=(visit_cmdref_node, depart_cmdref_node), 

312 text=(visit_cmdref_node, depart_cmdref_node), 

313 md=(visit_cmdref_node, depart_cmdref_node), 

314 rst=(visit_cmdref_node, depart_cmdref_node)) 

315 

316 app.add_directive('cmdref', CmdRef) 

317 app.add_directive('cmdreflist', CmdRefList) 

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

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

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

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

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