Completed
Push — master ( a2eb28...52e99c )
by Satoru
03:28
created

Parser.type()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
#
2
# Copyright (C) 2012 - 2018 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
# pylint: disable=unused-argument
6
r"""Abstract implementation of backend modules:
7
8
Backend module must implement a parser class inherits :class:`Parser` or its
9
children classes of this module and override all or some of the methods as
10
needed:
11
12
  - :meth:`load_from_string`: Load config from string
13
  - :meth:`load_from_stream`: Load config from a file or file-like object
14
  - :meth:`load_from_path`: Load config from file of given path
15
  - :meth:`dump_to_string`: Dump config as a string
16
  - :meth:`dump_to_stream`: Dump config to a file or file-like object
17
  - :meth:`dump_to_path`: Dump config to a file of given path
18
19
Changelog:
20
21
.. versionchanged:: 0.9.5
22
23
   - Make :class:`Parser` inherited from
24
     :class:`~anyconfig.processors.Processor`
25
26
.. versionchanged:: 0.9.1
27
28
   - Rename the member _dict_options to `_dict_opts` to make consistent w/
29
     other members such as _load_opts.
30
31
.. versionchanged:: 0.8.3
32
33
   - Add `_ordered` membmer and a class method :meth:` ordered to
34
     :class:`Parser`.
35
   - Add `_dict_options` member to the class :class:`Parser`.
36
37
.. versionchanged:: 0.2
38
39
   - The methods :meth:`load_impl`, :meth:`dump_impl` are deprecated and
40
     replaced with :meth:`load_from_stream` and :meth:`load_from_path`,
41
     :meth:`dump_to_string` and :meth:`dump_to_path` respectively.
42
"""
43
from __future__ import absolute_import
44
45
import functools
46
import logging
47
import os
48
49
import anyconfig.compat
50
import anyconfig.processors
51
import anyconfig.utils
52
53
54
LOGGER = logging.getLogger(__name__)
55
TEXT_FILE = True
56
57
58
def ensure_outdir_exists(filepath):
59
    """
60
    Make dir to dump `filepath` if that dir does not exist.
61
62
    :param filepath: path of file to dump
63
    """
64
    outdir = os.path.dirname(filepath)
65
66
    if outdir and not os.path.exists(outdir):
67
        LOGGER.debug("Making output dir: %s", outdir)
68
        os.makedirs(outdir)
69
70
71
def to_method(func):
72
    """
73
    Lift :func:`func` to a method; it will be called with the first argument
74
    `self` ignored.
75
76
    :param func: Any callable object
77
    """
78
    @functools.wraps(func)
79
    def wrapper(*args, **kwargs):
80
        """Wrapper function.
81
        """
82
        return func(*args[1:], **kwargs)
83
84
    return wrapper
85
86
87
def _not_implemented(*args, **kwargs):
88
    """
89
    Utility function to raise NotImplementedError.
90
    """
91
    raise NotImplementedError()
92
93
94
class TextFilesMixin(object):
95
    """Mixin class to open configuration files as a plain text.
96
97
    Arguments of :func:`open` is different depends on python versions.
98
99
    - python 2: https://docs.python.org/2/library/functions.html#open
100
    - python 3: https://docs.python.org/3/library/functions.html#open
101
    """
102
    _open_flags = ('r', 'w')
103
104
    @classmethod
105
    def ropen(cls, filepath, **kwargs):
106
        """
107
        :param filepath: Path to file to open to read data
108
        """
109
        return open(filepath, cls._open_flags[0], **kwargs)
110
111
    @classmethod
112
    def wopen(cls, filepath, **kwargs):
113
        """
114
        :param filepath: Path to file to open to write data to
115
        """
116
        return open(filepath, cls._open_flags[1], **kwargs)
117
118
119
class BinaryFilesMixin(TextFilesMixin):
120
    """Mixin class to open binary (byte string) configuration files.
121
    """
122
    _open_flags = ('rb', 'wb')
123
124
125
class LoaderMixin(object):
126
    """
127
    Mixin class to load data.
128
129
    Inherited classes must implement the following methods.
130
131
    - :meth:`load_from_string`: Load config from string
132
    - :meth:`load_from_stream`: Load config from a file or file-like object
133
    - :meth:`load_from_path`: Load config from file of given path
134
135
    Member variables:
136
137
    - _load_opts: Backend specific options on load
138
    - _ordered: True if the parser keep the order of items by default
139
    - _dict_opts: Backend options to customize dict class to make results
140
    """
141
    _load_opts = []
142
    _ordered = False
143
    _dict_opts = []
144
145
    @classmethod
146
    def ordered(cls):
147
        """
148
        :return: True if parser can keep the order of keys else False.
149
        """
150
        return cls._ordered
151
152
    @classmethod
153
    def dict_options(cls):
154
        """
155
        :return: List of dict factory options
156
        """
157
        return cls._dict_opts
158
159
    def _container_factory(self, **options):
160
        """
161
        The order of prirorities are ac_dict, backend specific dict class
162
        option, ac_ordered.
163
164
        :param options: Keyword options may contain 'ac_ordered'.
165
        :return: Factory (class or function) to make an container.
166
        """
167
        ac_dict = options.get("ac_dict", False)
168
        _dicts = [x for x in (options.get(o) for o in self.dict_options())
169
                  if x]
170
171
        if self.dict_options() and ac_dict and callable(ac_dict):
172
            return ac_dict  # Higher priority than ac_ordered.
173
        elif _dicts and callable(_dicts[0]):
174
            return _dicts[0]
175
        elif self.ordered() and options.get("ac_ordered", False):
176
            return anyconfig.compat.OrderedDict
177
178
        return dict
179
180
    def _load_options(self, container, **options):
181
        """
182
        Select backend specific loading options.
183
        """
184
        # Force set dict option if available in backend. For example,
185
        # options["object_hook"] will be OrderedDict if 'container' was
186
        # OrderedDict in JSON backend.
187
        for opt in self.dict_options():
188
            options.setdefault(opt, container)
189
190
        return anyconfig.utils.filter_options(self._load_opts, options)
191
192
    def load_from_string(self, content, container, **kwargs):
193
        """
194
        Load config from given string `content`.
195
196
        :param content: Config content string
197
        :param container: callble to make a container object later
198
        :param kwargs: optional keyword parameters to be sanitized :: dict
199
200
        :return: Dict-like object holding config parameters
201
        """
202
        _not_implemented(self, content, container, **kwargs)
203
204
    def load_from_path(self, filepath, container, **kwargs):
205
        """
206
        Load config from given file path `filepath`.
207
208
        :param filepath: Config file path
209
        :param container: callble to make a container object later
210
        :param kwargs: optional keyword parameters to be sanitized :: dict
211
212
        :return: Dict-like object holding config parameters
213
        """
214
        _not_implemented(self, filepath, container, **kwargs)
215
216
    def load_from_stream(self, stream, container, **kwargs):
217
        """
218
        Load config from given file like object `stream`.
219
220
        :param stream:  Config file or file like object
221
        :param container: callble to make a container object later
222
        :param kwargs: optional keyword parameters to be sanitized :: dict
223
224
        :return: Dict-like object holding config parameters
225
        """
226
        _not_implemented(self, stream, container, **kwargs)
227
228
    def loads(self, content, **options):
229
        """
230
        Load config from given string `content` after some checks.
231
232
        :param content:  Config file content
233
        :param options:
234
            options will be passed to backend specific loading functions.
235
            please note that options have to be sanitized w/
236
            :func:`~anyconfig.utils.filter_options` later to filter out options
237
            not in _load_opts.
238
239
        :return: dict or dict-like object holding configurations
240
        """
241
        container = self._container_factory(**options)
242
        if not content or content is None:
243
            return container()
244
245
        options = self._load_options(container, **options)
246
        return self.load_from_string(content, container, **options)
247
248
    def load(self, ioi, ac_ignore_missing=False, **options):
249
        """
250
        Load config from a file path or a file / file-like object which `ioi`
251
        refering after some checks.
252
253
        :param ioi:
254
            `~anyconfig.globals.IOInfo` namedtuple object provides various info
255
            of input object to load data from
256
257
        :param ac_ignore_missing:
258
            Ignore and just return empty result if given `input_` does not
259
            exist in actual.
260
        :param options:
261
            options will be passed to backend specific loading functions.
262
            please note that options have to be sanitized w/
263
            :func:`~anyconfig.utils.filter_options` later to filter out options
264
            not in _load_opts.
265
266
        :return: dict or dict-like object holding configurations
267
        """
268
        container = self._container_factory(**options)
269
        options = self._load_options(container, **options)
270
271
        if not ioi:
272
            return container()
273
274
        if anyconfig.utils.is_stream_ioinfo(ioi):
275
            cnf = self.load_from_stream(ioi.src, container, **options)
276
        else:
277
            if ac_ignore_missing and not os.path.exists(ioi.path):
278
                return container()
279
280
            cnf = self.load_from_path(ioi.path, container, **options)
281
282
        return cnf
283
284
285
class DumperMixin(object):
286
    """
287
    Mixin class to dump data.
288
289
    Inherited classes must implement the following methods.
290
291
    - :meth:`dump_to_string`: Dump config as a string
292
    - :meth:`dump_to_stream`: Dump config to a file or file-like object
293
    - :meth:`dump_to_path`: Dump config to a file of given path
294
295
    Member variables:
296
297
    - _dump_opts: Backend specific options on dump
298
    """
299
    _dump_opts = []
300
301
    def dump_to_string(self, cnf, **kwargs):
302
        """
303
        Dump config `cnf` to a string.
304
305
        :param cnf: Configuration data to dump
306
        :param kwargs: optional keyword parameters to be sanitized :: dict
307
308
        :return: string represents the configuration
309
        """
310
        _not_implemented(self, cnf, **kwargs)
311
312
    def dump_to_path(self, cnf, filepath, **kwargs):
313
        """
314
        Dump config `cnf` to a file `filepath`.
315
316
        :param cnf: Configuration data to dump
317
        :param filepath: Config file path
318
        :param kwargs: optional keyword parameters to be sanitized :: dict
319
        """
320
        _not_implemented(self, cnf, filepath, **kwargs)
321
322
    def dump_to_stream(self, cnf, stream, **kwargs):
323
        """
324
        Dump config `cnf` to a file-like object `stream`.
325
326
        TODO: How to process socket objects same as file objects ?
327
328
        :param cnf: Configuration data to dump
329
        :param stream:  Config file or file like object
330
        :param kwargs: optional keyword parameters to be sanitized :: dict
331
        """
332
        _not_implemented(self, cnf, stream, **kwargs)
333
334
    def dumps(self, cnf, **kwargs):
335
        """
336
        Dump config `cnf` to a string.
337
338
        :param cnf: Configuration data to dump
339
        :param kwargs: optional keyword parameters to be sanitized :: dict
340
341
        :return: string represents the configuration
342
        """
343
        kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs)
344
        return self.dump_to_string(cnf, **kwargs)
345
346
    def dump(self, cnf, ioi, **kwargs):
347
        """
348
        Dump config `cnf` to output object of which `ioi` refering.
349
350
        :param cnf: Configuration data to dump
351
        :param ioi:
352
            `~anyconfig.globals.IOInfo` namedtuple object provides various info
353
            of input object to load data from
354
355
        :param kwargs: optional keyword parameters to be sanitized :: dict
356
        :raises IOError, OSError, AttributeError: When dump failed.
357
        """
358
        kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs)
359
360
        if anyconfig.utils.is_stream_ioinfo(ioi):
361
            self.dump_to_stream(cnf, ioi.src, **kwargs)
362
        else:
363
            ensure_outdir_exists(ioi.path)
364
            self.dump_to_path(cnf, ioi.path, **kwargs)
365
366
367
class Parser(TextFilesMixin, LoaderMixin, DumperMixin,
368
             anyconfig.processors.Processor):
369
    """
370
    Abstract parser to provide basic implementation of some methods, interfaces
371
    and members.
372
373
    - _type: Parser type indicate which format it supports
374
    - _priority: Priority to select it if there are other parsers of same type
375
    - _extensions: File extensions of formats it supports
376
    - _open_flags: Opening flags to read and write files
377
378
    .. seealso:: the doc of :class:`~anyconfig.processors.Processor`
379
    """
380
    pass
381
382
383
class FromStringLoaderMixin(LoaderMixin):
384
    """
385
    Abstract config parser provides a method to load configuration from string
386
    content to help implement parser of which backend lacks of such function.
387
388
    Parser classes inherit this class have to override the method
389
    :meth:`load_from_string` at least.
390
    """
391
    def load_from_stream(self, stream, container, **kwargs):
392
        """
393
        Load config from given stream `stream`.
394
395
        :param stream: Config file or file-like object
396
        :param container: callble to make a container object later
397
        :param kwargs: optional keyword parameters to be sanitized :: dict
398
399
        :return: Dict-like object holding config parameters
400
        """
401
        return self.load_from_string(stream.read(), container, **kwargs)
402
403
    def load_from_path(self, filepath, container, **kwargs):
404
        """
405
        Load config from given file path `filepath`.
406
407
        :param filepath: Config file path
408
        :param container: callble to make a container object later
409
        :param kwargs: optional keyword parameters to be sanitized :: dict
410
411
        :return: Dict-like object holding config parameters
412
        """
413
        with self.ropen(filepath) as inp:
414
            return self.load_from_stream(inp, container, **kwargs)
415
416
417
class FromStreamLoaderMixin(LoaderMixin):
418
    """
419
    Abstract config parser provides a method to load configuration from string
420
    content to help implement parser of which backend lacks of such function.
421
422
    Parser classes inherit this class have to override the method
423
    :meth:`load_from_stream` at least.
424
    """
425
    def load_from_string(self, content, container, **kwargs):
426
        """
427
        Load config from given string `cnf_content`.
428
429
        :param content: Config content string
430
        :param container: callble to make a container object later
431
        :param kwargs: optional keyword parameters to be sanitized :: dict
432
433
        :return: Dict-like object holding config parameters
434
        """
435
        return self.load_from_stream(anyconfig.compat.StringIO(content),
436
                                     container, **kwargs)
437
438
    def load_from_path(self, filepath, container, **kwargs):
439
        """
440
        Load config from given file path `filepath`.
441
442
        :param filepath: Config file path
443
        :param container: callble to make a container object later
444
        :param kwargs: optional keyword parameters to be sanitized :: dict
445
446
        :return: Dict-like object holding config parameters
447
        """
448
        with self.ropen(filepath) as inp:
449
            return self.load_from_stream(inp, container, **kwargs)
450
451
452
class ToStringDumperMixin(DumperMixin):
453
    """
454
    Abstract config parser provides a method to dump configuration to a file or
455
    file-like object (stream) and a file of given path to help implement parser
456
    of which backend lacks of such functions.
457
458
    Parser classes inherit this class have to override the method
459
    :meth:`dump_to_string` at least.
460
    """
461
    def dump_to_path(self, cnf, filepath, **kwargs):
462
        """
463
        Dump config `cnf` to a file `filepath`.
464
465
        :param cnf: Configuration data to dump
466
        :param filepath: Config file path
467
        :param kwargs: optional keyword parameters to be sanitized :: dict
468
        """
469
        with self.wopen(filepath) as out:
470
            out.write(self.dump_to_string(cnf, **kwargs))
471
472
    def dump_to_stream(self, cnf, stream, **kwargs):
473
        """
474
        Dump config `cnf` to a file-like object `stream`.
475
476
        TODO: How to process socket objects same as file objects ?
477
478
        :param cnf: Configuration data to dump
479
        :param stream:  Config file or file like object
480
        :param kwargs: optional keyword parameters to be sanitized :: dict
481
        """
482
        stream.write(self.dump_to_string(cnf, **kwargs))
483
484
485
class ToStreamDumperMixin(DumperMixin):
486
    """
487
    Abstract config parser provides methods to dump configuration to a string
488
    content or a file of given path to help implement parser of which backend
489
    lacks of such functions.
490
491
    Parser classes inherit this class have to override the method
492
    :meth:`dump_to_stream` at least.
493
    """
494
    def dump_to_string(self, cnf, **kwargs):
495
        """
496
        Dump config `cnf` to a string.
497
498
        :param cnf: Configuration data to dump
499
        :param kwargs: optional keyword parameters to be sanitized :: dict
500
501
        :return: Dict-like object holding config parameters
502
        """
503
        stream = anyconfig.compat.StringIO()
504
        self.dump_to_stream(cnf, stream, **kwargs)
505
        return stream.getvalue()
506
507
    def dump_to_path(self, cnf, filepath, **kwargs):
508
        """
509
        Dump config `cnf` to a file `filepath`.
510
511
        :param cnf: Configuration data to dump
512
        :param filepath: Config file path
513
        :param kwargs: optional keyword parameters to be sanitized :: dict
514
        """
515
        with self.wopen(filepath) as out:
516
            self.dump_to_stream(cnf, out, **kwargs)
517
518
519
class StringParser(Parser, FromStringLoaderMixin, ToStringDumperMixin):
520
    """
521
    Abstract parser based on :meth:`load_from_string` and
522
    :meth:`dump_to_string`.
523
524
    Parser classes inherit this class must define these methods.
525
    """
526
    pass
527
528
529
class StreamParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin):
530
    """
531
    Abstract parser based on :meth:`load_from_stream` and
532
    :meth:`dump_to_stream`.
533
534
    Parser classes inherit this class must define these methods.
535
    """
536
    pass
537
538
539
def load_with_fn(load_fn, content_or_strm, container, **options):
540
    """
541
    Load data from given string or stream `content_or_strm`.
542
543
    :param load_fn: Callable to load data
544
    :param content_or_strm: data content or stream provides it
545
    :param container: callble to make a container object
546
    :param options: keyword options passed to `load_fn`
547
548
    :return: container object holding data
549
    """
550
    ret = load_fn(content_or_strm, **options)
551
    return container() if ret is None else container(ret)
552
553
554
def dump_with_fn(dump_fn, data, stream, **options):
555
    """
556
    Dump `data` to a string if `stream` is None, or dump `data` to a file or
557
    file-like object `stream`.
558
559
    :param dump_fn: Callable to dump data
560
    :param data: Data to dump
561
    :param stream:  File or file like object or None
562
    :param options: optional keyword parameters
563
564
    :return: String represents data if stream is None or None
565
    """
566
    if stream is None:
567
        return dump_fn(data, **options)
568
569
    return dump_fn(data, stream, **options)
570
571
572
class StringStreamFnParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin):
573
    """
574
    Abstract parser utilizes load and dump functions each backend module
575
    provides such like json.load{,s} and json.dump{,s} in JSON backend.
576
577
    Parser classes inherit this class must define the followings.
578
579
    - _load_from_string_fn: Callable to load data from string
580
    - _load_from_stream_fn: Callable to load data from stream (file object)
581
    - _dump_to_string_fn: Callable to dump data to string
582
    - _dump_to_stream_fn: Callable to dump data to stream (file object)
583
584
    .. note::
585
       Callables have to be wrapped with :func:`to_method` to make `self`
586
       passed to the methods created from them ignoring it.
587
588
    :seealso: :class:`anyconfig.backend.json.Parser`
589
    """
590
    _load_from_string_fn = None
591
    _load_from_stream_fn = None
592
    _dump_to_string_fn = None
593
    _dump_to_stream_fn = None
594
595
    def load_from_string(self, content, container, **options):
596
        """
597
        Load configuration data from given string `content`.
598
599
        :param content: Configuration string
600
        :param container: callble to make a container object
601
        :param options: keyword options passed to `_load_from_string_fn`
602
603
        :return: container object holding the configuration data
604
        """
605
        return load_with_fn(self._load_from_string_fn, content, container,
606
                            **options)
607
608
    def load_from_stream(self, stream, container, **options):
609
        """
610
        Load data from given stream `stream`.
611
612
        :param stream: Stream provides configuration data
613
        :param container: callble to make a container object
614
        :param options: keyword options passed to `_load_from_stream_fn`
615
616
        :return: container object holding the configuration data
617
        """
618
        return load_with_fn(self._load_from_stream_fn, stream, container,
619
                            **options)
620
621
    def dump_to_string(self, cnf, **kwargs):
622
        """
623
        Dump config `cnf` to a string.
624
625
        :param cnf: Configuration data to dump
626
        :param kwargs: optional keyword parameters to be sanitized :: dict
627
628
        :return: string represents the configuration
629
        """
630
        return dump_with_fn(self._dump_to_string_fn, cnf, None, **kwargs)
631
632
    def dump_to_stream(self, cnf, stream, **kwargs):
633
        """
634
        Dump config `cnf` to a file-like object `stream`.
635
636
        TODO: How to process socket objects same as file objects ?
637
638
        :param cnf: Configuration data to dump
639
        :param stream:  Config file or file like object
640
        :param kwargs: optional keyword parameters to be sanitized :: dict
641
        """
642
        dump_with_fn(self._dump_to_stream_fn, cnf, stream, **kwargs)
643
644
# vim:sw=4:ts=4:et:
645