Completed
Push — master ( 146025...77da4a )
by Satoru
01:11
created

multi_load()   D

Complexity

Conditions 8

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
c 1
b 0
f 1
dl 0
loc 74
rs 4.9791

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 - 2017 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.8.2
9
10
   - Added new API, version to provide version information.
11
12
.. versionadded:: 0.8.0
13
14
   - Removed set_loglevel API as it does not help much.
15
   - Added :func:`open` API to open files with appropriate open mode.
16
   - Added custom exception classes, :class:`UnknownParserTypeError` and
17
     :class:`UnknownFileTypeError` to express specific errors.
18
   - Change behavior of the API :func:`find_loader` and others to make them
19
     fail firt and raise exceptions (ValueError, UnknownParserTypeError or
20
     UnknownFileTypeError) as much as possible if wrong parser type for uknown
21
     file type was given.
22
23
.. versionadded:: 0.5.0
24
25
   - Most keyword arguments passed to APIs are now position independent.
26
   - Added ac_namedtuple parameter to \*load and \*dump APIs.
27
28
.. versionchanged:: 0.3
29
30
   - Replaced `forced_type` optional argument of some public APIs with
31
     `ac_parser` to allow skip of config parser search by passing parser object
32
     previously found and instantiated.
33
34
     Also removed some optional arguments, `ignore_missing`, `merge` and
35
     `marker`, from definitions of some public APIs as these may not be changed
36
     from default in common use cases.
37
38
.. versionchanged:: 0.2
39
40
   - Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
41
     :func:`load` and :func:`dump` can process a file/file-like object or a
42
     list of file/file-like objects instead of a file path or a list of file
43
     paths.
44
45
.. versionadded:: 0.2
46
47
   - Export factory method (create) of anyconfig.mergeabledict.MergeableDict
48
"""
49
from __future__ import absolute_import
50
51
import os.path
52
53
from anyconfig.globals import LOGGER
54
import anyconfig.backends
55
import anyconfig.backend.json
56
import anyconfig.compat
57
import anyconfig.globals
58
import anyconfig.mdicts
59
import anyconfig.template
60
import anyconfig.utils
61
62
# Import some global constants will be re-exported:
63
from anyconfig.backends import (
64
    UnknownParserTypeError, UnknownFileTypeError
65
)
66
from anyconfig.mdicts import (
67
    MS_REPLACE, MS_NO_REPLACE, MS_DICTS, MS_DICTS_AND_LISTS, MERGE_STRATEGIES,
68
    get, set_, to_container  # flake8: noqa
69
)
70
from anyconfig.schema import validate, gen_schema
71
from anyconfig.utils import is_path
72
73
# Re-export and aliases:
74
list_types = anyconfig.backends.list_types  # flake8: noqa
75
76
77
def _is_paths(maybe_paths):
78
    """
79
    Does given object `maybe_paths` consist of path or path pattern strings?
80
    """
81
    if anyconfig.utils.is_iterable(maybe_paths):
82
        return not getattr(maybe_paths, "read", False)
83
84
    return False  # Not an iterable at least.
85
86
87
def _maybe_validated(cnf, schema, format_checker=None, **options):
88
    """
89
    :param cnf: Configuration object
90
    :param schema: JSON schema object
91
    :param format_checker: A format property checker object of which class is
92
        inherited from jsonschema.FormatChecker, it's default if None given.
93
    :param options: Keyword options such as:
94
95
        - ac_namedtuple: Convert result to nested namedtuple object if True
96
97
    :return: Given `cnf` as it is if validation succeeds else None
98
    """
99
    valid = True
100
    if schema:
101
        (valid, msg) = validate(cnf, schema, format_checker=format_checker,
102
                                safe=True)
103
        if msg:
104
            LOGGER.warning(msg)
105
106
    if valid:
107
        if options.get("ac_namedtuple", False):
108
            return anyconfig.mdicts.convert_to(cnf, ac_namedtuple=True)
109
        else:
110
            return cnf
111
112
    return None
113
114
115
def version():
116
    """
117
    :return: A tuple of version info, (major, minor, release), e.g. (0, 8, 2)
118
    """
119
    return anyconfig.globals.VERSION.split('.')
120
121
122
def find_loader(path_or_stream, parser_or_type=None, is_path_=False):
123
    """
124
    Find out config parser object appropriate to load from a file of given path
125
    or file/file-like object.
126
127
    :param path_or_stream: Configuration file path or file / file-like object
128
    :param parser_or_type: Forced configuration parser type or parser object
129
    :param is_path_: True if given `path_or_stream` is a file path
130
131
    :return: Config parser instance or None
132
    """
133
    if anyconfig.backends.is_parser(parser_or_type):
134
        return parser_or_type
135
136
    try:
137
        psr = anyconfig.backends.find_parser(path_or_stream,
138
                                             forced_type=parser_or_type,
139
                                             is_path_=is_path_)
140
        LOGGER.debug("Using config parser: %r [%s]", psr, psr.type())
141
        return psr()  # TBD: Passing initialization arguments.
142
    except (ValueError, UnknownParserTypeError, UnknownFileTypeError):
143
        raise
144
145
146
def _load_schema(**options):
147
    """
148
    :param options: Optional keyword arguments such as
149
150
        - ac_template: Assume configuration file may be a template file and try
151
          to compile it AAR if True
152
        - ac_context: A dict presents context to instantiate template
153
        - ac_schema: JSON schema file path to validate given config file
154
    """
155
    ac_schema = options.get("ac_schema", None)
156
    if ac_schema is not None:
157
        # Try to detect the appropriate as it may be different from the
158
        # original config file's format, perhaps.
159
        options["ac_parser"] = None
160
        options["ac_schema"] = None  # Avoid infinite loop.
161
        LOGGER.info("Loading schema: %s", ac_schema)
162
        return load(ac_schema, **options)
163
164
    return None
165
166
167
def open(path, mode=None, ac_parser=None, **options):
168
    """
169
    Open given config file with appropriate open flag.
170
171
    :param path: Configuration file path
172
    :param mode:
173
        Can be 'r' and 'rb' for reading (default) or 'w', 'wb' for writing.
174
        Please note that even if you specify 'r' or 'w', it will be changed to
175
        'rb' or 'wb' if the backend selected, xml and configobj for example,
176
        for given config file prefer that.
177
    :param options:
178
        Optional keyword arguments passed to the internal file opening API of
179
        each backends such like 'buffering' optional parameter passed to
180
        builtin 'open' function.
181
182
    :return: A file object or None on any errors
183
    """
184
    psr = find_loader(path, parser_or_type=ac_parser, is_path_=True)
185
    if mode is not None and mode.startswith('w'):
186
        return psr.wopen(path, **options)
187
188
    return psr.ropen(path, **options)
189
190
191
def single_load(path_or_stream, ac_parser=None, ac_template=False,
192
                ac_context=None, **options):
193
    """
194
    Load single config file.
195
196
    .. note::
197
198
       :func:`load` is a preferable alternative and this API should be used
199
       only if there is a need to emphasize given file path is single one.
200
201
    :param path_or_stream: Configuration file path or file / file-like object
202
    :param ac_parser: Forced parser type or parser object
203
    :param ac_template: Assume configuration file may be a template file and
204
        try to compile it AAR if True
205
    :param ac_context: A dict presents context to instantiate template
206
    :param options: Optional keyword arguments such as:
207
208
        - Common options:
209
210
          - ac_namedtuple: Convert result to nested namedtuple object if True
211
          - ac_schema: JSON schema file path to validate given config file
212
213
        - Common backend options:
214
215
          - ignore_missing: Ignore and just return empty result if given file
216
            (``path_or_stream``) does not exist.
217
218
        - Backend specific options such as {"indent": 2} for JSON backend
219
220
    :return: dict or dict-like object supports merge operations
221
    """
222
    is_path_ = is_path(path_or_stream)
223
    if is_path_:
224
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
225
        filepath = path_or_stream
226
    else:
227
        filepath = anyconfig.utils.get_path_from_stream(path_or_stream)
228
229
    psr = find_loader(path_or_stream, ac_parser, is_path_)
230
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
231
                          **options)
232
    options["ac_schema"] = None  # It's not needed now.
233
234
    LOGGER.info("Loading: %s", filepath)
235
    if ac_template and filepath is not None:
236
        try:
237
            LOGGER.debug("Compiling: %s", filepath)
238
            content = anyconfig.template.render(filepath, ac_context)
239
            cnf = psr.loads(content, **options)
240
            return _maybe_validated(cnf, schema, **options)
241
242
        except Exception as exc:
243
            LOGGER.warning("Failed to compile %s, fallback to no template "
244
                           "mode, exc=%r", path_or_stream, exc)
245
246
    cnf = psr.load(path_or_stream, **options)
247
    return _maybe_validated(cnf, schema, **options)
248
249
250
def multi_load(paths, ac_parser=None, ac_template=False, ac_context=None,
251
               **options):
252
    """
253
    Load multiple config files.
254
255
    .. note::
256
257
       :func:`load` is a preferable alternative and this API should be used
258
       only if there is a need to emphasize given file paths are multiple ones.
259
260
    The first argument `paths` may be a list of config file paths or
261
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
262
    the dir /etc/foo/conf.d/, the followings give same results::
263
264
      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
265
                  "/etc/foo/conf.d/c.yml", ])
266
267
      multi_load("/etc/foo/conf.d/*.yml")
268
269
    :param paths: List of config file paths or a glob pattern to list paths, or
270
        a list of file/file-like objects
271
    :param ac_parser: Forced parser type or parser object
272
    :param ac_template: Assume configuration file may be a template file and
273
        try to compile it AAR if True
274
    :param ac_context: A dict presents context to instantiate template
275
    :param options: Optional keyword arguments:
276
277
        - Common options:
278
279
          - ac_merge (merge): Specify strategy of how to merge results loaded
280
            from multiple configuration files. See the doc of :mod:`m9dicts`
281
            for more details of strategies. The default is m9dicts.MS_DICTS.
282
283
          - ac_marker (marker): Globbing marker to detect paths patterns.
284
          - ac_namedtuple: Convert result to nested namedtuple object if True
285
          - ac_schema: JSON schema file path to validate given config file
286
287
        - Common backend options:
288
289
          - ignore_missing: Ignore and just return empty result if given file
290
            (``path_or_stream``) does not exist.
291
292
        - Backend specific options such as {"indent": 2} for JSON backend
293
294
    :return: dict or dict-like object supports merge operations
295
    """
296
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
297
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
298
                          **options)
299
    options["ac_schema"] = None  # It's not needed now.
300
301
    cnf = to_container(ac_context, **options)
302
    same_type = anyconfig.utils.are_same_file_types(paths)
303
304
    if is_path(paths) and marker in paths:
305
        paths = anyconfig.utils.sglob(paths)
306
307
    for path in paths:
308
        opts = options.copy()
309
        opts["ac_namedtuple"] = False  # Disabled within this loop.
310
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
311
        if is_path(path) and marker in path:
312
            cups = multi_load(path, ac_parser=ac_parser,
313
                              ac_template=ac_template, ac_context=cnf, **opts)
314
        else:
315
            if same_type:
316
                ac_parser = find_loader(path, ac_parser, is_path(path))
317
            cups = single_load(path, ac_parser=ac_parser,
318
                               ac_template=ac_template, ac_context=cnf, **opts)
319
320
        if cups:
321
            cnf.update(cups)
322
323
    return _maybe_validated(cnf, schema, **options)
324
325
326
def load(path_specs, ac_parser=None, ac_template=False, ac_context=None,
327
         **options):
328
    r"""
329
    Load single or multiple config files or multiple config files specified in
330
    given paths pattern.
331
332
    :param path_specs: Configuration file path or paths or its pattern such as
333
        r'/a/b/\*.json' or a list of files/file-like objects
334
    :param ac_parser: Forced parser type or parser object
335
    :param ac_template: Assume configuration file may be a template file and
336
        try to compile it AAR if True
337
    :param ac_context: A dict presents context to instantiate template
338
    :param options:
339
        Optional keyword arguments. See also the description of `options` in
340
        `multi_load` function.
341
342
    :return: dict or dict-like object supports merge operations
343
    """
344
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
345
346
    if is_path(path_specs) and marker in path_specs or _is_paths(path_specs):
347
        return multi_load(path_specs, ac_parser=ac_parser,
348
                          ac_template=ac_template, ac_context=ac_context,
349
                          **options)
350
    else:
351
        return single_load(path_specs, ac_parser=ac_parser,
352
                           ac_template=ac_template, ac_context=ac_context,
353
                           **options)
354
355
356
def loads(content, ac_parser=None, ac_template=False, ac_context=None,
357
          **options):
358
    """
359
    :param content: Configuration file's content
360
    :param ac_parser: Forced parser type or parser object
361
    :param ac_template: Assume configuration file may be a template file and
362
        try to compile it AAR if True
363
    :param ac_context: Context dict to instantiate template
364
    :param options:
365
        Optional keyword arguments. See also the description of `options` in
366
        `single_load` function.
367
368
    :return: dict or dict-like object supports merge operations
369
    """
370
    msg = "Try parsing with a built-in parser because %s"
371
    if ac_parser is None:
372
        LOGGER.warning(msg, "ac_parser was not given.")
373
        return None
374
375
    psr = find_loader(None, ac_parser)
376
    schema = None
377
    ac_schema = options.get("ac_schema", None)
378
    if ac_schema is not None:
379
        options["ac_schema"] = None
380
        schema = loads(ac_schema, ac_parser=psr, ac_template=ac_template,
381
                       ac_context=ac_context, **options)
382
383
    if ac_template:
384
        try:
385
            LOGGER.debug("Compiling")
386
            content = anyconfig.template.render_s(content, ac_context)
387
        except Exception as exc:
388
            LOGGER.warning("Failed to compile and fallback to no template "
389
                           "mode: '%s', exc=%r", content[:50] + '...', exc)
390
391
    cnf = psr.loads(content, **options)
392
    return _maybe_validated(cnf, schema, **options)
393
394
395
def _find_dumper(path_or_stream, ac_parser=None):
396
    """
397
    Find configuration parser to dump data.
398
399
    :param path_or_stream: Output file path or file / file-like object
400
    :param ac_parser: Forced parser type or parser object
401
402
    :return: Parser-inherited class object
403
    """
404
    return find_loader(path_or_stream, ac_parser)
405
406
407
def dump(data, path_or_stream, ac_parser=None, **options):
408
    """
409
    Save `data` as `path_or_stream`.
410
411
    :param data: Config data object (dict[-like] or namedtuple) to dump
412
    :param path_or_stream: Output file path or file / file-like object
413
    :param ac_parser: Forced parser type or parser object
414
    :param options:
415
        Backend specific optional arguments, e.g. {"indent": 2} for JSON
416
        loader/dumper backend
417
    """
418
    dumper = _find_dumper(path_or_stream, ac_parser)
419
    LOGGER.info("Dumping: %s",
420
                anyconfig.utils.get_path_from_stream(path_or_stream))
421
    if anyconfig.mdicts.is_namedtuple(data):
422
        data = to_container(data, **options)  # namedtuple -> dict-like
423
    dumper.dump(data, path_or_stream, **options)
424
425
426
def dumps(data, ac_parser=None, **options):
427
    """
428
    Return string representation of `data` in forced type format.
429
430
    :param data: Config data object to dump
431
    :param ac_parser: Forced parser type or parser object
432
    :param options: see :func:`dump`
433
434
    :return: Backend-specific string representation for the given data
435
    """
436
    if anyconfig.mdicts.is_namedtuple(data):
437
        data = to_container(data, **options)
438
    return _find_dumper(None, ac_parser).dumps(data, **options)
439
440
# vim:sw=4:ts=4:et:
441