Coverage for src / docstring_format_checker / cli.py: 100%

201 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-25 08:09 +0000

1# ============================================================================ # 

2# # 

3# Title: Title # 

4# Purpose: Purpose # 

5# Notes: Notes # 

6# Author: chrimaho # 

7# Created: Created # 

8# References: References # 

9# Sources: Sources # 

10# Edited: Edited # 

11# # 

12# ============================================================================ # 

13 

14 

15# ---------------------------------------------------------------------------- # 

16# # 

17# Overview #### 

18# # 

19# ---------------------------------------------------------------------------- # 

20 

21 

22# ---------------------------------------------------------------------------- # 

23# Description #### 

24# ---------------------------------------------------------------------------- # 

25 

26 

27""" 

28!!! note "Summary" 

29 Command-line interface for the docstring format checker. 

30""" 

31 

32 

33# ---------------------------------------------------------------------------- # 

34# # 

35# Setup #### 

36# # 

37# ---------------------------------------------------------------------------- # 

38 

39 

40## --------------------------------------------------------------------------- # 

41## Imports #### 

42## --------------------------------------------------------------------------- # 

43 

44 

45# ## Python StdLib Imports ---- 

46import os 

47from functools import partial 

48from pathlib import Path 

49from textwrap import dedent 

50from typing import Optional 

51 

52# ## Python Third Party Imports ---- 

53import pyfiglet 

54from rich.console import Console 

55from rich.markup import escape 

56from rich.panel import Panel 

57from rich.table import Table 

58from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo 

59 

60# ## Local First Party Imports ---- 

61from docstring_format_checker import __version__ 

62from docstring_format_checker.config import ( 

63 Config, 

64 find_config_file, 

65 load_config, 

66) 

67from docstring_format_checker.core import DocstringChecker, DocstringError 

68 

69 

70## --------------------------------------------------------------------------- # 

71## Exports #### 

72## --------------------------------------------------------------------------- # 

73 

74 

75__all__: list[str] = [ 

76 "main", 

77 "entry_point", 

78 "check_docstrings", 

79] 

80 

81 

82## --------------------------------------------------------------------------- # 

83## Constants #### 

84## --------------------------------------------------------------------------- # 

85 

86 

87NEW_LINE = "\n" 

88 

89 

90## --------------------------------------------------------------------------- # 

91## Helpers #### 

92## --------------------------------------------------------------------------- # 

93 

94 

95### Colours ---- 

96def _colour(text: str, colour: str) -> str: 

97 """ 

98 !!! note "Summary" 

99 Apply Rich colour markup to text. 

100 

101 Params: 

102 text (str): 

103 The text to colour. 

104 colour (str): 

105 The colour to apply, e.g., 'red', 'green', 'blue'. 

106 

107 Returns: 

108 (str): 

109 The text wrapped in Rich colour markup. 

110 """ 

111 return f"[{colour}]{text}[/{colour}]" 

112 

113 

114_green = partial(_colour, colour="green") 

115_red = partial(_colour, colour="red") 

116_cyan = partial(_colour, colour="cyan") 

117_blue = partial(_colour, colour="blue") 

118 

119 

120# ---------------------------------------------------------------------------- # 

121# # 

122# Main Application #### 

123# # 

124# ---------------------------------------------------------------------------- # 

125 

126 

127app = Typer( 

128 name="docstring-format-checker", 

129 help="A CLI tool to check and validate Python docstring formatting and completeness.", 

130 add_completion=False, 

131 rich_markup_mode="rich", 

132 add_help_option=False, # Disable automatic help so we can add our own with -h 

133) 

134console = Console() 

135 

136 

137# ---------------------------------------------------------------------------- # 

138# # 

139# Callbacks #### 

140# # 

141# ---------------------------------------------------------------------------- # 

142 

143 

144def _version_callback(ctx: Context, param: CallbackParam, value: bool) -> None: 

145 """ 

146 !!! note "Summary" 

147 Print version and exit. 

148 

149 Params: 

150 ctx (Context): 

151 The context object. 

152 param (CallbackParam): 

153 The parameter object. 

154 value (bool): 

155 The boolean value indicating if the flag was set. 

156 

157 Returns: 

158 (None): 

159 Nothing is returned. 

160 """ 

161 if value: 

162 echo(f"docstring-format-checker version {__version__}") 

163 raise Exit() 

164 

165 

166def _example_callback(ctx: Context, param: CallbackParam, value: Optional[str]) -> None: 

167 """ 

168 !!! note "Summary" 

169 Handle example flag and show appropriate example content. 

170 

171 Params: 

172 ctx (Context): 

173 The context object. 

174 param (CallbackParam): 

175 The parameter object. 

176 value (Optional[str]): 

177 The example type to show: 'config' or 'usage'. 

178 

179 Returns: 

180 (None): 

181 Nothing is returned. 

182 """ 

183 

184 if not value or ctx.resilient_parsing: 

185 return 

186 

187 if value == "config": 

188 _show_config_example_callback() 

189 elif value == "usage": 

190 _show_usage_examples_callback() 

191 else: 

192 console.print(_red(f"Error: Invalid example type '{value}'. Use 'config' or 'usage'.")) 

193 raise Exit(1) 

194 raise Exit() 

195 

196 

197def _show_usage_examples_callback() -> None: 

198 """ 

199 !!! note "Summary" 

200 Show examples and exit. 

201 

202 Returns: 

203 (None): 

204 Nothing is returned. 

205 """ 

206 

207 examples_content: str = dedent( 

208 f""" 

209 Execute the below commands in any terminal after installing the package. 

210 

211 {_blue("dfc myfile.py")} {_green("# Check a single Python file (list output)")} 

212 {_blue("dfc myfile.py other_file.py")} {_green("# Check multiple Python files")} 

213 {_blue("dfc src/")} {_green("# Check all Python files in src/ directory")} 

214 {_blue("dfc -x src/app/__init__.py src/")} {_green("# Check all Python files in src/ directory, excluding one init file")} 

215 {_blue("dfc --output=table myfile.py")} {_green("# Check with table output format")} 

216 {_blue("dfc -o list myfile.py")} {_green("# Check with list output format (default)")} 

217 {_blue("dfc --check myfile.py")} {_green("# Check and exit with error if issues found")} 

218 {_blue("dfc --quiet myfile.py")} {_green("# Check quietly, only show pass/fail")} 

219 {_blue("dfc --quiet --check myfile.py")} {_green("# Check quietly and exit with error if issues found")} 

220 {_blue("dfc . --exclude '*/tests/*'")} {_green("# Check current directory, excluding tests")} 

221 {_blue("dfc . -c custom.toml")} {_green("# Use custom configuration file")} 

222 {_blue("dfc --example=config")} {_green("# Show example configuration")} 

223 {_blue("dfc -e usage")} {_green("# Show usage examples (this help)")} 

224 """ 

225 ).strip() 

226 

227 panel = Panel( 

228 examples_content, 

229 title="Usage Examples", 

230 title_align="left", 

231 border_style="dim", 

232 padding=(0, 1), 

233 ) 

234 

235 console.print(panel) 

236 

237 

238def _show_config_example_callback() -> None: 

239 """ 

240 !!! note "Summary" 

241 Show configuration example and exit. 

242 

243 Returns: 

244 (None): 

245 Nothing is returned. 

246 """ 

247 

248 example_config: str = dedent( 

249 r""" 

250 Place the below config in your `pyproject.toml` file. 

251 

252 [blue]\[tool.dfc][/blue] 

253 [green]# or \[tool.docstring-format-checker][/green] 

254 [blue]allow_undefined_sections = false[/blue] 

255 [blue]require_docstrings = true[/blue] 

256 [blue]check_private = true[/blue] 

257 [blue]validate_param_types = true[/blue] 

258 [blue]optional_style = "validate"[/blue] [green]# "silent", "validate", or "strict"[/green] 

259 [blue]sections = [[/blue] 

260 [blue]{ order = 1, name = "summary", type = "free_text", required = true, admonition = "note", prefix = "!!!" },[/blue] 

261 [blue]{ order = 2, name = "details", type = "free_text", required = false, admonition = "abstract", prefix = "???+" },[/blue] 

262 [blue]{ order = 3, name = "params", type = "list_name_and_type", required = false },[/blue] 

263 [blue]{ order = 4, name = "raises", type = "list_type", required = false },[/blue] 

264 [blue]{ order = 5, name = "returns", type = "list_name_and_type", required = false },[/blue] 

265 [blue]{ order = 6, name = "yields", type = "list_type", required = false },[/blue] 

266 [blue]{ order = 7, name = "examples", type = "free_text", required = false, admonition = "example", prefix = "???+" },[/blue] 

267 [blue]{ order = 8, name = "notes", type = "free_text", required = false, admonition = "note", prefix = "???" },[/blue] 

268 [blue]][/blue] 

269 """ 

270 ).strip() 

271 

272 panel = Panel( 

273 example_config, 

274 title="Configuration Example", 

275 title_align="left", 

276 border_style="dim", 

277 padding=(0, 1), 

278 ) 

279 

280 # Print without Rich markup processing to avoid bracket interpretation 

281 console.print(panel) 

282 

283 

284def _help_callback_main(ctx: Context, param: CallbackParam, value: bool) -> None: 

285 """ 

286 !!! note "Summary" 

287 Show help and exit. 

288 

289 Params: 

290 ctx (Context): 

291 The context object. 

292 param (CallbackParam): 

293 The parameter object. 

294 value (bool): 

295 The boolean value indicating if the flag was set. 

296 

297 Returns: 

298 (None): 

299 Nothing is returned. 

300 """ 

301 

302 # Early exit if help flag is set 

303 if not value or ctx.resilient_parsing: 

304 return 

305 

306 # Determine terminal width for ASCII art 

307 try: 

308 terminal_width: int = os.get_terminal_size().columns 

309 except OSError: 

310 terminal_width = 80 

311 

312 # Determine title based on terminal width 

313 title: str = "dfc" if terminal_width < 130 else "docstring-format-checker" 

314 

315 # Print ASCII art title 

316 console.print( 

317 pyfiglet.figlet_format(title, font="standard", justify="left", width=140), 

318 style="magenta", 

319 markup=False, 

320 ) 

321 

322 # Show help message 

323 echo(ctx.get_help()) 

324 

325 # Show usage and config examples 

326 _show_usage_examples_callback() 

327 _show_config_example_callback() 

328 

329 raise Exit() 

330 

331 

332def _format_error_messages(error_message: str) -> str: 

333 """ 

334 !!! note "Summary" 

335 Format error messages for better readability in CLI output. 

336 

337 Params: 

338 error_message (str): 

339 The raw error message that may contain semicolon-separated errors 

340 

341 Returns: 

342 (str): 

343 Formatted error message with each error prefixed with "- " and separated by ";\n" 

344 """ 

345 if "; " in error_message: 

346 # Split by semicolon and rejoin with proper formatting 

347 errors: list[str] = error_message.split("; ") 

348 formatted_errors: list[str] = [f"- {error.strip()}" for error in errors if error.strip()] 

349 return ";\n".join(formatted_errors) + "." 

350 

351 # Single error message 

352 else: 

353 return f"- {error_message.strip()}." 

354 

355 

356def _display_results( 

357 results: dict[str, list[DocstringError]], 

358 quiet: bool, 

359 output: str, 

360 check: bool, 

361) -> int: 

362 """ 

363 !!! note "Summary" 

364 Display the results of docstring checking. 

365 

366 Params: 

367 results (dict[str, list[DocstringError]]): 

368 Dictionary mapping file paths to lists of errors 

369 quiet (bool): 

370 Whether to suppress success messages and error details 

371 output (str): 

372 Output format: 'table' or 'list' 

373 check (bool): 

374 Whether this is a check run (affects quiet behavior) 

375 

376 Returns: 

377 (int): 

378 Exit code (`0` for success, `1` for errors found) 

379 """ 

380 if not results: 

381 if not quiet: 

382 console.print(_green("✅ All docstrings are valid!")) 

383 return 0 

384 

385 # Count errors and generate summary statistics 

386 error_stats = _count_errors_and_files(results) 

387 

388 if quiet: 

389 _display_quiet_summary(error_stats) 

390 return 1 

391 

392 # Display detailed results based on output format 

393 if output == "table": 

394 _display_table_output(results) 

395 else: 

396 _display_list_output(results) 

397 

398 # Display final summary 

399 _display_final_summary(error_stats) 

400 return 1 

401 

402 

403def _count_errors_and_files(results: dict[str, list[DocstringError]]) -> dict[str, int]: 

404 """ 

405 !!! note "Summary" 

406 Count total errors, functions, and files from results. 

407 

408 Params: 

409 results (dict[str, list[DocstringError]]): 

410 Dictionary mapping file paths to lists of errors. 

411 

412 Returns: 

413 (dict[str, int]): 

414 Dictionary containing total_errors, total_functions, and total_files. 

415 """ 

416 total_individual_errors: int = 0 

417 total_functions: int = 0 

418 

419 for errors in results.values(): 

420 total_functions += len(errors) 

421 for error in errors: 

422 if "; " in error.message: 

423 individual_errors: list[str] = [msg.strip() for msg in error.message.split("; ") if msg.strip()] 

424 total_individual_errors += len(individual_errors) 

425 else: 

426 total_individual_errors += 1 

427 

428 return {"total_errors": total_individual_errors, "total_functions": total_functions, "total_files": len(results)} 

429 

430 

431def _display_quiet_summary(error_stats: dict[str, int]) -> None: 

432 """ 

433 !!! note "Summary" 

434 Display summary in quiet mode. 

435 

436 Params: 

437 error_stats (dict[str, int]): 

438 Dictionary containing total_errors, total_functions, and total_files. 

439 """ 

440 functions_text = ( 

441 "1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions" 

442 ) 

443 files_text: str = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files" 

444 

445 console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}")) 

446 

447 

448def _display_table_output(results: dict[str, list[DocstringError]]) -> None: 

449 """ 

450 !!! note "Summary" 

451 Display results in table format. 

452 

453 Params: 

454 results (dict[str, list[DocstringError]]): 

455 Dictionary mapping file paths to lists of errors. 

456 """ 

457 table = Table(show_header=True, header_style="bold magenta") 

458 table.add_column("File", style="cyan", no_wrap=False) 

459 table.add_column("Line", justify="right", style="white") 

460 table.add_column("Item", style="yellow") 

461 table.add_column("Type", style="blue") 

462 table.add_column("Error", style="red") 

463 

464 for file_path, errors in results.items(): 

465 for i, error in enumerate(errors): 

466 file_display = file_path if i == 0 else "" 

467 formatted_error_message = _format_error_messages(error.message) 

468 

469 table.add_row( 

470 file_display, 

471 str(error.line_number) if error.line_number > 0 else "", 

472 error.item_name, 

473 error.item_type, 

474 f"[red]{formatted_error_message}[/red]", 

475 ) 

476 console.print(table) 

477 

478 

479def _create_error_header(error: DocstringError) -> str: 

480 """ 

481 !!! note "Summary" 

482 Create formatted header for a single error. 

483 

484 Params: 

485 error (DocstringError): 

486 The error to create a header for. 

487 

488 Returns: 

489 (str): 

490 Formatted header string with line number, item type, and name. 

491 """ 

492 if error.line_number > 0: 

493 return f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':" 

494 else: 

495 return f" {_red('Error')} - {error.item_type} '{error.item_name}':" 

496 

497 

498def _split_error_messages(message: str) -> list[str]: 

499 """ 

500 !!! note "Summary" 

501 Split compound error message into individual messages. 

502 

503 Params: 

504 message (str): 

505 The error message to split. 

506 

507 Returns: 

508 (list[str]): 

509 List of individual error messages. 

510 """ 

511 if "; " in message: 

512 return [msg.strip() for msg in message.split("; ") if msg.strip()] 

513 else: 

514 return [message.strip()] 

515 

516 

517def _format_error_output(error: DocstringError) -> list[str]: 

518 """ 

519 !!! note "Summary" 

520 Format single error for display output. 

521 

522 Params: 

523 error (DocstringError): 

524 The error to format. 

525 

526 Returns: 

527 (list[str]): 

528 List of formatted lines to print. 

529 """ 

530 lines: list[str] = [_create_error_header(error)] 

531 individual_errors: list[str] = _split_error_messages(error.message) 

532 

533 for individual_error in individual_errors: 

534 # Escape square brackets for Rich markup using Rich's escape function 

535 individual_error: str = escape(individual_error) 

536 

537 # Check if this error has multi-line content (e.g., parameter type mismatches) 

538 if "\n" in individual_error: 

539 # Split by newlines and add 4 spaces of extra indentation to each line 

540 error_lines: list[str] = individual_error.split("\n") 

541 lines.append(f" - {error_lines[0]}") # First line gets the bullet 

542 for sub_line in error_lines[1:]: 

543 if sub_line.strip(): # Only add non-empty lines 

544 lines.append(f" {sub_line}") # Continuation lines get 4 spaces 

545 else: 

546 lines.append(f" - {individual_error}") 

547 

548 return lines 

549 

550 

551def _display_list_output(results: dict[str, list[DocstringError]]) -> None: 

552 """ 

553 !!! note "Summary" 

554 Display results in list format. 

555 

556 Params: 

557 results (dict[str, list[DocstringError]]): 

558 Dictionary mapping file paths to lists of errors. 

559 """ 

560 for file_path, errors in results.items(): 

561 console.print(f"{NEW_LINE}{_cyan(file_path)}") 

562 for error in errors: 

563 output_lines: list[str] = _format_error_output(error) 

564 for line in output_lines: 

565 console.print(line) 

566 

567 

568def _display_final_summary(error_stats: dict[str, int]) -> None: 

569 """ 

570 !!! note "Summary" 

571 Display the final summary line. 

572 

573 Params: 

574 error_stats (dict[str, int]): 

575 Dictionary containing total_errors, total_functions, and total_files. 

576 """ 

577 functions_text: str = ( 

578 "1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions" 

579 ) 

580 files_text: str = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files" 

581 

582 console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}")) 

583 

584 

585# ---------------------------------------------------------------------------- # 

586# # 

587# Main Logic #### 

588# # 

589# ---------------------------------------------------------------------------- # 

590 

591 

592# This will be the default behavior when no command is specified 

593def check_docstrings( 

594 paths: list[str], 

595 config: Optional[str] = None, 

596 exclude: Optional[list[str]] = None, 

597 quiet: bool = False, 

598 output: str = "list", 

599 check: bool = False, 

600) -> None: 

601 """ 

602 !!! note "Summary" 

603 Core logic for checking docstrings. 

604 

605 Params: 

606 paths (list[str]): 

607 The path(s) to the file(s) or directory(ies) to check. 

608 config (Optional[str]): 

609 The path to the configuration file. 

610 Default: `None`. 

611 exclude (Optional[list[str]]): 

612 List of glob patterns to exclude from checking. 

613 Default: `None`. 

614 quiet (bool): 

615 Whether to suppress output. 

616 Default: `False`. 

617 output (str): 

618 Output format: 'table' or 'list'. 

619 Default: `'list'`. 

620 check (bool): 

621 Whether to throw error if issues are found. 

622 Default: `False`. 

623 

624 Returns: 

625 (None): 

626 Nothing is returned. 

627 """ 

628 # Validate and process input paths 

629 target_paths: list[Path] = _validate_and_process_paths(paths) 

630 

631 # Load and validate configuration 

632 config_obj: Config = _load_and_validate_config(config, target_paths) 

633 

634 # Initialize checker and process all paths 

635 checker = DocstringChecker(config_obj) 

636 all_results: dict[str, list[DocstringError]] = _process_all_paths(checker, target_paths, exclude) 

637 

638 # Display results and handle exit 

639 exit_code: int = _display_results(all_results, quiet, output, check) 

640 if exit_code != 0: 

641 raise Exit(exit_code) 

642 

643 

644def _validate_and_process_paths(paths: list[str]) -> list[Path]: 

645 """ 

646 !!! note "Summary" 

647 Validate input paths and return valid paths. 

648 

649 Params: 

650 paths (list[str]): 

651 List of path strings to validate. 

652 

653 Raises: 

654 (Exit): 

655 If any paths do not exist. 

656 

657 Returns: 

658 (list[Path]): 

659 List of valid Path objects. 

660 """ 

661 path_objs: list[Path] = [Path(path) for path in paths] 

662 target_paths: list[Path] = [p for p in path_objs if p.exists()] 

663 invalid_paths: list[Path] = [p for p in path_objs if not p.exists()] 

664 

665 if invalid_paths: 

666 console.print( 

667 _red("[bold]Error: Paths do not exist:[/bold]"), 

668 NEW_LINE, 

669 NEW_LINE.join([f"- '{invalid_path}'" for invalid_path in invalid_paths]), 

670 ) 

671 raise Exit(1) 

672 

673 return target_paths 

674 

675 

676def _load_and_validate_config(config: Optional[str], target_paths: list[Path]) -> Config: 

677 """ 

678 !!! note "Summary" 

679 Load and validate configuration from file or auto-discovery. 

680 

681 Params: 

682 config (Optional[str]): 

683 Optional path to configuration file. 

684 target_paths (list[Path]): 

685 List of target paths for auto-discovery. 

686 

687 Raises: 

688 (Exit): 

689 If configuration loading fails. 

690 

691 Returns: 

692 (Config): 

693 Loaded configuration object. 

694 """ 

695 try: 

696 if config: 

697 return _load_explicit_config(config) 

698 else: 

699 return _load_auto_discovered_config(target_paths) 

700 except Exception as e: 

701 console.print(_red(f"Error loading configuration: {e}")) 

702 raise Exit(1) from e 

703 

704 

705def _load_explicit_config(config: str) -> Config: 

706 """ 

707 !!! note "Summary" 

708 Load configuration from explicitly specified path. 

709 

710 Params: 

711 config (str): 

712 Path to configuration file. 

713 

714 Raises: 

715 (Exit): 

716 If configuration file does not exist. 

717 

718 Returns: 

719 (Config): 

720 Loaded configuration object. 

721 """ 

722 config_path = Path(config) 

723 if not config_path.exists(): 

724 console.print(_red(f"Error: Configuration file does not exist: {config}")) 

725 raise Exit(1) 

726 return load_config(config_path) 

727 

728 

729def _load_auto_discovered_config(target_paths: list[Path]) -> Config: 

730 """ 

731 !!! note "Summary" 

732 Load configuration from auto-discovery or defaults. 

733 

734 Params: 

735 target_paths (list[Path]): 

736 List of target paths to search for configuration. 

737 

738 Returns: 

739 (Config): 

740 Loaded configuration object from found config or defaults. 

741 """ 

742 first_path: Path = target_paths[0] 

743 search_path: Path = first_path if first_path.is_dir() else first_path.parent 

744 found_config: Optional[Path] = find_config_file(search_path) 

745 

746 if found_config: 

747 return load_config(found_config) 

748 else: 

749 return load_config() 

750 

751 

752def _process_all_paths( 

753 checker: DocstringChecker, target_paths: list[Path], exclude: Optional[list[str]] 

754) -> dict[str, list[DocstringError]]: 

755 """ 

756 !!! note "Summary" 

757 Process all target paths and collect docstring errors. 

758 

759 Params: 

760 checker (DocstringChecker): 

761 The checker instance to use. 

762 target_paths (list[Path]): 

763 List of paths to check (files or directories). 

764 exclude (Optional[list[str]]): 

765 Optional list of exclusion patterns. 

766 

767 Raises: 

768 (Exit): 

769 If an error occurs during checking. 

770 

771 Returns: 

772 (dict[str, list[DocstringError]]): 

773 Dictionary mapping file paths to lists of errors. 

774 """ 

775 all_results: dict[str, list[DocstringError]] = {} 

776 

777 try: 

778 for target_path in target_paths: 

779 if target_path.is_file(): 

780 errors: list[DocstringError] = checker.check_file(target_path) 

781 if errors: 

782 all_results[str(target_path)] = errors 

783 else: 

784 directory_results: dict[str, list[DocstringError]] = checker.check_directory( 

785 target_path, exclude_patterns=exclude 

786 ) 

787 all_results.update(directory_results) 

788 except Exception as e: 

789 console.print(_red(f"Error during checking: {e}")) 

790 raise Exit(1) from e 

791 

792 return all_results 

793 

794 

795# ---------------------------------------------------------------------------- # 

796# # 

797# App Operators #### 

798# # 

799# ---------------------------------------------------------------------------- # 

800 

801 

802# Simple callback that only handles global options and delegates to subcommands 

803@app.callback(invoke_without_command=True) 

804def main( 

805 ctx: Context, 

806 paths: Optional[list[str]] = Argument(None, help="Path(s) to Python file(s) or directory(s) for DFC to check"), 

807 config: Optional[str] = Option(None, "--config", "-f", help="Path to configuration file (TOML format)"), 

808 exclude: Optional[list[str]] = Option( 

809 None, 

810 "--exclude", 

811 "-x", 

812 help="Glob patterns to exclude (can be used multiple times)", 

813 ), 

814 output: str = Option( 

815 "list", 

816 "--output", 

817 "-o", 

818 help="Output format: 'table' or 'list'", 

819 show_default=True, 

820 ), 

821 check: bool = Option( 

822 False, 

823 "--check", 

824 "-c", 

825 help="Throw error (exit 1) if any issues are found", 

826 ), 

827 quiet: bool = Option( 

828 False, 

829 "--quiet", 

830 "-q", 

831 help="Only output pass/fail confirmation, suppress errors unless failing", 

832 ), 

833 example: Optional[str] = Option( 

834 None, 

835 "--example", 

836 "-e", 

837 callback=_example_callback, 

838 is_eager=True, 

839 help="Show examples: 'config' for configuration example, 'usage' for usage examples", 

840 ), 

841 version: Optional[bool] = Option( 

842 None, 

843 "--version", 

844 "-v", 

845 callback=_version_callback, 

846 is_eager=True, 

847 help="Show version and exit", 

848 ), 

849 help_flag: Optional[bool] = Option( 

850 None, 

851 "--help", 

852 "-h", 

853 callback=_help_callback_main, 

854 is_eager=True, 

855 help="Show this message and exit", 

856 ), 

857) -> None: 

858 """ 

859 !!! note "Summary" 

860 Check Python docstring formatting and completeness. 

861 

862 ???+ abstract "Details" 

863 This tool analyzes Python files and validates that functions, methods, and classes have properly formatted docstrings according to the configured sections. 

864 

865 Params: 

866 ctx (Context): 

867 The context object for the command. 

868 paths (Optional[list[str]]): 

869 Path(s) to Python file(s) or directory(ies) to check. 

870 config (Optional[str]): 

871 Path to configuration file (TOML format). 

872 exclude (Optional[list[str]]): 

873 Glob patterns to exclude. 

874 output (str): 

875 Output format: 'table' or 'list'. 

876 check (bool): 

877 Throw error if any issues are found. 

878 quiet (bool): 

879 Only output pass/fail confirmation. 

880 example (Optional[str]): 

881 Show examples: 'config' or 'usage'. 

882 version (Optional[bool]): 

883 Show version and exit. 

884 help_flag (Optional[bool]): 

885 Show help message and exit. 

886 

887 Returns: 

888 (None): 

889 Nothing is returned. 

890 """ 

891 

892 # If no paths are provided, show help 

893 if not paths: 

894 echo(ctx.get_help()) 

895 raise Exit(0) 

896 

897 # Validate output format 

898 if output not in ["table", "list"]: 

899 console.print(_red(f"Error: Invalid output format '{output}'. Use 'table' or 'list'.")) 

900 raise Exit(1) 

901 

902 check_docstrings( 

903 paths=paths, 

904 config=config, 

905 exclude=exclude, 

906 quiet=quiet, 

907 output=output, 

908 check=check, 

909 ) 

910 

911 

912def entry_point() -> None: 

913 """ 

914 !!! note "Summary" 

915 Entry point for the CLI scripts defined in pyproject.toml. 

916 """ 

917 app()