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
« 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
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( # 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)
82 res, cont = BlocRef.private_run(self, add_container=True)
83 name = self.options.get("cmd", None)
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
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)
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)
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
150 name_fct = name_fct.strip()
151 fullname = fullname.strip()
152 name_cmd = name_cmd.strip()
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
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 = f"{name_cmd} --help"
173 pout = nodes.paragraph(content, content)
174 cont += pout
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
194 return res
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)
207class CmdRefList(BlocRefList):
208 """
209 A list of all *cmdref* entries, for a specific tag.
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
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)
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)
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]
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)
256def visit_cmdref_node(self, node):
257 """
258 visit_cmdref_node
259 """
260 self.visit_admonition(node)
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)
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)
279def depart_cmdreflist_node(self, node):
280 """
281 *depart_cmdref_node*
282 """
283 self.depart_admonition(node)
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)
294 app.add_config_value('cmdref_include_cmdrefs', True, 'html')
295 app.add_config_value('cmdref_link_only', False, 'html')
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))
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}