|
1
|
|
|
import os |
|
2
|
|
|
import sys |
|
3
|
|
|
from argparse import ArgumentParser |
|
4
|
|
|
|
|
5
|
|
|
from docutils import nodes |
|
6
|
|
|
from docutils.frontend import OptionParser |
|
7
|
|
|
from docutils.parsers.rst import Directive, Parser |
|
8
|
|
|
from docutils.parsers.rst.directives import flag, unchanged |
|
9
|
|
|
from docutils.statemachine import StringList |
|
10
|
|
|
from docutils.utils import new_document |
|
11
|
|
|
from sphinx.util.nodes import nested_parse_with_titles |
|
12
|
|
|
|
|
13
|
|
|
from sphinxarg.parser import parse_parser, parser_navigate |
|
14
|
|
|
|
|
15
|
|
|
from . import __version__ |
|
16
|
|
|
|
|
17
|
|
|
|
|
18
|
|
|
def map_nested_definitions(nested_content): |
|
19
|
|
|
if nested_content is None: |
|
20
|
|
|
raise Exception('Nested content should be iterable, not null') |
|
21
|
|
|
# build definition dictionary |
|
22
|
|
|
definitions = {} |
|
23
|
|
|
for item in nested_content: |
|
24
|
|
|
if not isinstance(item, nodes.definition_list): |
|
25
|
|
|
continue |
|
26
|
|
|
for subitem in item: |
|
27
|
|
|
if not isinstance(subitem, nodes.definition_list_item): |
|
28
|
|
|
continue |
|
29
|
|
|
if not len(subitem.children) > 0: |
|
30
|
|
|
continue |
|
31
|
|
|
classifier = '@after' |
|
32
|
|
|
idx = subitem.first_child_matching_class(nodes.classifier) |
|
33
|
|
|
if idx is not None: |
|
34
|
|
|
ci = subitem[idx] |
|
35
|
|
|
if len(ci.children) > 0: |
|
36
|
|
|
classifier = ci.children[0].astext() |
|
37
|
|
|
if classifier is not None and classifier not in ( |
|
38
|
|
|
'@replace', |
|
39
|
|
|
'@before', |
|
40
|
|
|
'@after', |
|
41
|
|
|
'@skip', |
|
42
|
|
|
): |
|
43
|
|
|
raise Exception(f'Unknown classifier: {classifier}') |
|
44
|
|
|
idx = subitem.first_child_matching_class(nodes.term) |
|
45
|
|
|
if idx is not None: |
|
46
|
|
|
term = subitem[idx] |
|
47
|
|
|
if len(term.children) > 0: |
|
48
|
|
|
term = term.children[0].astext() |
|
49
|
|
|
idx = subitem.first_child_matching_class(nodes.definition) |
|
50
|
|
|
if idx is not None: |
|
51
|
|
|
subcontent = [] |
|
52
|
|
|
for _ in subitem[idx]: |
|
53
|
|
|
if isinstance(_, nodes.definition_list): |
|
54
|
|
|
subcontent.append(_) |
|
55
|
|
|
definitions[term] = (classifier, subitem[idx], subcontent) |
|
56
|
|
|
|
|
57
|
|
|
return definitions |
|
58
|
|
|
|
|
59
|
|
|
|
|
60
|
|
|
def render_list(l, markdown_help, settings=None): |
|
61
|
|
|
""" |
|
62
|
|
|
Given a list of reStructuredText or MarkDown sections, return a docutils node list |
|
63
|
|
|
""" |
|
64
|
|
|
if len(l) == 0: |
|
65
|
|
|
return [] |
|
66
|
|
|
if markdown_help: |
|
67
|
|
|
from sphinxarg.markdown import parse_markdown_block |
|
68
|
|
|
|
|
69
|
|
|
return parse_markdown_block('\n\n'.join(l) + '\n') |
|
70
|
|
|
else: |
|
71
|
|
|
all_children = [] |
|
72
|
|
|
for element in l: |
|
73
|
|
|
if isinstance(element, str): |
|
74
|
|
|
if settings is None: |
|
75
|
|
|
settings = OptionParser(components=(Parser,)).get_default_values() |
|
76
|
|
|
document = new_document(None, settings) |
|
77
|
|
|
Parser().parse(element + '\n', document) |
|
78
|
|
|
all_children += document.children |
|
79
|
|
|
elif isinstance(element, nodes.definition): |
|
80
|
|
|
all_children += element |
|
81
|
|
|
|
|
82
|
|
|
return all_children |
|
83
|
|
|
|
|
84
|
|
|
|
|
85
|
|
|
def print_action_groups(data, nested_content, markdown_help=False, settings=None): |
|
86
|
|
|
""" |
|
87
|
|
|
Process all 'action groups', which are also include 'Options' and 'Required |
|
88
|
|
|
arguments'. A list of nodes is returned. |
|
89
|
|
|
""" |
|
90
|
|
|
definitions = map_nested_definitions(nested_content) |
|
91
|
|
|
nodes_list = [] |
|
92
|
|
|
if 'action_groups' in data: |
|
93
|
|
|
for action_group in data['action_groups']: |
|
94
|
|
|
# Every action group is comprised of a section, holding a title, the description, and the option group (members) |
|
95
|
|
|
section = nodes.section(ids=[action_group['title']]) |
|
96
|
|
|
section += nodes.title(action_group['title'], action_group['title']) |
|
97
|
|
|
|
|
98
|
|
|
desc = [] |
|
99
|
|
|
if action_group['description']: |
|
100
|
|
|
desc.append(action_group['description']) |
|
101
|
|
|
# Replace/append/prepend content to the description according to nested content |
|
102
|
|
|
subcontent = [] |
|
103
|
|
|
if action_group['title'] in definitions: |
|
104
|
|
|
classifier, s, subcontent = definitions[action_group['title']] |
|
105
|
|
|
if classifier == '@replace': |
|
106
|
|
|
desc = [s] |
|
107
|
|
|
elif classifier == '@after': |
|
108
|
|
|
desc.append(s) |
|
109
|
|
|
elif classifier == '@before': |
|
110
|
|
|
desc.insert(0, s) |
|
111
|
|
|
elif classifier == '@skip': |
|
112
|
|
|
continue |
|
113
|
|
|
if len(subcontent) > 0: |
|
114
|
|
|
for k, v in map_nested_definitions(subcontent).items(): |
|
115
|
|
|
definitions[k] = v |
|
116
|
|
|
# Render appropriately |
|
117
|
|
|
for element in render_list(desc, markdown_help): |
|
118
|
|
|
section += element |
|
119
|
|
|
|
|
120
|
|
|
local_definitions = definitions |
|
121
|
|
|
if len(subcontent) > 0: |
|
122
|
|
|
local_definitions = {k: v for k, v in definitions.items()} |
|
123
|
|
|
for k, v in map_nested_definitions(subcontent).items(): |
|
124
|
|
|
local_definitions[k] = v |
|
125
|
|
|
|
|
126
|
|
|
items = [] |
|
127
|
|
|
# Iterate over action group members |
|
128
|
|
|
for entry in action_group['options']: |
|
129
|
|
|
# Members will include: |
|
130
|
|
|
# default The default value. This may be ==SUPPRESS== |
|
131
|
|
|
# name A list of option names (e.g., ['-h', '--help'] |
|
132
|
|
|
# help The help message string |
|
133
|
|
|
# There may also be a 'choices' member. |
|
134
|
|
|
# Build the help text |
|
135
|
|
|
arg = [] |
|
136
|
|
|
if 'choices' in entry: |
|
137
|
|
|
arg.append(f"Possible choices: {', '.join(str(c) for c in entry['choices'])}\n") |
|
138
|
|
|
if 'help' in entry: |
|
139
|
|
|
arg.append(entry['help']) |
|
140
|
|
|
if entry['default'] is not None and entry['default'] not in [ |
|
141
|
|
|
'"==SUPPRESS=="', |
|
142
|
|
|
'==SUPPRESS==', |
|
143
|
|
|
]: |
|
144
|
|
|
if entry['default'] == '': |
|
145
|
|
|
arg.append('Default: ""') |
|
146
|
|
|
else: |
|
147
|
|
|
arg.append(f"Default: {entry['default']}") |
|
148
|
|
|
|
|
149
|
|
|
# Handle nested content, the term used in the dict has the comma removed for simplicity |
|
150
|
|
|
desc = arg |
|
151
|
|
|
term = ' '.join(entry['name']) |
|
152
|
|
|
if term in local_definitions: |
|
153
|
|
|
classifier, s, subcontent = local_definitions[term] |
|
154
|
|
|
if classifier == '@replace': |
|
155
|
|
|
desc = [s] |
|
156
|
|
|
elif classifier == '@after': |
|
157
|
|
|
desc.append(s) |
|
158
|
|
|
elif classifier == '@before': |
|
159
|
|
|
desc.insert(0, s) |
|
160
|
|
|
term = ', '.join(entry['name']) |
|
161
|
|
|
|
|
162
|
|
|
n = nodes.option_list_item( |
|
163
|
|
|
'', |
|
164
|
|
|
nodes.option_group('', nodes.option_string(text=term)), |
|
165
|
|
|
nodes.description('', *render_list(desc, markdown_help, settings)), |
|
166
|
|
|
) |
|
167
|
|
|
items.append(n) |
|
168
|
|
|
|
|
169
|
|
|
section += nodes.option_list('', *items) |
|
170
|
|
|
nodes_list.append(section) |
|
171
|
|
|
|
|
172
|
|
|
return nodes_list |
|
173
|
|
|
|
|
174
|
|
|
|
|
175
|
|
|
def print_subcommands(data, nested_content, markdown_help=False, settings=None): # noqa: N803 |
|
176
|
|
|
""" |
|
177
|
|
|
Each subcommand is a dictionary with the following keys: |
|
178
|
|
|
|
|
179
|
|
|
['usage', 'action_groups', 'bare_usage', 'name', 'help'] |
|
180
|
|
|
|
|
181
|
|
|
In essence, this is all tossed in a new section with the title 'name'. |
|
182
|
|
|
Apparently there can also be a 'description' entry. |
|
183
|
|
|
""" |
|
184
|
|
|
|
|
185
|
|
|
definitions = map_nested_definitions(nested_content) |
|
186
|
|
|
items = [] |
|
187
|
|
|
if 'children' in data: |
|
188
|
|
|
subcommands = nodes.section(ids=["Sub-commands:"]) |
|
189
|
|
|
subcommands += nodes.title('Sub-commands:', 'Sub-commands:') |
|
190
|
|
|
|
|
191
|
|
|
for child in data['children']: |
|
192
|
|
|
sec = nodes.section(ids=[child['name']]) |
|
193
|
|
|
sec += nodes.title(child['name'], child['name']) |
|
194
|
|
|
|
|
195
|
|
|
if 'description' in child and child['description']: |
|
196
|
|
|
desc = [child['description']] |
|
197
|
|
|
elif child['help']: |
|
198
|
|
|
desc = [child['help']] |
|
199
|
|
|
else: |
|
200
|
|
|
desc = ['Undocumented'] |
|
201
|
|
|
|
|
202
|
|
|
# Handle nested content |
|
203
|
|
|
subcontent = [] |
|
204
|
|
|
if child['name'] in definitions: |
|
205
|
|
|
classifier, s, subcontent = definitions[child['name']] |
|
206
|
|
|
if classifier == '@replace': |
|
207
|
|
|
desc = [s] |
|
208
|
|
|
elif classifier == '@after': |
|
209
|
|
|
desc.append(s) |
|
210
|
|
|
elif classifier == '@before': |
|
211
|
|
|
desc.insert(0, s) |
|
212
|
|
|
|
|
213
|
|
|
for element in render_list(desc, markdown_help): |
|
214
|
|
|
sec += element |
|
215
|
|
|
sec += nodes.literal_block(text=child['bare_usage']) |
|
216
|
|
|
for x in print_action_groups(child, nested_content + subcontent, markdown_help, settings=settings): |
|
217
|
|
|
sec += x |
|
218
|
|
|
|
|
219
|
|
|
for x in print_subcommands(child, nested_content + subcontent, markdown_help, settings=settings): |
|
220
|
|
|
sec += x |
|
221
|
|
|
|
|
222
|
|
|
if 'epilog' in child and child['epilog']: |
|
223
|
|
|
for element in render_list([child['epilog']], markdown_help): |
|
224
|
|
|
sec += element |
|
225
|
|
|
|
|
226
|
|
|
subcommands += sec |
|
227
|
|
|
items.append(subcommands) |
|
228
|
|
|
|
|
229
|
|
|
return items |
|
230
|
|
|
|
|
231
|
|
|
|
|
232
|
|
|
def ensure_unique_ids(items): |
|
233
|
|
|
""" |
|
234
|
|
|
If action groups are repeated, then links in the table of contents will |
|
235
|
|
|
just go to the first of the repeats. This may not be desirable, particularly |
|
236
|
|
|
in the case of subcommands where the option groups have different members. |
|
237
|
|
|
This function updates the title IDs by adding _repeatX, where X is a number |
|
238
|
|
|
so that the links are then unique. |
|
239
|
|
|
""" |
|
240
|
|
|
s = set() |
|
241
|
|
|
for item in items: |
|
242
|
|
|
for n in item.traverse(descend=True, siblings=True, ascend=False): |
|
243
|
|
|
if isinstance(n, nodes.section): |
|
244
|
|
|
ids = n['ids'] |
|
245
|
|
|
for idx, id in enumerate(ids): |
|
246
|
|
|
if id not in s: |
|
247
|
|
|
s.add(id) |
|
248
|
|
|
else: |
|
249
|
|
|
i = 1 |
|
250
|
|
|
while f"{id}_repeat{i}" in s: |
|
251
|
|
|
i += 1 |
|
252
|
|
|
ids[idx] = f"{id}_repeat{i}" |
|
253
|
|
|
s.add(ids[idx]) |
|
254
|
|
|
n['ids'] = ids |
|
255
|
|
|
|
|
256
|
|
|
|
|
257
|
|
|
class ArgParseDirective(Directive): |
|
258
|
|
|
has_content = True |
|
259
|
|
|
option_spec = dict( |
|
260
|
|
|
module=unchanged, |
|
261
|
|
|
func=unchanged, |
|
262
|
|
|
ref=unchanged, |
|
263
|
|
|
prog=unchanged, |
|
264
|
|
|
path=unchanged, |
|
265
|
|
|
nodefault=flag, |
|
266
|
|
|
nodefaultconst=flag, |
|
267
|
|
|
filename=unchanged, |
|
268
|
|
|
manpage=unchanged, |
|
269
|
|
|
nosubcommands=unchanged, |
|
270
|
|
|
passparser=flag, |
|
271
|
|
|
noepilog=unchanged, |
|
272
|
|
|
nodescription=unchanged, |
|
273
|
|
|
markdown=flag, |
|
274
|
|
|
markdownhelp=flag, |
|
275
|
|
|
) |
|
276
|
|
|
|
|
277
|
|
|
def _construct_manpage_specific_structure(self, parser_info): |
|
278
|
|
|
""" |
|
279
|
|
|
Construct a typical man page consisting of the following elements: |
|
280
|
|
|
NAME (automatically generated, out of our control) |
|
281
|
|
|
SYNOPSIS |
|
282
|
|
|
DESCRIPTION |
|
283
|
|
|
OPTIONS |
|
284
|
|
|
FILES |
|
285
|
|
|
SEE ALSO |
|
286
|
|
|
BUGS |
|
287
|
|
|
""" |
|
288
|
|
|
items = [] |
|
289
|
|
|
# SYNOPSIS section |
|
290
|
|
|
synopsis_section = nodes.section( |
|
291
|
|
|
'', |
|
292
|
|
|
nodes.title(text='Synopsis'), |
|
293
|
|
|
nodes.literal_block(text=parser_info["bare_usage"]), |
|
294
|
|
|
ids=['synopsis-section'], |
|
295
|
|
|
) |
|
296
|
|
|
items.append(synopsis_section) |
|
297
|
|
|
# DESCRIPTION section |
|
298
|
|
|
if 'nodescription' not in self.options: |
|
299
|
|
|
description_section = nodes.section( |
|
300
|
|
|
'', |
|
301
|
|
|
nodes.title(text='Description'), |
|
302
|
|
|
nodes.paragraph( |
|
303
|
|
|
text=parser_info.get( |
|
304
|
|
|
'description', |
|
305
|
|
|
parser_info.get('help', "undocumented").capitalize(), |
|
306
|
|
|
) |
|
307
|
|
|
), |
|
308
|
|
|
ids=['description-section'], |
|
309
|
|
|
) |
|
310
|
|
|
nested_parse_with_titles(self.state, self.content, description_section) |
|
311
|
|
|
items.append(description_section) |
|
312
|
|
|
if parser_info.get('epilog') and 'noepilog' not in self.options: |
|
313
|
|
|
# TODO: do whatever sphinx does to understand ReST inside |
|
314
|
|
|
# docstrings magically imported from other places. The nested |
|
315
|
|
|
# parse method invoked above seem to be able to do this but |
|
316
|
|
|
# I haven't found a way to do it for arbitrary text |
|
317
|
|
|
if description_section: |
|
|
|
|
|
|
318
|
|
|
description_section += nodes.paragraph(text=parser_info['epilog']) |
|
319
|
|
|
else: |
|
320
|
|
|
description_section = nodes.paragraph(text=parser_info['epilog']) |
|
321
|
|
|
items.append(description_section) |
|
322
|
|
|
# OPTIONS section |
|
323
|
|
|
options_section = nodes.section('', nodes.title(text='Options'), ids=['options-section']) |
|
324
|
|
|
if 'args' in parser_info: |
|
325
|
|
|
options_section += nodes.paragraph() |
|
326
|
|
|
options_section += nodes.subtitle(text='Positional arguments:') |
|
327
|
|
|
options_section += self._format_positional_arguments(parser_info) |
|
328
|
|
|
for action_group in parser_info['action_groups']: |
|
329
|
|
|
if 'options' in parser_info: |
|
330
|
|
|
options_section += nodes.paragraph() |
|
331
|
|
|
options_section += nodes.subtitle(text=action_group['title']) |
|
332
|
|
|
options_section += self._format_optional_arguments(action_group) |
|
333
|
|
|
|
|
334
|
|
|
# NOTE: we cannot generate NAME ourselves. It is generated by |
|
335
|
|
|
# docutils.writers.manpage |
|
336
|
|
|
# TODO: items.append(files) |
|
337
|
|
|
# TODO: items.append(see also) |
|
338
|
|
|
# TODO: items.append(bugs) |
|
339
|
|
|
|
|
340
|
|
|
if len(options_section.children) > 1: |
|
341
|
|
|
items.append(options_section) |
|
342
|
|
|
if 'nosubcommands' not in self.options: |
|
343
|
|
|
# SUBCOMMANDS section (non-standard) |
|
344
|
|
|
subcommands_section = nodes.section('', nodes.title(text='Sub-Commands'), ids=['subcommands-section']) |
|
345
|
|
|
if 'children' in parser_info: |
|
346
|
|
|
subcommands_section += self._format_subcommands(parser_info) |
|
347
|
|
|
if len(subcommands_section) > 1: |
|
348
|
|
|
items.append(subcommands_section) |
|
349
|
|
|
if os.getenv("INCLUDE_DEBUG_SECTION"): |
|
350
|
|
|
import json |
|
351
|
|
|
|
|
352
|
|
|
# DEBUG section (non-standard) |
|
353
|
|
|
debug_section = nodes.section( |
|
354
|
|
|
'', |
|
355
|
|
|
nodes.title(text="Argparse + Sphinx Debugging"), |
|
356
|
|
|
nodes.literal_block(text=json.dumps(parser_info, indent=' ')), |
|
357
|
|
|
ids=['debug-section'], |
|
358
|
|
|
) |
|
359
|
|
|
items.append(debug_section) |
|
360
|
|
|
return items |
|
361
|
|
|
|
|
362
|
|
|
def _format_positional_arguments(self, parser_info): |
|
363
|
|
|
assert 'args' in parser_info |
|
364
|
|
|
items = [] |
|
365
|
|
|
for arg in parser_info['args']: |
|
366
|
|
|
arg_items = [] |
|
367
|
|
|
if arg['help']: |
|
368
|
|
|
arg_items.append(nodes.paragraph(text=arg['help'])) |
|
369
|
|
|
elif 'choices' not in arg: |
|
370
|
|
|
arg_items.append(nodes.paragraph(text='Undocumented')) |
|
371
|
|
|
if 'choices' in arg: |
|
372
|
|
|
arg_items.append(nodes.paragraph(text='Possible choices: ' + ', '.join(arg['choices']))) |
|
373
|
|
|
items.append( |
|
374
|
|
|
nodes.option_list_item( |
|
375
|
|
|
'', |
|
376
|
|
|
nodes.option_group('', nodes.option('', nodes.option_string(text=arg['metavar']))), |
|
377
|
|
|
nodes.description('', *arg_items), |
|
378
|
|
|
) |
|
379
|
|
|
) |
|
380
|
|
|
return nodes.option_list('', *items) |
|
381
|
|
|
|
|
382
|
|
|
def _format_optional_arguments(self, parser_info): |
|
383
|
|
|
assert 'options' in parser_info |
|
384
|
|
|
items = [] |
|
385
|
|
|
for opt in parser_info['options']: |
|
386
|
|
|
names = [] |
|
387
|
|
|
opt_items = [] |
|
388
|
|
|
for name in opt['name']: |
|
389
|
|
|
option_declaration = [nodes.option_string(text=name)] |
|
390
|
|
|
if opt['default'] is not None and opt['default'] not in [ |
|
391
|
|
|
'"==SUPPRESS=="', |
|
392
|
|
|
'==SUPPRESS==', |
|
393
|
|
|
]: |
|
394
|
|
|
option_declaration += nodes.option_argument('', text='=' + str(opt['default'])) |
|
395
|
|
|
names.append(nodes.option('', *option_declaration)) |
|
396
|
|
|
if opt['help']: |
|
397
|
|
|
opt_items.append(nodes.paragraph(text=opt['help'])) |
|
398
|
|
|
elif 'choices' not in opt: |
|
399
|
|
|
opt_items.append(nodes.paragraph(text='Undocumented')) |
|
400
|
|
|
if 'choices' in opt: |
|
401
|
|
|
opt_items.append(nodes.paragraph(text='Possible choices: ' + ', '.join(opt['choices']))) |
|
402
|
|
|
items.append( |
|
403
|
|
|
nodes.option_list_item( |
|
404
|
|
|
'', |
|
405
|
|
|
nodes.option_group('', *names), |
|
406
|
|
|
nodes.description('', *opt_items), |
|
407
|
|
|
) |
|
408
|
|
|
) |
|
409
|
|
|
return nodes.option_list('', *items) |
|
410
|
|
|
|
|
411
|
|
|
def _format_subcommands(self, parser_info): |
|
412
|
|
|
assert 'children' in parser_info |
|
413
|
|
|
items = [] |
|
414
|
|
|
for subcmd in parser_info['children']: |
|
415
|
|
|
subcmd_items = [] |
|
416
|
|
|
if subcmd['help']: |
|
417
|
|
|
subcmd_items.append(nodes.paragraph(text=subcmd['help'])) |
|
418
|
|
|
else: |
|
419
|
|
|
subcmd_items.append(nodes.paragraph(text='Undocumented')) |
|
420
|
|
|
items.append( |
|
421
|
|
|
nodes.definition_list_item( |
|
422
|
|
|
'', |
|
423
|
|
|
nodes.term('', '', nodes.strong(text=subcmd['bare_usage'])), |
|
424
|
|
|
nodes.definition('', *subcmd_items), |
|
425
|
|
|
) |
|
426
|
|
|
) |
|
427
|
|
|
return nodes.definition_list('', *items) |
|
428
|
|
|
|
|
429
|
|
|
def _nested_parse_paragraph(self, text): |
|
430
|
|
|
content = nodes.paragraph() |
|
431
|
|
|
self.state.nested_parse(StringList(text.split("\n")), 0, content) |
|
432
|
|
|
return content |
|
433
|
|
|
|
|
434
|
|
|
def run(self): |
|
435
|
|
|
if 'module' in self.options and 'func' in self.options: |
|
436
|
|
|
module_name = self.options['module'] |
|
437
|
|
|
attr_name = self.options['func'] |
|
438
|
|
|
elif 'ref' in self.options: |
|
439
|
|
|
_parts = self.options['ref'].split('.') |
|
440
|
|
|
module_name = '.'.join(_parts[0:-1]) |
|
441
|
|
|
attr_name = _parts[-1] |
|
442
|
|
|
elif 'filename' in self.options and 'func' in self.options: |
|
443
|
|
|
mod = {} |
|
444
|
|
|
try: |
|
445
|
|
|
f = open(self.options['filename']) |
|
446
|
|
|
except OSError: |
|
447
|
|
|
# try open with abspath |
|
448
|
|
|
f = open(os.path.abspath(self.options['filename'])) |
|
449
|
|
|
code = compile(f.read(), self.options['filename'], 'exec') |
|
450
|
|
|
exec(code, mod) |
|
451
|
|
|
attr_name = self.options['func'] |
|
452
|
|
|
func = mod[attr_name] |
|
453
|
|
|
else: |
|
454
|
|
|
raise self.error(':module: and :func: should be specified, or :ref:, or :filename: and :func:') |
|
455
|
|
|
|
|
456
|
|
|
# Skip this if we're dealing with a local file, since it obviously can't be imported |
|
457
|
|
|
if 'filename' not in self.options: |
|
458
|
|
|
try: |
|
459
|
|
|
mod = __import__(module_name, globals(), locals(), [attr_name]) |
|
|
|
|
|
|
460
|
|
|
except ImportError: |
|
461
|
|
|
raise self.error(f'Failed to import "{attr_name}" from "{module_name}".\n{sys.exc_info()[1]}') |
|
462
|
|
|
|
|
463
|
|
|
if not hasattr(mod, attr_name): |
|
464
|
|
|
raise self.error(('Module "%s" has no attribute "%s"\n' 'Incorrect argparse :module: or :func: values?') % (module_name, attr_name)) |
|
465
|
|
|
func = getattr(mod, attr_name) |
|
466
|
|
|
|
|
467
|
|
|
if isinstance(func, ArgumentParser): |
|
468
|
|
|
parser = func |
|
469
|
|
|
elif 'passparser' in self.options: |
|
470
|
|
|
parser = ArgumentParser() |
|
471
|
|
|
func(parser) |
|
472
|
|
|
else: |
|
473
|
|
|
parser = func() |
|
474
|
|
|
if 'path' not in self.options: |
|
475
|
|
|
self.options['path'] = '' |
|
476
|
|
|
path = str(self.options['path']) |
|
477
|
|
|
if 'prog' in self.options: |
|
478
|
|
|
parser.prog = self.options['prog'] |
|
479
|
|
|
result = parse_parser( |
|
480
|
|
|
parser, |
|
481
|
|
|
skip_default_values='nodefault' in self.options, |
|
482
|
|
|
skip_default_const_values='nodefaultconst' in self.options, |
|
483
|
|
|
) |
|
484
|
|
|
result = parser_navigate(result, path) |
|
485
|
|
|
if 'manpage' in self.options: |
|
486
|
|
|
return self._construct_manpage_specific_structure(result) |
|
487
|
|
|
|
|
488
|
|
|
# Handle nested content, where markdown needs to be preprocessed |
|
489
|
|
|
items = [] |
|
490
|
|
|
nested_content = nodes.paragraph() |
|
491
|
|
|
if 'markdown' in self.options: |
|
492
|
|
|
from sphinxarg.markdown import parse_markdown_block |
|
493
|
|
|
|
|
494
|
|
|
items.extend(parse_markdown_block('\n'.join(self.content) + '\n')) |
|
495
|
|
|
else: |
|
496
|
|
|
self.state.nested_parse(self.content, self.content_offset, nested_content) |
|
497
|
|
|
nested_content = nested_content.children |
|
498
|
|
|
# add common content between |
|
499
|
|
|
for item in nested_content: |
|
500
|
|
|
if not isinstance(item, nodes.definition_list): |
|
501
|
|
|
items.append(item) |
|
502
|
|
|
|
|
503
|
|
|
markdown_help = False |
|
504
|
|
|
if 'markdownhelp' in self.options: |
|
505
|
|
|
markdown_help = True |
|
506
|
|
|
if 'description' in result and 'nodescription' not in self.options: |
|
507
|
|
|
if markdown_help: |
|
508
|
|
|
items.extend(render_list([result['description']], True)) |
|
509
|
|
|
else: |
|
510
|
|
|
items.append(self._nested_parse_paragraph(result['description'])) |
|
511
|
|
|
items.append(nodes.literal_block(text=result['usage'])) |
|
512
|
|
|
items.extend( |
|
513
|
|
|
print_action_groups( |
|
514
|
|
|
result, |
|
515
|
|
|
nested_content, |
|
516
|
|
|
markdown_help, |
|
517
|
|
|
settings=self.state.document.settings, |
|
518
|
|
|
) |
|
519
|
|
|
) |
|
520
|
|
|
if 'nosubcommands' not in self.options: |
|
521
|
|
|
items.extend( |
|
522
|
|
|
print_subcommands( |
|
523
|
|
|
result, |
|
524
|
|
|
nested_content, |
|
525
|
|
|
markdown_help, |
|
526
|
|
|
settings=self.state.document.settings, |
|
527
|
|
|
) |
|
528
|
|
|
) |
|
529
|
|
|
if 'epilog' in result and 'noepilog' not in self.options: |
|
530
|
|
|
items.append(self._nested_parse_paragraph(result['epilog'])) |
|
531
|
|
|
|
|
532
|
|
|
# Traverse the returned nodes, modifying the title IDs as necessary to avoid repeats |
|
533
|
|
|
ensure_unique_ids(items) |
|
534
|
|
|
|
|
535
|
|
|
return items |
|
536
|
|
|
|
|
537
|
|
|
|
|
538
|
|
|
def setup(app): |
|
539
|
|
|
app.add_directive('argparse', ArgParseDirective) |
|
540
|
|
|
return {'parallel_read_safe': True, 'version': __version__} |
|
541
|
|
|
|