File size: 6,363 Bytes
2f85de4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# python3.7
"""Contains the utility functions for parsing arguments."""

import json
import argparse
import click

__all__ = [
    'parse_int', 'parse_float', 'parse_bool', 'parse_index', 'parse_json',
    'IntegerParamType', 'FloatParamType', 'BooleanParamType', 'IndexParamType',
    'JsonParamType', 'DictAction'
]


def parse_int(arg):
    """Parses an argument to integer.

    Support converting string `none` and `null` to `None`.
    """
    if arg is None:
        return None
    if isinstance(arg, str) and arg.lower() in ['none', 'null']:
        return None
    return int(arg)


def parse_float(arg):
    """Parses an argument to float number.

    Support converting string `none` and `null` to `None`.
    """
    if arg is None:
        return None
    if isinstance(arg, str) and arg.lower() in ['none', 'null']:
        return None
    return float(arg)


def parse_bool(arg):
    """Parses an argument to boolean.

    `None` will be converted to `False`.
    """
    if isinstance(arg, bool):
        return arg
    if arg is None:
        return False
    if arg.lower() in ['1', 'true', 't', 'yes', 'y']:
        return True
    if arg.lower() in ['0', 'false', 'f', 'no', 'n', 'none', 'null']:
        return False
    raise ValueError(f'`{arg}` cannot be converted to boolean!')


def parse_index(arg, min_val=None, max_val=None):
    """Parses indices.

    If the input is a list or tuple, this function has no effect.

    If the input is a string, it can be either a comma separated list of numbers
    `1, 3, 5`, or a dash separated range `3 - 10`. Spaces in the string will be
    ignored.

    Args:
        arg: The input argument to parse indices from.
        min_val: If not `None`, this function will check that all indices are
            equal to or larger than this value. (default: None)
        max_val: If not `None`, this function will check that all indices are
            equal to or smaller than this field. (default: None)

    Returns:
        A list of integers.

    Raises:
        ValueError: If the input is invalid, i.e., neither a list or tuple, nor
            a string.
    """
    if arg is None or arg == '':
        indices = []
    elif isinstance(arg, int):
        indices = [arg]
    elif isinstance(arg, (list, tuple)):
        indices = list(arg)
    elif isinstance(arg, str):
        indices = []
        if arg.lower() not in ['none', 'null']:
            splits = arg.replace(' ', '').split(',')
            for split in splits:
                numbers = list(map(int, split.split('-')))
                if len(numbers) == 1:
                    indices.append(numbers[0])
                elif len(numbers) == 2:
                    indices.extend(list(range(numbers[0], numbers[1] + 1)))
    else:
        raise ValueError(f'Invalid type of input: `{type(arg)}`!')

    assert isinstance(indices, list)
    indices = sorted(list(set(indices)))
    for idx in indices:
        assert isinstance(idx, int)
        if min_val is not None:
            assert idx >= min_val, f'{idx} is smaller than min val `{min_val}`!'
        if max_val is not None:
            assert idx <= max_val, f'{idx} is larger than max val `{max_val}`!'

    return indices


def parse_json(arg):
    """Parses a string-like argument following JSON format.

    - `None` arguments will be kept.
    - Non-string arguments will be kept.
    """
    if not isinstance(arg, str):
        return arg
    try:
        return json.loads(arg)
    except json.decoder.JSONDecodeError:
        return arg


class IntegerParamType(click.ParamType):
    """Defines a `click.ParamType` to parse integer arguments."""

    name = 'int'

    def convert(self, value, param, ctx):  # pylint: disable=inconsistent-return-statements
        try:
            return parse_int(value)
        except ValueError:
            self.fail(f'`{value}` cannot be parsed as an integer!', param, ctx)


class FloatParamType(click.ParamType):
    """Defines a `click.ParamType` to parse float arguments."""

    name = 'float'

    def convert(self, value, param, ctx):  # pylint: disable=inconsistent-return-statements
        try:
            return parse_float(value)
        except ValueError:
            self.fail(f'`{value}` cannot be parsed as a float!', param, ctx)


class BooleanParamType(click.ParamType):
    """Defines a `click.ParamType` to parse boolean arguments."""

    name = 'bool'

    def convert(self, value, param, ctx):  # pylint: disable=inconsistent-return-statements
        try:
            return parse_bool(value)
        except ValueError:
            self.fail(f'`{value}` cannot be parsed as a boolean!', param, ctx)


class IndexParamType(click.ParamType):
    """Defines a `click.ParamType` to parse indices arguments."""

    name = 'index'

    def __init__(self, min_val=None, max_val=None):
        self.min_val = min_val
        self.max_val = max_val

    def convert(self, value, param, ctx):  # pylint: disable=inconsistent-return-statements
        try:
            return parse_index(value, self.min_val, self.max_val)
        except ValueError:
            self.fail(
                f'`{value}` cannot be parsed as a list of indices!', param, ctx)


class JsonParamType(click.ParamType):
    """Defines a `click.ParamType` to parse arguments following JSON format."""

    name = 'json'

    def convert(self, value, param, ctx):
        return parse_json(value)


class DictAction(argparse.Action):
    """Argparse action to split each argument into (key, value) pair.

    Each argument should be with `key=value` format, where `value` should be a
    string with JSON format.

    For example, with an argparse:

    parser.add_argument('--options', nargs='+', action=DictAction)

    , you can use following arguments in the command line:

    --options \
        a=1 \
        b=1.5
        c=true \
        d=null \
        e=[1,2,3,4,5] \
        f='{"x":1,"y":2,"z":3}' \

    NOTE: No space is allowed in each argument. Also, the dictionary-type
    argument should be quoted with single quotation marks `'`.
    """

    def __call__(self, parser, namespace, values, option_string=None):
        options = {}
        for argument in values:
            key, val = argument.split('=', maxsplit=1)
            options[key] = parse_json(val)
        setattr(namespace, self.dest, options)