1import configparser 2import dataclasses 3from dataclasses import dataclass 4from pathlib import Path 5from typing import Callable 6from typing import ClassVar 7from typing import Optional 8from typing import Union 9 10from .helpers import make_path 11 12 13class ConfigError(BaseException): 14 pass 15 16 17class MissingConfig(ConfigError): 18 pass 19 20 21class MissingConfigSection(ConfigError): 22 pass 23 24 25class MissingConfigItem(ConfigError): 26 pass 27 28 29class ConfigValueTypeError(ConfigError): 30 pass 31 32 33class _GetterDispatch: 34 def __init__(self, initialdata, default_getter: Callable): 35 self.default_getter = default_getter 36 self.data = initialdata 37 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 e 52 else: 53 with open(filepath, encoding="utf-8") as f: 54 cfg.read_file(f) 55 return cfg 56 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 nse 66 except configparser.NoOptionError as noe: 67 raise MissingConfigItem(f"No config item for {option}") from noe 68 except ValueError as ve: 69 # ConfigParser.getboolean, .getint, .getfloat raise ValueError 70 # on bad types 71 raise ConfigValueTypeError( 72 f"Wrong value type for {option}" 73 ) from ve 74 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 e 82 return rv 83 84 return caller 85 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 builtins 93 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 value 101 getters.update( 102 { 103 type_: _build_getter( 104 cfg_obj, cfg_section, "get", converter=converter_fn 105 ) 106 for type_, converter_fn in converters.items() 107 } 108 ) 109 110 return _GetterDispatch(getters, default_getter) 111 112 113@dataclass 114class ReadsCfg: 115 section_header: ClassVar[str] 116 converters: ClassVar[Optional[dict]] = None 117 118 @classmethod 119 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.converters 123 ) 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