Coverage for lintro / utils / tool_options.py: 91%
44 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tool options parsing and coercion utilities.
3Handles parsing of CLI tool options and coercing string values to typed Python values.
4"""
6from lintro.enums.boolean_string import BooleanString
9def _coerce_value(raw: str) -> object:
10 """Coerce a raw CLI value into a typed Python value.
12 Rules:
13 - "True"/"False" (case-insensitive) -> bool
14 - "None"/"null" (case-insensitive) -> None
15 - integer (e.g., 88) -> int
16 - float (e.g., 0.75) -> float
17 - list via pipe-delimited values (e.g., "E|F|W") -> list[str]
18 Pipe is chosen to avoid conflict with the top-level comma separator.
19 - otherwise -> original string
21 Args:
22 raw: str: Raw CLI value to coerce.
24 Returns:
25 object: Coerced value.
26 """
27 s = raw.strip()
28 # Lists via pipe (e.g., select=E|F)
29 if "|" in s:
30 return [part.strip() for part in s.split("|") if part.strip()]
32 low = s.lower()
33 if low == BooleanString.TRUE:
34 return True
35 if low == BooleanString.FALSE:
36 return False
37 if low in {BooleanString.NONE, BooleanString.NULL}:
38 return None
40 # Try int
41 try:
42 return int(s)
43 except ValueError:
44 pass
46 # Try float
47 try:
48 return float(s)
49 except ValueError:
50 pass
52 return s
55def parse_tool_options(tool_options: str | None) -> dict[str, dict[str, object]]:
56 """Parse tool options string into a typed dictionary.
58 Args:
59 tool_options: str | None: String in format
60 "tool:option=value,tool2:option=value". Multiple values for a single
61 option can be provided using pipe separators (e.g., select=E|F).
63 Returns:
64 dict[str, dict[str, object]]: Mapping tool names to typed options.
65 """
66 if not tool_options:
67 return {}
69 tool_option_dict: dict[str, dict[str, object]] = {}
70 for opt in tool_options.split(","):
71 opt = opt.strip()
72 if not opt:
73 continue
74 if ":" not in opt:
75 # Skip malformed fragment
76 continue
77 tool_name, tool_opt = opt.split(":", 1)
78 if "=" not in tool_opt:
79 # Skip malformed fragment
80 continue
81 opt_name, opt_value = tool_opt.split("=", 1)
82 tool_name = tool_name.strip().lower()
83 opt_name = opt_name.strip()
84 opt_value = opt_value.strip()
85 if not tool_name or not opt_name:
86 continue
87 if tool_name not in tool_option_dict:
88 tool_option_dict[tool_name] = {}
89 tool_option_dict[tool_name][opt_name] = _coerce_value(opt_value)
91 return tool_option_dict