docstring-format-checker 1.11.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,917 @@
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 ----
46
+ import os
47
+ from functools import partial
48
+ from pathlib import Path
49
+ from textwrap import dedent
50
+ from typing import Optional
51
+
52
+ # ## Python Third Party Imports ----
53
+ import pyfiglet
54
+ from rich.console import Console
55
+ from rich.markup import escape
56
+ from rich.panel import Panel
57
+ from rich.table import Table
58
+ from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo
59
+
60
+ # ## Local First Party Imports ----
61
+ from docstring_format_checker import __version__
62
+ from docstring_format_checker.config import (
63
+ Config,
64
+ find_config_file,
65
+ load_config,
66
+ )
67
+ from 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
+
87
+ NEW_LINE = "\n"
88
+
89
+
90
+ ## --------------------------------------------------------------------------- #
91
+ ## Helpers ####
92
+ ## --------------------------------------------------------------------------- #
93
+
94
+
95
+ ### Colours ----
96
+ def _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
+
127
+ app = 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
+ )
134
+ console = Console()
135
+
136
+
137
+ # ---------------------------------------------------------------------------- #
138
+ # #
139
+ # Callbacks ####
140
+ # #
141
+ # ---------------------------------------------------------------------------- #
142
+
143
+
144
+ def _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
+
166
+ def _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
+
197
+ def _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
+
238
+ def _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
+
284
+ def _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
+
332
+ def _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
+
356
+ def _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
+
403
+ def _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
+
431
+ def _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
+
448
+ def _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
+
479
+ def _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
+
498
+ def _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
+
517
+ def _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
+
551
+ def _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
+
568
+ def _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
593
+ def 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
+
644
+ def _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
+
676
+ def _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
+
705
+ def _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
+
729
+ def _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
+
752
+ def _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)
804
+ def 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
+
912
+ def entry_point() -> None:
913
+ """
914
+ !!! note "Summary"
915
+ Entry point for the CLI scripts defined in pyproject.toml.
916
+ """
917
+ app()