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

StringStreamFnParser   A

Complexity

Total Complexity 4

Size/Duplication

Total Lines 74
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 74
rs 10
wmc 4

4 Methods

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