summaryrefslogtreecommitdiff
path: root/mako/testing/_config.py
blob: 4ee3d0a6ed770aba19adb757ffd552d504c5b9e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import configparser
import dataclasses
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
from typing import ClassVar
from typing import Optional
from typing import Union

from .helpers import make_path


class ConfigError(BaseException):
    pass


class MissingConfig(ConfigError):
    pass


class MissingConfigSection(ConfigError):
    pass


class MissingConfigItem(ConfigError):
    pass


class ConfigValueTypeError(ConfigError):
    pass


class _GetterDispatch:
    def __init__(self, initialdata, default_getter: Callable):
        self.default_getter = default_getter
        self.data = initialdata

    def get_fn_for_type(self, type_):
        return self.data.get(type_, self.default_getter)

    def get_typed_value(self, type_, name):
        get_fn = self.get_fn_for_type(type_)
        return get_fn(name)


def _parse_cfg_file(filespec: Union[Path, str]):
    cfg = configparser.ConfigParser()
    try:
        filepath = make_path(filespec, check_exists=True)
    except FileNotFoundError as e:
        raise MissingConfig(f"No config file found at {filespec}") from e
    else:
        with open(filepath, encoding="utf-8") as f:
            cfg.read_file(f)
        return cfg


def _build_getter(cfg_obj, cfg_section, method, converter=None):
    def caller(option, **kwargs):
        try:
            rv = getattr(cfg_obj, method)(cfg_section, option, **kwargs)
        except configparser.NoSectionError as nse:
            raise MissingConfigSection(
                f"No config section named {cfg_section}"
            ) from nse
        except configparser.NoOptionError as noe:
            raise MissingConfigItem(f"No config item for {option}") from noe
        except ValueError as ve:
            # ConfigParser.getboolean, .getint, .getfloat raise ValueError
            # on bad types
            raise ConfigValueTypeError(
                f"Wrong value type for {option}"
            ) from ve
        else:
            if converter:
                try:
                    rv = converter(rv)
                except Exception as e:
                    raise ConfigValueTypeError(
                        f"Wrong value type for {option}"
                    ) from e
            return rv

    return caller


def _build_getter_dispatch(cfg_obj, cfg_section, converters=None):
    converters = converters or {}

    default_getter = _build_getter(cfg_obj, cfg_section, "get")

    # support ConfigParser builtins
    getters = {
        int: _build_getter(cfg_obj, cfg_section, "getint"),
        bool: _build_getter(cfg_obj, cfg_section, "getboolean"),
        float: _build_getter(cfg_obj, cfg_section, "getfloat"),
        str: default_getter,
    }

    # use ConfigParser.get and convert value
    getters.update(
        {
            type_: _build_getter(
                cfg_obj, cfg_section, "get", converter=converter_fn
            )
            for type_, converter_fn in converters.items()
        }
    )

    return _GetterDispatch(getters, default_getter)


@dataclass
class ReadsCfg:
    section_header: ClassVar[str]
    converters: ClassVar[Optional[dict]] = None

    @classmethod
    def from_cfg_file(cls, filespec: Union[Path, str]):
        cfg = _parse_cfg_file(filespec)
        dispatch = _build_getter_dispatch(
            cfg, cls.section_header, converters=cls.converters
        )
        kwargs = {
            field.name: dispatch.get_typed_value(field.type, field.name)
            for field in dataclasses.fields(cls)
        }
        return cls(**kwargs)