Completed
Push — master ( 6b15fe...54e04e )
by Satoru
26:08
created

ToStreamDumperMixin.dump_to_string()   A

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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