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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Declarative DSL for tool option specifications.
3This module provides a type-safe, declarative way to define tool options
4with built-in validation and CLI argument generation.
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"""
15from __future__ import annotations
17from dataclasses import dataclass, field
18from enum import Enum, auto
19from typing import Any, Generic, TypeVar
21from lintro.tools.core.option_validators import (
22 validate_bool,
23 validate_int,
24 validate_list,
25 validate_positive_int,
26 validate_str,
27)
29T = TypeVar("T")
32class OptionType(Enum):
33 """Types of options supported by tool plugins."""
35 BOOL = auto()
36 INT = auto()
37 POSITIVE_INT = auto()
38 STR = auto()
39 LIST = auto()
40 ENUM = auto()
43@dataclass
44class OptionSpec(Generic[T]):
45 """Specification for a single tool option.
47 Encapsulates all information needed to validate, store, and convert
48 a tool option to CLI arguments.
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 """
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
72 def validate(self, value: Any) -> None:
73 """Validate a value against this option's specification.
75 Args:
76 value: The value to validate.
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
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 )
107 def to_cli_args(self, value: Any) -> list[str]:
108 """Convert a value to CLI arguments.
110 Args:
111 value: The value to convert.
113 Returns:
114 List of CLI arguments (empty if value is None or False for bools).
115 """
116 if value is None:
117 return []
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 []
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
132 # For all other types, include flag and value
133 return [self.cli_flag, str(value)]
136@dataclass
137class ToolOptionsSpec:
138 """Collection of option specifications for a tool.
140 Provides methods to add options and validate/convert option values.
142 Attributes:
143 options: Dictionary mapping option names to their specifications.
144 """
146 options: dict[str, OptionSpec[Any]] = field(default_factory=dict)
148 def add(self, spec: OptionSpec[Any]) -> ToolOptionsSpec:
149 """Add an option specification.
151 Args:
152 spec: The option specification to add.
154 Returns:
155 Self for method chaining.
156 """
157 self.options[spec.name] = spec
158 return self
160 def validate_all(self, values: dict[str, Any]) -> None:
161 """Validate all provided values against their specifications.
163 Args:
164 values: Dictionary of option names to values.
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)
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")
178 def to_cli_args(self, values: dict[str, Any]) -> list[str]:
179 """Convert all values to CLI arguments.
181 Args:
182 values: Dictionary of option names to values.
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
193 def get_defaults(self) -> dict[str, Any]:
194 """Get default values for all options.
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 }
206# =============================================================================
207# Convenience builders
208# =============================================================================
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.
219 Args:
220 name: The option name.
221 cli_flag: The CLI flag.
222 default: Default value.
223 description: Human-readable description.
225 Returns:
226 An OptionSpec for a boolean option.
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 )
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.
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.
258 Returns:
259 An OptionSpec for an integer option.
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 )
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.
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.
289 Returns:
290 An OptionSpec for a positive integer option.
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 )
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.
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.
320 Returns:
321 An OptionSpec for a string option.
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 )
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.
344 Args:
345 name: The option name.
346 cli_flag: The CLI flag.
347 default: Default value.
348 description: Human-readable description.
350 Returns:
351 An OptionSpec for a list option.
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 )
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.
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.
381 Returns:
382 An OptionSpec for an enum option.
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 )