Coverage for lintro / tools / core / option_spec.py: 99%

91 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Declarative DSL for tool option specifications. 

2 

3This module provides a type-safe, declarative way to define tool options 

4with built-in validation and CLI argument generation. 

5 

6Example usage: 

7 BLACK_OPTIONS = ( 

8 ToolOptionsSpec() 

9 .add(int_option("line_length", "--line-length", default=88)) 

10 .add(str_option("target_version", "--target-version")) 

11 .add(bool_option("preview", "--preview", default=False)) 

12 ) 

13""" 

14 

15from __future__ import annotations 

16 

17from dataclasses import dataclass, field 

18from enum import Enum, auto 

19from typing import Any, Generic, TypeVar 

20 

21from lintro.tools.core.option_validators import ( 

22 validate_bool, 

23 validate_int, 

24 validate_list, 

25 validate_positive_int, 

26 validate_str, 

27) 

28 

29T = TypeVar("T") 

30 

31 

32class OptionType(Enum): 

33 """Types of options supported by tool plugins.""" 

34 

35 BOOL = auto() 

36 INT = auto() 

37 POSITIVE_INT = auto() 

38 STR = auto() 

39 LIST = auto() 

40 ENUM = auto() 

41 

42 

43@dataclass 

44class OptionSpec(Generic[T]): 

45 """Specification for a single tool option. 

46 

47 Encapsulates all information needed to validate, store, and convert 

48 a tool option to CLI arguments. 

49 

50 Attributes: 

51 name: The option name as used in Python (e.g., "line_length"). 

52 cli_flag: The CLI flag (e.g., "--line-length"). 

53 option_type: The type of the option value. 

54 default: Default value if not specified. 

55 description: Human-readable description. 

56 min_value: For int types, the minimum allowed value. 

57 max_value: For int types, the maximum allowed value. 

58 choices: For enum types, the allowed values. 

59 required: Whether the option is required. 

60 """ 

61 

62 name: str 

63 cli_flag: str 

64 option_type: OptionType 

65 default: T | None = None 

66 description: str = "" 

67 min_value: int | None = None 

68 max_value: int | None = None 

69 choices: list[str] | None = None 

70 required: bool = False 

71 

72 def validate(self, value: Any) -> None: 

73 """Validate a value against this option's specification. 

74 

75 Args: 

76 value: The value to validate. 

77 

78 Raises: 

79 ValueError: If validation fails. 

80 """ 

81 if value is None: 

82 if self.required: 

83 raise ValueError(f"{self.name} is required") 

84 return 

85 

86 if self.option_type == OptionType.BOOL: 

87 validate_bool(value, self.name) 

88 elif self.option_type == OptionType.INT: 

89 validate_int(value, self.name, self.min_value, self.max_value) 

90 elif self.option_type == OptionType.POSITIVE_INT: 

91 validate_positive_int(value, self.name) 

92 elif self.option_type == OptionType.STR: 

93 validate_str(value, self.name) 

94 if self.choices and value not in self.choices: 

95 raise ValueError( 

96 f"{self.name} must be one of: {', '.join(self.choices)}", 

97 ) 

98 elif self.option_type == OptionType.LIST: 

99 validate_list(value, self.name) 

100 elif self.option_type == OptionType.ENUM: 

101 validate_str(value, self.name) 

102 if self.choices and value not in self.choices: 

103 raise ValueError( 

104 f"{self.name} must be one of: {', '.join(self.choices)}", 

105 ) 

106 

107 def to_cli_args(self, value: Any) -> list[str]: 

108 """Convert a value to CLI arguments. 

109 

110 Args: 

111 value: The value to convert. 

112 

113 Returns: 

114 List of CLI arguments (empty if value is None or False for bools). 

115 """ 

116 if value is None: 

117 return [] 

118 

119 if self.option_type == OptionType.BOOL: 

120 # For boolean flags, only include if True 

121 if value: 

122 return [self.cli_flag] 

123 return [] 

124 

125 if self.option_type == OptionType.LIST: 

126 # For lists, include the flag for each item 

127 args = [] 

128 for item in value: 

129 args.extend([self.cli_flag, str(item)]) 

130 return args 

131 

132 # For all other types, include flag and value 

133 return [self.cli_flag, str(value)] 

134 

135 

136@dataclass 

137class ToolOptionsSpec: 

138 """Collection of option specifications for a tool. 

139 

140 Provides methods to add options and validate/convert option values. 

141 

142 Attributes: 

143 options: Dictionary mapping option names to their specifications. 

144 """ 

145 

146 options: dict[str, OptionSpec[Any]] = field(default_factory=dict) 

147 

148 def add(self, spec: OptionSpec[Any]) -> ToolOptionsSpec: 

149 """Add an option specification. 

150 

151 Args: 

152 spec: The option specification to add. 

153 

154 Returns: 

155 Self for method chaining. 

156 """ 

157 self.options[spec.name] = spec 

158 return self 

159 

160 def validate_all(self, values: dict[str, Any]) -> None: 

161 """Validate all provided values against their specifications. 

162 

163 Args: 

164 values: Dictionary of option names to values. 

165 

166 Raises: 

167 ValueError: If any validation fails. 

168 """ 

169 for name, value in values.items(): 

170 if name in self.options: 

171 self.options[name].validate(value) 

172 

173 # Check for required options 

174 for name, spec in self.options.items(): 

175 if spec.required and name not in values: 

176 raise ValueError(f"{name} is required") 

177 

178 def to_cli_args(self, values: dict[str, Any]) -> list[str]: 

179 """Convert all values to CLI arguments. 

180 

181 Args: 

182 values: Dictionary of option names to values. 

183 

184 Returns: 

185 List of CLI arguments. 

186 """ 

187 args = [] 

188 for name, value in values.items(): 

189 if name in self.options: 

190 args.extend(self.options[name].to_cli_args(value)) 

191 return args 

192 

193 def get_defaults(self) -> dict[str, Any]: 

194 """Get default values for all options. 

195 

196 Returns: 

197 Dictionary of option names to their default values. 

198 """ 

199 return { 

200 name: spec.default 

201 for name, spec in self.options.items() 

202 if spec.default is not None 

203 } 

204 

205 

206# ============================================================================= 

207# Convenience builders 

208# ============================================================================= 

209 

210 

211def bool_option( 

212 name: str, 

213 cli_flag: str, 

214 default: bool | None = None, 

215 description: str = "", 

216) -> OptionSpec[bool]: 

217 """Create a boolean option specification. 

218 

219 Args: 

220 name: The option name. 

221 cli_flag: The CLI flag. 

222 default: Default value. 

223 description: Human-readable description. 

224 

225 Returns: 

226 An OptionSpec for a boolean option. 

227 

228 Example: 

229 bool_option("preview", "--preview", default=False) 

230 """ 

231 return OptionSpec( 

232 name=name, 

233 cli_flag=cli_flag, 

234 option_type=OptionType.BOOL, 

235 default=default, 

236 description=description, 

237 ) 

238 

239 

240def int_option( 

241 name: str, 

242 cli_flag: str, 

243 default: int | None = None, 

244 min_value: int | None = None, 

245 max_value: int | None = None, 

246 description: str = "", 

247) -> OptionSpec[int]: 

248 """Create an integer option specification. 

249 

250 Args: 

251 name: The option name. 

252 cli_flag: The CLI flag. 

253 default: Default value. 

254 min_value: Minimum allowed value. 

255 max_value: Maximum allowed value. 

256 description: Human-readable description. 

257 

258 Returns: 

259 An OptionSpec for an integer option. 

260 

261 Example: 

262 int_option("line_length", "--line-length", default=88, min_value=1) 

263 """ 

264 return OptionSpec( 

265 name=name, 

266 cli_flag=cli_flag, 

267 option_type=OptionType.INT, 

268 default=default, 

269 min_value=min_value, 

270 max_value=max_value, 

271 description=description, 

272 ) 

273 

274 

275def positive_int_option( 

276 name: str, 

277 cli_flag: str, 

278 default: int | None = None, 

279 description: str = "", 

280) -> OptionSpec[int]: 

281 """Create a positive integer option specification. 

282 

283 Args: 

284 name: The option name. 

285 cli_flag: The CLI flag. 

286 default: Default value (must be positive). 

287 description: Human-readable description. 

288 

289 Returns: 

290 An OptionSpec for a positive integer option. 

291 

292 Example: 

293 positive_int_option("timeout", "--timeout", default=30) 

294 """ 

295 return OptionSpec( 

296 name=name, 

297 cli_flag=cli_flag, 

298 option_type=OptionType.POSITIVE_INT, 

299 default=default, 

300 description=description, 

301 ) 

302 

303 

304def str_option( 

305 name: str, 

306 cli_flag: str, 

307 default: str | None = None, 

308 choices: list[str] | None = None, 

309 description: str = "", 

310) -> OptionSpec[str]: 

311 """Create a string option specification. 

312 

313 Args: 

314 name: The option name. 

315 cli_flag: The CLI flag. 

316 default: Default value. 

317 choices: Allowed values (optional). 

318 description: Human-readable description. 

319 

320 Returns: 

321 An OptionSpec for a string option. 

322 

323 Example: 

324 str_option("target_version", "--target-version", choices=["py38", "py39"]) 

325 """ 

326 return OptionSpec( 

327 name=name, 

328 cli_flag=cli_flag, 

329 option_type=OptionType.STR, 

330 default=default, 

331 choices=choices, 

332 description=description, 

333 ) 

334 

335 

336def list_option( 

337 name: str, 

338 cli_flag: str, 

339 default: list[str] | None = None, 

340 description: str = "", 

341) -> OptionSpec[list[str]]: 

342 """Create a list option specification. 

343 

344 Args: 

345 name: The option name. 

346 cli_flag: The CLI flag. 

347 default: Default value. 

348 description: Human-readable description. 

349 

350 Returns: 

351 An OptionSpec for a list option. 

352 

353 Example: 

354 list_option("ignore", "--ignore", default=["E501"]) 

355 """ 

356 return OptionSpec( 

357 name=name, 

358 cli_flag=cli_flag, 

359 option_type=OptionType.LIST, 

360 default=default, 

361 description=description, 

362 ) 

363 

364 

365def enum_option( 

366 name: str, 

367 cli_flag: str, 

368 choices: list[str], 

369 default: str | None = None, 

370 description: str = "", 

371) -> OptionSpec[str]: 

372 """Create an enum option specification. 

373 

374 Args: 

375 name: The option name. 

376 cli_flag: The CLI flag. 

377 choices: Allowed values. 

378 default: Default value (must be in choices). 

379 description: Human-readable description. 

380 

381 Returns: 

382 An OptionSpec for an enum option. 

383 

384 Example: 

385 enum_option("severity", "--severity", choices=["error", "warning", "info"]) 

386 """ 

387 return OptionSpec( 

388 name=name, 

389 cli_flag=cli_flag, 

390 option_type=OptionType.ENUM, 

391 default=default, 

392 choices=choices, 

393 description=description, 

394 )