Completed
Push — master ( 6ef2f5...b75cb1 )
by Satoru
53s
created

single_load()   A

Complexity

Conditions 1

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 1
Metric Value
cc 1
c 6
b 1
f 1
dl 0
loc 57
rs 9.6818

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 - 2018 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
import warnings
63
64
# Import some global constants will be re-exported:
65
from anyconfig.globals import (
66
    LOGGER, IOI_PATH_OBJ, UnknownParserTypeError, UnknownFileTypeError
67
)
68
import anyconfig.backends
69
import anyconfig.backend.json
70
import anyconfig.compat
71
import anyconfig.query
72
import anyconfig.globals
73
import anyconfig.dicts
74
import anyconfig.template
75
import anyconfig.utils
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
83
# Re-export and aliases:
84
list_types = anyconfig.backends.list_types  # flake8: noqa
85
86
87
def version():
88
    """
89
    :return: A tuple of version info, (major, minor, release), e.g. (0, 8, 2)
90
    """
91
    return anyconfig.globals.VERSION.split('.')
92
93
94
def _try_validate(cnf, schema, **options):
95
    """
96
    :param cnf: Mapping object represents configuration data
97
    :param schema: JSON schema object
98
    :param options: Keyword options passed to :func:`~jsonschema.validate`
99
100
    :return: Given `cnf` as it is if validation succeeds else None
101
    """
102
    valid = True
103
    if schema:
104
        (valid, msg) = validate(cnf, schema, **options)
105
        if msg:
106
            LOGGER.warning(msg)
107
108
    if valid:
109
        return cnf
110
111
    return None
112
113
114
def find_loader(path, parser_or_type=None):
115
    """
116
    Find out parser object appropriate to load configuration from a file of
117
    given path or file or file-like object.
118
119
    :param path:
120
        Configuration file path or file or file-like object or pathlib.Path
121
        object if it's available
122
    :param parser_or_type:
123
        Forced configuration parser type or parser object itself
124
125
    :return:
126
        An instance of a class inherits :class:`~anyconfig.backend.base.Parser`
127
        or None
128
    """
129
    if anyconfig.backends.is_parser(parser_or_type):
130
        return parser_or_type
131
132
    try:
133
        return anyconfig.backends.find_parser(path,
134
                                              forced_type=parser_or_type)
135
    except (ValueError, UnknownParserTypeError, UnknownFileTypeError):
136
        raise
137
138
139
def _maybe_schema(**options):
140
    """
141
    :param options: Optional keyword arguments such as
142
143
        - ac_template: Assume configuration file may be a template file and try
144
          to compile it AAR if True
145
        - ac_context: Mapping object presents context to instantiate template
146
        - ac_schema: JSON schema file path to validate configuration files
147
148
    :return: Mapping object or None means some errors
149
    """
150
    ac_schema = options.get("ac_schema", None)
151
    if ac_schema is not None:
152
        # Try to detect the appropriate parser to load the schema data as it
153
        # may be different from the original config file's format, perhaps.
154
        options["ac_parser"] = None
155
        options["ac_schema"] = None  # Avoid infinite loop.
156
        LOGGER.info("Loading schema: %s", ac_schema)
157
        return load(ac_schema, **options)
158
159
    return None
160
161
162
# pylint: disable=redefined-builtin
163
def open(path, mode=None, ac_parser=None, **options):
164
    """
165
    Open given configuration file with appropriate open flag.
166
167
    :param path: Configuration file path
168
    :param mode:
169
        Can be 'r' and 'rb' for reading (default) or 'w', 'wb' for writing.
170
        Please note that even if you specify 'r' or 'w', it will be changed to
171
        'rb' or 'wb' if selected backend, xml and configobj for example, for
172
        given config file prefer that.
173
    :param options:
174
        Optional keyword arguments passed to the internal file opening APIs of
175
        each backends such like 'buffering' optional parameter passed to
176
        builtin 'open' function.
177
178
    :return: A file object or None on any errors
179
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
180
    """
181
    psr = anyconfig.backends.find_parser(path, forced_type=ac_parser)
182
183
    if mode is not None and mode.startswith('w'):
184
        return psr.wopen(path, **options)
185
186
    return psr.ropen(path, **options)
187
188
189
def _single_load(input_, ac_parser=None, ac_template=False,
190
                 ac_context=None, **options):
191
    """
192
    :param input_:
193
        File path or file or file-like object or pathlib.Path object represents
194
        the file or a namedtuple `~anyconfig.globals.IOInfo` object represents
195
        some input to load some data from
196
    :param ac_parser: Forced parser type or parser object itself
197
    :param ac_template:
198
        Assume configuration file may be a template file and try to compile it
199
        AAR if True
200
    :param ac_context: A dict presents context to instantiate template
201
    :param options:
202
        Optional keyword arguments :func:`single_load` supports except for
203
        ac_schema and ac_query
204
205
    :return: Mapping object
206
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
207
    """
208
    ioi = anyconfig.backends.inspect_io_obj(input_, forced_type=ac_parser)
209
    (psr, filepath) = (ioi.parser, ioi.path)
210
211
    # .. note::
212
    #    This will be kept for backward compatibility until 'ignore_missing'
213
    #    option is deprecated and removed completely.
214
    if "ignore_missing" in options:
215
        warnings.warn("keyword option 'ignore_missing' is deprecated, use "
216
                      "'ac_ignore_missing' isntead", DeprecationWarning)
217
        options["ac_ignore_missing"] = options["ignore_missing"]
218
219
    LOGGER.info("Loading: %s", filepath)
220
    if ac_template and filepath is not None:
221
        content = anyconfig.template.try_render(filepath=filepath,
222
                                                ctx=ac_context)
223
        if content is not None:
224
            return psr.loads(content, **options)
225
226
    return psr.load(ioi, **options)
227
228
229
def single_load(input_, ac_parser=None, ac_template=False,
230
                ac_context=None, **options):
231
    """
232
    Load single configuration file.
233
234
    .. note::
235
236
       :func:`load` is a preferable alternative and this API should be used
237
       only if there is a need to emphasize given input `input_` is single one.
238
239
    :param input_:
240
        File path or file or file-like object or pathlib.Path object represents
241
        the file or a namedtuple `~anyconfig.globals.IOInfo` object represents
242
        some input to load some data from
243
    :param ac_parser: Forced parser type or parser object itself
244
    :param ac_template:
245
        Assume configuration file may be a template file and try to compile it
246
        AAR if True
247
    :param ac_context: A dict presents context to instantiate template
248
    :param options: Optional keyword arguments such as:
249
250
        - Options common in :func:`single_load`, :func:`multi_load`,
251
          :func:`load` and :func:`loads`:
252
253
          - ac_dict: callable (function or class) to make mapping objects from
254
            loaded data if the selected backend can customize that such as JSON
255
            which supports that with 'object_pairs_hook' option, or None. If
256
            this option was not given or None, dict or :class:`OrderedDict`
257
            will be used to make result as mapping object depends on if
258
            ac_ordered (see below) is True and selected backend can keep the
259
            order of items loaded. See also :meth:`_container_factory` of
260
            :class:`~anyconfig.backend.base.Parser` for more implementation
261
            details.
262
263
          - ac_ordered: True if you want to keep resuls ordered. Please note
264
            that order of items may be lost depends on the selected backend.
265
266
          - ac_schema: JSON schema file path to validate given config file
267
          - ac_query: JMESPath expression to query data
268
269
        - Common backend options:
270
271
          - ac_ignore_missing:
272
            Ignore and just return empty result if given file ``input_`` does
273
            not exist actually.
274
275
        - Backend specific options such as {"indent": 2} for JSON backend
276
277
    :return: Mapping object
278
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
279
    """
280
    cnf = _single_load(input_, ac_parser=ac_parser, ac_template=ac_template,
281
                       ac_context=ac_context, **options)
282
    schema = _maybe_schema(ac_template=ac_template, ac_context=ac_context,
283
                           **options)
284
    cnf = _try_validate(cnf, schema, **options)
285
    return anyconfig.query.query(cnf, **options)
286
287
288
def multi_load(inputs, ac_parser=None, ac_template=False, ac_context=None,
289
               **options):
290
    r"""
291
    Load multiple config files.
292
293
    .. note::
294
295
       :func:`load` is a preferable alternative and this API should be used
296
       only if there is a need to emphasize given inputs are multiple ones.
297
298
    The first argument `inputs` may be a list of file paths or a glob pattern
299
    specifying that. That is, if a.yml, b.yml and c.yml are in the dir
300
    /etc/foo/conf.d/, the followings give same results::
301
302
      multi_load(["/etc/foo/conf.d/a.yml", "/etc/foo/conf.d/b.yml",
303
                  "/etc/foo/conf.d/c.yml", ])
304
305
      multi_load("/etc/foo/conf.d/*.yml")
306
307
    :param inputs:
308
        A list of file path or a glob pattern such as r'/a/b/\*.json'to list of
309
        files, file or file-like object or pathlib.Path object represents the
310
        file or a namedtuple `~anyconfig.globals.IOInfo` object represents some
311
        input to load some data from
312
    :param ac_parser: Forced parser type or parser object
313
    :param ac_template: Assume configuration file may be a template file and
314
        try to compile it AAR if True
315
    :param ac_context: Mapping object presents context to instantiate template
316
    :param options: Optional keyword arguments:
317
318
        - ac_dict, ac_ordered, ac_schema and ac_query are the options common in
319
          :func:`single_load`, :func:`multi_load`, :func:`load`: and
320
          :func:`loads`. See the descriptions of them in :func:`single_load`.
321
322
        - Options specific to this function and :func:`load`:
323
324
          - ac_merge (merge): Specify strategy of how to merge results loaded
325
            from multiple configuration files. See the doc of
326
            :mod:`anyconfig.dicts` for more details of strategies. The default
327
            is anyconfig.dicts.MS_DICTS.
328
329
          - ac_marker (marker): Globbing marker to detect paths patterns.
330
331
        - Common backend options:
332
333
          - ignore_missing: Ignore and just return empty result if given file
334
            ``path`` does not exist.
335
336
        - Backend specific options such as {"indent": 2} for JSON backend
337
338
    :return: Mapping object or any query result might be primitive objects
339
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
340
    """
341
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
342
    schema = _maybe_schema(ac_template=ac_template, ac_context=ac_context,
343
                           **options)
344
    options["ac_schema"] = None  # Avoid to load schema more than twice.
345
346
    paths = anyconfig.utils.expand_paths(inputs, marker=marker)
347
    if anyconfig.utils.are_same_file_types(paths):
348
        ac_parser = anyconfig.backends.find_parser(paths[0], ac_parser)
349
350
    cnf = ac_context
351
    for path in paths:
352
        opts = options.copy()
353
        cups = _single_load(path, ac_parser=ac_parser,
354
                            ac_template=ac_template, ac_context=cnf, **opts)
355
        if cups:
356
            if cnf is None:
357
                cnf = cups
358
            else:
359
                merge(cnf, cups, **options)
360
361
    if cnf is None:
362
        return anyconfig.dicts.convert_to({}, **options)
363
364
    cnf = _try_validate(cnf, schema, **options)
365
    return anyconfig.query.query(cnf, **options)
366
367
368
def load(path_specs, ac_parser=None, ac_dict=None, ac_template=False,
369
         ac_context=None, **options):
370
    r"""
371
    Load single or multiple config files or multiple config files specified in
372
    given paths pattern.
373
374
    :param path_specs:
375
        A list of file path or a glob pattern such as r'/a/b/\*.json'to list of
376
        files, file or file-like object or pathlib.Path object represents the
377
        file or a namedtuple `~anyconfig.globals.IOInfo` object represents some
378
        input to load some data from
379
    :param ac_parser: Forced parser type or parser object
380
    :param ac_dict:
381
        callable (function or class) to make mapping object will be returned as
382
        a result or None. If not given or ac_dict is None, default mapping
383
        object used to store resutls is dict or
384
        :class:`~collections.OrderedDict` if ac_ordered is True and selected
385
        backend can keep the order of items in mapping objects.
386
387
    :param ac_template: Assume configuration file may be a template file and
388
        try to compile it AAR if True
389
    :param ac_context: A dict presents context to instantiate template
390
    :param options:
391
        Optional keyword arguments. See also the description of `options` in
392
        :func:`single_load` and :func:`multi_load`
393
394
    :return: Mapping object or any query result might be primitive objects
395
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
396
    """
397
    marker = options.setdefault("ac_marker", options.get("marker", '*'))
398
399
    if anyconfig.utils.is_path_like_object(path_specs, marker):
400
        return single_load(path_specs, ac_parser=ac_parser, ac_dict=ac_dict,
401
                           ac_template=ac_template, ac_context=ac_context,
402
                           **options)
403
404
    if not anyconfig.utils.is_paths(path_specs, marker):
405
        raise ValueError("Something goes wrong with your input %r", path_specs)
406
407
    return multi_load(path_specs, ac_parser=ac_parser, ac_dict=ac_dict,
408
                      ac_template=ac_template, ac_context=ac_context,
409
                      **options)
410
411
412
def loads(content, ac_parser=None, ac_dict=None, ac_template=False,
413
          ac_context=None, **options):
414
    """
415
    :param content: Configuration file's content
416
    :param ac_parser: Forced parser type or parser object
417
    :param ac_dict:
418
        callable (function or class) to make mapping object will be returned as
419
        a result or None. If not given or ac_dict is None, default mapping
420
        object used to store resutls is dict or
421
        :class:`~collections.OrderedDict` if ac_ordered is True and selected
422
        backend can keep the order of items in mapping objects.
423
    :param ac_template: Assume configuration file may be a template file and
424
        try to compile it AAR if True
425
    :param ac_context: Context dict to instantiate template
426
    :param options:
427
        Optional keyword arguments. See also the description of `options` in
428
        :func:`single_load` function.
429
430
    :return: Mapping object or any query result might be primitive objects
431
    :raises: ValueError, UnknownParserTypeError
432
    """
433
    if ac_parser is None:
434
        LOGGER.warning("ac_parser was not given but it's must to find correct "
435
                       "parser to load configurations from string.")
436
        return None
437
438
    psr = anyconfig.backends.find_parser_by_type(ac_parser)
439
    schema = None
440
    ac_schema = options.get("ac_schema", None)
441
    if ac_schema is not None:
442
        options["ac_schema"] = None
443
        schema = loads(ac_schema, ac_parser=psr, ac_dict=ac_dict,
444
                       ac_template=ac_template, ac_context=ac_context,
445
                       **options)
446
447
    if ac_template:
448
        compiled = anyconfig.template.try_render(content=content,
449
                                                 ctx=ac_context)
450
        if compiled is not None:
451
            content = compiled
452
453
    cnf = psr.loads(content, ac_dict=ac_dict, **options)
454
    cnf = _try_validate(cnf, schema, **options)
455
    return anyconfig.query.query(cnf, **options)
456
457
458
def dump(data, path, ac_parser=None, **options):
459
    """
460
    Save `data` as `path`.
461
462
    :param data: A mapping object may have configurations data to dump
463
    :param path:
464
        Output file path or file / file-like object or pathlib.Path object
465
    :param ac_parser: Forced parser type or parser object
466
    :param options:
467
        Backend specific optional arguments, e.g. {"indent": 2} for JSON
468
        loader/dumper backend
469
470
    :raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
471
    """
472
    ioi = anyconfig.backends.inspect_io_obj(path, forced_type=ac_parser)
473
    LOGGER.info("Dumping: %s", ioi.path)
474
    ioi.parser.dump(data, ioi, **options)
475
476
477
def dumps(data, ac_parser=None, **options):
478
    """
479
    Return string representation of `data` in forced type format.
480
481
    :param data: Config data object to dump
482
    :param ac_parser: Forced parser type or parser object
483
    :param options: see :func:`dump`
484
485
    :return: Backend-specific string representation for the given data
486
    :raises: ValueError, UnknownParserTypeError
487
    """
488
    psr = anyconfig.backends.find_parser_by_type(ac_parser)
489
    return psr.dumps(data, **options)
490
491
492
def query(data, expression, **options):
493
    """
494
    API just wraps :func:`anyconfig.query.query`.
495
496
    :param data: Config data object to query
497
    :param options: Ignored in current implementation
498
499
    :return: Query result object may be primitive (int, str, etc.) or dict.
500
    """
501
    return anyconfig.query.query(data, ac_query=expression)
502
503
# vim:sw=4:ts=4:et:
504