Completed
Push — master ( bff2ab...5dc771 )
by Satoru
01:07
created

multi_load()   C

Complexity

Conditions 8

Size

Total Lines 69

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
dl 0
loc 69
rs 5.2436

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