docstring-format-checker 0.1.0__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,716 @@
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
+ from pathlib import Path
47
+ from textwrap import dedent
48
+ from typing import Optional, Union
49
+
50
+ # ## Python Third Party Imports ----
51
+ from rich.console import Console
52
+ from rich.panel import Panel
53
+ from rich.table import Table
54
+ from toolbox_python.bools import strtobool
55
+ from typer import (
56
+ Argument,
57
+ BadParameter,
58
+ CallbackParam,
59
+ Context,
60
+ Exit,
61
+ Option,
62
+ Typer,
63
+ echo,
64
+ )
65
+
66
+ # ## Local First Party Imports ----
67
+ from docstring_format_checker import __version__
68
+ from docstring_format_checker.config import SectionConfig, find_config_file, load_config
69
+ from docstring_format_checker.core import DocstringChecker, DocstringError
70
+
71
+
72
+ ## --------------------------------------------------------------------------- #
73
+ ## Exports ####
74
+ ## --------------------------------------------------------------------------- #
75
+
76
+
77
+ __all__: list[str] = [
78
+ "main",
79
+ "config_example",
80
+ "check",
81
+ "entry_point",
82
+ ]
83
+
84
+
85
+ ## --------------------------------------------------------------------------- #
86
+ ## Constants ####
87
+ ## --------------------------------------------------------------------------- #
88
+
89
+
90
+ NEW_LINE = "\n"
91
+
92
+
93
+ # ---------------------------------------------------------------------------- #
94
+ # #
95
+ # Main Application ####
96
+ # #
97
+ # ---------------------------------------------------------------------------- #
98
+
99
+
100
+ app = Typer(
101
+ name="docstring-format-checker",
102
+ help="A CLI tool to check and validate Python docstring formatting and completeness.",
103
+ add_completion=False,
104
+ rich_markup_mode="rich",
105
+ add_help_option=False, # Disable automatic help so we can add our own with -h
106
+ )
107
+ console = Console()
108
+
109
+
110
+ # ---------------------------------------------------------------------------- #
111
+ # #
112
+ # Callbacks ####
113
+ # #
114
+ # ---------------------------------------------------------------------------- #
115
+
116
+
117
+ def _version_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
118
+ """
119
+ !!! note "Summary"
120
+ Print version and exit.
121
+
122
+ Params:
123
+ ctx (Context):
124
+ The context object.
125
+ param (CallbackParam):
126
+ The parameter object.
127
+ value (bool):
128
+ The boolean value indicating if the flag was set.
129
+
130
+ Returns:
131
+ (None):
132
+ Nothing is returned.
133
+ """
134
+ if value:
135
+ echo(f"docstring-format-checker version {__version__}")
136
+ raise Exit()
137
+
138
+
139
+ def _help_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
140
+ """
141
+ !!! note "Summary"
142
+ Show help and exit.
143
+
144
+ Params:
145
+ ctx (Context):
146
+ The context object.
147
+ param (CallbackParam):
148
+ The parameter object.
149
+ value (bool):
150
+ The boolean value indicating if the flag was set.
151
+
152
+ Returns:
153
+ (None):
154
+ Nothing is returned.
155
+ """
156
+ if not value or ctx.resilient_parsing:
157
+ return
158
+ echo(ctx.get_help())
159
+ raise Exit()
160
+
161
+
162
+ def _parse_boolean_flag(ctx: Context, param: CallbackParam, value: Optional[str]) -> Optional[bool]:
163
+ """
164
+ !!! note "Summary"
165
+ Parse boolean flag that accepts various true/false values.
166
+
167
+ Params:
168
+ ctx (Context):
169
+ The context object.
170
+ param (CallbackParam):
171
+ The parameter object.
172
+ value (Optional[str]):
173
+ The string value of the flag.
174
+
175
+ Returns:
176
+ (Optional[bool]):
177
+ The parsed boolean value or `None` if not provided.
178
+ """
179
+
180
+ # Handle the case where the flag is provided without a value (e.g., just --recursive or -r)
181
+ # In this case, Typer doesn't call the callback, so we need to handle it differently
182
+ if value is None:
183
+ # This means the flag wasn't provided at all, use default
184
+ return True
185
+
186
+ # If value is an empty string, it means the flag was provided without a value
187
+ if value == "":
188
+ return True
189
+
190
+ try:
191
+ return strtobool(value)
192
+ except ValueError as e:
193
+ raise BadParameter(
194
+ message=(
195
+ f"Invalid boolean value: '{value}'.{NEW_LINE}"
196
+ "Use one of: true/false, t/f, yes/no, y/n, 1/0, or on/off."
197
+ )
198
+ ) from e
199
+
200
+
201
+ def _parse_recursive_flag(value: str) -> bool:
202
+ """
203
+ !!! note "Summary"
204
+ Parse recursive flag using `strtobool()` utility.
205
+
206
+ Params:
207
+ value (str):
208
+ The string value of the flag.
209
+
210
+ Returns:
211
+ (bool):
212
+ The parsed boolean value.
213
+ """
214
+ return strtobool(value)
215
+
216
+
217
+ def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
218
+ """
219
+ !!! note "Summary"
220
+ Show examples and exit.
221
+
222
+ Params:
223
+ ctx (Context):
224
+ The context object.
225
+ param (CallbackParam):
226
+ The parameter object.
227
+ value (bool):
228
+ The boolean value indicating if the flag was set.
229
+
230
+ Returns:
231
+ (None):
232
+ Nothing is returned.
233
+ """
234
+
235
+ if not value or ctx.resilient_parsing:
236
+ return
237
+
238
+ examples_content: str = dedent(
239
+ """
240
+ [green]dfc check myfile.py[/green] Check a single Python file
241
+ [green]dfc check src/[/green] Check all Python files in src/ directory
242
+ [green]dfc check . --exclude "*/tests/*"[/green] Check current directory, excluding tests
243
+ [green]dfc check . -c custom.toml[/green] Use custom configuration file
244
+ [green]dfc check . --verbose[/green] Show detailed validation output
245
+ [green]dfc config-example[/green] Show example configuration
246
+ """
247
+ ).strip()
248
+
249
+ panel = Panel(
250
+ examples_content,
251
+ title="Examples",
252
+ title_align="left",
253
+ border_style="dim",
254
+ padding=(0, 1),
255
+ )
256
+
257
+ console.print(panel)
258
+ raise Exit()
259
+
260
+
261
+ def _show_check_examples_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
262
+ """
263
+ !!! note "Summary"
264
+ Show check command examples and exit.
265
+
266
+ Params:
267
+ ctx (Context):
268
+ The context object.
269
+ param (CallbackParam):
270
+ The parameter object.
271
+ value (bool):
272
+ The boolean value indicating if the flag was set.
273
+
274
+ Returns:
275
+ (None):
276
+ Nothing is returned.
277
+ """
278
+
279
+ if not value or ctx.resilient_parsing:
280
+ return
281
+
282
+ examples_content: str = dedent(
283
+ """
284
+ [green]dfc check myfile.py[/green] Check a single Python file
285
+ [green]dfc check src/[/green] Check all Python files in src/ directory
286
+ [green]dfc check . --exclude "*/tests/*"[/green] Check current directory, excluding tests
287
+ [green]dfc check . --config custom.toml[/green] Use custom configuration file
288
+ [green]dfc check . --verbose --recursive[/green] Show detailed output for all subdirectories
289
+ [green]dfc check . --quiet[/green] Only show errors, suppress success messages
290
+ """
291
+ )
292
+
293
+ panel = Panel(
294
+ examples_content,
295
+ title="Check Command Examples",
296
+ title_align="left",
297
+ border_style="dim",
298
+ padding=(0, 1),
299
+ )
300
+
301
+ console.print(panel)
302
+ raise Exit()
303
+
304
+
305
+ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verbose: bool) -> int:
306
+ """
307
+ !!! note "Summary"
308
+ Display the results of docstring checking.
309
+
310
+ Params:
311
+ results (dict[str, list[DocstringError]]):
312
+ Dictionary mapping file paths to lists of errors
313
+ quiet (bool):
314
+ Whether to suppress success messages
315
+ verbose (bool):
316
+ Whether to show detailed output
317
+
318
+ Returns:
319
+ (int):
320
+ Exit code (`0` for success, `1` for errors found)
321
+ """
322
+ if not results:
323
+ if not quiet:
324
+ console.print("[green]✓ All docstrings are valid![/green]")
325
+ return 0
326
+
327
+ # Count total errors
328
+ total_errors: int = sum(len(errors) for errors in results.values())
329
+ total_files: int = len(results)
330
+
331
+ if verbose:
332
+ # Show detailed table
333
+ table = Table(show_header=True, header_style="bold magenta")
334
+ table.add_column("File", style="cyan", no_wrap=False)
335
+ table.add_column("Line", justify="right", style="white")
336
+ table.add_column("Item", style="yellow")
337
+ table.add_column("Type", style="blue")
338
+ table.add_column("Error", style="red")
339
+
340
+ for file_path, errors in results.items():
341
+ for i, error in enumerate(errors):
342
+ file_display: str = file_path if i == 0 else ""
343
+ table.add_row(
344
+ file_display,
345
+ str(error.line_number) if error.line_number > 0 else "",
346
+ error.item_name,
347
+ error.item_type,
348
+ error.message,
349
+ )
350
+ console.print(table)
351
+
352
+ else:
353
+ # Show compact output
354
+ for file_path, errors in results.items():
355
+ console.print(f"{NEW_LINE}[cyan]{file_path}[/cyan]")
356
+ for error in errors:
357
+ if error.line_number > 0:
358
+ console.print(
359
+ f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {error.message}"
360
+ )
361
+ else:
362
+ console.print(f" [red]Error[/red]: {error.message}")
363
+
364
+ # Summary
365
+ console.print(f"{NEW_LINE}[red]Found {total_errors} error(s) in {total_files} file(s)[/red]")
366
+
367
+ return 1
368
+
369
+
370
+ # ---------------------------------------------------------------------------- #
371
+ # #
372
+ # Main Logic ####
373
+ # #
374
+ # ---------------------------------------------------------------------------- #
375
+
376
+
377
+ # This will be the default behavior when no command is specified
378
+ def _check_docstrings(
379
+ path: str,
380
+ config: Optional[str] = None,
381
+ recursive: bool = True,
382
+ exclude: Optional[list[str]] = None,
383
+ quiet: bool = False,
384
+ verbose: bool = False,
385
+ ) -> None:
386
+ """
387
+ !!! note "Summary"
388
+ Core logic for checking docstrings.
389
+
390
+ Params:
391
+ path (str):
392
+ The path to the file or directory to check.
393
+ config (Optional[str]):
394
+ The path to the configuration file.
395
+ recursive (bool):
396
+ Whether to check files recursively.
397
+ exclude (Optional[list[str]]):
398
+ List of glob patterns to exclude from checking.
399
+ quiet (bool):
400
+ Whether to suppress output.
401
+ verbose (bool):
402
+ Whether to show detailed output.
403
+
404
+ Returns:
405
+ (None):
406
+ Nothing is returned.
407
+ """
408
+
409
+ target_path = Path(path)
410
+
411
+ # Validate target path
412
+ if not target_path.exists():
413
+ console.print(f"[red]Error: Path does not exist: {path}[/red]")
414
+ raise Exit(1)
415
+
416
+ # Load configuration
417
+ try:
418
+ if config:
419
+ config_path = Path(config)
420
+ if not config_path.exists():
421
+ console.print(f"[red]Error: Configuration file does not exist: {config}[/red]")
422
+ raise Exit(1)
423
+ sections_config = load_config(config_path)
424
+ else:
425
+ # Try to find config file automatically
426
+ found_config: Union[Path, None] = find_config_file(
427
+ target_path if target_path.is_dir() else target_path.parent
428
+ )
429
+ if found_config:
430
+ if verbose:
431
+ console.print(f"[blue]Using configuration from: {found_config}[/blue]")
432
+ sections_config: list[SectionConfig] = load_config(found_config)
433
+ else:
434
+ if verbose:
435
+ console.print("[blue]Using default configuration[/blue]")
436
+ sections_config: list[SectionConfig] = load_config()
437
+
438
+ except Exception as e:
439
+ console.print(f"[red]Error loading configuration: {e}[/red]")
440
+ raise Exit(1)
441
+
442
+ # Initialize checker
443
+ checker = DocstringChecker(sections_config)
444
+
445
+ # Check files
446
+ try:
447
+ if target_path.is_file():
448
+ if verbose:
449
+ console.print(f"[blue]Checking file: {target_path}[/blue]")
450
+ errors: list[DocstringError] = checker.check_file(target_path)
451
+ results: dict[str, list[DocstringError]] = {str(target_path): errors} if errors else {}
452
+ else:
453
+ if verbose:
454
+ console.print(f"[blue]Checking directory: {target_path} (recursive={recursive})[/blue]")
455
+ results: dict[str, list[DocstringError]] = checker.check_directory(
456
+ target_path, recursive=recursive, exclude_patterns=exclude
457
+ )
458
+ except Exception as e:
459
+ console.print(f"[red]Error during checking: {e}[/red]")
460
+ raise Exit(1)
461
+
462
+ # Display results
463
+ exit_code: int = _display_results(results, quiet, verbose)
464
+
465
+ if exit_code != 0:
466
+ raise Exit(exit_code)
467
+
468
+
469
+ # ---------------------------------------------------------------------------- #
470
+ # #
471
+ # App Operators ####
472
+ # #
473
+ # ---------------------------------------------------------------------------- #
474
+
475
+
476
+ # Simple callback that only handles global options and delegates to subcommands
477
+ @app.callback(invoke_without_command=True)
478
+ def main(
479
+ ctx: Context,
480
+ version: Optional[bool] = Option(
481
+ None,
482
+ "--version",
483
+ "-v",
484
+ callback=_version_callback,
485
+ is_eager=True,
486
+ help="Show version and exit",
487
+ ),
488
+ examples: Optional[bool] = Option(
489
+ None,
490
+ "--examples",
491
+ "-e",
492
+ callback=_show_examples_callback,
493
+ is_eager=True,
494
+ help="Show usage examples and exit",
495
+ ),
496
+ help_flag: Optional[bool] = Option(
497
+ None,
498
+ "--help",
499
+ "-h",
500
+ callback=_help_callback,
501
+ is_eager=True,
502
+ help="Show this message and exit",
503
+ ),
504
+ ) -> None:
505
+ """
506
+ !!! note "Summary"
507
+ Check Python docstring formatting and completeness.
508
+
509
+ ???+ abstract "Details"
510
+ This tool analyzes Python files and validates that functions, methods, and classes have properly formatted docstrings according to the configured sections.
511
+
512
+ Params:
513
+ ctx (Context):
514
+ The context object for the command.
515
+ version (Optional[bool]):
516
+ Show version and exit.
517
+ examples (Optional[bool]):
518
+ Show usage examples and exit.
519
+ help_flag (Optional[bool]):
520
+ Show help message and exit.
521
+
522
+ Returns:
523
+ (None):
524
+ Nothing is returned.
525
+ """
526
+ # If no subcommand is provided, show help
527
+ if ctx.invoked_subcommand is None:
528
+ echo(ctx.get_help())
529
+ raise Exit()
530
+
531
+
532
+ @app.command(
533
+ rich_help_panel="Commands",
534
+ add_help_option=False, # Disable automatic help so we can add our own with -h
535
+ )
536
+ def check(
537
+ path: str = Argument(..., help="Path to Python file or directory to check"),
538
+ config: Optional[str] = Option(None, "--config", "-c", help="Path to configuration file (TOML format)"),
539
+ recursive: str = Option(
540
+ "true",
541
+ "--recursive",
542
+ "-r",
543
+ help="Check directories recursively (default: true). Accepts: true/false, t/f, yes/no, y/n, 1/0, on/off",
544
+ ),
545
+ exclude: Optional[list[str]] = Option(
546
+ None,
547
+ "--exclude",
548
+ "-x",
549
+ help="Glob patterns to exclude (can be used multiple times)",
550
+ ),
551
+ quiet: bool = Option(False, "--quiet", "-q", help="Only show errors, no success messages"),
552
+ verbose: bool = Option(False, "--verbose", "-n", help="Show detailed output"),
553
+ examples: Optional[bool] = Option(
554
+ None,
555
+ "--examples",
556
+ "-e",
557
+ callback=_show_check_examples_callback,
558
+ is_eager=True,
559
+ help="Show usage examples and exit",
560
+ ),
561
+ help_flag: Optional[bool] = Option(
562
+ None,
563
+ "--help",
564
+ "-h",
565
+ callback=_help_callback,
566
+ is_eager=True,
567
+ help="Show this message and exit",
568
+ ),
569
+ ) -> None:
570
+ """
571
+ !!! note "Summary"
572
+ Check docstrings in Python files.
573
+
574
+ ???+ abstract "Details"
575
+ This command checks the docstrings in the specified Python file or directory.
576
+
577
+ Params:
578
+ path (str):
579
+ The path to the Python file or directory to check.
580
+ config (Optional[str]):
581
+ The path to the configuration file (TOML format).
582
+ recursive (bool):
583
+ Whether to check directories recursively.
584
+ exclude (list[str]):
585
+ Glob patterns to exclude (can be used multiple times).
586
+ quiet (bool):
587
+ Whether to only show errors, no success messages.
588
+ verbose (bool):
589
+ Whether to show detailed output.
590
+ examples (Optional[bool]):
591
+ Show usage examples and exit.
592
+ help_flag (Optional[bool]):
593
+ Show help message and exit.
594
+
595
+ Returns:
596
+ (None):
597
+ Nothing is returned.
598
+ """
599
+ # Parse the recursive string value into a boolean
600
+ try:
601
+ recursive_bool: bool = _parse_recursive_flag(recursive)
602
+ except ValueError as e:
603
+ raise BadParameter(
604
+ message=(
605
+ f"Invalid value for --recursive: '{recursive}'.{NEW_LINE}"
606
+ "Use one of: true/false, t/f, yes/no, y/n, 1/0, or on/off."
607
+ )
608
+ ) from e
609
+ _check_docstrings(path, config, recursive_bool, exclude, quiet, verbose)
610
+
611
+
612
+ @app.command(
613
+ rich_help_panel="Commands",
614
+ add_help_option=False, # Disable automatic help so we can add our own with -h
615
+ )
616
+ def config_example(
617
+ help_flag: Optional[bool] = Option(
618
+ None,
619
+ "--help",
620
+ "-h",
621
+ callback=_help_callback,
622
+ is_eager=True,
623
+ help="Show this message and exit",
624
+ ),
625
+ ) -> None:
626
+ """
627
+ !!! note "Summary"
628
+ Show example configuration file.
629
+
630
+ Params:
631
+ help_flag (Optional[bool]):
632
+ Show help message and exit.
633
+
634
+ Returns:
635
+ (None):
636
+ Nothing is returned.
637
+ """
638
+ example_config: str = dedent(
639
+ """
640
+ # Example configuration for docstring-format-checker
641
+ # Place this in your pyproject.toml file
642
+
643
+ [tool.dfc]
644
+ # or [tool.docstring-format-checker]
645
+
646
+ [[tool.dfc.sections]]
647
+ order = 1
648
+ name = "summary"
649
+ type = "free_text"
650
+ admonition = "note"
651
+ prefix = "!!!"
652
+ required = true
653
+
654
+ [[tool.dfc.sections]]
655
+ order = 2
656
+ name = "details"
657
+ type = "free_text"
658
+ admonition = "info"
659
+ prefix = "???+"
660
+ required = false
661
+
662
+ [[tool.dfc.sections]]
663
+ order = 3
664
+ name = "params"
665
+ type = "list_name_and_type"
666
+ required = true
667
+
668
+ [[tool.dfc.sections]]
669
+ order = 4
670
+ name = "returns"
671
+ type = "list_name_and_type"
672
+ required = false
673
+
674
+ [[tool.dfc.sections]]
675
+ order = 5
676
+ name = "yields"
677
+ type = "list_type"
678
+ required = false
679
+
680
+ [[tool.dfc.sections]]
681
+ order = 6
682
+ name = "raises"
683
+ type = "list_type"
684
+ required = false
685
+
686
+ [[tool.dfc.sections]]
687
+ order = 7
688
+ name = "examples"
689
+ type = "free_text"
690
+ admonition = "example"
691
+ prefix = "???+"
692
+ required = false
693
+
694
+ [[tool.dfc.sections]]
695
+ order = 8
696
+ name = "notes"
697
+ type = "free_text"
698
+ admonition = "note"
699
+ prefix = "???"
700
+ required = false
701
+ """.strip()
702
+ )
703
+
704
+ print(example_config)
705
+
706
+
707
+ def entry_point() -> None:
708
+ """
709
+ !!! note "Summary"
710
+ Entry point for the CLI scripts defined in pyproject.toml.
711
+ """
712
+ app()
713
+
714
+
715
+ if __name__ == "__main__":
716
+ app()