Coverage for src/lilbee/core/config/parsing.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Parsing helpers shared by the Config field validators and the resolver's soft checks.""" 

2 

3from collections.abc import Mapping 

4from types import MappingProxyType 

5 

6# Matches what pydantic itself accepts for a bool field. Every other bool on 

7# Config is coerced by pydantic, so a narrower vocabulary here would make the 

8# same env spelling mean different things on different fields of one object. 

9_BOOL_TRUE = frozenset({"true", "t", "yes", "y", "on", "1"}) 

10_BOOL_FALSE = frozenset({"false", "f", "no", "n", "off", "0"}) 

11_AUTO_ALIASES = frozenset({"", "auto", "none"}) 

12 

13 

14def parse_bool(raw: str) -> bool: 

15 """Parse a boolean env string; raises ValueError on anything else.""" 

16 normalized = raw.strip().lower() 

17 if normalized in _BOOL_TRUE: 

18 return True 

19 if normalized in _BOOL_FALSE: 

20 return False 

21 raise ValueError(f"Invalid boolean: {raw!r}") 

22 

23 

24def parse_tristate_bool(raw: str) -> bool | None: 

25 """Parse an auto/on/off string: blank, ``auto`` or ``none`` means auto-detect.""" 

26 if raw.strip().lower() in _AUTO_ALIASES: 

27 return None 

28 return parse_bool(raw) 

29 

30 

31def parse_optional_int( 

32 raw: str, *, aliases: Mapping[str, int] = MappingProxyType({}) 

33) -> int | None: 

34 """Parse an optional integer: blank/``auto``/``none`` is None; *aliases* map other labels.""" 

35 label = raw.strip().lower() 

36 if label in _AUTO_ALIASES: 

37 return None 

38 if label in aliases: 

39 return aliases[label] 

40 return int(label) 

41 

42 

43def parse_gpu_device_list(raw: str) -> str | None: 

44 """Parse a comma-separated device index list; blank/auto/all/none means every device.""" 

45 label = raw.strip().lower() 

46 if label in _AUTO_ALIASES or label == "all": 

47 return None 

48 parts = [p.strip() for p in raw.split(",") if p.strip()] 

49 if not parts: 

50 return None 

51 for part in parts: 

52 if not part.lstrip("-").isdigit(): 

53 raise ValueError(f"Invalid device index: {part!r}") 

54 return ",".join(parts)