codekingpro/portable-devtools
114k
1import configparser2import dataclasses3from dataclasses import dataclass4from pathlib import Path5from typing import Callable6from typing import ClassVar7from typing import Optional8from typing import Union9 10from .helpers import make_path11 12 13class ConfigError(BaseException):14 pass15 16 17class MissingConfig(ConfigError):18 pass19 20 21class MissingConfigSection(ConfigError):22 pass23 24 25class MissingConfigItem(ConfigError):26 pass27 28 29class ConfigValueTypeError(ConfigError):30 pass31 32 33class _GetterDispatch:34 def __init__(self, initialdata, default_getter: Callable):35 self.default_getter = default_getter36 self.data = initialdata37 38 def get_fn_for_type(self, type_):39 return self.data.get(type_, self.default_getter)40 41 def get_typed_value(self, type_, name):42 get_fn = self.get_fn_for_type(type_)43 return get_fn(name)44 45 46def _parse_cfg_file(filespec: Union[Path, str]):47 cfg = configparser.ConfigParser()48 try:49 filepath = make_path(filespec, check_exists=True)50 except FileNotFoundError as e:51 raise MissingConfig(f"No config file found at {filespec}") from e52 else:53 with open(filepath, encoding="utf-8") as f:54 cfg.read_file(f)55 return cfg56 57 58def _build_getter(cfg_obj, cfg_section, method, converter=None):59 def caller(option, **kwargs):60 try:61 rv = getattr(cfg_obj, method)(cfg_section, option, **kwargs)62 except configparser.NoSectionError as nse:63 raise MissingConfigSection(64 f"No config section named {cfg_section}"65 ) from nse66 except configparser.NoOptionError as noe:67 raise MissingConfigItem(f"No config item for {option}") from noe68 except ValueError as ve:69 # ConfigParser.getboolean, .getint, .getfloat raise ValueError70 # on bad types71 raise ConfigValueTypeError(72 f"Wrong value type for {option}"73 ) from ve74 else:75 if converter:76 try:77 rv = converter(rv)78 except Exception as e:79 raise ConfigValueTypeError(80 f"Wrong value type for {option}"81 ) from e82 return rv83 84 return caller85 86 87def _build_getter_dispatch(cfg_obj, cfg_section, converters=None):88 converters = converters or {}89 90 default_getter = _build_getter(cfg_obj, cfg_section, "get")91 92 # support ConfigParser builtins93 getters = {94 int: _build_getter(cfg_obj, cfg_section, "getint"),95 bool: _build_getter(cfg_obj, cfg_section, "getboolean"),96 float: _build_getter(cfg_obj, cfg_section, "getfloat"),97 str: default_getter,98 }99 100 # use ConfigParser.get and convert value101 getters.update(102 {103 type_: _build_getter(104 cfg_obj, cfg_section, "get", converter=converter_fn105 )106 for type_, converter_fn in converters.items()107 }108 )109 110 return _GetterDispatch(getters, default_getter)111 112 113@dataclass114class ReadsCfg:115 section_header: ClassVar[str]116 converters: ClassVar[Optional[dict]] = None117 118 @classmethod119 def from_cfg_file(cls, filespec: Union[Path, str]):120 cfg = _parse_cfg_file(filespec)121 dispatch = _build_getter_dispatch(122 cfg, cls.section_header, converters=cls.converters123 )124 kwargs = {125 field.name: dispatch.get_typed_value(field.type, field.name)126 for field in dataclasses.fields(cls)127 }128 return cls(**kwargs)129 