Last active
August 29, 2025 01:15
-
-
Save mmorton/eccf27bf6ac61df466969c2140b924c6 to your computer and use it in GitHub Desktop.
A super simple implementation of a "typed" configuration in Python.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import inspect | |
| import json | |
| import os | |
| from dataclasses import dataclass, field, is_dataclass | |
| from typing import Any, Callable, Dict, List, Type, Union, get_type_hints | |
| import toml | |
| _META = "__configurable_meta__" | |
| _COMP = "__configurable_oncomplete__" | |
| def convert_keys_to_int(d: Dict) -> Dict: | |
| return ( | |
| { | |
| int(k): v if not isinstance(v, dict) else convert_keys_to_int(v) | |
| for k, v in d.items() | |
| } | |
| if d is not None | |
| else {} | |
| ) | |
| @dataclass | |
| class ConfigurableMeta: | |
| attr_helpers: Dict[str, Callable[[Any], Any]] = field(default_factory=lambda: {}) | |
| _NONE = ConfigurableMeta(attr_helpers={}) | |
| def configurable(cls=None, attr_helpers: Dict[str, Callable[[Any], Any]] = None): | |
| def wrap(cls): | |
| setattr(cls, _META, ConfigurableMeta(attr_helpers=attr_helpers)) | |
| return cls | |
| if cls is None: | |
| return wrap | |
| return wrap(cls) | |
| def configurable_oncomplete(func): | |
| setattr(func, _COMP, True) | |
| return func | |
| def is_configurable(obj): | |
| cls = obj if isinstance(obj, type) else type(obj) | |
| return hasattr(cls, _META) | |
| def _get_meta(obj) -> ConfigurableMeta: | |
| cls = obj if isinstance(obj, type) else type(obj) | |
| return getattr(cls, _META) if hasattr(cls, _META) else _NONE | |
| def _def_attr_helper(v): | |
| return v | |
| def _get_data_name(attr_name: str) -> str: | |
| return attr_name.replace("_", "").lower() | |
| def _join_env_names(*args: List[str], separator: str = "_") -> str: | |
| return separator.join( | |
| map(lambda v: v.upper(), filter(lambda v: v is not None and len(v) > 0, args)) | |
| ) | |
| def _deserialize_env(prop_type: Type, v: str) -> Any: | |
| if prop_type == str: | |
| return v | |
| if prop_type == bool: | |
| if v == "0": | |
| return False | |
| if v == "1": | |
| return True | |
| return json.loads(v) | |
| def _create_data_map(data: Dict[str, Any]) -> Dict[str, Any]: | |
| data_map = ( | |
| {_get_data_name(str(k)): v for k, v in data.items()} if data is not None else {} | |
| ) | |
| data_map.update(data) | |
| return data_map | |
| def marshal_config(target: Any, data: Dict[str, Any] = None, env_prefix: str = ""): | |
| meta = _get_meta(target) | |
| data = _create_data_map(data) | |
| for attr_name, attr_type in get_type_hints(target).items(): | |
| data_name = _get_data_name(attr_name) | |
| if is_configurable(attr_type) or is_dataclass(attr_type): | |
| v = getattr(target, attr_name) | |
| v = marshal_config( | |
| v if v is not None else attr_type(), | |
| data[data_name] if data_name in data else {}, | |
| env_prefix=_join_env_names(env_prefix, attr_name), | |
| ) | |
| setattr(target, attr_name, v) | |
| else: | |
| env_name = _join_env_names(env_prefix, attr_name) | |
| v = ( | |
| meta.attr_helpers[attr_name] | |
| if attr_name in meta.attr_helpers | |
| else _def_attr_helper | |
| )( | |
| _deserialize_env(attr_type, os.environ[env_name]) | |
| if env_name in os.environ | |
| else data[data_name] | |
| if data_name in data | |
| else None | |
| ) | |
| if v is not None: | |
| setattr(target, attr_name, v) | |
| for method_name, _ in inspect.getmembers( | |
| target, predicate=lambda x: inspect.ismethod(x) and getattr(x, _COMP, False) | |
| ): | |
| func = getattr(target, method_name) | |
| func() | |
| return target | |
| @dataclass | |
| class LogConfig: | |
| level: str = "INFO" | |
| @dataclass | |
| class SomeValue: | |
| name: str = "is something" | |
| @dataclass | |
| @configurable( | |
| attr_helpers={ | |
| "values": lambda v: [marshal_config(SomeValue(), e) for e in v] | |
| if v is not None | |
| else [] | |
| }, | |
| ) | |
| class SomeConfig: | |
| enable: bool = True | |
| values: List[SomeValue] = field(default_factory=lambda: []) | |
| @dataclass | |
| class Config: | |
| log: LogConfig | |
| some: SomeConfig | |
| __ok: bool = False | |
| def __init__(self): | |
| self.log = LogConfig() | |
| self.some = SomeConfig() | |
| @configurable_oncomplete | |
| def setup(self): | |
| self.__ok = True | |
| def get_config_from(path: str = None, **kwargs) -> Config: | |
| data = toml.load(path) if os.path.exists(path) else None | |
| return marshal_config(Config(**kwargs), data) | |
| def get_config(search_paths: List[str] = None, **kwargs) -> Config: | |
| for path in search_paths: | |
| if os.path.exists(path): | |
| return get_config_from(path, **kwargs) | |
| conf = Config(**kwargs) | |
| conf.setup() | |
| return conf |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment