Completed
Push — master ( 3e9026...2ae9de )
by Satoru
01:07
created

parse_args()   A

Complexity

Conditions 3

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
dl 0
loc 74
rs 9.0335

How to fix   Long Method   

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:

1
#
2
# Author: Satoru SATOH <ssato redhat.com>
3
# License: MIT
4
#
5
"""CLI frontend module for anyconfig.
6
"""
7
from __future__ import absolute_import, print_function
8
9
import argparse
10
import codecs
11
import locale
12
import logging
13
import os
14
import sys
15
16
import anyconfig.api as API
17
import anyconfig.compat
18
import anyconfig.globals
19
import anyconfig.parser
20
import anyconfig.utils
21
22
23
_ENCODING = locale.getdefaultlocale()[1]
24
25
LOGGER = logging.getLogger("anyconfig")
26
LOGGER.addHandler(logging.StreamHandler())
27
LOGGER.setLevel(logging.WARN)
28
29
if anyconfig.compat.IS_PYTHON_3:
30
    import io
31
32
    _ENCODING = _ENCODING.lower()
33
34
    # TODO: What should be done for an error, "AttributeError: '_io.StringIO'
35
    # object has no attribute 'buffer'"?
36
    try:
37
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding=_ENCODING)
38
        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding=_ENCODING)
39
    except AttributeError:
40
        pass
41
else:
42
    sys.stdout = codecs.getwriter(_ENCODING)(sys.stdout)
43
    sys.stderr = codecs.getwriter(_ENCODING)(sys.stderr)
44
45
USAGE = """\
46
%(prog)s [Options...] CONF_PATH_OR_PATTERN_0 [CONF_PATH_OR_PATTERN_1 ..]
47
48
Examples:
49
  %(prog)s --list  # -> Supported config types: configobj, ini, json, ...
50
  # Merge and/or convert input config to output config [file]
51
  %(prog)s -I yaml -O yaml /etc/xyz/conf.d/a.conf
52
  %(prog)s -I yaml '/etc/xyz/conf.d/*.conf' -o xyz.conf --otype json
53
  %(prog)s '/etc/xyz/conf.d/*.json' -o xyz.yml \\
54
    --atype json -A '{"obsoletes": "syscnf", "conflicts": "syscnf-old"}'
55
  %(prog)s '/etc/xyz/conf.d/*.json' -o xyz.yml \\
56
    -A obsoletes:syscnf;conflicts:syscnf-old
57
  %(prog)s /etc/foo.json /etc/foo/conf.d/x.json /etc/foo/conf.d/y.json
58
  %(prog)s '/etc/foo.d/*.json' -M noreplace
59
  # Query/Get/set part of input config
60
  %(prog)s '/etc/foo.d/*.json' --query 'locs[?state == 'T'].name | sort(@)'
61
  %(prog)s '/etc/foo.d/*.json' --get a.b.c
62
  %(prog)s '/etc/foo.d/*.json' --set a.b.c=1
63
  # Validate with JSON schema or generate JSON schema:
64
  %(prog)s --validate -S foo.conf.schema.yml '/etc/foo.d/*.xml'
65
  %(prog)s --gen-schema '/etc/foo.d/*.xml' -o foo.conf.schema.yml"""
66
67
DEFAULTS = dict(loglevel=1, list=False, output=None, itype=None,
68
                otype=None, atype=None, merge=API.MS_DICTS,
69
                ignore_missing=False, template=False, env=False,
70
                schema=None, validate=False, gen_schema=False)
71
72
73
def to_log_level(level):
74
    """
75
    :param level: Logging level in int = 0 .. 2
76
77
    >>> to_log_level(0) == logging.WARN
78
    True
79
    >>> to_log_level(5)  # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
80
    Traceback (most recent call last):
81
        ...
82
    ValueError: wrong log level passed: 5
83
    >>>
84
    """
85
    if not (level >= 0 and level < 3):
86
        raise ValueError("wrong log level passed: " + str(level))
87
88
    return [logging.WARN, logging.INFO, logging.DEBUG][level]
89
90
91
_ATYPE_HELP_FMT = """\
92
Explicitly select type of argument to provide configs from %s.
93
94
If this option is not set, original parser is used: 'K:V' will become {K: V},
95
'K:V_0,V_1,..' will become {K: [V_0, V_1, ...]}, and 'K_0:V_0;K_1:V_1' will
96
become {K_0: V_0, K_1: V_1} (where the tyep of K is str, type of V is one of
97
Int, str, etc."""
98
99
_QUERY_HELP = ("Query with JMESPath expression language. See "
100
               "http://jmespath.org for more about JMESPath expression. "
101
               "This option is not used with --get option at the same time. "
102
               "Please note that python module to support JMESPath "
103
               "expression (https://pypi.python.org/pypi/jmespath/) is "
104
               "required to use this option")
105
_GET_HELP = ("Specify key path to get part of config, for example, "
106
             "'--get a.b.c' to config {'a': {'b': {'c': 0, 'd': 1}}} "
107
             "gives 0 and '--get a.b' to the same config gives "
108
             "{'c': 0, 'd': 1}. Path expression can be JSON Pointer "
109
             "expression (http://tools.ietf.org/html/rfc6901) such like "
110
             "'', '/a~1b', '/m~0n'. "
111
             "This option is not used with --query option at the same time. ")
112
_SET_HELP = ("Specify key path to set (update) part of config, for "
113
             "example, '--set a.b.c=1' to a config {'a': {'b': {'c': 0, "
114
             "'d': 1}}} gives {'a': {'b': {'c': 1, 'd': 1}}}.")
115
116
117
def make_parser(defaults=None):
118
    """
119
    :param defaults: Default option values
120
    """
121
    if defaults is None:
122
        defaults = DEFAULTS
123
124
    ctypes = API.list_types()
125
    ctypes_s = ", ".join(ctypes)
126
    type_help = "Select type of %s config files from " + \
127
        ctypes_s + " [Automatically detected by file ext]"
128
129
    mts = API.MERGE_STRATEGIES
130
    mts_s = ", ".join(mts)
131
    mt_help = "Select strategy to merge multiple configs from " + \
132
        mts_s + " [%(merge)s]" % defaults
133
134
    parser = argparse.ArgumentParser(USAGE)
135
    parser.set_defaults(**defaults)
136
137
    parser.add_argument("inputs", type=str, nargs='*', help="Input files")
138
    parser.add_argument("--version", action="version",
139
                        version="%%(prog)s %s" % anyconfig.globals.VERSION)
140
141
    lpog = parser.add_argument_group("List specific options")
142
    lpog.add_argument("-L", "--list", action="store_true",
143
                      help="List supported config types")
144
145
    spog = parser.add_argument_group("Schema specific options")
146
    spog.add_argument("--validate", action="store_true",
147
                      help="Only validate input files and do not output. "
148
                           "You must specify schema file with -S/--schema "
149
                           "option.")
150
    spog.add_argument("--gen-schema", action="store_true",
151
                      help="Generate JSON schema for givne config file[s] "
152
                           "and output it instead of (merged) configuration.")
153
154
    gspog = parser.add_argument_group("Query/Get/set options")
155
    gspog.add_argument("-Q", "--query", help=_QUERY_HELP)
156
    gspog.add_argument("--get", help=_GET_HELP)
157
    gspog.add_argument("--set", help=_SET_HELP)
158
159
    parser.add_argument("-o", "--output", help="Output file path")
160
    parser.add_argument("-I", "--itype", choices=ctypes,
161
                        help=(type_help % "Input"))
162
    parser.add_argument("-O", "--otype", choices=ctypes,
163
                        help=(type_help % "Output"))
164
    parser.add_argument("-M", "--merge", choices=mts, help=mt_help)
165
    parser.add_argument("-A", "--args", help="Argument configs to override")
166
    parser.add_argument("--atype", choices=ctypes,
167
                        help=_ATYPE_HELP_FMT % ctypes_s)
168
169
    parser.add_argument("-x", "--ignore-missing", action="store_true",
170
                        help="Ignore missing input files")
171
    parser.add_argument("-T", "--template", action="store_true",
172
                        help="Enable template config support")
173
    parser.add_argument("-E", "--env", action="store_true",
174
                        help="Load configuration defaults from "
175
                             "environment values")
176
    parser.add_argument("-S", "--schema", help="Specify Schema file[s] path")
177
    parser.add_argument("-s", "--silent", action="store_const",
178
                        dest="loglevel", const=0, help="Silent or quiet mode")
179
    parser.add_argument("-q", "--quiet", action="store_const", dest="loglevel",
180
                        const=0, help="Same as --silent option")
181
    parser.add_argument("-v", "--verbose", action="store_const",
182
                        dest="loglevel", const=2, help="Verbose mode")
183
    return parser
184
185
186
def _exit_with_output(content, exit_code=0):
187
    """
188
    Exit the program with printing out messages.
189
190
    :param content: content to print out
191
    :param exit_code: Exit code
192
    """
193
    (sys.stdout if exit_code == 0 else sys.stderr).write(content + "\n")
194
    sys.exit(exit_code)
195
196
197
def _check_options_and_args(parser, args):
198
    """
199
    Show supported config format types or usage.
200
201
    :param parser: Option parser object
202
    :param args: Arguments argparse.ArgumentParser.parse_args returns
203
    """
204
    if not args.inputs:
205
        if args.list:
206
            tlist = ", ".join(API.list_types())
207
            _exit_with_output("Supported config types: " + tlist)
208
        elif args.env:
209
            cnf = os.environ.copy()
210
            _output_result(cnf, args.output, args.otype or "json", None, None)
211
            sys.exit(0)
212
        else:
213
            parser.print_usage()
214
            sys.exit(1)
215
216
    if args.validate and args.schema is None:
217
        _exit_with_output("--validate option requires --scheme option", 1)
218
219
220
def _exit_if_load_failure(cnf, msg):
221
    """
222
    :param cnf: Loaded configuration object or None indicates load failure
223
    :param msg: Message to print out if failure
224
    """
225
    if cnf is None:
226
        _exit_with_output(msg, 1)
227
228
229
def _exit_if_only_to_validate(only_to_validate):
230
    """
231
    :param only_to_validate: True if it's only to validate
232
    """
233
    if only_to_validate:
234
        _exit_with_output("Validation succeds")
235
236
237
def _do_query(cnf, exp):
238
    """
239
    :param cnf: Configuration object to print out
240
    :param exp: A string represents JMESPath expression to query loaded data
241
    :return: Query result object if no error
242
    """
243
    return API.query(cnf, exp)
244
245
246
def _do_get(cnf, get_path):
247
    """
248
    :param cnf: Configuration object to print out
249
    :param get_path: key path given in --get option
250
    :return: updated Configuration object if no error
251
    """
252
    (cnf, err) = API.get(cnf, get_path)
253
    if cnf is None:  # Failed to get the result.
254
        _exit_with_output("Failed to get result: err=%s" % err, 1)
255
256
    return cnf
257
258
259
def _output_type_by_input_path(inpath, itype, fmsg):
260
    """
261
    :param inpath: Input file path
262
    :param itype: Input type or None
263
    :param fmsg: message if it cannot detect otype by `inpath`
264
    :return: Output type :: str
265
    """
266
    msg = ("Specify inpath and/or outpath type[s] with -I/--itype "
267
           "or -O/--otype option explicitly")
268
    if itype is None:
269
        try:
270
            otype = API.find_loader(inpath).type()
271
        except API.UnknownFileTypeError:
272
            _exit_with_output((fmsg % inpath) + msg, 1)
273
        except ValueError:
274
            _exit_with_output(msg, 1)
275
    else:
276
        otype = itype
277
278
    return otype
279
280
281
def _try_dump(cnf, outpath, otype, fmsg):
282
    """
283
    :param cnf: Configuration object to print out
284
    :param outpath: Output file path or None
285
    :param otype: Output type or None
286
    :param fmsg: message if it cannot detect otype by `inpath`
287
    """
288
    try:
289
        API.dump(cnf, outpath, otype)
290
    except API.UnknownFileTypeError:
291
        _exit_with_output(fmsg % outpath, 1)
292
    except API.UnknownParserTypeError:
293
        _exit_with_output("Invalid output type '%s'" % otype, 1)
294
295
296
def _output_result(cnf, outpath, otype, inpath, itype):
297
    """
298
    :param cnf: Configuration object to print out
299
    :param outpath: Output file path or None
300
    :param otype: Output type or None
301
    :param inpath: Input file path
302
    :param itype: Input type or None
303
    """
304
    fmsg = ("Uknown file type and cannot detect appropriate backend "
305
            "from its extension, '%s'")
306
307
    if not anyconfig.utils.is_dict_like(cnf):
308
        _exit_with_output(str(cnf))  # Print primitive types as it is.
309
310
    if not outpath or outpath == "-":
311
        outpath = sys.stdout
312
        if otype is None:
313
            otype = _output_type_by_input_path(inpath, itype, fmsg)
314
315
    _try_dump(cnf, outpath, otype, fmsg)
316
317
318
def _load_diff(args):
319
    """
320
    :param args: :class:`~argparse.Namespace` object
321
    """
322
    try:
323
        diff = API.load(args.inputs, args.itype,
324
                        ignore_missing=args.ignore_missing,
325
                        ac_merge=args.merge,
326
                        ac_template=args.template,
327
                        ac_schema=args.schema)
328
    except API.UnknownParserTypeError:
329
        _exit_with_output("Wrong input type '%s'" % args.itype, 1)
330
    except API.UnknownFileTypeError:
331
        _exit_with_output("No appropriate backend was found for given file "
332
                          "'%s'" % args.itype, 1)
333
    _exit_if_load_failure(diff,
334
                          "Failed to load: args=%s" % ", ".join(args.inputs))
335
336
    return diff
337
338
339
def main(argv=None):
340
    """
341
    :param argv: Argument list to parse or None (sys.argv will be set).
342
    """
343
    parser = make_parser()
344
    args = parser.parse_args((argv if argv else sys.argv)[1:])
345
    LOGGER.setLevel(to_log_level(args.loglevel))
346
347
    _check_options_and_args(parser, args)
348
349
    cnf = os.environ.copy() if args.env else {}
350
    diff = _load_diff(args)
351
    API.merge(cnf, diff)
352
353
    if args.args:
354
        diff = anyconfig.parser.parse(args.args)
355
        API.merge(cnf, diff)
356
357
    _exit_if_only_to_validate(args.validate)
358
359
    if args.gen_schema:
360
        cnf = API.gen_schema(cnf)
361
362
    if args.query:
363
        cnf = _do_query(cnf, args.query)
364
    elif args.get:
365
        cnf = _do_get(cnf, args.get)
366
367
    if args.set:
368
        (key, val) = args.set.split('=')
369
        API.set_(cnf, key, anyconfig.parser.parse(val))
370
371
    _output_result(cnf, args.output, args.otype, args.inputs, args.itype)
372
373
374
if __name__ == '__main__':
375
    main(sys.argv)
376
377
# vim:sw=4:ts=4:et:
378