docstring-format-checker 0.4.0__tar.gz → 0.6.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docstring-format-checker
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: A CLI tool to check and validate Python docstring formatting and completeness
5
5
  Author: Chris Mahoney
6
6
  Author-email: Chris Mahoney <docstring-format-checker@data-science-extensions.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docstring-format-checker"
3
- version = "v0.4.0"
3
+ version = "v0.6.0"
4
4
  description = "A CLI tool to check and validate Python docstring formatting and completeness"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,7 +4,7 @@ Docstring Format Checker.
4
4
  A CLI tool to check and validate Python docstring formatting and completeness.
5
5
  """
6
6
 
7
- __version__ = "v0.4.0"
7
+ __version__ = "v0.6.0"
8
8
  __author__ = "Chris Mahoney"
9
9
  __email__ = "docstring-format-checker@data-science-extensions.com"
10
10
 
@@ -43,25 +43,16 @@
43
43
 
44
44
 
45
45
  # ## Python StdLib Imports ----
46
+ from functools import partial
46
47
  from pathlib import Path
47
48
  from textwrap import dedent
48
- from typing import Optional, Union
49
+ from typing import Optional
49
50
 
50
51
  # ## Python Third Party Imports ----
51
52
  from rich.console import Console
52
53
  from rich.panel import Panel
53
54
  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
- )
55
+ from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo
65
56
 
66
57
  # ## Local First Party Imports ----
67
58
  from docstring_format_checker import __version__
@@ -76,8 +67,6 @@ from docstring_format_checker.core import DocstringChecker, DocstringError
76
67
 
77
68
  __all__: list[str] = [
78
69
  "main",
79
- "config_example",
80
- "check",
81
70
  "entry_point",
82
71
  ]
83
72
 
@@ -90,6 +79,22 @@ __all__: list[str] = [
90
79
  NEW_LINE = "\n"
91
80
 
92
81
 
82
+ ## --------------------------------------------------------------------------- #
83
+ ## Helpers ####
84
+ ## --------------------------------------------------------------------------- #
85
+
86
+
87
+ ### Colours ----
88
+ def _colour(text: str, colour: str) -> str:
89
+ return f"[{colour}]{text}[/{colour}]"
90
+
91
+
92
+ _green = partial(_colour, colour="green")
93
+ _red = partial(_colour, colour="red")
94
+ _cyan = partial(_colour, colour="cyan")
95
+ _blue = partial(_colour, colour="blue")
96
+
97
+
93
98
  # ---------------------------------------------------------------------------- #
94
99
  # #
95
100
  # Main Application ####
@@ -136,7 +141,7 @@ def _version_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
136
141
  raise Exit()
137
142
 
138
143
 
139
- def _help_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
144
+ def _help_callback_main(ctx: Context, param: CallbackParam, value: bool) -> None:
140
145
  """
141
146
  !!! note "Summary"
142
147
  Show help and exit.
@@ -159,10 +164,10 @@ def _help_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
159
164
  raise Exit()
160
165
 
161
166
 
162
- def _parse_boolean_flag(ctx: Context, param: CallbackParam, value: Optional[str]) -> Optional[bool]:
167
+ def _example_callback(ctx: Context, param: CallbackParam, value: Optional[str]) -> None:
163
168
  """
164
169
  !!! note "Summary"
165
- Parse boolean flag that accepts various true/false values.
170
+ Handle example flag and show appropriate example content.
166
171
 
167
172
  Params:
168
173
  ctx (Context):
@@ -170,51 +175,26 @@ def _parse_boolean_flag(ctx: Context, param: CallbackParam, value: Optional[str]
170
175
  param (CallbackParam):
171
176
  The parameter object.
172
177
  value (Optional[str]):
173
- The string value of the flag.
178
+ The example type to show: 'config' or 'usage'.
174
179
 
175
180
  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:
181
+ (None):
182
+ Nothing is returned.
202
183
  """
203
- !!! note "Summary"
204
- Parse recursive flag using `strtobool()` utility.
205
184
 
206
- Params:
207
- value (str):
208
- The string value of the flag.
185
+ if not value or ctx.resilient_parsing:
186
+ return
209
187
 
210
- Returns:
211
- (bool):
212
- The parsed boolean value.
213
- """
214
- return strtobool(value)
188
+ if value == "config":
189
+ _show_config_example_callback()
190
+ elif value == "usage":
191
+ _show_usage_examples_callback()
192
+ else:
193
+ console.print(_red(f"Error: Invalid example type '{value}'. Use 'config' or 'usage'."))
194
+ raise Exit(1)
215
195
 
216
196
 
217
- def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
197
+ def _show_usage_examples_callback() -> None:
218
198
  """
219
199
  !!! note "Summary"
220
200
  Show examples and exit.
@@ -232,17 +212,19 @@ def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) ->
232
212
  Nothing is returned.
233
213
  """
234
214
 
235
- if not value or ctx.resilient_parsing:
236
- return
237
-
238
215
  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
216
+ f"""
217
+ {_green("dfc myfile.py")} Check a single Python file (list output)
218
+ {_green("dfc src/")} Check all Python files in src/ directory
219
+ {_green("dfc --output=table myfile.py")} Check with table output format
220
+ {_green("dfc -o list myfile.py")} Check with list output format (default)
221
+ {_green("dfc --check myfile.py")} Check and exit with error if issues found
222
+ {_green("dfc --quiet myfile.py")} Check quietly, only show pass/fail
223
+ {_green("dfc --quiet --check myfile.py")} Check quietly and exit with error if issues found
224
+ {_green("dfc . --exclude '*/tests/*'")} Check current directory, excluding tests
225
+ {_green("dfc . -c custom.toml")} Use custom configuration file
226
+ {_green("dfc --example=config")} Show example configuration
227
+ {_green("dfc -e usage")} Show usage examples (this help)
246
228
  """
247
229
  ).strip()
248
230
 
@@ -258,10 +240,10 @@ def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) ->
258
240
  raise Exit()
259
241
 
260
242
 
261
- def _show_check_examples_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
243
+ def _show_config_example_callback() -> None:
262
244
  """
263
245
  !!! note "Summary"
264
- Show check command examples and exit.
246
+ Show configuration example and exit.
265
247
 
266
248
  Params:
267
249
  ctx (Context):
@@ -276,29 +258,74 @@ def _show_check_examples_callback(ctx: Context, param: CallbackParam, value: boo
276
258
  Nothing is returned.
277
259
  """
278
260
 
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
261
+ example_config: str = dedent(
290
262
  """
291
- )
263
+ # Example configuration for docstring-format-checker
264
+ # Place this in your pyproject.toml file
292
265
 
293
- panel = Panel(
294
- examples_content,
295
- title="Check Command Examples",
296
- title_align="left",
297
- border_style="dim",
298
- padding=(0, 1),
299
- )
266
+ [tool.dfc]
267
+ # or [tool.docstring-format-checker]
300
268
 
301
- console.print(panel)
269
+ [[tool.dfc.sections]]
270
+ order = 1
271
+ name = "summary"
272
+ type = "free_text"
273
+ admonition = "note"
274
+ prefix = "!!!"
275
+ required = true
276
+
277
+ [[tool.dfc.sections]]
278
+ order = 2
279
+ name = "details"
280
+ type = "free_text"
281
+ admonition = "info"
282
+ prefix = "???+"
283
+ required = false
284
+
285
+ [[tool.dfc.sections]]
286
+ order = 3
287
+ name = "params"
288
+ type = "list_name_and_type"
289
+ required = true
290
+
291
+ [[tool.dfc.sections]]
292
+ order = 4
293
+ name = "returns"
294
+ type = "list_name_and_type"
295
+ required = false
296
+
297
+ [[tool.dfc.sections]]
298
+ order = 5
299
+ name = "yields"
300
+ type = "list_type"
301
+ required = false
302
+
303
+ [[tool.dfc.sections]]
304
+ order = 6
305
+ name = "raises"
306
+ type = "list_type"
307
+ required = false
308
+
309
+ [[tool.dfc.sections]]
310
+ order = 7
311
+ name = "examples"
312
+ type = "free_text"
313
+ admonition = "example"
314
+ prefix = "???+"
315
+ required = false
316
+
317
+ [[tool.dfc.sections]]
318
+ order = 8
319
+ name = "notes"
320
+ type = "free_text"
321
+ admonition = "note"
322
+ prefix = "???"
323
+ required = false
324
+ """
325
+ ).strip()
326
+
327
+ # Print without Rich markup processing to avoid bracket interpretation
328
+ console.print(example_config, markup=False)
302
329
  raise Exit()
303
330
 
304
331
 
@@ -325,7 +352,7 @@ def _format_error_messages(error_message: str) -> str:
325
352
  return f"- {error_message.strip()}."
326
353
 
327
354
 
328
- def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verbose: bool) -> int:
355
+ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, output: str, check: bool) -> int:
329
356
  """
330
357
  !!! note "Summary"
331
358
  Display the results of docstring checking.
@@ -334,9 +361,11 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
334
361
  results (dict[str, list[DocstringError]]):
335
362
  Dictionary mapping file paths to lists of errors
336
363
  quiet (bool):
337
- Whether to suppress success messages
338
- verbose (bool):
339
- Whether to show detailed output
364
+ Whether to suppress success messages and error details
365
+ output (str):
366
+ Output format: 'table' or 'list'
367
+ check (bool):
368
+ Whether this is a check run (affects quiet behavior)
340
369
 
341
370
  Returns:
342
371
  (int):
@@ -344,14 +373,42 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
344
373
  """
345
374
  if not results:
346
375
  if not quiet:
347
- console.print("[green]✓ All docstrings are valid![/green]")
376
+ console.print(_green("✓ All docstrings are valid!"))
348
377
  return 0
349
378
 
350
- # Count total errors
351
- total_errors: int = sum(len(errors) for errors in results.values())
379
+ # Count total errors (individual error messages, not error objects)
380
+ total_individual_errors: int = 0
381
+ total_functions: int = 0
382
+
383
+ for errors in results.values():
384
+ total_functions += len(errors) # Count functions/items with errors
385
+ for error in errors:
386
+ # Count individual error messages within each error object
387
+ if "; " in error.message:
388
+ individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
389
+ total_individual_errors += len(individual_errors)
390
+ else:
391
+ total_individual_errors += 1
392
+
393
+ total_errors: int = total_individual_errors
352
394
  total_files: int = len(results)
353
395
 
354
- if verbose:
396
+ if quiet:
397
+ # In quiet mode, only show summary with improved format
398
+ if total_functions == 1:
399
+ functions_text = f"1 function"
400
+ else:
401
+ functions_text = f"{total_functions} functions"
402
+
403
+ if total_files == 1:
404
+ files_text = f"1 file"
405
+ else:
406
+ files_text = f"{total_files} files"
407
+
408
+ console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
409
+ return 1
410
+
411
+ if output == "table":
355
412
  # Show detailed table
356
413
  table = Table(show_header=True, header_style="bold magenta")
357
414
  table.add_column("File", style="cyan", no_wrap=False)
@@ -377,22 +434,43 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
377
434
  console.print(table)
378
435
 
379
436
  else:
380
- # Show compact output
437
+ # Show compact output - each individual error on its own line
381
438
  for file_path, errors in results.items():
382
- console.print(f"{NEW_LINE}[cyan]{file_path}[/cyan]")
439
+ console.print(f"{NEW_LINE}{_cyan(file_path)}")
383
440
  for error in errors:
384
- # Format error message with improved formatting
385
- formatted_error_message: str = _format_error_messages(error.message)
386
-
387
- if error.line_number > 0:
388
- console.print(
389
- f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error_message}"
390
- )
441
+ # Split error message into individual errors for list mode
442
+ if "; " in error.message:
443
+ individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
444
+ for individual_error in individual_errors:
445
+ formatted_error = f"- {individual_error}"
446
+ if error.line_number > 0:
447
+ console.print(
448
+ f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error}"
449
+ )
450
+ else:
451
+ console.print(f" {_red('Error')}: {formatted_error}")
391
452
  else:
392
- console.print(f" [red]Error[/red]: {formatted_error_message}")
453
+ # Single error message
454
+ formatted_error = f"- {error.message.strip()}"
455
+ if error.line_number > 0:
456
+ console.print(
457
+ f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error}"
458
+ )
459
+ else:
460
+ console.print(f" {_red('Error')}: {formatted_error}")
461
+
462
+ # Summary - more descriptive message
463
+ if total_functions == 1:
464
+ functions_text = f"1 function"
465
+ else:
466
+ functions_text = f"{total_functions} functions"
393
467
 
394
- # Summary
395
- console.print(f"{NEW_LINE}[red]Found {total_errors} error(s) in {total_files} file(s)[/red]")
468
+ if total_files == 1:
469
+ files_text = f"1 file"
470
+ else:
471
+ files_text = f"{total_files} files"
472
+
473
+ console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
396
474
 
397
475
  return 1
398
476
 
@@ -405,13 +483,13 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
405
483
 
406
484
 
407
485
  # This will be the default behavior when no command is specified
408
- def _check_docstrings(
486
+ def check_docstrings(
409
487
  path: str,
410
488
  config: Optional[str] = None,
411
- recursive: bool = True,
412
489
  exclude: Optional[list[str]] = None,
413
490
  quiet: bool = False,
414
- verbose: bool = False,
491
+ output: str = "list",
492
+ check: bool = False,
415
493
  ) -> None:
416
494
  """
417
495
  !!! note "Summary"
@@ -422,14 +500,19 @@ def _check_docstrings(
422
500
  The path to the file or directory to check.
423
501
  config (Optional[str]):
424
502
  The path to the configuration file.
425
- recursive (bool):
426
- Whether to check files recursively.
503
+ Default: `None`.
427
504
  exclude (Optional[list[str]]):
428
505
  List of glob patterns to exclude from checking.
506
+ Default: `None`.
429
507
  quiet (bool):
430
508
  Whether to suppress output.
431
- verbose (bool):
432
- Whether to show detailed output.
509
+ Default: `False`.
510
+ output (str):
511
+ Output format: 'table' or 'list'.
512
+ Default: `'list'`.
513
+ check (bool):
514
+ Whether to throw error if issues are found.
515
+ Default: `False`.
433
516
 
434
517
  Returns:
435
518
  (None):
@@ -440,7 +523,7 @@ def _check_docstrings(
440
523
 
441
524
  # Validate target path
442
525
  if not target_path.exists():
443
- console.print(f"[red]Error: Path does not exist: {path}[/red]")
526
+ console.print(_red(f"Error: Path does not exist: '{path}'"))
444
527
  raise Exit(1)
445
528
 
446
529
  # Load configuration
@@ -448,25 +531,19 @@ def _check_docstrings(
448
531
  if config:
449
532
  config_path = Path(config)
450
533
  if not config_path.exists():
451
- console.print(f"[red]Error: Configuration file does not exist: {config}[/red]")
534
+ console.print(_red(f"Error: Configuration file does not exist: {config}"))
452
535
  raise Exit(1)
453
536
  sections_config = load_config(config_path)
454
537
  else:
455
538
  # Try to find config file automatically
456
- found_config: Union[Path, None] = find_config_file(
457
- target_path if target_path.is_dir() else target_path.parent
458
- )
539
+ found_config: Optional[Path] = find_config_file(target_path if target_path.is_dir() else target_path.parent)
459
540
  if found_config:
460
- if verbose:
461
- console.print(f"[blue]Using configuration from: {found_config}[/blue]")
462
541
  sections_config: list[SectionConfig] = load_config(found_config)
463
542
  else:
464
- if verbose:
465
- console.print("[blue]Using default configuration[/blue]")
466
543
  sections_config: list[SectionConfig] = load_config()
467
544
 
468
545
  except Exception as e:
469
- console.print(f"[red]Error loading configuration: {e}[/red]")
546
+ console.print(_red(f"Error loading configuration: {e}"))
470
547
  raise Exit(1)
471
548
 
472
549
  # Initialize checker
@@ -475,23 +552,18 @@ def _check_docstrings(
475
552
  # Check files
476
553
  try:
477
554
  if target_path.is_file():
478
- if verbose:
479
- console.print(f"[blue]Checking file: {target_path}[/blue]")
480
555
  errors: list[DocstringError] = checker.check_file(target_path)
481
556
  results: dict[str, list[DocstringError]] = {str(target_path): errors} if errors else {}
482
557
  else:
483
- if verbose:
484
- console.print(f"[blue]Checking directory: {target_path} (recursive={recursive})[/blue]")
485
- results: dict[str, list[DocstringError]] = checker.check_directory(
486
- target_path, recursive=recursive, exclude_patterns=exclude
487
- )
558
+ results: dict[str, list[DocstringError]] = checker.check_directory(target_path, exclude_patterns=exclude)
488
559
  except Exception as e:
489
- console.print(f"[red]Error during checking: {e}[/red]")
560
+ console.print(_red(f"Error during checking: {e}"))
490
561
  raise Exit(1)
491
562
 
492
563
  # Display results
493
- exit_code: int = _display_results(results, quiet, verbose)
564
+ exit_code: int = _display_results(results, quiet, output, check)
494
565
 
566
+ # Always exit with error code if issues are found, regardless of check flag
495
567
  if exit_code != 0:
496
568
  raise Exit(exit_code)
497
569
 
@@ -507,6 +579,41 @@ def _check_docstrings(
507
579
  @app.callback(invoke_without_command=True)
508
580
  def main(
509
581
  ctx: Context,
582
+ path: Optional[str] = Argument(None, help="Path to Python file or directory to check"),
583
+ config: Optional[str] = Option(None, "--config", "-f", help="Path to configuration file (TOML format)"),
584
+ exclude: Optional[list[str]] = Option(
585
+ None,
586
+ "--exclude",
587
+ "-x",
588
+ help="Glob patterns to exclude (can be used multiple times)",
589
+ ),
590
+ output: str = Option(
591
+ "list",
592
+ "--output",
593
+ "-o",
594
+ help="Output format: 'table' or 'list'",
595
+ show_default=True,
596
+ ),
597
+ check: bool = Option(
598
+ False,
599
+ "--check",
600
+ "-c",
601
+ help="Throw error (exit 1) if any issues are found",
602
+ ),
603
+ quiet: bool = Option(
604
+ False,
605
+ "--quiet",
606
+ "-q",
607
+ help="Only output pass/fail confirmation, suppress errors unless failing",
608
+ ),
609
+ example: Optional[str] = Option(
610
+ None,
611
+ "--example",
612
+ "-e",
613
+ callback=_example_callback,
614
+ is_eager=True,
615
+ help="Show examples: 'config' for configuration example, 'usage' for usage examples",
616
+ ),
510
617
  version: Optional[bool] = Option(
511
618
  None,
512
619
  "--version",
@@ -515,19 +622,11 @@ def main(
515
622
  is_eager=True,
516
623
  help="Show version and exit",
517
624
  ),
518
- examples: Optional[bool] = Option(
519
- None,
520
- "--examples",
521
- "-e",
522
- callback=_show_examples_callback,
523
- is_eager=True,
524
- help="Show usage examples and exit",
525
- ),
526
625
  help_flag: Optional[bool] = Option(
527
626
  None,
528
627
  "--help",
529
628
  "-h",
530
- callback=_help_callback,
629
+ callback=_help_callback_main,
531
630
  is_eager=True,
532
631
  help="Show this message and exit",
533
632
  ),
@@ -542,122 +641,22 @@ def main(
542
641
  Params:
543
642
  ctx (Context):
544
643
  The context object for the command.
545
- version (Optional[bool]):
546
- Show version and exit.
547
- examples (Optional[bool]):
548
- Show usage examples and exit.
549
- help_flag (Optional[bool]):
550
- Show help message and exit.
551
-
552
- Returns:
553
- (None):
554
- Nothing is returned.
555
- """
556
- # If no subcommand is provided, show help
557
- if ctx.invoked_subcommand is None:
558
- echo(ctx.get_help())
559
- raise Exit()
560
-
561
-
562
- @app.command(
563
- rich_help_panel="Commands",
564
- add_help_option=False, # Disable automatic help so we can add our own with -h
565
- )
566
- def check(
567
- path: str = Argument(..., help="Path to Python file or directory to check"),
568
- config: Optional[str] = Option(None, "--config", "-c", help="Path to configuration file (TOML format)"),
569
- recursive: str = Option(
570
- "true",
571
- "--recursive",
572
- "-r",
573
- help="Check directories recursively (default: true). Accepts: true/false, t/f, yes/no, y/n, 1/0, on/off",
574
- ),
575
- exclude: Optional[list[str]] = Option(
576
- None,
577
- "--exclude",
578
- "-x",
579
- help="Glob patterns to exclude (can be used multiple times)",
580
- ),
581
- quiet: bool = Option(False, "--quiet", "-q", help="Only show errors, no success messages"),
582
- verbose: bool = Option(False, "--verbose", "-n", help="Show detailed output"),
583
- examples: Optional[bool] = Option(
584
- None,
585
- "--examples",
586
- "-e",
587
- callback=_show_check_examples_callback,
588
- is_eager=True,
589
- help="Show usage examples and exit",
590
- ),
591
- help_flag: Optional[bool] = Option(
592
- None,
593
- "--help",
594
- "-h",
595
- callback=_help_callback,
596
- is_eager=True,
597
- help="Show this message and exit",
598
- ),
599
- ) -> None:
600
- """
601
- !!! note "Summary"
602
- Check docstrings in Python files.
603
-
604
- ???+ abstract "Details"
605
- This command checks the docstrings in the specified Python file or directory.
606
-
607
- Params:
608
- path (str):
609
- The path to the Python file or directory to check.
644
+ path (Optional[str]):
645
+ Path to Python file or directory to check.
610
646
  config (Optional[str]):
611
- The path to the configuration file (TOML format).
612
- recursive (bool):
613
- Whether to check directories recursively.
614
- exclude (list[str]):
615
- Glob patterns to exclude (can be used multiple times).
647
+ Path to configuration file (TOML format).
648
+ exclude (Optional[list[str]]):
649
+ Glob patterns to exclude.
650
+ output (str):
651
+ Output format: 'table' or 'list'.
652
+ check (bool):
653
+ Throw error if any issues are found.
616
654
  quiet (bool):
617
- Whether to only show errors, no success messages.
618
- verbose (bool):
619
- Whether to show detailed output.
620
- examples (Optional[bool]):
621
- Show usage examples and exit.
622
- help_flag (Optional[bool]):
623
- Show help message and exit.
624
-
625
- Returns:
626
- (None):
627
- Nothing is returned.
628
- """
629
- # Parse the recursive string value into a boolean
630
- try:
631
- recursive_bool: bool = _parse_recursive_flag(recursive)
632
- except ValueError as e:
633
- raise BadParameter(
634
- message=(
635
- f"Invalid value for --recursive: '{recursive}'.{NEW_LINE}"
636
- "Use one of: true/false, t/f, yes/no, y/n, 1/0, or on/off."
637
- )
638
- ) from e
639
- _check_docstrings(path, config, recursive_bool, exclude, quiet, verbose)
640
-
641
-
642
- @app.command(
643
- rich_help_panel="Commands",
644
- add_help_option=False, # Disable automatic help so we can add our own with -h
645
- )
646
- def config_example(
647
- help_flag: Optional[bool] = Option(
648
- None,
649
- "--help",
650
- "-h",
651
- callback=_help_callback,
652
- is_eager=True,
653
- help="Show this message and exit",
654
- ),
655
- ) -> None:
656
- """
657
- !!! note "Summary"
658
- Show example configuration file.
659
-
660
- Params:
655
+ Only output pass/fail confirmation.
656
+ example (Optional[str]):
657
+ Show examples: 'config' or 'usage'.
658
+ version (Optional[bool]):
659
+ Show version and exit.
661
660
  help_flag (Optional[bool]):
662
661
  Show help message and exit.
663
662
 
@@ -665,74 +664,26 @@ def config_example(
665
664
  (None):
666
665
  Nothing is returned.
667
666
  """
668
- example_config: str = dedent(
669
- """
670
- # Example configuration for docstring-format-checker
671
- # Place this in your pyproject.toml file
672
-
673
- [tool.dfc]
674
- # or [tool.docstring-format-checker]
675
-
676
- [[tool.dfc.sections]]
677
- order = 1
678
- name = "summary"
679
- type = "free_text"
680
- admonition = "note"
681
- prefix = "!!!"
682
- required = true
683
-
684
- [[tool.dfc.sections]]
685
- order = 2
686
- name = "details"
687
- type = "free_text"
688
- admonition = "info"
689
- prefix = "???+"
690
- required = false
691
-
692
- [[tool.dfc.sections]]
693
- order = 3
694
- name = "params"
695
- type = "list_name_and_type"
696
- required = true
697
-
698
- [[tool.dfc.sections]]
699
- order = 4
700
- name = "returns"
701
- type = "list_name_and_type"
702
- required = false
703
667
 
704
- [[tool.dfc.sections]]
705
- order = 5
706
- name = "yields"
707
- type = "list_type"
708
- required = false
709
-
710
- [[tool.dfc.sections]]
711
- order = 6
712
- name = "raises"
713
- type = "list_type"
714
- required = false
668
+ # If no path is provided, show help
669
+ if path is None:
670
+ echo(ctx.get_help())
671
+ raise Exit(0)
715
672
 
716
- [[tool.dfc.sections]]
717
- order = 7
718
- name = "examples"
719
- type = "free_text"
720
- admonition = "example"
721
- prefix = "???+"
722
- required = false
673
+ # Validate output format
674
+ if output not in ["table", "list"]:
675
+ console.print(_red(f"Error: Invalid output format '{output}'. Use 'table' or 'list'."))
676
+ raise Exit(1)
723
677
 
724
- [[tool.dfc.sections]]
725
- order = 8
726
- name = "notes"
727
- type = "free_text"
728
- admonition = "note"
729
- prefix = "???"
730
- required = false
731
- """.strip()
678
+ check_docstrings(
679
+ path=path,
680
+ config=config,
681
+ exclude=exclude,
682
+ quiet=quiet,
683
+ output=output,
684
+ check=check,
732
685
  )
733
686
 
734
- print(example_config)
735
-
736
687
 
737
688
  def entry_point() -> None:
738
689
  """
@@ -167,18 +167,15 @@ class DocstringChecker:
167
167
  def check_directory(
168
168
  self,
169
169
  directory_path: Union[str, Path],
170
- recursive: bool = True,
171
170
  exclude_patterns: Optional[list[str]] = None,
172
171
  ) -> dict[str, list[DocstringError]]:
173
172
  """
174
173
  !!! note "Summary"
175
- Check docstrings in all Python files in a directory.
174
+ Check docstrings in all Python files in a directory recursively.
176
175
 
177
176
  Params:
178
177
  directory_path (Union[str, Path]):
179
178
  Path to the directory to check.
180
- recursive (bool):
181
- Whether to check subdirectories recursively.
182
179
  exclude_patterns (Optional[list[str]]):
183
180
  List of glob patterns to exclude.
184
181
 
@@ -200,13 +197,7 @@ class DocstringChecker:
200
197
  if not directory_path.is_dir():
201
198
  raise DirectoryNotFoundError(f"Path is not a directory: {directory_path}")
202
199
 
203
- # Find all Python files
204
- if recursive:
205
- pattern = "**/*.py"
206
- else:
207
- pattern = "*.py"
208
-
209
- python_files: list[Path] = list(directory_path.glob(pattern))
200
+ python_files: list[Path] = list(directory_path.glob("**/*.py"))
210
201
 
211
202
  # Filter out excluded patterns
212
203
  if exclude_patterns:
@@ -971,9 +962,41 @@ class DocstringChecker:
971
962
  if current_section and stripped_line and not stripped_line.startswith(("!", "?", "#")):
972
963
  # Look for parameter/type definitions
973
964
  if ":" in stripped_line:
965
+ # Skip description lines that start with common description words
966
+ description_prefixes = [
967
+ "default:",
968
+ "note:",
969
+ "example:",
970
+ "see:",
971
+ "warning:",
972
+ "info:",
973
+ "tip:",
974
+ "returns:",
975
+ ]
976
+ is_description_line = any(
977
+ stripped_line.lower().startswith(prefix) for prefix in description_prefixes
978
+ )
979
+
980
+ # Skip lines that are clearly descriptions (containing "Default:", etc.)
981
+ if (
982
+ is_description_line
983
+ or "Default:" in stripped_line
984
+ or "Output format:" in stripped_line
985
+ or "Show examples:" in stripped_line
986
+ ):
987
+ continue
988
+
974
989
  # For list_name_and_type sections, check format like "name (type):" or "(type):"
975
990
  if current_section.type == "list_name_and_type":
976
991
  # Pattern: name (type): or (type):
992
+ # But skip if it doesn't look like a parameter definition (e.g., has multiple words before the colon)
993
+ colon_part = stripped_line.split(":")[0].strip()
994
+ # Skip if it contains phrases that indicate it's a description, not a parameter
995
+ if any(
996
+ word in colon_part.lower() for word in ["default", "output", "format", "show", "example"]
997
+ ):
998
+ continue
999
+
977
1000
  if not re.search(r"\([^)]+\):", stripped_line):
978
1001
  errors.append(
979
1002
  f"Section '{current_section.name}' (type: '{current_section.type}') requires "