Completed
Pull Request — master (#39)
by Satoru
01:04
created

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