Completed
Push — master ( f240dc...134f6d )
by Satoru
58s
created

set_loglevel()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
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.7.99
9
   Removed set_loglevel API as it does not help much.
10
11
.. versionadded:: 0.5.0
12
   - Most keyword arguments passed to APIs are now position independent.
13
   - Added ac_namedtuple parameter to \*load and \*dump APIs.
14
15
.. versionchanged:: 0.3
16
   Replaced `forced_type` optional argument of some public APIs with
17
   `ac_parser` to allow skip of config parser search by passing parser object
18
   previously found and instantiated.
19
20
   Also removed some optional arguments, `ignore_missing`, `merge` and
21
   `marker`, from definitions of some public APIs as these may not be changed
22
   from default in common use cases.
23
24
.. versionchanged:: 0.2
25
   Now APIs :func:`find_loader`, :func:`single_load`, :func:`multi_load`,
26
   :func:`load` and :func:`dump` can process a file/file-like object or a list
27
   of file/file-like objects instead of a file path or a list of file paths.
28
29
.. versionadded:: 0.2
30
   Export factory method (create) of anyconfig.mergeabledict.MergeableDict
31
"""
32
from __future__ import absolute_import
33
34
import os.path
35
36
from anyconfig.globals import LOGGER
37
import anyconfig.backends
38
import anyconfig.backend.json
39
import anyconfig.compat
40
import anyconfig.mdicts
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
    valid = True
79
    if schema:
80
        (valid, msg) = validate(cnf, schema, format_checker=format_checker,
81
                                safe=True)
82
        if msg:
83
            LOGGER.warning(msg)
84
85
    if valid:
86
        if options.get("ac_namedtuple", False):
87
            return anyconfig.mdicts.convert_to(cnf, ac_namedtuple=True)
88
        else:
89
            return cnf
90
91
    return None
92
93
94
def find_loader(path_or_stream, parser_or_type=None, is_path_=False):
95
    """
96
    Find out config parser object appropriate to load from a file of given path
97
    or file/file-like object.
98
99
    :param path_or_stream: Configuration file path or file / file-like object
100
    :param parser_or_type: Forced configuration parser type or parser object
101
    :param is_path_: True if given `path_or_stream` is a file path
102
103
    :return: Config parser instance or None
104
    """
105
    if anyconfig.backends.is_parser(parser_or_type):
106
        return parser_or_type
107
108
    (psr, err) = anyconfig.backends.find_parser(path_or_stream, parser_or_type,
109
                                                is_path_=is_path_)
110
    if err:
111
        LOGGER.error(err)
112
113
    if psr is None:
114
        if parser_or_type is None:
115
            LOGGER.warning("No parser (type) was given!")
116
        else:
117
            LOGGER.warning("Parser %s was not found!", str(parser_or_type))
118
        return None
119
120
    LOGGER.debug("Using config parser: %r [%s]", psr, psr.type())
121
    return psr()  # TBD: Passing initialization arguments.
122
123
124
def _load_schema(**options):
125
    """
126
    :param options: Optional keyword arguments such as
127
128
        - ac_template: Assume configuration file may be a template file and try
129
          to compile it AAR if True
130
        - ac_context: A dict presents context to instantiate template
131
        - ac_schema: JSON schema file path to validate given config file
132
    """
133
    ac_schema = options.get("ac_schema", None)
134
    if ac_schema is not None:
135
        # Try to detect the appropriate as it may be different from the
136
        # original config file's format, perhaps.
137
        options["ac_parser"] = None
138
        options["ac_schema"] = None  # Avoid infinite loop.
139
        LOGGER.info("Loading schema: %s", ac_schema)
140
        return load(ac_schema, **options)
141
142
    return None
143
144
145
def single_load(path_or_stream, ac_parser=None, ac_template=False,
146
                ac_context=None, **options):
147
    """
148
    Load single config file.
149
150
    :param path_or_stream: Configuration file path or file / file-like object
151
    :param ac_parser: Forced parser type or parser object
152
    :param ac_template: Assume configuration file may be a template file and
153
        try to compile it AAR if True
154
    :param ac_context: A dict presents context to instantiate template
155
    :param options: Optional keyword arguments such as:
156
157
        - Common options:
158
159
          - ac_namedtuple: Convert result to nested namedtuple object if True
160
          - ac_schema: JSON schema file path to validate given config file
161
162
        - Common backend options:
163
164
          - ignore_missing: Ignore and just return empty result if given file
165
            (``path_or_stream``) does not exist.
166
167
        - Backend specific options such as {"indent": 2} for JSON backend
168
169
    :return: dict or dict-like object supports merge operations
170
    """
171
    is_path_ = is_path(path_or_stream)
172
    if is_path_:
173
        path_or_stream = anyconfig.utils.ensure_expandusr(path_or_stream)
174
        filepath = path_or_stream
175
    else:
176
        filepath = anyconfig.utils.get_path_from_stream(path_or_stream)
177
178
    psr = find_loader(path_or_stream, ac_parser, is_path_)
179
    if psr is None:
180
        return None
181
182
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
183
                          **options)
184
    options["ac_schema"] = None  # It's not needed now.
185
186
    LOGGER.info("Loading: %s", filepath)
187
    if ac_template and filepath is not None:
188
        try:
189
            LOGGER.debug("Compiling: %s", filepath)
190
            content = anyconfig.template.render(filepath, ac_context)
191
            cnf = psr.loads(content, **options)
192
            return _maybe_validated(cnf, schema, **options)
193
194
        except Exception as exc:
195
            LOGGER.warning("Failed to compile %s, fallback to no template "
196
                           "mode, exc=%r", path_or_stream, exc)
197
198
    cnf = psr.load(path_or_stream, **options)
199
    return _maybe_validated(cnf, schema, **options)
200
201
202
def multi_load(paths, ac_parser=None, ac_template=False, ac_context=None,
203
               **options):
204
    """
205
    Load multiple config files.
206
207
    The first argument `paths` may be a list of config file paths or
208
    a glob pattern specifying that. That is, if a.yml, b.yml and c.yml are in
209
    the dir /etc/foo/conf.d/, the followings give same results::
210
211
      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
212
                  "/etc/foo/conf.d/c.yml", ])
213
214
      multi_load("/etc/foo/conf.d/*.yml")
215
216
    :param paths: List of config file paths or a glob pattern to list paths, or
217
        a list of file/file-like objects
218
    :param ac_parser: Forced parser type or parser object
219
    :param ac_template: Assume configuration file may be a template file and
220
        try to compile it AAR if True
221
    :param ac_context: A dict presents context to instantiate template
222
    :param options: Optional keyword arguments:
223
224
        - Common options:
225
226
          - ac_merge (merge): Specify strategy of how to merge results loaded
227
            from multiple configuration files. See the doc of :mod:`m9dicts`
228
            for more details of strategies. The default is m9dicts.MS_DICTS.
229
230
          - ac_marker (marker): Globbing marker to detect paths patterns.
231
          - ac_namedtuple: Convert result to nested namedtuple object if True
232
          - ac_schema: JSON schema file path to validate given config file
233
234
        - Common backend options:
235
236
          - ignore_missing: Ignore and just return empty result if given file
237
            (``path_or_stream``) does not exist.
238
239
        - Backend specific options such as {"indent": 2} for JSON backend
240
241
    :return: dict or dict-like object supports merge operations
242
    """
243
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
244
    schema = _load_schema(ac_template=ac_template, ac_context=ac_context,
245
                          **options)
246
    options["ac_schema"] = None  # It's not needed now.
247
248
    cnf = to_container(ac_context, **options)
249
    same_type = anyconfig.utils.are_same_file_types(paths)
250
251
    if is_path(paths) and marker in paths:
252
        paths = anyconfig.utils.sglob(paths)
253
254
    for path in paths:
255
        opts = options.copy()
256
        opts["ac_namedtuple"] = False  # Disabled within this loop.
257
        # Nested patterns like ['*.yml', '/a/b/c.yml'].
258
        if is_path(path) and marker in path:
259
            cups = multi_load(path, ac_parser=ac_parser,
260
                              ac_template=ac_template, ac_context=cnf, **opts)
261
        else:
262
            if same_type:
263
                ac_parser = find_loader(path, ac_parser, is_path(path))
264
            cups = single_load(path, ac_parser=ac_parser,
265
                               ac_template=ac_template, ac_context=cnf, **opts)
266
267
        if cups:
268
            cnf.update(cups)
269
270
    return _maybe_validated(cnf, schema, **options)
271
272
273
def load(path_specs, ac_parser=None, ac_template=False, ac_context=None,
274
         **options):
275
    r"""
276
    Load single or multiple config files or multiple config files specified in
277
    given paths pattern.
278
279
    :param path_specs: Configuration file path or paths or its pattern such as
280
        r'/a/b/\*.json' or a list of files/file-like objects
281
    :param ac_parser: Forced parser type or parser object
282
    :param ac_template: Assume configuration file may be a template file and
283
        try to compile it AAR if True
284
    :param ac_context: A dict presents context to instantiate template
285
    :param options:
286
        Optional keyword arguments. See also the description of `options` in
287
        `multi_load` function.
288
289
    :return: dict or dict-like object supports merge operations
290
    """
291
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
292
293
    if is_path(path_specs) and marker in path_specs or _is_paths(path_specs):
294
        return multi_load(path_specs, ac_parser=ac_parser,
295
                          ac_template=ac_template, ac_context=ac_context,
296
                          **options)
297
    else:
298
        return single_load(path_specs, ac_parser=ac_parser,
299
                           ac_template=ac_template, ac_context=ac_context,
300
                           **options)
301
302
303
def loads(content, ac_parser=None, ac_template=False, ac_context=None,
304
          **options):
305
    """
306
    :param content: Configuration file's content
307
    :param ac_parser: Forced parser type or parser object
308
    :param ac_template: Assume configuration file may be a template file and
309
        try to compile it AAR if True
310
    :param ac_context: Context dict to instantiate template
311
    :param options:
312
        Optional keyword arguments. See also the description of `options` in
313
        `single_load` function.
314
315
    :return: dict or dict-like object supports merge operations
316
    """
317
    msg = "Try parsing with a built-in parser because %s"
318
    if ac_parser is None:
319
        LOGGER.warning(msg, "ac_parser was not given.")
320
        return None
321
322
    psr = find_loader(None, ac_parser)
323
    if psr is None:
324
        LOGGER.warning(msg, "parser '%s' was not found" % ac_parser)
325
        return None
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