ansible-later/env_27/lib/python2.7/site-packages/anyconfig/backend/base.py
2019-04-11 13:00:36 +02:00

669 lines
22 KiB
Python

#
# Copyright (C) 2012 - 2018 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-argument
r"""Abstract implementation of backend modules:
Backend module must implement a parser class inherits :class:`Parser` or its
children classes of this module and override all or some of the methods as
needed:
- :meth:`load_from_string`: Load config from string
- :meth:`load_from_stream`: Load config from a file or file-like object
- :meth:`load_from_path`: Load config from file of given path
- :meth:`dump_to_string`: Dump config as a string
- :meth:`dump_to_stream`: Dump config to a file or file-like object
- :meth:`dump_to_path`: Dump config to a file of given path
Changelog:
.. versionchanged:: 0.9.5
- Make :class:`Parser` inherited from
:class:`~anyconfig.models.processor.Processor`
- introduce the member _allow_primitives and the class method
allow_primitives to :class:`Parser` to allow parsers to load and return
data of primitive types other than mapping objects
.. versionchanged:: 0.9.1
- Rename the member _dict_options to `_dict_opts` to make consistent w/
other members such as _load_opts.
.. versionchanged:: 0.8.3
- Add `_ordered` membmer and a class method :meth:` ordered to
:class:`Parser`.
- Add `_dict_options` member to the class :class:`Parser`.
.. versionchanged:: 0.2
- The methods :meth:`load_impl`, :meth:`dump_impl` are deprecated and
replaced with :meth:`load_from_stream` and :meth:`load_from_path`,
:meth:`dump_to_string` and :meth:`dump_to_path` respectively.
"""
from __future__ import absolute_import
import functools
import logging
import os
import anyconfig.compat
import anyconfig.globals
import anyconfig.processors
import anyconfig.utils
LOGGER = logging.getLogger(__name__)
TEXT_FILE = True
def ensure_outdir_exists(filepath):
"""
Make dir to dump `filepath` if that dir does not exist.
:param filepath: path of file to dump
"""
outdir = os.path.dirname(filepath)
if outdir and not os.path.exists(outdir):
LOGGER.debug("Making output dir: %s", outdir)
os.makedirs(outdir)
def to_method(func):
"""
Lift :func:`func` to a method; it will be called with the first argument
`self` ignored.
:param func: Any callable object
"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function.
"""
return func(*args[1:], **kwargs)
return wrapper
def _not_implemented(*args, **kwargs):
"""
Utility function to raise NotImplementedError.
"""
raise NotImplementedError()
class TextFilesMixin(object):
"""Mixin class to open configuration files as a plain text.
Arguments of :func:`open` is different depends on python versions.
- python 2: https://docs.python.org/2/library/functions.html#open
- python 3: https://docs.python.org/3/library/functions.html#open
"""
_open_flags = ('r', 'w')
@classmethod
def ropen(cls, filepath, **kwargs):
"""
:param filepath: Path to file to open to read data
"""
return open(filepath, cls._open_flags[0], **kwargs)
@classmethod
def wopen(cls, filepath, **kwargs):
"""
:param filepath: Path to file to open to write data to
"""
return open(filepath, cls._open_flags[1], **kwargs)
class BinaryFilesMixin(TextFilesMixin):
"""Mixin class to open binary (byte string) configuration files.
"""
_open_flags = ('rb', 'wb')
class LoaderMixin(object):
"""
Mixin class to load data.
Inherited classes must implement the following methods.
- :meth:`load_from_string`: Load config from string
- :meth:`load_from_stream`: Load config from a file or file-like object
- :meth:`load_from_path`: Load config from file of given path
Member variables:
- _load_opts: Backend specific options on load
- _ordered: True if the parser keep the order of items by default
- _allow_primitives: True if the parser.load* may return objects of
primitive data types other than mapping types such like JSON parser
- _dict_opts: Backend options to customize dict class to make results
"""
_load_opts = []
_ordered = False
_allow_primitives = False
_dict_opts = []
@classmethod
def ordered(cls):
"""
:return: True if parser can keep the order of keys else False.
"""
return cls._ordered
@classmethod
def allow_primitives(cls):
"""
:return: True if the parser.load* may return objects of primitive data
types other than mapping types such like JSON parser
"""
return cls._allow_primitives
@classmethod
def dict_options(cls):
"""
:return: List of dict factory options
"""
return cls._dict_opts
def _container_factory(self, **options):
"""
The order of prirorities are ac_dict, backend specific dict class
option, ac_ordered.
:param options: Keyword options may contain 'ac_ordered'.
:return: Factory (class or function) to make an container.
"""
ac_dict = options.get("ac_dict", False)
_dicts = [x for x in (options.get(o) for o in self.dict_options())
if x]
if self.dict_options() and ac_dict and callable(ac_dict):
return ac_dict # Higher priority than ac_ordered.
if _dicts and callable(_dicts[0]):
return _dicts[0]
if self.ordered() and options.get("ac_ordered", False):
return anyconfig.compat.OrderedDict
return dict
def _load_options(self, container, **options):
"""
Select backend specific loading options.
"""
# Force set dict option if available in backend. For example,
# options["object_hook"] will be OrderedDict if 'container' was
# OrderedDict in JSON backend.
for opt in self.dict_options():
options.setdefault(opt, container)
return anyconfig.utils.filter_options(self._load_opts, options)
def load_from_string(self, content, container, **kwargs):
"""
Load config from given string `content`.
:param content: Config content string
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
_not_implemented(self, content, container, **kwargs)
def load_from_path(self, filepath, container, **kwargs):
"""
Load config from given file path `filepath`.
:param filepath: Config file path
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
_not_implemented(self, filepath, container, **kwargs)
def load_from_stream(self, stream, container, **kwargs):
"""
Load config from given file like object `stream`.
:param stream: Config file or file like object
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
_not_implemented(self, stream, container, **kwargs)
def loads(self, content, **options):
"""
Load config from given string `content` after some checks.
:param content: Config file content
:param options:
options will be passed to backend specific loading functions.
please note that options have to be sanitized w/
:func:`~anyconfig.utils.filter_options` later to filter out options
not in _load_opts.
:return: dict or dict-like object holding configurations
"""
container = self._container_factory(**options)
if not content or content is None:
return container()
options = self._load_options(container, **options)
return self.load_from_string(content, container, **options)
def load(self, ioi, ac_ignore_missing=False, **options):
"""
Load config from a file path or a file / file-like object which `ioi`
refering after some checks.
:param ioi:
`~anyconfig.globals.IOInfo` namedtuple object provides various info
of input object to load data from
:param ac_ignore_missing:
Ignore and just return empty result if given `input_` does not
exist in actual.
:param options:
options will be passed to backend specific loading functions.
please note that options have to be sanitized w/
:func:`~anyconfig.utils.filter_options` later to filter out options
not in _load_opts.
:return: dict or dict-like object holding configurations
"""
container = self._container_factory(**options)
options = self._load_options(container, **options)
if not ioi:
return container()
if anyconfig.utils.is_stream_ioinfo(ioi):
cnf = self.load_from_stream(ioi.src, container, **options)
else:
if ac_ignore_missing and not os.path.exists(ioi.path):
return container()
cnf = self.load_from_path(ioi.path, container, **options)
return cnf
class DumperMixin(object):
"""
Mixin class to dump data.
Inherited classes must implement the following methods.
- :meth:`dump_to_string`: Dump config as a string
- :meth:`dump_to_stream`: Dump config to a file or file-like object
- :meth:`dump_to_path`: Dump config to a file of given path
Member variables:
- _dump_opts: Backend specific options on dump
"""
_dump_opts = []
def dump_to_string(self, cnf, **kwargs):
"""
Dump config `cnf` to a string.
:param cnf: Configuration data to dump
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: string represents the configuration
"""
_not_implemented(self, cnf, **kwargs)
def dump_to_path(self, cnf, filepath, **kwargs):
"""
Dump config `cnf` to a file `filepath`.
:param cnf: Configuration data to dump
:param filepath: Config file path
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
_not_implemented(self, cnf, filepath, **kwargs)
def dump_to_stream(self, cnf, stream, **kwargs):
"""
Dump config `cnf` to a file-like object `stream`.
TODO: How to process socket objects same as file objects ?
:param cnf: Configuration data to dump
:param stream: Config file or file like object
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
_not_implemented(self, cnf, stream, **kwargs)
def dumps(self, cnf, **kwargs):
"""
Dump config `cnf` to a string.
:param cnf: Configuration data to dump
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: string represents the configuration
"""
kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs)
return self.dump_to_string(cnf, **kwargs)
def dump(self, cnf, ioi, **kwargs):
"""
Dump config `cnf` to output object of which `ioi` refering.
:param cnf: Configuration data to dump
:param ioi:
`~anyconfig.globals.IOInfo` namedtuple object provides various info
of input object to load data from
:param kwargs: optional keyword parameters to be sanitized :: dict
:raises IOError, OSError, AttributeError: When dump failed.
"""
kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs)
if anyconfig.utils.is_stream_ioinfo(ioi):
self.dump_to_stream(cnf, ioi.src, **kwargs)
else:
ensure_outdir_exists(ioi.path)
self.dump_to_path(cnf, ioi.path, **kwargs)
class Parser(TextFilesMixin, LoaderMixin, DumperMixin,
anyconfig.models.processor.Processor):
"""
Abstract parser to provide basic implementation of some methods, interfaces
and members.
- _type: Parser type indicate which format it supports
- _priority: Priority to select it if there are other parsers of same type
- _extensions: File extensions of formats it supports
- _open_flags: Opening flags to read and write files
.. seealso:: the doc of :class:`~anyconfig.models.processor.Processor`
"""
pass
class FromStringLoaderMixin(LoaderMixin):
"""
Abstract config parser provides a method to load configuration from string
content to help implement parser of which backend lacks of such function.
Parser classes inherit this class have to override the method
:meth:`load_from_string` at least.
"""
def load_from_stream(self, stream, container, **kwargs):
"""
Load config from given stream `stream`.
:param stream: Config file or file-like object
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
return self.load_from_string(stream.read(), container, **kwargs)
def load_from_path(self, filepath, container, **kwargs):
"""
Load config from given file path `filepath`.
:param filepath: Config file path
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
with self.ropen(filepath) as inp:
return self.load_from_stream(inp, container, **kwargs)
class FromStreamLoaderMixin(LoaderMixin):
"""
Abstract config parser provides a method to load configuration from string
content to help implement parser of which backend lacks of such function.
Parser classes inherit this class have to override the method
:meth:`load_from_stream` at least.
"""
def load_from_string(self, content, container, **kwargs):
"""
Load config from given string `cnf_content`.
:param content: Config content string
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
return self.load_from_stream(anyconfig.compat.StringIO(content),
container, **kwargs)
def load_from_path(self, filepath, container, **kwargs):
"""
Load config from given file path `filepath`.
:param filepath: Config file path
:param container: callble to make a container object later
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
with self.ropen(filepath) as inp:
return self.load_from_stream(inp, container, **kwargs)
class ToStringDumperMixin(DumperMixin):
"""
Abstract config parser provides a method to dump configuration to a file or
file-like object (stream) and a file of given path to help implement parser
of which backend lacks of such functions.
Parser classes inherit this class have to override the method
:meth:`dump_to_string` at least.
"""
def dump_to_path(self, cnf, filepath, **kwargs):
"""
Dump config `cnf` to a file `filepath`.
:param cnf: Configuration data to dump
:param filepath: Config file path
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
with self.wopen(filepath) as out:
out.write(self.dump_to_string(cnf, **kwargs))
def dump_to_stream(self, cnf, stream, **kwargs):
"""
Dump config `cnf` to a file-like object `stream`.
TODO: How to process socket objects same as file objects ?
:param cnf: Configuration data to dump
:param stream: Config file or file like object
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
stream.write(self.dump_to_string(cnf, **kwargs))
class ToStreamDumperMixin(DumperMixin):
"""
Abstract config parser provides methods to dump configuration to a string
content or a file of given path to help implement parser of which backend
lacks of such functions.
Parser classes inherit this class have to override the method
:meth:`dump_to_stream` at least.
"""
def dump_to_string(self, cnf, **kwargs):
"""
Dump config `cnf` to a string.
:param cnf: Configuration data to dump
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: Dict-like object holding config parameters
"""
stream = anyconfig.compat.StringIO()
self.dump_to_stream(cnf, stream, **kwargs)
return stream.getvalue()
def dump_to_path(self, cnf, filepath, **kwargs):
"""
Dump config `cnf` to a file `filepath`.
:param cnf: Configuration data to dump
:param filepath: Config file path
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
with self.wopen(filepath) as out:
self.dump_to_stream(cnf, out, **kwargs)
class StringParser(Parser, FromStringLoaderMixin, ToStringDumperMixin):
"""
Abstract parser based on :meth:`load_from_string` and
:meth:`dump_to_string`.
Parser classes inherit this class must define these methods.
"""
pass
class StreamParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin):
"""
Abstract parser based on :meth:`load_from_stream` and
:meth:`dump_to_stream`.
Parser classes inherit this class must define these methods.
"""
pass
def load_with_fn(load_fn, content_or_strm, container, allow_primitives=False,
**options):
"""
Load data from given string or stream `content_or_strm`.
:param load_fn: Callable to load data
:param content_or_strm: data content or stream provides it
:param container: callble to make a container object
:param allow_primitives:
True if the parser.load* may return objects of primitive data types
other than mapping types such like JSON parser
:param options: keyword options passed to `load_fn`
:return: container object holding data
"""
ret = load_fn(content_or_strm, **options)
if anyconfig.utils.is_dict_like(ret):
return container() if (ret is None or not ret) else container(ret)
return ret if allow_primitives else container(ret)
def dump_with_fn(dump_fn, data, stream, **options):
"""
Dump `data` to a string if `stream` is None, or dump `data` to a file or
file-like object `stream`.
:param dump_fn: Callable to dump data
:param data: Data to dump
:param stream: File or file like object or None
:param options: optional keyword parameters
:return: String represents data if stream is None or None
"""
if stream is None:
return dump_fn(data, **options)
return dump_fn(data, stream, **options)
class StringStreamFnParser(Parser, FromStreamLoaderMixin, ToStreamDumperMixin):
"""
Abstract parser utilizes load and dump functions each backend module
provides such like json.load{,s} and json.dump{,s} in JSON backend.
Parser classes inherit this class must define the followings.
- _load_from_string_fn: Callable to load data from string
- _load_from_stream_fn: Callable to load data from stream (file object)
- _dump_to_string_fn: Callable to dump data to string
- _dump_to_stream_fn: Callable to dump data to stream (file object)
.. note::
Callables have to be wrapped with :func:`to_method` to make `self`
passed to the methods created from them ignoring it.
:seealso: :class:`anyconfig.backend.json.Parser`
"""
_load_from_string_fn = None
_load_from_stream_fn = None
_dump_to_string_fn = None
_dump_to_stream_fn = None
def load_from_string(self, content, container, **options):
"""
Load configuration data from given string `content`.
:param content: Configuration string
:param container: callble to make a container object
:param options: keyword options passed to `_load_from_string_fn`
:return: container object holding the configuration data
"""
return load_with_fn(self._load_from_string_fn, content, container,
allow_primitives=self.allow_primitives(),
**options)
def load_from_stream(self, stream, container, **options):
"""
Load data from given stream `stream`.
:param stream: Stream provides configuration data
:param container: callble to make a container object
:param options: keyword options passed to `_load_from_stream_fn`
:return: container object holding the configuration data
"""
return load_with_fn(self._load_from_stream_fn, stream, container,
allow_primitives=self.allow_primitives(),
**options)
def dump_to_string(self, cnf, **kwargs):
"""
Dump config `cnf` to a string.
:param cnf: Configuration data to dump
:param kwargs: optional keyword parameters to be sanitized :: dict
:return: string represents the configuration
"""
return dump_with_fn(self._dump_to_string_fn, cnf, None, **kwargs)
def dump_to_stream(self, cnf, stream, **kwargs):
"""
Dump config `cnf` to a file-like object `stream`.
TODO: How to process socket objects same as file objects ?
:param cnf: Configuration data to dump
:param stream: Config file or file like object
:param kwargs: optional keyword parameters to be sanitized :: dict
"""
dump_with_fn(self._dump_to_stream_fn, cnf, stream, **kwargs)
# vim:sw=4:ts=4:et: