fix: handle missing environment variables more robustly

This commit is contained in:
Danielle McLean 2024-06-28 14:14:24 +10:00
parent fcf7254e64
commit 3b7ddfa718
Signed by: 00dani
GPG key ID: 6854781A0488421C
4 changed files with 61 additions and 7 deletions

View file

@ -5,7 +5,7 @@
groups = ["default", "all", "dev"] groups = ["default", "all", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:17fdf74bf4b2980c66c106cebcb936921e671e8adaeb2f2e95d95f255704aa38" content_hash = "sha256:73f93e2fcfc5fc5af9acfdc9604b9f27d3eb5d9c6012f600c671523c2f78bf4c"
[[package]] [[package]]
name = "aiocache" name = "aiocache"
@ -80,6 +80,16 @@ files = [
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, {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]] [[package]]
name = "class-doc" name = "class-doc"
version = "0.2.6" version = "0.2.6"

View file

@ -14,6 +14,7 @@ dependencies = [
"pytomlpp>=1.0.13", "pytomlpp>=1.0.13",
"apischema>=0.18.1", "apischema>=0.18.1",
"yarl>=1.9.4", "yarl>=1.9.4",
"boltons>=24.0.0",
] ]
readme = "README.md" readme = "README.md"

View file

@ -1,12 +1,32 @@
from collections.abc import Mapping
from os import environ from os import environ
from typing import TypeVar
from apischema import deserialize from apischema import deserialize
from boltons.iterutils import remap
from pytomlpp import load from pytomlpp import load
from xdg_base_dirs import xdg_config_home from xdg_base_dirs import xdg_config_home
from .model import Config 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<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: def loadConfigFromFile() -> Config:
@ -17,14 +37,14 @@ def loadConfigFromFile() -> Config:
def loadConfigFromEnv() -> Config: def loadConfigFromEnv() -> Config:
port = int(environ["MPD_PORT"]) if "MPD_PORT" in environ else None port = int(environ.pop("MPD_PORT")) if "MPD_PORT" in environ else None
host = environ.get("MPD_HOST") host = environ.pop("MPD_HOST", None)
password = environ.get("MPD_PASSWORD") password = environ.pop("MPD_PASSWORD", None)
cache = environ.get("MPD_NOW_PLAYABLE_CACHE") cache = environ.pop("MPD_NOW_PLAYABLE_CACHE", None)
if password is None and host is not None and "@" in host: if password is None and host is not None and "@" in host:
password, host = host.split("@", maxsplit=1) password, host = host.split("@", maxsplit=1)
data = {"cache": cache, "mpd": {"port": port, "host": host, "password": password}} data = {"cache": cache, "mpd": {"port": port, "host": host, "password": password}}
return deserialize(Config, data) return deserialize(Config, withoutNones(data))
def loadConfig() -> Config: def loadConfig() -> Config:

View file

@ -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]: ...