mpd-now-playable/src/mpd_now_playable/config/load.py
Danielle McLean 27d8c37139
Significantly overhaul configuration management
Everything now uses bog-standard Python dataclasses, with Pydantic
providing validation and type conversion through separate classes using
its type adapter feature. It's also possible to define your classes
using Pydantic's own model type directly, making the type adapter
unnecessary, but I didn't want to do things that way because no actual
validation is needed when constructing a Song instance for example.
Having Pydantic do its thing only on-demand was preferable.

I tried a number of validation libraries before settling on Pydantic for
this. It's not the fastest option out there (msgspec is I think), but it
makes adding support for third-party types like yarl.URL really easy, it
generates a nice clean JSON Schema which is easy enough to adjust to my
requirements through its GenerateJsonSchema hooks, and raw speed isn't
all that important anyway since this is a single-user desktop program
that reads its configuration file once on startup.

Also, MessagePack is now mandatory if you're caching to an external
service. It just didn't make a whole lot sense to explicitly install
mpd-now-playable's Redis or Memcached support and then use pickling with
them.

With all this fussing around done, I'm probably finally ready to
actually use that configuration file to configure new features! Yay!
2024-07-01 00:10:17 +10:00

53 lines
2 KiB
Python

from collections.abc import Mapping
from os import environ
from typing import TypeVar
from boltons.iterutils import remap
from pytomlpp import load
from xdg_base_dirs import xdg_config_home
from .model import Config
__all__ = ("loadConfig",)
K = TypeVar("K")
V = TypeVar("V")
# Sadly this is the kind of function that's incredibly easy to type statically
# in something like TypeScript, but apparently impossible to type statically in
# Python. Basically the TypeScript typing looks like this:
# type OmitNulls<T> = {[K in keyof T]?: OmitNulls<NonNullable<T[K]>>};
# function withoutNulls<T>(data: T): OmitNulls<T>;
# But the Python type system (currently?) lacks mapped and index types, so you
# can't recurse over an arbitrary type's structure as OmitNulls<T> does, as
# well as conditional types so you can't easily define a "filtering" utility
# type like NonNullable<T>. Python's type system also doesn't infer a
# dictionary literal as having a structural type by default in the way
# TypeScript does, of course, so that part wouldn't work anyway.
def withoutNones(data: Mapping[K, V | None]) -> Mapping[K, V]:
return remap(data, lambda p, k, v: v is not None)
def loadConfigFromFile() -> Config:
path = xdg_config_home() / "mpd-now-playable" / "config.toml"
data = load(path)
print(f"Loaded your configuration from {path}")
return Config.schema.validate_python(data)
def loadConfigFromEnv() -> Config:
port = int(environ.pop("MPD_PORT")) if "MPD_PORT" in environ else None
host = environ.pop("MPD_HOST", None)
password = environ.pop("MPD_PASSWORD", None)
cache = environ.pop("MPD_NOW_PLAYABLE_CACHE", None)
if password is None and host is not None and "@" in host:
password, host = host.split("@", maxsplit=1)
data = {"cache": cache, "mpd": {"port": port, "host": host, "password": password}}
return Config.schema.validate_python(withoutNones(data))
def loadConfig() -> Config:
try:
return loadConfigFromFile()
except FileNotFoundError:
return loadConfigFromEnv()