Passed
Push — master ( 63ff1a...3c0f18 )
by Jan
07:51 queued 14s
created

sphinxarg.parser.parse_parser()   F

Complexity

Conditions 33

Size

Total Lines 127
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 33
eloc 92
nop 3
dl 0
loc 127
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like sphinxarg.parser.parse_parser() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import re
2
from argparse import _HelpAction, _StoreConstAction, _SubParsersAction
3
4
5
class NavigationException(Exception):
6
    pass
7
8
9
def parser_navigate(parser_result, path, current_path=None):
10
    if isinstance(path, str):
11
        if path == '':
12
            return parser_result
13
        path = re.split(r'\s+', path)
14
    current_path = current_path or []
15
    if len(path) == 0:
16
        return parser_result
17
    if 'children' not in parser_result:
18
        raise NavigationException(f"Current parser has no child elements.  (path: {' '.join(current_path)})")
19
    next_hop = path.pop(0)
20
    for child in parser_result['children']:
21
        # identifer is only used for aliased subcommands
22
        identifier = child['identifier'] if 'identifier' in child else child['name']
23
        if identifier == next_hop:
24
            current_path.append(next_hop)
25
            return parser_navigate(child, path, current_path)
26
    raise NavigationException(f"Current parser has no child element with name: {next_hop}  (path: {' '.join(current_path)})")
27
28
29
def _try_add_parser_attribute(data, parser, attribname):
30
    attribval = getattr(parser, attribname, None)
31
    if attribval is None:
32
        return
33
    if not isinstance(attribval, str):
34
        return
35
    if len(attribval) > 0:
36
        data[attribname] = attribval
37
38
39
def _format_usage_without_prefix(parser):
40
    """
41
    Use private argparse APIs to get the usage string without
42
    the 'usage: ' prefix.
43
    """
44
    fmt = parser._get_formatter()
45
    fmt.add_usage(parser.usage, parser._actions, parser._mutually_exclusive_groups, prefix='')
46
    return fmt.format_help().strip()
47
48
49
def parse_parser(parser, data=None, **kwargs):
50
    if data is None:
51
        data = {
52
            'name': '',
53
            'usage': parser.format_usage().strip(),
54
            'bare_usage': _format_usage_without_prefix(parser),
55
            'prog': parser.prog,
56
        }
57
    _try_add_parser_attribute(data, parser, 'description')
58
    _try_add_parser_attribute(data, parser, 'epilog')
59
    for action in parser._get_positional_actions():
60
        if not isinstance(action, _SubParsersAction):
61
            continue
62
        helps = {}
63
        for item in action._choices_actions:
64
            helps[item.dest] = item.help
65
66
        # commands which share an existing parser are an alias,
67
        # don't duplicate docs
68
        subsection_alias = {}
69
        subsection_alias_names = set()
70
        for name, subaction in action._name_parser_map.items():
71
            if subaction not in subsection_alias:
72
                subsection_alias[subaction] = []
73
            else:
74
                subsection_alias[subaction].append(name)
75
                subsection_alias_names.add(name)
76
77
        for name, subaction in action._name_parser_map.items():
78
            if name in subsection_alias_names:
79
                continue
80
            subalias = subsection_alias[subaction]
81
            subaction.prog = f'{parser.prog} {name}'
82
            subdata = {
83
                'name': name if not subalias else f"{name} ({', '.join(subalias)})",
84
                'help': helps.get(name, ''),
85
                'usage': subaction.format_usage().strip(),
86
                'bare_usage': _format_usage_without_prefix(subaction),
87
            }
88
            if subalias:
89
                subdata['identifier'] = name
90
            parse_parser(subaction, subdata, **kwargs)
91
            data.setdefault('children', []).append(subdata)
92
93
    show_defaults = True
94
    if kwargs.get('skip_default_values', False) is True:
95
        show_defaults = False
96
    show_defaults_const = show_defaults
97
    if kwargs.get('skip_default_const_values', False) is True:
98
        show_defaults_const = False
99
100
    # argparse stores the different groups as a list in parser._action_groups
101
    # the first element of the list holds the positional arguments, the
102
    # second the option arguments not in groups, and subsequent elements
103
    # argument groups with positional and optional parameters
104
    action_groups = []
105
    for action_group in parser._action_groups:
106
        options_list = []
107
        for action in action_group._group_actions:
108
            if isinstance(action, _HelpAction):
109
                continue
110
111
            # Quote default values for string/None types
112
            default = action.default
113
            if action.default not in ['', None, True, False] and action.type in [None, str] and isinstance(action.default, str):
114
                default = f'"{default}"'
115
116
            # fill in any formatters, like %(default)s
117
            format_dict = dict(vars(action), prog=data.get('prog', ''), default=default)
118
            format_dict['default'] = default
119
            help_str = action.help or ''  # Ensure we don't print None
120
            try:
121
                help_str = help_str % format_dict
122
            except Exception:
123
                pass
124
125
            # Options have the option_strings set, positional arguments don't
126
            name = action.option_strings
127
            if name == []:
128
                if action.metavar is None:
129
                    name = [action.dest]
130
                else:
131
                    name = [action.metavar]
132
            # Skip lines for subcommands
133
            if name == ['==SUPPRESS==']:
134
                continue
135
136
            if isinstance(action, _StoreConstAction):
137
                option = {
138
                    'name': name,
139
                    'default': default if show_defaults_const else '==SUPPRESS==',
140
                    'help': help_str,
141
                }
142
            else:
143
                option = {
144
                    'name': name,
145
                    'default': default if show_defaults else '==SUPPRESS==',
146
                    'help': help_str,
147
                }
148
            if action.choices:
149
                option['choices'] = action.choices
150
            if "==SUPPRESS==" not in option['help']:
151
                options_list.append(option)
152
153
        if len(options_list) == 0:
154
            continue
155
156
        # Upper case "Positional Arguments" and "Optional Arguments" titles
157
        # Since python-3.10 'optional arguments' changed to 'options'
158
        # more info: https://github.com/python/cpython/pull/23858
159
        if action_group.title == 'optional arguments' or action_group.title == 'options':
160
            action_group.title = 'Named Arguments'
161
        if action_group.title == 'positional arguments':
162
            action_group.title = 'Positional Arguments'
163
164
        group = {
165
            'title': action_group.title,
166
            'description': action_group.description,
167
            'options': options_list,
168
        }
169
170
        action_groups.append(group)
171
172
    if len(action_groups) > 0:
173
        data['action_groups'] = action_groups
174
175
    return data
176