File: parser.py

package info (click to toggle)
sphinx-argparse 0.5.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 288 kB
  • sloc: python: 1,514; makefile: 133
file content (181 lines) | stat: -rw-r--r-- 6,615 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
from __future__ import annotations

import contextlib
import re
from argparse import _HelpAction, _StoreConstAction, _SubParsersAction


class NavigationException(Exception):  # noqa: N818
    pass


def parser_navigate(parser_result, path, current_path=None):
    if isinstance(path, str):
        if path == '':
            return parser_result
        path = re.split(r'\s+', path)
    current_path = current_path or []
    if len(path) == 0:
        return parser_result
    if 'children' not in parser_result:
        msg = f"Current parser has no child elements.  (path: {' '.join(current_path)})"
        raise NavigationException(msg)
    next_hop = path.pop(0)
    for child in parser_result['children']:
        # identifer is only used for aliased subcommands
        identifier = child['identifier'] if 'identifier' in child else child['name']
        if identifier == next_hop:
            current_path.append(next_hop)
            return parser_navigate(child, path, current_path)
    msg = (
        f"Current parser has no child element with name: {next_hop} "
        f"(path: {' '.join(current_path)})"
    )
    raise NavigationException(msg)


def _try_add_parser_attribute(data, parser, attribname):
    attribval = getattr(parser, attribname, None)
    if attribval is None:
        return
    if not isinstance(attribval, str):
        return
    if len(attribval) > 0:
        data[attribname] = attribval


def _format_usage_without_prefix(parser):
    """
    Use private argparse APIs to get the usage string without
    the 'usage: ' prefix.
    """
    fmt = parser._get_formatter()
    fmt.add_usage(parser.usage, parser._actions, parser._mutually_exclusive_groups, prefix='')
    return fmt.format_help().strip()


def parse_parser(parser, data=None, **kwargs):
    if data is None:
        data = {
            'name': '',
            'usage': parser.format_usage().strip(),
            'bare_usage': _format_usage_without_prefix(parser),
            'prog': parser.prog,
        }
    _try_add_parser_attribute(data, parser, 'description')
    _try_add_parser_attribute(data, parser, 'epilog')
    for action in parser._get_positional_actions():
        if not isinstance(action, _SubParsersAction):
            continue
        helps = {}
        for item in action._choices_actions:
            helps[item.dest] = item.help

        # commands which share an existing parser are an alias,
        # don't duplicate docs
        subsection_alias = {}
        subsection_alias_names = set()
        for name, subaction in action._name_parser_map.items():
            if subaction not in subsection_alias:
                subsection_alias[subaction] = []
            else:
                subsection_alias[subaction].append(name)
                subsection_alias_names.add(name)

        for name, subaction in action._name_parser_map.items():
            if name in subsection_alias_names:
                continue
            subalias = subsection_alias[subaction]
            subaction.prog = f'{parser.prog} {name}'
            subdata = {
                'name': name if not subalias else f"{name} ({', '.join(subalias)})",
                'help': helps.get(name, ''),
                'usage': subaction.format_usage().strip(),
                'bare_usage': _format_usage_without_prefix(subaction),
            }
            if subalias:
                subdata['identifier'] = name
            parse_parser(subaction, subdata, **kwargs)
            data.setdefault('children', []).append(subdata)

    show_defaults = True
    if kwargs.get('skip_default_values', False) is True:
        show_defaults = False
    show_defaults_const = show_defaults
    if kwargs.get('skip_default_const_values', False) is True:
        show_defaults_const = False

    # argparse stores the different groups as a list in parser._action_groups
    # the first element of the list holds the positional arguments, the
    # second the option arguments not in groups, and subsequent elements
    # argument groups with positional and optional parameters
    action_groups = []
    for action_group in parser._action_groups:
        options_list = []
        for action in action_group._group_actions:
            if isinstance(action, _HelpAction):
                continue

            # Quote default values for string/None types
            default = action.default
            if (
                default is not None
                and not isinstance(default, bool)
                and action.type in {None, str}
                and isinstance(default, str)
            ):
                default = f"'{default}'"

            # fill in any formatters, like %(default)s
            format_dict = dict(vars(action), prog=data.get('prog', ''), default=default)
            format_dict['default'] = default
            help_str = action.help or ''  # Ensure we don't print None
            with contextlib.suppress(Exception):
                help_str = help_str % format_dict

            # Options have the option_strings set, positional arguments don't
            name = action.option_strings
            if name == []:
                name = [action.dest] if action.metavar is None else [action.metavar]
            # Skip lines for subcommands
            if name == ['==SUPPRESS==']:
                continue

            if isinstance(action, _StoreConstAction):
                option = {
                    'name': name,
                    'default': default if show_defaults_const else '==SUPPRESS==',
                    'help': help_str,
                }
            else:
                option = {
                    'name': name,
                    'default': default if show_defaults else '==SUPPRESS==',
                    'help': help_str,
                }
            if action.choices:
                option['choices'] = action.choices
            if '==SUPPRESS==' not in option['help']:
                options_list.append(option)

        if len(options_list) == 0:
            continue

        # Upper case "Positional Arguments" and "Named Arguments" titles
        if action_group.title == 'options':
            action_group.title = 'Named Arguments'
        if action_group.title == 'positional arguments':
            action_group.title = 'Positional Arguments'

        group = {
            'title': action_group.title,
            'description': action_group.description,
            'options': options_list,
        }

        action_groups.append(group)

    if len(action_groups) > 0:
        data['action_groups'] = action_groups

    return data