multi_load()   C
last analyzed

Complexity

Conditions 6

Size

Total Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 2 Features 0
Metric Value
cc 6
c 4
b 2
f 0
dl 0
loc 83
rs 6.623

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
# Copyright (C) 2012 - 2018 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
# pylint: disable=unused-import,import-error,invalid-name
6
r"""Public APIs of anyconfig module.
7
8
.. versionadded:: 0.9.5
9
10
   - Added pathlib support. Now all of load and dump APIs can process
11
     pathlib.Path object basically.
12
   - 'ignore_missing' keyword option for load APIs are now marked as deprecated
13
     and will be removed soon.
14
   - Allow to load data other than mapping objects for some backends such as
15
     JSON and YAML.
16
17
.. versionadded:: 0.8.3
18
19
   - Added ac_dict keyword option to pass dict factory (any callable like
20
     function or class) to make dict-like object in backend parsers.
21
   - Added ac_query keyword option to query data with JMESPath expression.
22
   - Added experimental query api to query data with JMESPath expression.
23
   - Removed ac_namedtuple keyword option.
24
   - Export :func:`merge`.
25
   - Stop exporting :func:`to_container` which was deprecated and removed.
26
27
.. versionadded:: 0.8.2
28
29
   - Added new API, version to provide version information.
30
31
.. versionadded:: 0.8.0
32
33
   - Removed set_loglevel API as it does not help much.
34
   - Added :func:`open` API to open files with appropriate open mode.
35
   - Added custom exception classes, :class:`UnknownProcessorTypeError` and
36
     :class:`UnknownFileTypeError` to express specific errors.
37
   - Change behavior of the API :func:`find_loader` and others to make them
38
     fail firt and raise exceptions (ValueError, UnknownProcessorTypeError or
39
     UnknownFileTypeError) as much as possible if wrong parser type for uknown
40
     file type was given.
41
42
.. versionadded:: 0.5.0
43
44
   - Most keyword arguments passed to APIs are now position independent.
45
   - Added ac_namedtuple parameter to \*load and \*dump APIs.
46
47
.. versionchanged:: 0.3
48
49
   - Replaced `forced_type` optional argument of some public APIs with
50
     `ac_parser` to allow skip of config parser search by passing parser object
51
     previously found and instantiated.
52
53
     Also removed some optional arguments, `ignore_missing`, `merge` and
54
     `marker`, from definitions of some public APIs as these may not be changed
55
     from default in common use cases.
56
57
.. versionchanged:: 0.2
58
59
   - Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
60
     :func:`load` and :func:`dump` can process a file/file-like object or a
61
     list of file/file-like objects instead of a file path or a list of file
62
     paths.
63
64
.. versionadded:: 0.2
65
66
   - Export factory method (create) of anyconfig.mergeabledict.MergeableDict
67
"""
68
from __future__ import absolute_import
69
70
import os.path
71
import warnings
72
73
# Import some global constants will be re-exported:
74
from anyconfig.globals import (
75
    LOGGER, IOI_PATH_OBJ, UnknownProcessorTypeError, UnknownFileTypeError
76
)
77
import anyconfig.backends
78
import anyconfig.backend.json
79
import anyconfig.compat
80
import anyconfig.query
81
import anyconfig.globals
82
import anyconfig.dicts
83
import anyconfig.template
84
import anyconfig.utils
85
86
from anyconfig.dicts import (
87
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
88
    get, set_, merge  # flake8: noqa
89
)
90
from anyconfig.schema import validate, gen_schema
91
92
# Re-export and aliases:
93
list_types = anyconfig.backends.list_types  # flake8: noqa
94
95
96
def version():
97
    """
98
    :return: A tuple of version info, (major, minor, release), e.g. (0, 8, 2)
99
    """
100
    return anyconfig.globals.VERSION.split('.')
101
102
103
def _try_validate(cnf, schema, **options):
104
    """
105
    :param cnf: Mapping object represents configuration data
106
    :param schema: JSON schema object
107
    :param options: Keyword options passed to :func:`~jsonschema.validate`
108
109
    :return: Given `cnf` as it is if validation succeeds else None
110
    """
111
    valid = True
112
    if schema:
113
        (valid, msg) = validate(cnf, schema, **options)
114
        if msg:
115
            LOGGER.warning(msg)
116
117
    if valid:
118
        return cnf
119
120
    return None
121
122
123
def find_loader(path, parser_or_type=None):
124
    """
125
    Find out parser object appropriate to load configuration from a file of
126
    given path or file or file-like object.
127
128
    :param path:
129
        Configuration file path or file or file-like object or pathlib.Path
130
        object if it's available
131
    :param parser_or_type:
132
        Forced configuration parser type or parser object itself
133
134
    :return:
135
        An instance of a class inherits :class:`~anyconfig.backend.base.Parser`
136
        or None
137
    """
138
    if anyconfig.backends.is_parser(parser_or_type):
139
        return parser_or_type
140
141
    try:
142
        return anyconfig.backends.find_parser(path,
143
                                              forced_type=parser_or_type)
144
    except (ValueError, UnknownProcessorTypeError, UnknownFileTypeError):
145
        raise
146
147
148
def _maybe_schema(**options):
149
    """
150
    :param options: Optional keyword arguments such as
151
152
        - ac_template: Assume configuration file may be a template file and try
153
          to compile it AAR if True
154
        - ac_context: Mapping object presents context to instantiate template
155
        - ac_schema: JSON schema file path to validate configuration files
156
157
    :return: Mapping object or None means some errors
158
    """
159
    ac_schema = options.get("ac_schema", None)
160
    if ac_schema is not None:
161
        # Try to detect the appropriate parser to load the schema data as it
162
        # may be different from the original config file's format, perhaps.
163
        options["ac_parser"] = None
164
        options["ac_schema"] = None  # Avoid infinite loop.
165
        LOGGER.info("Loading schema: %s", ac_schema)
166
        return load(ac_schema, **options)
167
168
    return None
169
170
171
# pylint: disable=redefined-builtin
172
def open(path, mode=None, ac_parser=None, **options):
173
    """
174
    Open given configuration file with appropriate open flag.
175
176
    :param path: Configuration file path
177
    :param mode:
178
        Can be 'r' and 'rb' for reading (default) or 'w', 'wb' for writing.
179
        Please note that even if you specify 'r' or 'w', it will be changed to
180
        'rb' or 'wb' if selected backend, xml and configobj for example, for
181
        given config file prefer that.
182
    :param options:
183
        Optional keyword arguments passed to the internal file opening APIs of
184
        each backends such like 'buffering' optional parameter passed to
185
        builtin 'open' function.
186
187
    :return: A file object or None on any errors
188
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
189
    """
190
    psr = anyconfig.backends.find_parser(path, forced_type=ac_parser)
191
192
    if mode is not None and mode.startswith('w'):
193
        return psr.wopen(path, **options)
194
195
    return psr.ropen(path, **options)
196
197
198
def _single_load(input_, ac_parser=None, ac_template=False,
199
                 ac_context=None, **options):
200
    """
201
    :param input_:
202
        File path or file or file-like object or pathlib.Path object represents
203
        the file or a namedtuple `~anyconfig.globals.IOInfo` object represents
204
        some input to load some data from
205
    :param ac_parser: Forced parser type or parser object itself
206
    :param ac_template:
207
        Assume configuration file may be a template file and try to compile it
208
        AAR if True
209
    :param ac_context: A dict presents context to instantiate template
210
    :param options:
211
        Optional keyword arguments :func:`single_load` supports except for
212
        ac_schema and ac_query
213
214
    :return: Mapping object
215
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
216
    """
217
    ioi = anyconfig.backends.inspect_io_obj(input_, forced_type=ac_parser)
218
    (psr, filepath) = (ioi.processor, ioi.path)
219
220
    # .. note::
221
    #    This will be kept for backward compatibility until 'ignore_missing'
222
    #    option is deprecated and removed completely.
223
    if "ignore_missing" in options:
224
        warnings.warn("keyword option 'ignore_missing' is deprecated, use "
225
                      "'ac_ignore_missing' instead", DeprecationWarning)
226
        options["ac_ignore_missing"] = options["ignore_missing"]
227
228
    LOGGER.info("Loading: %s", filepath)
229
    if ac_template and filepath is not None:
230
        content = anyconfig.template.try_render(filepath=filepath,
231
                                                ctx=ac_context, **options)
232
        if content is not None:
233
            return psr.loads(content, **options)
234
235
    return psr.load(ioi, **options)
236
237
238
def single_load(input_, ac_parser=None, ac_template=False,
239
                ac_context=None, **options):
240
    """
241
    Load single configuration file.
242
243
    .. note::
244
245
       :func:`load` is a preferable alternative and this API should be used
246
       only if there is a need to emphasize given input `input_` is single one.
247
248
    :param input_:
249
        File path or file or file-like object or pathlib.Path object represents
250
        the file or a namedtuple `~anyconfig.globals.IOInfo` object represents
251
        some input to load some data from
252
    :param ac_parser: Forced parser type or parser object itself
253
    :param ac_template:
254
        Assume configuration file may be a template file and try to compile it
255
        AAR if True
256
    :param ac_context: A dict presents context to instantiate template
257
    :param options: Optional keyword arguments such as:
258
259
        - Options common in :func:`single_load`, :func:`multi_load`,
260
          :func:`load` and :func:`loads`:
261
262
          - ac_dict: callable (function or class) to make mapping objects from
263
            loaded data if the selected backend can customize that such as JSON
264
            which supports that with 'object_pairs_hook' option, or None. If
265
            this option was not given or None, dict or :class:`OrderedDict`
266
            will be used to make result as mapping object depends on if
267
            ac_ordered (see below) is True and selected backend can keep the
268
            order of items loaded. See also :meth:`_container_factory` of
269
            :class:`~anyconfig.backend.base.Parser` for more implementation
270
            details.
271
272
          - ac_ordered: True if you want to keep resuls ordered. Please note
273
            that order of items may be lost depends on the selected backend.
274
275
          - ac_schema: JSON schema file path to validate given config file
276
          - ac_query: JMESPath expression to query data
277
278
        - Common backend options:
279
280
          - ac_ignore_missing:
281
            Ignore and just return empty result if given file ``input_`` does
282
            not exist actually.
283
284
        - Backend specific options such as {"indent": 2} for JSON backend
285
286
    :return: Mapping object
287
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
288
    """
289
    cnf = _single_load(input_, ac_parser=ac_parser, ac_template=ac_template,
290
                       ac_context=ac_context, **options)
291
    schema = _maybe_schema(ac_template=ac_template, ac_context=ac_context,
292
                           **options)
293
    cnf = _try_validate(cnf, schema, **options)
294
    return anyconfig.query.query(cnf, **options)
295
296
297
def multi_load(inputs, ac_parser=None, ac_template=False, ac_context=None,
298
               **options):
299
    r"""
300
    Load multiple config files.
301
302
    .. note::
303
304
       :func:`load` is a preferable alternative and this API should be used
305
       only if there is a need to emphasize given inputs are multiple ones.
306
307
    The first argument `inputs` may be a list of a file paths or a glob pattern
308
    specifying them or a pathlib.Path object represents file[s] or a namedtuple
309
    `~anyconfig.globals.IOInfo` object represents some inputs to load some data
310
    from.
311
312
    About glob patterns, for example, is, if a.yml, b.yml and c.yml are in the
313
    dir /etc/foo/conf.d/, the followings give same results::
314
315
      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
316
                  "/etc/foo/conf.d/c.yml", ])
317
318
      multi_load("/etc/foo/conf.d/*.yml")
319
320
    :param inputs:
321
        A list of file path or a glob pattern such as r'/a/b/\*.json'to list of
322
        files, file or file-like object or pathlib.Path object represents the
323
        file or a namedtuple `~anyconfig.globals.IOInfo` object represents some
324
        inputs to load some data from
325
    :param ac_parser: Forced parser type or parser object
326
    :param ac_template: Assume configuration file may be a template file and
327
        try to compile it AAR if True
328
    :param ac_context: Mapping object presents context to instantiate template
329
    :param options: Optional keyword arguments:
330
331
        - ac_dict, ac_ordered, ac_schema and ac_query are the options common in
332
          :func:`single_load`, :func:`multi_load`, :func:`load`: and
333
          :func:`loads`. See the descriptions of them in :func:`single_load`.
334
335
        - Options specific to this function and :func:`load`:
336
337
          - ac_merge (merge): Specify strategy of how to merge results loaded
338
            from multiple configuration files. See the doc of
339
            :mod:`anyconfig.dicts` for more details of strategies. The default
340
            is anyconfig.dicts.MS_DICTS.
341
342
          - ac_marker (marker): Globbing marker to detect paths patterns.
343
344
        - Common backend options:
345
346
          - ignore_missing: Ignore and just return empty result if given file
347
            ``path`` does not exist.
348
349
        - Backend specific options such as {"indent": 2} for JSON backend
350
351
    :return: Mapping object or any query result might be primitive objects
352
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
353
    """
354
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
355
    schema = _maybe_schema(ac_template=ac_template, ac_context=ac_context,
356
                           **options)
357
    options["ac_schema"] = None  # Avoid to load schema more than twice.
358
359
    paths = anyconfig.utils.expand_paths(inputs, marker=marker)
360
    if anyconfig.utils.are_same_file_types(paths):
361
        ac_parser = anyconfig.backends.find_parser(paths[0],
362
                                                   forced_type=ac_parser)
363
364
    cnf = ac_context
365
    for path in paths:
366
        opts = options.copy()
367
        cups = _single_load(path, ac_parser=ac_parser,
368
                            ac_template=ac_template, ac_context=cnf, **opts)
369
        if cups:
370
            if cnf is None:
371
                cnf = cups
372
            else:
373
                merge(cnf, cups, **options)
374
375
    if cnf is None:
376
        return anyconfig.dicts.convert_to({}, **options)
377
378
    cnf = _try_validate(cnf, schema, **options)
379
    return anyconfig.query.query(cnf, **options)
380
381
382
def load(path_specs, ac_parser=None, ac_dict=None, ac_template=False,
383
         ac_context=None, **options):
384
    r"""
385
    Load single or multiple config files or multiple config files specified in
386
    given paths pattern or pathlib.Path object represents config files or a
387
    namedtuple `~anyconfig.globals.IOInfo` object represents some inputs.
388
389
    :param path_specs:
390
        A list of file path or a glob pattern such as r'/a/b/\*.json'to list of
391
        files, file or file-like object or pathlib.Path object represents the
392
        file or a namedtuple `~anyconfig.globals.IOInfo` object represents some
393
        inputs to load some data from.
394
    :param ac_parser: Forced parser type or parser object
395
    :param ac_dict:
396
        callable (function or class) to make mapping object will be returned as
397
        a result or None. If not given or ac_dict is None, default mapping
398
        object used to store resutls is dict or
399
        :class:`~collections.OrderedDict` if ac_ordered is True and selected
400
        backend can keep the order of items in mapping objects.
401
402
    :param ac_template: Assume configuration file may be a template file and
403
        try to compile it AAR if True
404
    :param ac_context: A dict presents context to instantiate template
405
    :param options:
406
        Optional keyword arguments. See also the description of `options` in
407
        :func:`single_load` and :func:`multi_load`
408
409
    :return: Mapping object or any query result might be primitive objects
410
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
411
    """
412
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
413
414
    if anyconfig.utils.is_path_like_object(path_specs, marker):
415
        return single_load(path_specs, ac_parser=ac_parser, ac_dict=ac_dict,
416
                           ac_template=ac_template, ac_context=ac_context,
417
                           **options)
418
419
    if not anyconfig.utils.is_paths(path_specs, marker):
420
        raise ValueError("Something goes wrong with your input %r", path_specs)
421
422
    return multi_load(path_specs, ac_parser=ac_parser, ac_dict=ac_dict,
423
                      ac_template=ac_template, ac_context=ac_context,
424
                      **options)
425
426
427
def loads(content, ac_parser=None, ac_dict=None, ac_template=False,
428
          ac_context=None, **options):
429
    """
430
    :param content: Configuration file's content (a string)
431
    :param ac_parser: Forced parser type or parser object
432
    :param ac_dict:
433
        callable (function or class) to make mapping object will be returned as
434
        a result or None. If not given or ac_dict is None, default mapping
435
        object used to store resutls is dict or
436
        :class:`~collections.OrderedDict` if ac_ordered is True and selected
437
        backend can keep the order of items in mapping objects.
438
    :param ac_template: Assume configuration file may be a template file and
439
        try to compile it AAR if True
440
    :param ac_context: Context dict to instantiate template
441
    :param options:
442
        Optional keyword arguments. See also the description of `options` in
443
        :func:`single_load` function.
444
445
    :return: Mapping object or any query result might be primitive objects
446
    :raises: ValueError, UnknownProcessorTypeError
447
    """
448
    if ac_parser is None:
449
        LOGGER.warning("ac_parser was not given but it's must to find correct "
450
                       "parser to load configurations from string.")
451
        return None
452
453
    psr = anyconfig.backends.find_parser_by_type(ac_parser)
454
    schema = None
455
    ac_schema = options.get("ac_schema", None)
456
    if ac_schema is not None:
457
        options["ac_schema"] = None
458
        schema = loads(ac_schema, ac_parser=psr, ac_dict=ac_dict,
459
                       ac_template=ac_template, ac_context=ac_context,
460
                       **options)
461
462
    if ac_template:
463
        compiled = anyconfig.template.try_render(content=content,
464
                                                 ctx=ac_context, **options)
465
        if compiled is not None:
466
            content = compiled
467
468
    cnf = psr.loads(content, ac_dict=ac_dict, **options)
469
    cnf = _try_validate(cnf, schema, **options)
470
    return anyconfig.query.query(cnf, **options)
471
472
473
def dump(data, out, ac_parser=None, **options):
474
    """
475
    Save `data` to `out`.
476
477
    :param data: A mapping object may have configurations data to dump
478
    :param out:
479
        An output file path or a file or a file-like object or a `pathlib.Path`
480
        object represents the file or a namedtuple `~anyconfig.globals.IOInfo`
481
        object represents output to dump some data to.
482
    :param ac_parser: Forced parser type or parser object
483
    :param options:
484
        Backend specific optional arguments, e.g. {"indent": 2} for JSON
485
        loader/dumper backend
486
487
    :raises: ValueError, UnknownProcessorTypeError, UnknownFileTypeError
488
    """
489
    ioi = anyconfig.backends.inspect_io_obj(out, forced_type=ac_parser)
490
    LOGGER.info("Dumping: %s", ioi.path)
491
    ioi.processor.dump(data, ioi, **options)
492
493
494
def dumps(data, ac_parser=None, **options):
495
    """
496
    Return string representation of `data` in forced type format.
497
498
    :param data: Config data object to dump
499
    :param ac_parser: Forced parser type or parser object
500
    :param options: see :func:`dump`
501
502
    :return: Backend-specific string representation for the given data
503
    :raises: ValueError, UnknownProcessorTypeError
504
    """
505
    psr = anyconfig.backends.find_parser_by_type(ac_parser)
506
    return psr.dumps(data, **options)
507
508
509
def query(data, expression, **options):
510
    """
511
    API just wraps :func:`anyconfig.query.query`.
512
513
    :param data: Config data object to query
514
    :param expression: JMESPath expression to query data
515
    :param options: Ignored in current implementation
516
517
    :return: Query result object may be a primitive (int, str, etc.) or dict.
518
    """
519
    return anyconfig.query.query(data, ac_query=expression)
520
521
# vim:sw=4:ts=4:et:
522