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

1"""Tool options parsing and coercion utilities. 

2 

3Handles parsing of CLI tool options and coercing string values to typed Python values. 

4""" 

5 

6from lintro.enums.boolean_string import BooleanString 

7 

8 

9def _coerce_value(raw: str) -> object: 

10 """Coerce a raw CLI value into a typed Python value. 

11 

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 

20 

21 Args: 

22 raw: str: Raw CLI value to coerce. 

23 

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()] 

31 

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 

39 

40 # Try int 

41 try: 

42 return int(s) 

43 except ValueError: 

44 pass 

45 

46 # Try float 

47 try: 

48 return float(s) 

49 except ValueError: 

50 pass 

51 

52 return s 

53 

54 

55def parse_tool_options(tool_options: str | None) -> dict[str, dict[str, object]]: 

56 """Parse tool options string into a typed dictionary. 

57 

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). 

62 

63 Returns: 

64 dict[str, dict[str, object]]: Mapping tool names to typed options. 

65 """ 

66 if not tool_options: 

67 return {} 

68 

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) 

90 

91 return tool_option_dict