Coverage for src/lilbee/core/config/load_warnings.py: 100%
44 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""What a settings source reports about a value it refuses: a warning, or a variable that stops."""
3from __future__ import annotations
5import hashlib
6import logging
7import os
8import threading
9from collections.abc import Iterator
10from contextlib import contextmanager
11from contextvars import ContextVar
13log = logging.getLogger(__name__)
15_MARK_LENGTH = 12
17# A child inherits the marks the startup load logged, so it does not log them again.
18SHOWN_ENV_VAR = "LILBEE_LOAD_WARNINGS_SHOWN"
19_MARK_SEPARATOR = ","
21# Marks of the warnings this process already logged, seeded from the parent's.
22_shown: set[str] = {
23 mark for mark in os.environ.get(SHOWN_ENV_VAR, "").split(_MARK_SEPARATOR) if mark
24}
25_shown_lock = threading.Lock()
27_collected: ContextVar[list[str] | None] = ContextVar("lilbee_load_warnings", default=None)
30class RefusedVariableError(ValueError):
31 """The environment sets a LILBEE_* variable lilbee does not run with."""
34def _mark(message: str) -> str:
35 """A short stable name for *message* that fits an environment variable."""
36 return hashlib.sha256(message.encode("utf-8")).hexdigest()[:_MARK_LENGTH]
39def warn_on_load(message: str, version: str = "") -> None:
40 """Log *message* unless this process already did, and keep it for a collecting load.
42 With *version*, the state of the file the message is about, a message that
43 was logged for another state of that file is logged again.
44 """
45 found = _collected.get()
46 if found is not None and message not in found:
47 found.append(message)
48 mark = _mark(message + version)
49 with _shown_lock:
50 if mark in _shown:
51 return
52 _shown.add(mark)
53 log.warning("%s", message)
56def shown_marks() -> str:
57 """The marks logged so far, as the value a child inherits in ``SHOWN_ENV_VAR``."""
58 with _shown_lock:
59 return _MARK_SEPARATOR.join(sorted(_shown))
62def forget_shown() -> None:
63 """Forget which warnings this process logged, so the next one logs again."""
64 with _shown_lock:
65 _shown.clear()
68def refuse_variable(message: str) -> None:
69 """Raise RefusedVariableError, except in a collecting load, whose entry point raises it."""
70 if _collected.get() is None:
71 raise RefusedVariableError(message)
74@contextmanager
75def collecting() -> Iterator[list[str]]:
76 """Yield the list that receives each load warning; a refused variable does not raise inside."""
77 found: list[str] = []
78 token = _collected.set(found)
79 try:
80 yield found
81 finally:
82 _collected.reset(token)