From 3618e43ffbc7375bf13fc7a2d51931df1b8df7fd Mon Sep 17 00:00:00 2001 From: PureFunctor Date: Fri, 11 Jun 2021 01:45:17 +0800 Subject: [PATCH] Extract deserialization into _FromToml --- stlt/config.py | 85 ++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/stlt/config.py b/stlt/config.py index 0b1ae8d..e5ffd43 100644 --- a/stlt/config.py +++ b/stlt/config.py @@ -1,6 +1,7 @@ """Reading and writing user configuration and secrets.""" from __future__ import annotations +from abc import ABC from importlib import resources from pathlib import Path import typing as t @@ -15,8 +16,44 @@ DEFAULT_CONFIG_FILE = toml.loads(resources.read_text(assets, "config.toml")) +_FromTomlType = t.TypeVar("_FromTomlType", bound="_FromToml") + + +class _FromToml(ABC): + """Implements deserialization from `toml` into `attrs`.""" + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: # pragma: no cover + ... + + @classmethod + def from_dict(cls: t.Type[_FromTomlType], data: t.Mapping) -> _FromTomlType: + """Deserialize some `data` into an `attrs`-based `cls`.""" + kwargs = {} + + for field in attr.fields(cls): + name = field.name + meta = field.metadata + + try: + if section := meta.get("section", False): + kwargs[name] = _nested_get(data, section) + else: + kwargs[name] = data[name] + except KeyError as e: + if "section" in meta: + err = f"Missing required section {e.args[0]}" + else: + err = f"Missing required key {name}" + raise ConfigError(err) + + if builder := meta.get("builder", False): + kwargs[name] = builder(kwargs[name]) + + return cls(**kwargs) + + @attr.s(slots=True) -class OAuthConfig: +class OAuthConfig(_FromToml): """Configuration data class for `SpotifyOAuth`.""" client_id: str = attr.ib() @@ -24,42 +61,16 @@ class OAuthConfig: redirect_uri: str = attr.ib() scope: str = attr.ib() - @classmethod - def from_dict(cls, data: t.Mapping) -> OAuthConfig: - """Create an `OAuthConfig` from a `toml`-based mapping.""" - fields = {} - for field in attr.fields(cls): - name = field.name - try: - fields[name] = data[name] - except KeyError: - err = f"Missing required key '{name}'" - raise ConfigError(err) from None - return cls(**fields) - @attr.s -class CacheConfig: +class CacheConfig(_FromToml): """Configuration data class for the cache.""" auth_cache: Path = attr.ib(converter=Path) - @classmethod - def from_dict(cls, data: t.Mapping) -> CacheConfig: - """Create a `CacheConfig` from a `toml`-based mapping.""" - fields = {} - for field in attr.fields(cls): - name = field.name - try: - fields[name] = data[name] - except KeyError: - err = f"Missing required key '{name}'" - raise ConfigError(err) from None - return cls(**fields) - @attr.s(slots=True) -class Config: +class Config(_FromToml): """Configuration data class for the project.""" oauth: OAuthConfig = attr.ib( @@ -70,22 +81,6 @@ class Config: metadata={"section": ["cache"], "builder": CacheConfig.from_dict} ) - @classmethod - def from_dict(cls, data: t.Mapping) -> Config: - """Create a `Config` from a `toml`-based mapping.""" - sections = {} - for field in attr.fields(cls): - name = field.name - meta = field.metadata - builder = meta["builder"] - section = meta["section"] - try: - sections[name] = builder(_nested_get(data, section)) - except KeyError as e: - err = f"Missing required section '{e.args[0]}'" - raise ConfigError(err) from None - return cls(**sections) - def ensure_config(config: Path) -> None: """Ensure that the `config` file exists and is valid."""