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
16class cmdref_node(nodes.admonition):
17 """
18 defines ``cmdref`` node
19 """
20 pass
23class cmdreflist(nodes.General, nodes.Element):
24 """
25 defines ``cmdreflist`` node
26 """
27 pass
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:
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``
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.
48 .. todoext::
49 :title: cmdref does not display anything if the content is empty.
50 :tag: bug
51 :issue: 51
52 """
54 node_class = cmdref_node
55 name_sphinx = "cmdref"
57 option_spec = dict(cmd=directives.unchanged,
58 path=directives.unchanged,
59 **BlocRef.option_spec)
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)
81 res, cont = BlocRef.private_run(self, add_container=True)
82 name = self.options.get("cmd", None)
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
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))
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))
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
148 name_fct = name_fct.strip()
149 fullname = fullname.strip()
150 name_cmd = name_cmd.strip()
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
164 if obj is not None:
165 stio = StringIO()
167 def local_print(*li):
168 "local function"
169 stio.write(" ".join(str(_) for _ in li) + "\n")
170 obj(args=['--help'], fLOG=local_print)
172 content = "{0} --help".format(name_cmd)
173 pout = nodes.paragraph(content, content)
174 cont += pout
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
196 return res
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)
209class CmdRefList(BlocRefList):
210 """
211 A list of all *cmdref* entries, for a specific tag.
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
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)
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)
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]
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)
258def visit_cmdref_node(self, node):
259 """
260 visit_cmdref_node
261 """
262 self.visit_admonition(node)
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)
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)
281def depart_cmdreflist_node(self, node):
282 """
283 *depart_cmdref_node*
284 """
285 self.depart_admonition(node)
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)
296 app.add_config_value('cmdref_include_cmdrefs', True, 'html')
297 app.add_config_value('cmdref_link_only', False, 'html')
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))
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}