FromStreamLoaderMixin   A
last analyzed

Complexity

Total Complexity 3

Size/Duplication

Total Lines 33
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 33
rs 10
wmc 3

2 Methods

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