Completed
Push — master ( 98e1d4...d1821a )
by Satoru
01:27
created

_maybe_validated()   A

Complexity

Conditions 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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