_config.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import configparser
  2. import dataclasses
  3. from dataclasses import dataclass
  4. from pathlib import Path
  5. from typing import Callable
  6. from typing import ClassVar
  7. from typing import Optional
  8. from typing import Union
  9. from .helpers import make_path
  10. class ConfigError(BaseException):
  11. pass
  12. class MissingConfig(ConfigError):
  13. pass
  14. class MissingConfigSection(ConfigError):
  15. pass
  16. class MissingConfigItem(ConfigError):
  17. pass
  18. class ConfigValueTypeError(ConfigError):
  19. pass
  20. class _GetterDispatch:
  21. def __init__(self, initialdata, default_getter: Callable):
  22. self.default_getter = default_getter
  23. self.data = initialdata
  24. def get_fn_for_type(self, type_):
  25. return self.data.get(type_, self.default_getter)
  26. def get_typed_value(self, type_, name):
  27. get_fn = self.get_fn_for_type(type_)
  28. return get_fn(name)
  29. def _parse_cfg_file(filespec: Union[Path, str]):
  30. cfg = configparser.ConfigParser()
  31. try:
  32. filepath = make_path(filespec, check_exists=True)
  33. except FileNotFoundError as e:
  34. raise MissingConfig(f"No config file found at {filespec}") from e
  35. else:
  36. with open(filepath, encoding="utf-8") as f:
  37. cfg.read_file(f)
  38. return cfg
  39. def _build_getter(cfg_obj, cfg_section, method, converter=None):
  40. def caller(option, **kwargs):
  41. try:
  42. rv = getattr(cfg_obj, method)(cfg_section, option, **kwargs)
  43. except configparser.NoSectionError as nse:
  44. raise MissingConfigSection(
  45. f"No config section named {cfg_section}"
  46. ) from nse
  47. except configparser.NoOptionError as noe:
  48. raise MissingConfigItem(f"No config item for {option}") from noe
  49. except ValueError as ve:
  50. # ConfigParser.getboolean, .getint, .getfloat raise ValueError
  51. # on bad types
  52. raise ConfigValueTypeError(
  53. f"Wrong value type for {option}"
  54. ) from ve
  55. else:
  56. if converter:
  57. try:
  58. rv = converter(rv)
  59. except Exception as e:
  60. raise ConfigValueTypeError(
  61. f"Wrong value type for {option}"
  62. ) from e
  63. return rv
  64. return caller
  65. def _build_getter_dispatch(cfg_obj, cfg_section, converters=None):
  66. converters = converters or {}
  67. default_getter = _build_getter(cfg_obj, cfg_section, "get")
  68. # support ConfigParser builtins
  69. getters = {
  70. int: _build_getter(cfg_obj, cfg_section, "getint"),
  71. bool: _build_getter(cfg_obj, cfg_section, "getboolean"),
  72. float: _build_getter(cfg_obj, cfg_section, "getfloat"),
  73. str: default_getter,
  74. }
  75. # use ConfigParser.get and convert value
  76. getters.update(
  77. {
  78. type_: _build_getter(
  79. cfg_obj, cfg_section, "get", converter=converter_fn
  80. )
  81. for type_, converter_fn in converters.items()
  82. }
  83. )
  84. return _GetterDispatch(getters, default_getter)
  85. @dataclass
  86. class ReadsCfg:
  87. section_header: ClassVar[str]
  88. converters: ClassVar[Optional[dict]] = None
  89. @classmethod
  90. def from_cfg_file(cls, filespec: Union[Path, str]):
  91. cfg = _parse_cfg_file(filespec)
  92. dispatch = _build_getter_dispatch(
  93. cfg, cls.section_header, converters=cls.converters
  94. )
  95. kwargs = {
  96. field.name: dispatch.get_typed_value(field.type, field.name)
  97. for field in dataclasses.fields(cls)
  98. }
  99. return cls(**kwargs)