Skip to content

Commit

Permalink
Merge branch 'pathlib'
Browse files Browse the repository at this point in the history
  • Loading branch information
ssato committed Jun 2, 2018
2 parents 6ef2f5c + 10ed950 commit b75cb1b
Show file tree
Hide file tree
Showing 26 changed files with 848 additions and 344 deletions.
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# see: http://about.travis-ci.org/docs/user/languages/python/
language: python
python:
#- 2.6
- 2.7
- 3.3
# - 3.3
- 3.4
- 3.5
- 3.6
# It looks failed in Travis-CI environment.
# - 3.7
# see: http://docs.travis-ci.com/user/caching/#pip-cache
cache: pip
#matrix:
Expand Down
226 changes: 125 additions & 101 deletions anyconfig/api.py

Large diffs are not rendered by default.

48 changes: 28 additions & 20 deletions anyconfig/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@


LOGGER = logging.getLogger(__name__)
TEXT_FILE = True


def ensure_outdir_exists(filepath):
Expand Down Expand Up @@ -238,15 +239,17 @@ def loads(self, content, **options):
options = self._load_options(container, **options)
return self.load_from_string(content, container, **options)

def load(self, path_or_stream, ignore_missing=False, **options):
def load(self, ioi, ac_ignore_missing=False, **options):
"""
Load config from a file path or a file / file-like object
`path_or_stream` after some checks.
Load config from a file path or a file / file-like object which `ioi`
refering after some checks.
:param path_or_stream: Config file path or file{,-like} object
:param ignore_missing:
Ignore and just return None if given `path_or_stream` is not a file
/ file-like object (thus, it should be a file path) and does not
: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.
Expand All @@ -259,13 +262,16 @@ def load(self, path_or_stream, ignore_missing=False, **options):
container = self._container_factory(**options)
options = self._load_options(container, **options)

if isinstance(path_or_stream, anyconfig.compat.STR_TYPES):
if ignore_missing and not os.path.exists(path_or_stream):
return container()
if not ioi:
return container()

cnf = self.load_from_path(path_or_stream, container, **options)
if anyconfig.utils.is_stream_ioinfo(ioi):
cnf = self.load_from_stream(ioi.src, container, **options)
else:
cnf = self.load_from_stream(path_or_stream, container, **options)
if ac_ignore_missing and not os.path.exists(ioi.path):
return container()

cnf = self.load_from_path(ioi.path, container, **options)

return cnf

Expand Down Expand Up @@ -331,23 +337,25 @@ def dumps(self, cnf, **kwargs):
kwargs = anyconfig.utils.filter_options(self._dump_opts, kwargs)
return self.dump_to_string(cnf, **kwargs)

def dump(self, cnf, path_or_stream, **kwargs):
def dump(self, cnf, ioi, **kwargs):
"""
Dump config `cnf` to a filepath or file-like object
`path_or_stream`.
Dump config `cnf` to output object of which `ioi` refering.
:param cnf: Configuration data to dump
:param path_or_stream: Config file path or file{,-like} object
: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 isinstance(path_or_stream, anyconfig.compat.STR_TYPES):
ensure_outdir_exists(path_or_stream)
self.dump_to_path(cnf, path_or_stream, **kwargs)
if anyconfig.utils.is_stream_ioinfo(ioi):
self.dump_to_stream(cnf, ioi.src, **kwargs)
else:
self.dump_to_stream(cnf, path_or_stream, **kwargs)
ensure_outdir_exists(ioi.path)
self.dump_to_path(cnf, ioi.path, **kwargs)


class Parser(TextFilesMixin, LoaderMixin, DumperMixin):
Expand Down
1 change: 1 addition & 0 deletions anyconfig/backend/configobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (C) 2013 - 2017 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=deprecated-method
r"""Configobj backend:
- Format to support: configobj, http://goo.gl/JbP2Kp (readthedocs.org)
Expand Down
2 changes: 1 addition & 1 deletion anyconfig/backend/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright (C) 2011 - 2017 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=unused-argument
# pylint: disable=unused-argument, deprecated-method
r"""INI backend:
- Format to support: INI or INI like ones
Expand Down
140 changes: 46 additions & 94 deletions anyconfig/backends.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (C) 2011 - 2016 Satoru SATOH <ssato @ redhat.com>
# Copyright (C) 2011 - 2018 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# Suppress:
Expand All @@ -16,6 +16,7 @@
import pkg_resources

import anyconfig.compat
import anyconfig.ioinfo
import anyconfig.utils

import anyconfig.backend.base
Expand All @@ -26,6 +27,7 @@
import anyconfig.backend.shellvars
import anyconfig.backend.xml


LOGGER = logging.getLogger(__name__)
PARSERS = [anyconfig.backend.ini.Parser, anyconfig.backend.json.Parser,
anyconfig.backend.pickle.Parser,
Expand Down Expand Up @@ -59,20 +61,6 @@
continue


class UnknownParserTypeError(RuntimeError):
"""Raise if no parsers were found for given type."""
def __init__(self, forced_type):
msg = "No parser found for type '%s'" % forced_type
super(UnknownParserTypeError, self).__init__(msg)


class UnknownFileTypeError(RuntimeError):
"""Raise if not parsers were found for given file path."""
def __init__(self, path):
msg = "No parser found for file '%s'" % path
super(UnknownFileTypeError, self).__init__(msg)


def fst(tpl):
"""
>>> fst((0, 1))
Expand Down Expand Up @@ -149,104 +137,68 @@ def _list_parsers_by_extension(cps):
_PARSERS_BY_EXT = tuple(_list_parsers_by_extension(PARSERS))


def find_by_file(path_or_stream, cps=_PARSERS_BY_EXT, is_path_=False):
def inspect_io_obj(obj, cps_by_ext=_PARSERS_BY_EXT,
cps_by_type=_PARSERS_BY_TYPE, forced_type=None):
"""
Find config parser by the extension of file `path_or_stream`, file path or
stream (a file or file-like objects).
Inspect a given object `obj` which may be a path string, file / file-like
object, pathlib.Path object or `~anyconfig.globals.IOInfo` namedtuple
object, and find out appropriate parser object to load or dump from/to it
along with other I/O information.
:param path_or_stream: Config file path or file/file-like object
:param cps:
A tuple of pairs of (type, parser_class) or None if you want to compute
this value dynamically.
:param is_path_: True if given `path_or_stream` is a file path
:param obj:
a file path string, file / file-like object, pathlib.Path object or
`~anyconfig.globals.IOInfo` object
:param forced_type: Forced type of parser to load or dump
:return: Config Parser class found
>>> find_by_file("a.missing_cnf_ext") is None
True
>>> strm = anyconfig.compat.StringIO()
>>> find_by_file(strm) is None
True
>>> find_by_file("a.json")
<class 'anyconfig.backend.json.Parser'>
>>> find_by_file("a.json", is_path_=True)
<class 'anyconfig.backend.json.Parser'>
:return: anyconfig.globals.IOInfo object :: namedtuple
:raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
"""
if cps is None:
cps = _list_parsers_by_extension(PARSERS)
return anyconfig.ioinfo.make(obj, cps_by_ext, cps_by_type,
forced_type=forced_type)

if not is_path_ and not anyconfig.utils.is_path(path_or_stream):
path_or_stream = anyconfig.utils.get_path_from_stream(path_or_stream)
if path_or_stream is None:
return None # There is no way to detect file path.

ext_ref = anyconfig.utils.get_file_extension(path_or_stream)
return next((psrs[-1] for ext, psrs in cps if ext == ext_ref), None)


def find_by_type(cptype, cps=_PARSERS_BY_TYPE):
def find_parser_by_type(forced_type, cps_by_ext=_PARSERS_BY_EXT,
cps_by_type=_PARSERS_BY_TYPE):
"""
Find config parser by file's extension.
Find out appropriate parser object to load inputs of given type.
:param cptype: Config file's type
:param cps:
A list of pairs (type, parser_class) or None if you want to compute
this value dynamically.
:param forced_type: Forced parser type
:param cps_by_type: A list of pairs (parser_type, [parser_class])
:return: Config Parser class found
>>> find_by_type("missing_type") is None
True
:return:
An instance of :class:`~anyconfig.backend.base.Parser` or None means no
appropriate parser was found
:raises: UnknownParserTypeError
"""
if cps is None:
cps = _list_parsers_by_type(PARSERS)
if forced_type is None or not forced_type:
raise ValueError("forced_type must be a some string")

return next((psrs[-1] or None for t, psrs in cps if t == cptype), None)
return anyconfig.ioinfo.find_parser(None, cps_by_ext=cps_by_ext,
cps_by_type=cps_by_type,
forced_type=forced_type)


def find_parser(path_or_stream, forced_type=None, is_path_=False):
def find_parser(obj, forced_type=None):
"""
Find out config parser object appropriate to load from a file of given path
or file/file-like object.
Find out appropriate parser object to load from a file of given path or
file/file-like object.
:param path_or_stream: Configuration file path or file / file-like object
:param obj:
a file path string, file / file-like object, pathlib.Path object or
`~anyconfig.globals.IOInfo` object
:param forced_type: Forced configuration parser type
:param is_path_: True if given `path_or_stream` is a file path
:return: A tuple of (Parser class or None, "" or error message)
>>> find_parser(None) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
ValueError: path_or_stream or forced_type must be some value
>>> find_parser(None, "type_not_exist"
... ) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
UnknownParserTypeError: No parser found for type 'type_not_exist'
>>> find_parser("cnf.ext_not_found"
... ) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
UnknownFileTypeError: No parser found for file 'cnf.ext_not_found'
>>> find_parser(None, "ini")
<class 'anyconfig.backend.ini.Parser'>
>>> find_parser("cnf.json")
<class 'anyconfig.backend.json.Parser'>
>>> find_parser("cnf.json", is_path_=True)
<class 'anyconfig.backend.json.Parser'>
:raises: ValueError, UnknownParserTypeError, UnknownFileTypeError
"""
if not path_or_stream and forced_type is None:
raise ValueError("path_or_stream or forced_type must be some value")

if forced_type is not None:
parser = find_by_type(forced_type)
if parser is None:
raise UnknownParserTypeError(forced_type)
else:
parser = find_by_file(path_or_stream, is_path_=is_path_)
if parser is None:
raise UnknownFileTypeError(path_or_stream)

return parser
if anyconfig.utils.is_ioinfo(obj):
return obj.parser # It must have this.

ioi = inspect_io_obj(obj, _PARSERS_BY_EXT, _PARSERS_BY_TYPE, forced_type)
psr = ioi.parser
LOGGER.debug("Using parser %r [%s] for input type %s",
psr, psr.type(), ioi.type)
return psr


def list_types(cps=_PARSERS_BY_TYPE):
Expand Down
2 changes: 1 addition & 1 deletion anyconfig/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def _load_diff(args):
"""
try:
diff = API.load(args.inputs, args.itype,
ignore_missing=args.ignore_missing,
ac_ignore_missing=args.ignore_missing,
ac_merge=args.merge,
ac_template=args.template,
ac_schema=args.schema)
Expand Down
5 changes: 5 additions & 0 deletions anyconfig/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import itertools
import sys

try:
import pathlib # flake8: noqa
except ImportError:
pathlib = None

try:
from logging import NullHandler
except ImportError: # python < 2.7 doesn't have it.
Expand Down
24 changes: 23 additions & 1 deletion anyconfig/globals.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#
# Copyright (C) 2013 - 2017 Satoru SATOH <ssato @ redhat.com>
# Copyright (C) 2013 - 2018 Satoru SATOH <ssato @ redhat.com>
# License: MIT
#
# pylint: disable=invalid-name
"""anyconfig globals.
"""
import collections
import anyconfig.init


Expand All @@ -13,4 +15,24 @@

LOGGER = anyconfig.init.getLogger(PACKAGE)

IOI_KEYS = "src type path parser opener".split()
IOInfo = collections.namedtuple("IOInfo", IOI_KEYS)

IOI_TYPES = (IOI_NONE, IOI_PATH_STR, IOI_PATH_OBJ, IOI_STREAM) = \
(None, "path", "pathlib.Path", "stream")


class UnknownParserTypeError(RuntimeError):
"""Raise if no parsers were found for given type."""
def __init__(self, forced_type):
msg = "No parser found for type '%s'" % forced_type
super(UnknownParserTypeError, self).__init__(msg)


class UnknownFileTypeError(RuntimeError):
"""Raise if not parsers were found for given file path."""
def __init__(self, path):
msg = "No parser found for file '%s'" % path
super(UnknownFileTypeError, self).__init__(msg)

# vim:sw=4:ts=4:et:
Loading

0 comments on commit b75cb1b

Please sign in to comment.