PKdQO6d~~pyconfs/__init__.py"""PyConfs, unified handling of configuration files for Python See {url} for more information. Current maintainers: -------------------- {maintainers} """ # Standard library imports from collections import namedtuple as _namedtuple from datetime import date as _date # PyConfs imports from pyconfs.configuration import Configuration # noqa # Version of PyConfs # # This is automatically set using the bumpversion tool __version__ = "0.1.0" # Homepage for PyConfs __url__ = "https://pyconfs.readthedocs.io/" # Authors/maintainers of Pyplugs _Author = _namedtuple("_Author", ["name", "email", "start", "end"]) _AUTHORS = [ _Author("Geir Arne Hjelle", "geirarne@gmail.com", _date(2019, 4, 1), _date.max) ] __author__ = ", ".join(a.name for a in _AUTHORS if a.start < _date.today() < a.end) __contact__ = ", ".join(a.email for a in _AUTHORS if a.start < _date.today() < a.end) # Update doc with info about maintainers def _update_doc(doc: str) -> str: """Add information to doc-string Args: doc: The doc-string to update. Returns: The updated doc-string. """ # Maintainers maintainer_list = [ f"+ {a.name} <{a.email}>" for a in _AUTHORS if a.start < _date.today() < a.end ] maintainers = "\n".join(maintainer_list) # Add to doc-string return doc.format(maintainers=maintainers, url=__url__) __doc__ = _update_doc(__doc__) PKUcQO"<<pyconfs/configuration.py"""A PyConfs Configuration object The `Configuration` is a collection of `ConfigurationEntry` objects. Entries can be grouped inside nested Configurations as well. """ # Standard library imports import functools import pathlib import textwrap from collections import UserDict from datetime import date, datetime from typing import Any, Dict, List, Optional, Set, Tuple, Union # PyConfs imports from pyconfs import converters, exceptions, readers def _dispatch_to(converter): """Decorator for dispatching methods to converter functions""" def _decorator_dispatch_to(func): @functools.wraps(func) def _wrapper_dispatch_to(self, key: str, **options): return converter(value=func(self, key), **options) return _wrapper_dispatch_to return _decorator_dispatch_to class Configuration(UserDict): def __init__(self, name: str = None) -> None: """Create an empty configuration""" super().__init__() self.name = "pyconfs.Configuration" if name is None else name @classmethod def from_dict( cls, entries: Dict[str, Any], name: Optional[str] = None ) -> "Configuration": """Create a Configuration from a dictionary""" cfg = cls(name=name) cfg.update_from_dict(entries) return cfg @classmethod def from_file( cls, file_path: Union[str, pathlib.Path], file_format: Optional[str] = None, name: Optional[str] = None, ) -> "Configuration": """Create a Configuration from a file""" file_path = pathlib.Path(file_path) name = file_path.name if name is None else name cfg = cls(name=name) cfg.update_from_file(file_path=file_path, file_format=file_format) return cfg def update_entry(self, key: str, value: Any) -> None: """Update one entry in configuration""" if isinstance(value, dict): self.data[key] = self.from_dict(value, name=key) else: self.data[key] = value def update_from_dict(self, entries: Dict[str, Any]) -> None: """Update the configuration from a dictionary""" for key, value in entries.items(): self.update_entry(key=key, value=value) def update_from_file( self, file_path: Union[str, pathlib.Path], file_format: Optional[str] = None ) -> None: """Update the configuration from a file""" file_path = pathlib.Path(file_path) file_format = ( readers.guess_format(file_path) if file_format is None else file_format ) entries = readers.read(file_format, file_path=file_path) self.update_from_dict(entries) def as_str(self, indent: int = 4, key_width: int = 30) -> str: """Represent Configuration as a string""" lines = [f"[{self.name}]"] for key, value in self.data.items(): if isinstance(value, self.__class__): value_str = value.as_str(indent=indent, key_width=key_width) lines.append("\n" + textwrap.indent(value_str, " " * indent)) else: lines.append(f"{key:<{key_width}}= {value!r}") return "\n".join(lines) # # Add converters used to convert entries to certain types # def _get_value(self, key: str) -> Any: """Get single value, raise an error if key points to a Configuration object""" value = self.data[key] if isinstance(value, self.__class__): raise exceptions.EntryError(f"{self.name}.{key!r} is a Configuration") return value @_dispatch_to(converters.to_str) def to_str(self, key: str) -> str: """Convert entry to string""" return self._get_value(key) @_dispatch_to(converters.to_int) def to_int(self, key: str) -> int: """Convert entry to integer number""" return self._get_value(key) @_dispatch_to(converters.to_float) def to_float(self, key: str) -> float: """Convert entry to a floating point number""" return self._get_value(key) @_dispatch_to(converters.to_bool) def to_bool(self, key: str) -> bool: """Convert entry to a boolean""" return self._get_value(key) @_dispatch_to(converters.to_date) def to_date(self, key: str) -> date: """Convert entry to a date""" return self._get_value(key) @_dispatch_to(converters.to_datetime) def to_datetime(self, key: str) -> datetime: """Convert entry to a datetime""" return self._get_value(key) @_dispatch_to(converters.to_path) def to_path(self, key: str) -> pathlib.Path: """Convert entry to a path""" return self._get_value(key) @_dispatch_to(converters.to_list) def to_list(self, key: str) -> List[Any]: """Convert entry to a list""" return self._get_value(key) @_dispatch_to(converters.to_dict) def to_dict(self, key: str) -> Dict[str, Any]: """Convert entry to a dictionary""" return self._get_value(key) @_dispatch_to(converters.to_set) def to_set(self, key: str) -> Set[Any]: """Convert entry to a set""" return self._get_value(key) @_dispatch_to(converters.to_tuple) def to_tuple(self, key: str, **options: Any) -> Tuple[Any, ...]: """Convert entry to a tuple""" return self._get_value(key) # # Dunder methods # def __dir__(self) -> List[str]: """Add sections and entries to list of attributes""" return list(super().__dir__()) + list(self.data.keys()) def __getattr__(self, key: str) -> Union["Configuration", "ConfigurationEntry"]: return self[key] def __repr__(self): """Simple representation of a Configuration""" return f"{self.__class__.__name__}(name={self.name!r})" def __str__(self): """Full representation of a Configuration""" return self.as_str() PKdQOҚ,= pyconfs/converters.py"""Functions that convert strings to other datatypes """ # Standard library imports import functools import pathlib import re from datetime import date, datetime from typing import Any, Callable, Dict, List, Set, Tuple # Third party imports import pyplugs # PyConfs imports from pyconfs import exceptions # Set up pyplugs plugins package, _, plugin = __name__.rpartition(".") convert = functools.partial(pyplugs.call, package, plugin) info = functools.partial(pyplugs.info, package, plugin) names = functools.partial(pyplugs.funcs, package, plugin) # Mappings for conversion to booleans _BOOLEAN_STATES = { "0": False, "1": True, "false": False, "true": True, "no": False, "yes": True, "off": False, "on": True, } @pyplugs.register def to_str(value: str) -> str: """Convert value to a string""" return str(value) @pyplugs.register def to_int(value: str) -> int: """Convert value to an integer number""" return int(value) @pyplugs.register def to_float(value: str) -> float: """Convert value to a floating point number""" return float(value) @pyplugs.register def to_bool(value: str) -> bool: """Convert value to a boolean""" try: return _BOOLEAN_STATES[str(value).lower()] except KeyError: raise exceptions.ConversionError( f"Value {value!r} can not be converted to boolean" ) from None @pyplugs.register def to_date(value: str, format="%Y-%m-%d") -> date: """Convert value to a date""" return datetime.strptime(str(value), format=format).date() @pyplugs.register def to_datetime(value: str, format="%Y-%m-%d %H:%M:%S") -> datetime: """Convert value to a datetime""" return datetime.strptime(str(value), format=format) @pyplugs.register def to_path(value: str) -> pathlib.Path: """Convert value to a path""" return pathlib.Path(value) @pyplugs.register def to_list( value: str, split_re: str = r"[\s,]+", converter: Callable[[str], Any] = str, max_split: int = 0, ) -> List[Any]: """Convert value to a list""" return [ converter(s) for s in re.split(split_re, str(value), maxsplit=max_split) if s ] @pyplugs.register def to_dict( value: str, item_split_re: str = r",\n?", key_value_split_re: str = r"[:]", converter: Callable[[str], Any] = str.strip, max_split: int = 0, ) -> Dict[str, Any]: """Convert value to a dictionary""" items = [ re.split(key_value_split_re, s, maxsplit=1) for s in re.split(item_split_re, str(value), maxsplit=max_split) if s ] return {k: converter(v) for k, v in items} @pyplugs.register def to_set( value: str, split_re: str = r"[\s,]+", converter: Callable[[str], Any] = str, max_split: int = 0, ) -> Set[Any]: """Convert value to a set""" return { converter(s) for s in re.split(split_re, str(value), maxsplit=max_split) if s } @pyplugs.register def to_tuple( value: str, split_re: str = r"[\s,]+", converter: Callable[[str], Any] = str, max_split: int = 0, ) -> Tuple[Any, ...]: """Convert value to a tuple""" return tuple( converter(s) for s in re.split(split_re, str(value), maxsplit=max_split) if s ) PKvPOW1Kpyconfs/exceptions.py"""Exceptions for the PyConfs package Custom exceptions used by PyConfs for more helpful error messages """ class PyConfsException(Exception): """Base class for all PyConfs exceptions""" class ConversionError(PyConfsException): """Conversion of Configuration entries failed""" class EntryError(PyConfsException): """Something is wrong with a given entry""" class UnknownFormat(PyConfsException): """PyConfs does not know the given format""" PKdQO=]]pyconfs/readers/__init__.py"""Plugins for reading configuration file formats """ # Standard library imports import pathlib # Third party imports import pyplugs # PyConfs imports from pyconfs.exceptions import UnknownFormat names = pyplugs.names_factory(__package__) read = pyplugs.call_factory(__package__) def guess_format(file_path: pathlib.Path) -> str: """Guess the format of a file based on the file suffix""" for reader in names(): if pyplugs.call(__package__, reader, func="is_format", file_path=file_path): return reader raise UnknownFormat(f"Could not guess format of {file_path}") PKbQO%--pyconfs/readers/ini.py"""Reader for ini-files based on ConfigParser """ # Standard library imports import pathlib from configparser import ConfigParser from typing import Any, Dict # Third party imports import pyplugs # PyConfs imports from pyconfs import converters _SUFFIXES = {".ini", ".conf"} _TYPE_SUFFIX = ":type" @pyplugs.register def read_ini_file(file_path: pathlib.Path) -> Dict[str, Any]: """Use ConfigParser to read an ini-file""" cfg = ConfigParser(delimiters=("=",)) files = cfg.read(file_path) if not files: raise FileNotFoundError(f"{file_path} could not be opened by ConfigParser") # Ini-files organize into sections with key, value pairs return _convert_types( { name: {k: v for k, v in section.items()} for name, section in cfg.items() if not name == "DEFAULT" } ) def _convert_types(entries): """Convert values inside entries dictionary based on type information Ini-files by default does not support type information (everything is a string) Add possibility to specify types using a special :-syntax (see _TYPE_SUFFIX) """ for key, value in entries.copy().items(): # Recursively convert types in subdictionaries if isinstance(value, dict): _convert_types(value) continue # Find type information if not key.endswith(_TYPE_SUFFIX): continue master = key.partition(_TYPE_SUFFIX)[0] if master not in entries: continue # Convert entry to the given type entries[master] = converters.convert(f"to_{value}", value=entries[master]) del entries[key] @pyplugs.register def is_format(file_path: pathlib.Path) -> bool: """Is the given file of ini-format?""" return file_path.suffix in _SUFFIXES PK[PO$011pyconfs-0.1.0.dist-info/LICENSEMIT License Copyright (c) 2019 Geir Arne Hjelle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HPOpyconfs-0.1.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!HY. pyconfs-0.1.0.dist-info/METADATAVn6}WLKrR a'"k0Ԭ,%$k}]TOyΜp.:G3yZ'u›5%`c뭃u|uYrۤp}+9AL*=@ym'*̥Bsm ]S]bdxN2ƥIK_ԳX2ydpL o.>]\~>a6s醐$x]*hJŎjҲ)|DiV~#’KBNۜvwpҢ:wS{vNXiRDgX1{PX^UXqBLH) S} (CW#e >OٌyTg+atGX$*}O<\\IWp$F!˜6cr%NN9Gx9U}?_Y[^>[/eŌ#Ƶ6R)o-n=ȌRI{d\g5eCNHS+95][\-9jj|Lqx!pb T7 :W|ow߅ T6م1T2Tnɒ*QJhḷēS>< Z~&" ~Caއ(kZl6c/ 3Gc3LA};Y+$R02Y v|6cmg~%{A/^i:>UmƳAo%Dh27OZd'XXPWheZa!RZ)x?!YV(<<8"!!]l`8aݣzIӏTm g {ؠՊ7ρ<_ ܪQN:6>JZϞ̂kjn?PS~`[Ɇ2p8PsЊNrקy\l>غ DVehn"&sh(4+Nck6hRxsP$!~tU>`&}%v{QV PK!Hd(pyconfs-0.1.0.dist-info/RECORDuKs@}~ Y6 BڂE׏3V͸9˯ν"OwBhN[B&4@Ze3-"Y6geKiԏ+o9Vu"/#: HG6UҲ#f`s4T@e$E.۸n4.kVm0kؚ6ڝGtsE?Z܇q]YӠ+a*>.ӹ%G7m#"#¿q{lZy4%vE/Aɓ5Ns 4(*9>i eVuZp0NS[e7v1Uzp,7'$M<)Mc.YO>2#?m=g^5H[rgex,`'/]_|aL|'"σUNMkS 3HyiZxյ` DҟF <ρ^Oo>qTK9F%ҙ3oPKdQO6d~~pyconfs/__init__.pyPKUcQO"<<pyconfs/configuration.pyPKdQOҚ,= !pyconfs/converters.pyPKvPOW1K*pyconfs/exceptions.pyPKdQO=]],pyconfs/readers/__init__.pyPKbQO%--.pyconfs/readers/ini.pyPK[PO$011 6pyconfs-0.1.0.dist-info/LICENSEPK!HPO{:pyconfs-0.1.0.dist-info/WHEELPK!HY. ;pyconfs-0.1.0.dist-info/METADATAPK!Hd(Z?pyconfs-0.1.0.dist-info/RECORDPK {A