Completed
Push — master ( 0dfbce...9b7a95 )
by Satoru
18:11 queued 14:58
created

anyconfig.find_loader()   B

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 24
rs 8.6845
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 err:
119
        LOGGER.error(err)
120
121
    if psr is None:
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
    if ac_parser is None:
322
        LOGGER.warning("No type or parser was given. Try to parse...")
323
        return anyconfig.parser.parse(content)
324
325
    psr = find_loader(None, ac_parser)
326
    if psr is None:
327
        return anyconfig.parser.parse(content)
328
329
    schema = None
330
    ac_schema = options.get("ac_schema", None)
331
    if ac_schema is not None:
332
        options["ac_schema"] = None
333
        schema = loads(ac_schema, ac_parser=psr, ac_template=ac_template,
334
                       ac_context=ac_context, **options)
335
336
    if ac_template:
337
        try:
338
            LOGGER.debug("Compiling")
339
            content = anyconfig.template.render_s(content, ac_context)
340
        except Exception as exc:
341
            LOGGER.warning("Failed to compile and fallback to no template "
342
                           "mode: '%s', exc=%r", content[:50] + '...', exc)
343
344
    cnf = psr.loads(content, **options)
345
    return _maybe_validated(cnf, schema, **options)
346
347
348
def _find_dumper(path_or_stream, ac_parser=None):
349
    """
350
    Find configuration parser to dump data.
351
352
    :param path_or_stream: Output file path or file / file-like object
353
    :param ac_parser: Forced parser type or parser object
354
355
    :return: Parser-inherited class object
356
    """
357
    psr = find_loader(path_or_stream, ac_parser)
358
359
    if psr is None or not getattr(psr, "dump", False):
360
        LOGGER.warning("Dump method not implemented. Fallback to json.Parser")
361
        psr = anyconfig.backend.json.Parser()
362
363
    return psr
364
365
366
def dump(data, path_or_stream, ac_parser=None, **options):
367
    """
368
    Save `data` as `path_or_stream`.
369
370
    :param data: Config data object (dict[-like] or namedtuple) to dump
371
    :param path_or_stream: Output file path or file / file-like object
372
    :param ac_parser: Forced parser type or parser object
373
    :param options:
374
        Backend specific optional arguments, e.g. {"indent": 2} for JSON
375
        loader/dumper backend
376
    """
377
    dumper = _find_dumper(path_or_stream, ac_parser)
378
    LOGGER.info("Dumping: %s",
379
                anyconfig.utils.get_path_from_stream(path_or_stream))
380
    if anyconfig.mdicts.is_namedtuple(data):
381
        data = to_container(data, **options)  # namedtuple -> dict-like
382
    dumper.dump(data, path_or_stream, **options)
383
384
385
def dumps(data, ac_parser=None, **options):
386
    """
387
    Return string representation of `data` in forced type format.
388
389
    :param data: Config data object to dump
390
    :param ac_parser: Forced parser type or parser object
391
    :param options: see :func:`dump`
392
393
    :return: Backend-specific string representation for the given data
394
    """
395
    if anyconfig.mdicts.is_namedtuple(data):
396
        data = to_container(data, **options)
397
    return _find_dumper(None, ac_parser).dumps(data, **options)
398
399
# vim:sw=4:ts=4:et:
400