docstring-format-checker 1.3.0__tar.gz → 1.5.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: 1.3.0
3
+ Version: 1.5.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>
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
20
21
  Classifier: Intended Audience :: Developers
21
22
  Classifier: Environment :: Console
22
23
  Requires-Dist: typer>=0.9.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docstring-format-checker"
3
- version = "1.3.0"
3
+ version = "1.5.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"
@@ -23,6 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.11",
24
24
  "Programming Language :: Python :: 3.12",
25
25
  "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
26
27
  "Intended Audience :: Developers",
27
28
  "Environment :: Console",
28
29
  ]
@@ -83,6 +84,7 @@ test = [
83
84
  "pytest-sugar==1.*",
84
85
  "pytest-xdist==3.*",
85
86
  "requests==2.*",
87
+ "complexipy==4.*",
86
88
  ]
87
89
 
88
90
  [tool.black]
@@ -147,6 +149,32 @@ lines_after_imports = 2
147
149
  [tool.codespell]
148
150
  ignore-words-list = "demog"
149
151
 
152
+ [tool.pylint.main]
153
+ disable = [
154
+ "C0103", # invalid-name
155
+ "C0301", # line-too-long
156
+ "C0302", # too-many-lines
157
+ "R0913", # too-many-arguments
158
+ "R0914", # too-many-locals
159
+ "R0915", # too-many-statements
160
+ "R0917", # too-many-positional-arguments
161
+ "R1705", # no-else-return
162
+ "R1724", # no-else-continue
163
+ "W0107", # unnecessary-pass
164
+ "W0212", # protected-access
165
+ "W0612", # unused-variable
166
+ "W0613", # unused-argument
167
+ "W0622", # redefined-builtin
168
+ "W0718", # broad-exception-caught
169
+ ]
170
+
171
+ [tool.complexipy]
172
+ paths = "src/docstring_format_checker"
173
+ max-complexity-allowed = 15
174
+ quiet = false
175
+ ignore-complexity = false
176
+ sort = "asc"
177
+
150
178
  [tool.bump_version.replacements]
151
179
  files = [
152
180
  { file = "src/docstring_format_checker/__init__.py", pattern = "__version__ = \"{VERSION}\"" },
@@ -159,14 +187,14 @@ allow_undefined_sections = false
159
187
  require_docstrings = true
160
188
  check_private = true
161
189
  sections = [
162
- { order = 1, name = "summary", type = "free_text", required = true, admonition = "note", prefix = "!!!" },
163
- { order = 2, name = "details", type = "free_text", required = false, admonition = "abstract", prefix = "???+" },
164
- { order = 3, name = "params", type = "list_name_and_type", required = false },
165
- { order = 4, name = "raises", type = "list_type", required = false },
166
- { order = 5, name = "returns", type = "list_name_and_type", required = false },
167
- { order = 6, name = "yields", type = "list_type", required = false },
168
- { order = 7, name = "examples", type = "free_text", required = false, admonition = "example", prefix = "???+" },
169
- { order = 8, name = "notes", type = "free_text", required = false, admonition = "note", prefix = "???" },
190
+ { order=1, name="summary", type="free_text", required=true, admonition="note", prefix="!!!" },
191
+ { order=2, name="details", type="free_text", required=false, admonition="abstract", prefix="???+" },
192
+ { order=3, name="params", type="list_name_and_type", required=false, admonition=false },
193
+ { order=4, name="raises", type="list_type", required=false, admonition=false },
194
+ { order=5, name="returns", type="list_name_and_type", required=false, admonition=false },
195
+ { order=6, name="yields", type="list_type", required=false, admonition=false },
196
+ { order=7, name="examples", type="free_text", required=false, admonition="example", prefix="???+" },
197
+ { order=8, name="notes", type="free_text", required=false, admonition="note", prefix="???" },
170
198
  ]
171
199
 
172
200
  [build-system]
@@ -360,12 +360,18 @@ def _format_error_messages(error_message: str) -> str:
360
360
  errors: list[str] = error_message.split("; ")
361
361
  formatted_errors: list[str] = [f"- {error.strip()}" for error in errors if error.strip()]
362
362
  return ";\n".join(formatted_errors) + "."
363
+
364
+ # Single error message
363
365
  else:
364
- # Single error message
365
366
  return f"- {error_message.strip()}."
366
367
 
367
368
 
368
- def _display_results(results: dict[str, list[DocstringError]], quiet: bool, output: str, check: bool) -> int:
369
+ def _display_results(
370
+ results: dict[str, list[DocstringError]],
371
+ quiet: bool,
372
+ output: str,
373
+ check: bool,
374
+ ) -> int:
369
375
  """
370
376
  !!! note "Summary"
371
377
  Display the results of docstring checking.
@@ -386,100 +392,193 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, outp
386
392
  """
387
393
  if not results:
388
394
  if not quiet:
389
- console.print(_green("✓ All docstrings are valid!"))
395
+ console.print(_green("✅ All docstrings are valid!"))
390
396
  return 0
391
397
 
392
- # Count total errors (individual error messages, not error objects)
398
+ # Count errors and generate summary statistics
399
+ error_stats = _count_errors_and_files(results)
400
+
401
+ if quiet:
402
+ _display_quiet_summary(error_stats)
403
+ return 1
404
+
405
+ # Display detailed results based on output format
406
+ if output == "table":
407
+ _display_table_output(results)
408
+ else:
409
+ _display_list_output(results)
410
+
411
+ # Display final summary
412
+ _display_final_summary(error_stats)
413
+ return 1
414
+
415
+
416
+ def _count_errors_and_files(results: dict[str, list[DocstringError]]) -> dict[str, int]:
417
+ """
418
+ !!! note "Summary"
419
+ Count total errors, functions, and files from results.
420
+
421
+ Params:
422
+ results (dict[str, list[DocstringError]]):
423
+ Dictionary mapping file paths to lists of errors.
424
+
425
+ Returns:
426
+ (dict[str, int]):
427
+ Dictionary containing total_errors, total_functions, and total_files.
428
+ """
393
429
  total_individual_errors: int = 0
394
430
  total_functions: int = 0
395
431
 
396
432
  for errors in results.values():
397
- total_functions += len(errors) # Count functions/items with errors
433
+ total_functions += len(errors)
398
434
  for error in errors:
399
- # Count individual error messages within each error object
400
435
  if "; " in error.message:
401
- individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
436
+ individual_errors: list[str] = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
402
437
  total_individual_errors += len(individual_errors)
403
438
  else:
404
439
  total_individual_errors += 1
405
440
 
406
- total_errors: int = total_individual_errors
407
- total_files: int = len(results)
441
+ return {"total_errors": total_individual_errors, "total_functions": total_functions, "total_files": len(results)}
408
442
 
409
- if quiet:
410
- # In quiet mode, only show summary with improved format
411
- if total_functions == 1:
412
- functions_text = f"1 function"
413
- else:
414
- functions_text = f"{total_functions} functions"
415
443
 
416
- if total_files == 1:
417
- files_text = f"1 file"
418
- else:
419
- files_text = f"{total_files} files"
444
+ def _display_quiet_summary(error_stats: dict[str, int]) -> None:
445
+ """
446
+ !!! note "Summary"
447
+ Display summary in quiet mode.
420
448
 
421
- console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
422
- return 1
449
+ Params:
450
+ error_stats (dict[str, int]):
451
+ Dictionary containing total_errors, total_functions, and total_files.
452
+ """
453
+ functions_text = (
454
+ "1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions"
455
+ )
456
+ files_text: str = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files"
423
457
 
424
- if output == "table":
425
- # Show detailed table
426
- table = Table(show_header=True, header_style="bold magenta")
427
- table.add_column("File", style="cyan", no_wrap=False)
428
- table.add_column("Line", justify="right", style="white")
429
- table.add_column("Item", style="yellow")
430
- table.add_column("Type", style="blue")
431
- table.add_column("Error", style="red")
432
-
433
- for file_path, errors in results.items():
434
- for i, error in enumerate(errors):
435
- file_display: str = file_path if i == 0 else ""
436
-
437
- # Format error message with improved formatting
438
- formatted_error_message: str = _format_error_messages(error.message)
439
-
440
- table.add_row(
441
- file_display,
442
- str(error.line_number) if error.line_number > 0 else "",
443
- error.item_name,
444
- error.item_type,
445
- formatted_error_message,
446
- )
447
- console.print(table)
458
+ console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}"))
448
459
 
460
+
461
+ def _display_table_output(results: dict[str, list[DocstringError]]) -> None:
462
+ """
463
+ !!! note "Summary"
464
+ Display results in table format.
465
+
466
+ Params:
467
+ results (dict[str, list[DocstringError]]):
468
+ Dictionary mapping file paths to lists of errors.
469
+ """
470
+ table = Table(show_header=True, header_style="bold magenta")
471
+ table.add_column("File", style="cyan", no_wrap=False)
472
+ table.add_column("Line", justify="right", style="white")
473
+ table.add_column("Item", style="yellow")
474
+ table.add_column("Type", style="blue")
475
+ table.add_column("Error", style="red")
476
+
477
+ for file_path, errors in results.items():
478
+ for i, error in enumerate(errors):
479
+ file_display = file_path if i == 0 else ""
480
+ formatted_error_message = _format_error_messages(error.message)
481
+
482
+ table.add_row(
483
+ file_display,
484
+ str(error.line_number) if error.line_number > 0 else "",
485
+ error.item_name,
486
+ error.item_type,
487
+ formatted_error_message,
488
+ )
489
+ console.print(table)
490
+
491
+
492
+ def _create_error_header(error: DocstringError) -> str:
493
+ """
494
+ !!! note "Summary"
495
+ Create formatted header for a single error.
496
+
497
+ Params:
498
+ error (DocstringError):
499
+ The error to create a header for.
500
+
501
+ Returns:
502
+ (str):
503
+ Formatted header string with line number, item type, and name.
504
+ """
505
+ if error.line_number > 0:
506
+ return f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':"
449
507
  else:
450
- # Show compact output with grouped errors under function/class headers
451
- for file_path, errors in results.items():
452
- console.print(f"{NEW_LINE}{_cyan(file_path)}")
453
- for error in errors:
454
- # Print the header line with line number, item type and name
455
- if error.line_number > 0:
456
- console.print(f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':")
457
- else:
458
- console.print(f" {_red('Error')} - {error.item_type} '{error.item_name}':")
459
-
460
- # Split error message into individual errors and indent them
461
- if "; " in error.message:
462
- individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
463
- for individual_error in individual_errors:
464
- console.print(f" - {individual_error}")
465
- else:
466
- # Single error message
467
- console.print(f" - {error.message.strip()}")
468
-
469
- # Summary - more descriptive message
470
- if total_functions == 1:
471
- functions_text = f"1 function"
472
- else:
473
- functions_text = f"{total_functions} functions"
508
+ return f" {_red('Error')} - {error.item_type} '{error.item_name}':"
509
+
474
510
 
475
- if total_files == 1:
476
- files_text = f"1 file"
511
+ def _split_error_messages(message: str) -> list[str]:
512
+ """
513
+ !!! note "Summary"
514
+ Split compound error message into individual messages.
515
+
516
+ Params:
517
+ message (str):
518
+ The error message to split.
519
+
520
+ Returns:
521
+ (list[str]):
522
+ List of individual error messages.
523
+ """
524
+ if "; " in message:
525
+ return [msg.strip() for msg in message.split("; ") if msg.strip()]
477
526
  else:
478
- files_text = f"{total_files} files"
527
+ return [message.strip()]
479
528
 
480
- console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
481
529
 
482
- return 1
530
+ def _format_error_output(error: DocstringError) -> list[str]:
531
+ """
532
+ !!! note "Summary"
533
+ Format single error for display output.
534
+
535
+ Params:
536
+ error (DocstringError):
537
+ The error to format.
538
+
539
+ Returns:
540
+ (list[str]):
541
+ List of formatted lines to print.
542
+ """
543
+ lines: list[str] = [_create_error_header(error)]
544
+ individual_errors: list[str] = _split_error_messages(error.message)
545
+ for individual_error in individual_errors:
546
+ lines.append(f" - {individual_error}")
547
+ return lines
548
+
549
+
550
+ def _display_list_output(results: dict[str, list[DocstringError]]) -> None:
551
+ """
552
+ !!! note "Summary"
553
+ Display results in list format.
554
+
555
+ Params:
556
+ results (dict[str, list[DocstringError]]):
557
+ Dictionary mapping file paths to lists of errors.
558
+ """
559
+ for file_path, errors in results.items():
560
+ console.print(f"{NEW_LINE}{_cyan(file_path)}")
561
+ for error in errors:
562
+ output_lines: list[str] = _format_error_output(error)
563
+ for line in output_lines:
564
+ console.print(line)
565
+
566
+
567
+ def _display_final_summary(error_stats: dict[str, int]) -> None:
568
+ """
569
+ !!! note "Summary"
570
+ Display the final summary line.
571
+
572
+ Params:
573
+ error_stats (dict[str, int]):
574
+ Dictionary containing total_errors, total_functions, and total_files.
575
+ """
576
+ functions_text: str = (
577
+ "1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions"
578
+ )
579
+ files_text: str = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files"
580
+
581
+ console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}"))
483
582
 
484
583
 
485
584
  # ---------------------------------------------------------------------------- #
@@ -525,45 +624,153 @@ def check_docstrings(
525
624
  (None):
526
625
  Nothing is returned.
527
626
  """
627
+ # Validate and process input paths
628
+ target_paths: list[Path] = _validate_and_process_paths(paths)
629
+
630
+ # Load and validate configuration
631
+ config_obj: Config = _load_and_validate_config(config, target_paths)
528
632
 
529
- # Validate all target paths
633
+ # Initialize checker and process all paths
634
+ checker = DocstringChecker(config_obj)
635
+ all_results: dict[str, list[DocstringError]] = _process_all_paths(checker, target_paths, exclude)
636
+
637
+ # Display results and handle exit
638
+ exit_code: int = _display_results(all_results, quiet, output, check)
639
+ if exit_code != 0:
640
+ raise Exit(exit_code)
641
+
642
+
643
+ def _validate_and_process_paths(paths: list[str]) -> list[Path]:
644
+ """
645
+ !!! note "Summary"
646
+ Validate input paths and return valid paths.
647
+
648
+ Params:
649
+ paths (list[str]):
650
+ List of path strings to validate.
651
+
652
+ Raises:
653
+ (Exit):
654
+ If any paths do not exist.
655
+
656
+ Returns:
657
+ (list[Path]):
658
+ List of valid Path objects.
659
+ """
530
660
  path_objs: list[Path] = [Path(path) for path in paths]
531
661
  target_paths: list[Path] = [p for p in path_objs if p.exists()]
532
662
  invalid_paths: list[Path] = [p for p in path_objs if not p.exists()]
533
663
 
534
- if len(invalid_paths) > 0:
664
+ if invalid_paths:
535
665
  console.print(
536
- _red(f"[bold]Error: Paths do not exist:[/bold]"),
666
+ _red("[bold]Error: Paths do not exist:[/bold]"),
537
667
  NEW_LINE,
538
668
  NEW_LINE.join([f"- '{invalid_path}'" for invalid_path in invalid_paths]),
539
669
  )
540
670
  raise Exit(1)
541
671
 
542
- # Load configuration (use first path for config discovery if no config specified)
672
+ return target_paths
673
+
674
+
675
+ def _load_and_validate_config(config: Optional[str], target_paths: list[Path]) -> Config:
676
+ """
677
+ !!! note "Summary"
678
+ Load and validate configuration from file or auto-discovery.
679
+
680
+ Params:
681
+ config (Optional[str]):
682
+ Optional path to configuration file.
683
+ target_paths (list[Path]):
684
+ List of target paths for auto-discovery.
685
+
686
+ Raises:
687
+ (Exit):
688
+ If configuration loading fails.
689
+
690
+ Returns:
691
+ (Config):
692
+ Loaded configuration object.
693
+ """
543
694
  try:
544
695
  if config:
545
- config_path = Path(config)
546
- if not config_path.exists():
547
- console.print(_red(f"Error: Configuration file does not exist: {config}"))
548
- raise Exit(1)
549
- config_obj = load_config(config_path)
696
+ return _load_explicit_config(config)
550
697
  else:
551
- # Try to find config file automatically using the first path
552
- first_path: Path = target_paths[0]
553
- found_config: Optional[Path] = find_config_file(first_path if first_path.is_dir() else first_path.parent)
554
- if found_config:
555
- config_obj: Config = load_config(found_config)
556
- else:
557
- config_obj: Config = load_config()
558
-
698
+ return _load_auto_discovered_config(target_paths)
559
699
  except Exception as e:
560
700
  console.print(_red(f"Error loading configuration: {e}"))
701
+ raise Exit(1) from e
702
+
703
+
704
+ def _load_explicit_config(config: str) -> Config:
705
+ """
706
+ !!! note "Summary"
707
+ Load configuration from explicitly specified path.
708
+
709
+ Params:
710
+ config (str):
711
+ Path to configuration file.
712
+
713
+ Raises:
714
+ (Exit):
715
+ If configuration file does not exist.
716
+
717
+ Returns:
718
+ (Config):
719
+ Loaded configuration object.
720
+ """
721
+ config_path = Path(config)
722
+ if not config_path.exists():
723
+ console.print(_red(f"Error: Configuration file does not exist: {config}"))
561
724
  raise Exit(1)
725
+ return load_config(config_path)
562
726
 
563
- # Initialize checker
564
- checker = DocstringChecker(config_obj)
565
727
 
566
- # Check all paths and collect results
728
+ def _load_auto_discovered_config(target_paths: list[Path]) -> Config:
729
+ """
730
+ !!! note "Summary"
731
+ Load configuration from auto-discovery or defaults.
732
+
733
+ Params:
734
+ target_paths (list[Path]):
735
+ List of target paths to search for configuration.
736
+
737
+ Returns:
738
+ (Config):
739
+ Loaded configuration object from found config or defaults.
740
+ """
741
+ first_path: Path = target_paths[0]
742
+ search_path: Path = first_path if first_path.is_dir() else first_path.parent
743
+ found_config: Optional[Path] = find_config_file(search_path)
744
+
745
+ if found_config:
746
+ return load_config(found_config)
747
+ else:
748
+ return load_config()
749
+
750
+
751
+ def _process_all_paths(
752
+ checker: DocstringChecker, target_paths: list[Path], exclude: Optional[list[str]]
753
+ ) -> dict[str, list[DocstringError]]:
754
+ """
755
+ !!! note "Summary"
756
+ Process all target paths and collect docstring errors.
757
+
758
+ Params:
759
+ checker (DocstringChecker):
760
+ The checker instance to use.
761
+ target_paths (list[Path]):
762
+ List of paths to check (files or directories).
763
+ exclude (Optional[list[str]]):
764
+ Optional list of exclusion patterns.
765
+
766
+ Raises:
767
+ (Exit):
768
+ If an error occurs during checking.
769
+
770
+ Returns:
771
+ (dict[str, list[DocstringError]]):
772
+ Dictionary mapping file paths to lists of errors.
773
+ """
567
774
  all_results: dict[str, list[DocstringError]] = {}
568
775
 
569
776
  try:
@@ -577,17 +784,11 @@ def check_docstrings(
577
784
  target_path, exclude_patterns=exclude
578
785
  )
579
786
  all_results.update(directory_results)
580
-
581
787
  except Exception as e:
582
788
  console.print(_red(f"Error during checking: {e}"))
583
- raise Exit(1)
789
+ raise Exit(1) from e
584
790
 
585
- # Display results
586
- exit_code: int = _display_results(all_results, quiet, output, check)
587
-
588
- # Always exit with error code if issues are found, regardless of check flag
589
- if exit_code != 0:
590
- raise Exit(exit_code)
791
+ return all_results
591
792
 
592
793
 
593
794
  # ---------------------------------------------------------------------------- #