PKQO"ʲ~~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.1" # 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__) PK{QO(H%%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 re import textwrap from collections import UserDict from datetime import date, datetime from typing import Any, Callable, 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: Optional[str] = None, _vars: Optional[Dict[str, str]] = None ) -> None: """Create an empty configuration""" super().__init__() self.name = "pyconfs.Configuration" if name is None else name self.vars = {} if _vars is None else _vars @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.setdefault(key, self.__class__(name=key, _vars=self.vars)) self.data[key].update_from_dict(value) 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) @property def sections(self) -> List["Configuration"]: """List of sections in Configuration Only actual sections are included, not top level entries. """ return [s for s in self.data.values() if isinstance(s, self.__class__)] @property def section_names(self) -> List[str]: """List names of sections in Configuration Only actual sections are included, not top level entries. """ return [n for n, s in self.data.items() if isinstance(s, self.__class__)] @property def entries(self) -> List[Tuple[str, Any]]: """List of key, value entries in Configuration Only actual entries are included, not subsections. """ return [ (k, v) for k, v in self.data.items() if not isinstance(v, self.__class__) ] @property def entry_values(self) -> List[Any]: """List of values in Configuration Only actual values are included, not subsections. """ return [v for v in self.data.values() if not isinstance(v, self.__class__)] @property def entry_keys(self) -> List[str]: """List of keys in Configuration Only actual keys are included, not subsections. """ return [k for k, v in self.data.items() if not isinstance(v, self.__class__)] def replace( self, key: str, *, converter: Optional[Callable[[str], Any]] = None, default: Optional[str] = None, **replace_vars: str, ) -> Any: """Replace values in an entry based on {} format strings""" all_vars = {**self.vars, **replace_vars} replaced = _replace(self.data[key], replace_vars=all_vars, default=default) if converter is not None: if isinstance(converter, str): replaced = converters.convert(f"to_{converter}", value=replaced) else: replaced = converter(replaced) return replaced 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() def _replace( string: str, replace_vars: Dict[str, str], default: Optional[str] = None ) -> str: """Replace format style variables in a string Handles nested replacements by first replacing the replace_vars. Format specifiers (after colon, :) are allowed, but can not contain nested format strings. This function is used instead of str.format for three reasons. It handles: - that not all pairs of {...} are replaced at once - optional default values for variables that are not specified - nested replacements where values of replace_vars may be replaced themselves Credit: Originally written for `midgard.config.Configuration` Args: string: Original string replace_vars: Variables that can be replaced default: Optional default value used for vars not in replace_vars. """ matches = re.finditer(r"\{(\w+)(:[^\{\}]*)?\}", string) for match in matches: var = match.group(1) var_expr = match.string[slice(*match.span())] replacement = replace_vars.get(var) if replacement is None: # Default replacements replacement = var_expr if default is None else default else: # Nested replacements replacement = _replace(replacement, replace_vars, default) # Use str.format to handle format specifiers string = string.replace(var_expr, var_expr.format(**{var: replacement})) return string PK\{QO309 9 pyconfs/converters.py"""Functions that convert strings to other datatypes """ # Standard library imports import functools import os.path 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""" # Handle ~ shortcut for home directories if "~" in value: value = os.path.expanduser(value) 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}") PKfQOVAApyconfs/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] return entries @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.1.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.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,szd&Y)r$[)T&UrPK!H;q pyconfs-0.1.1.dist-info/METADATAVmo6_qk7t")퀭PmA6F9 !Ě9^t$x/:G3yZ'u›5%`ckAӺ,mRj|J%fP*SA!ʼܓ*̥BsmIbh K$ge3*™X9\|}fm "YITДeS‘N[ ’KBNbN_`ߵ~:.VQ+]7xJ"ӫ+\ 6%aa깯ebtLGI0Q6Lb%0{#t#lNJAJZϞkQ~FtlgDb=NpkCA+Zy\j>غ@DVehnZ"&sh(5+Nkk46hxs`(!4~ ]}dL8J?PK!HTz^pyconfs-0.1.1.dist-info/RECORDuK@}]RPhMF~d1179/fJu} T%A轙pɴF4ޤ9dKX78y;bFzk~[?Y6!~czmv`;D֚osdyl-Jx쳖dmm.G|13Q ^JD>ߕ] /0G4k~=k3|s/cC=sDnrx[,9{tКzhywҰ(mD LJIyqQD[Z{(t:[B]ʢXYfʎe?lK_kp>l0:Ëq@a`*HdI i>~R,Ƅ~-eO`Y~wsi}jf=n^/!PutGN5>Fn ]~iziFE*F2 zPKQO"ʲ~~pyconfs/__init__.pyPK{QO(H%%pyconfs/configuration.pyPK\{QO309 9 +pyconfs/converters.pyPKvPOW1K8pyconfs/exceptions.pyPKdQO=]]:pyconfs/readers/__init__.pyPKfQOVAA=pyconfs/readers/ini.pyPK[PO$011Epyconfs-0.1.1.dist-info/LICENSEPK!HPOqIpyconfs-0.1.1.dist-info/WHEELPK!H;q Ipyconfs-0.1.1.dist-info/METADATAPK!HTz^ONpyconfs-0.1.1.dist-info/RECORDPK sP