mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-26 06:40:42 +00:00
669 lines
22 KiB
Python
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:
|