diff --git a/pdm.lock b/pdm.lock index d27ab83..dab350d 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "all", "dev"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:17fdf74bf4b2980c66c106cebcb936921e671e8adaeb2f2e95d95f255704aa38" +content_hash = "sha256:73f93e2fcfc5fc5af9acfdc9604b9f27d3eb5d9c6012f600c671523c2f78bf4c" [[package]] name = "aiocache" @@ -80,6 +80,16 @@ files = [ {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] +[[package]] +name = "boltons" +version = "24.0.0" +requires_python = ">=3.7" +summary = "When they're not builtins, they're boltons." +files = [ + {file = "boltons-24.0.0-py3-none-any.whl", hash = "sha256:9618695a6ec4f50412e7072e5d78910a00b4111d0b9b549e4a3d60bc321e7807"}, + {file = "boltons-24.0.0.tar.gz", hash = "sha256:7153feccaea1ff2e1472f68d4b57fadb796a2ee49d29f638f1c9cd8fb5ebd916"}, +] + [[package]] name = "class-doc" version = "0.2.6" diff --git a/pyproject.toml b/pyproject.toml index 0aedf38..4c7aaf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pytomlpp>=1.0.13", "apischema>=0.18.1", "yarl>=1.9.4", + "boltons>=24.0.0", ] readme = "README.md" diff --git a/src/mpd_now_playable/config/load.py b/src/mpd_now_playable/config/load.py index e74aa04..9e79c09 100644 --- a/src/mpd_now_playable/config/load.py +++ b/src/mpd_now_playable/config/load.py @@ -1,12 +1,32 @@ +from collections.abc import Mapping from os import environ +from typing import TypeVar from apischema import deserialize +from boltons.iterutils import remap from pytomlpp import load from xdg_base_dirs import xdg_config_home from .model import Config -__all__ = "loadConfig" +__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 = {[K in keyof T]?: OmitNulls>}; +# function withoutNulls(data: T): OmitNulls; +# But the Python type system (currently?) lacks mapped and index types, so you +# can't recurse over an arbitrary type's structure as OmitNulls does, as +# well as conditional types so you can't easily define a "filtering" utility +# type like NonNullable. 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: @@ -17,14 +37,14 @@ def loadConfigFromFile() -> Config: def loadConfigFromEnv() -> Config: - port = int(environ["MPD_PORT"]) if "MPD_PORT" in environ else None - host = environ.get("MPD_HOST") - password = environ.get("MPD_PASSWORD") - cache = environ.get("MPD_NOW_PLAYABLE_CACHE") + 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 deserialize(Config, data) + return deserialize(Config, withoutNones(data)) def loadConfig() -> Config: diff --git a/stubs/boltons/iterutils.pyi b/stubs/boltons/iterutils.pyi new file mode 100644 index 0000000..2b56e0a --- /dev/null +++ b/stubs/boltons/iterutils.pyi @@ -0,0 +1,23 @@ +from collections.abc import Callable, Mapping +from typing import TypeAlias, TypeVar + +# Apparently you need Python 3.13 for type var defaults to work. But since this +# is just a stub file, it's okay if they aren't supported at runtime. +KIn = TypeVar("KIn") +KOut = TypeVar("KOut", default=KIn) +VIn = TypeVar("VIn") +VOut = TypeVar("VOut", default=VIn) + +Path: TypeAlias = tuple[KIn, ...] + +# remap() is Complicated and really difficult to define a type for, so I'm not +# surprised the boltons package doesn't try to type it for you. This particular +# type declaration works fine for my use of the function, but it's actually +# vastly more flexible than that - it'll accept any iterable as the root, not +# just mappings, and you can provide "enter" and "exit" callables to support +# arbitrary data structures. +def remap( + root: Mapping[KIn, VIn], + visit: Callable[[Path[KIn], KIn, VIn], tuple[KOut, VOut] | bool], + reraise_visit: bool = False, +) -> Mapping[KOut, VOut]: ...