docstring-format-checker 0.5.0__tar.gz → 0.7.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.5.0
3
+ Version: 0.7.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.5.0"
3
+ version = "v0.7.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.5.0"
7
+ __version__ = "v0.7.0"
8
8
  __author__ = "Chris Mahoney"
9
9
  __email__ = "docstring-format-checker@data-science-extensions.com"
10
10
 
@@ -376,13 +376,36 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, outp
376
376
  console.print(_green("✓ All docstrings are valid!"))
377
377
  return 0
378
378
 
379
- # Count total errors
380
- 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
381
394
  total_files: int = len(results)
382
395
 
383
396
  if quiet:
384
- # In quiet mode, only show summary
385
- console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {total_files} file(s)"))
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}"))
386
409
  return 1
387
410
 
388
411
  if output == "table":
@@ -411,22 +434,37 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, outp
411
434
  console.print(table)
412
435
 
413
436
  else:
414
- # Show compact output
437
+ # Show compact output with grouped errors under function/class headers
415
438
  for file_path, errors in results.items():
416
439
  console.print(f"{NEW_LINE}{_cyan(file_path)}")
417
440
  for error in errors:
418
- # Format error message with improved formatting
419
- formatted_error_message: str = _format_error_messages(error.message)
420
-
441
+ # Print the header line with line number, item type and name
421
442
  if error.line_number > 0:
422
- console.print(
423
- f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error_message}"
424
- )
443
+ console.print(f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':")
444
+ else:
445
+ console.print(f" {_red('Error')} - {error.item_type} '{error.item_name}':")
446
+
447
+ # Split error message into individual errors and indent them
448
+ if "; " in error.message:
449
+ individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
450
+ for individual_error in individual_errors:
451
+ console.print(f" - {individual_error}")
425
452
  else:
426
- console.print(f" {_red('Error')}: {formatted_error_message}")
453
+ # Single error message
454
+ console.print(f" - {error.message.strip()}")
455
+
456
+ # Summary - more descriptive message
457
+ if total_functions == 1:
458
+ functions_text = f"1 function"
459
+ else:
460
+ functions_text = f"{total_functions} functions"
461
+
462
+ if total_files == 1:
463
+ files_text = f"1 file"
464
+ else:
465
+ files_text = f"{total_files} files"
427
466
 
428
- # Summary
429
- console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {total_files} file(s)"))
467
+ console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
430
468
 
431
469
  return 1
432
470
 
@@ -439,7 +477,7 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, outp
439
477
 
440
478
 
441
479
  # This will be the default behavior when no command is specified
442
- def _check_docstrings(
480
+ def check_docstrings(
443
481
  path: str,
444
482
  config: Optional[str] = None,
445
483
  exclude: Optional[list[str]] = None,
@@ -456,14 +494,19 @@ def _check_docstrings(
456
494
  The path to the file or directory to check.
457
495
  config (Optional[str]):
458
496
  The path to the configuration file.
497
+ Default: `None`.
459
498
  exclude (Optional[list[str]]):
460
499
  List of glob patterns to exclude from checking.
500
+ Default: `None`.
461
501
  quiet (bool):
462
502
  Whether to suppress output.
503
+ Default: `False`.
463
504
  output (str):
464
505
  Output format: 'table' or 'list'.
506
+ Default: `'list'`.
465
507
  check (bool):
466
508
  Whether to throw error if issues are found.
509
+ Default: `False`.
467
510
 
468
511
  Returns:
469
512
  (None):
@@ -626,7 +669,7 @@ def main(
626
669
  console.print(_red(f"Error: Invalid output format '{output}'. Use 'table' or 'list'."))
627
670
  raise Exit(1)
628
671
 
629
- _check_docstrings(
672
+ check_docstrings(
630
673
  path=path,
631
674
  config=config,
632
675
  exclude=exclude,
@@ -924,6 +924,7 @@ class DocstringChecker:
924
924
  # Check each line in the docstring
925
925
  lines: list[str] = docstring.split("\n")
926
926
  current_section = None
927
+ type_line_indent = None # Track indentation of type definition lines
927
928
 
928
929
  for i, line in enumerate(lines):
929
930
  stripped_line: str = line.strip()
@@ -938,6 +939,7 @@ class DocstringChecker:
938
939
  current_section: Optional[SectionConfig] = next(
939
940
  (s for s in parentheses_sections if s.name.lower() == section_name), None
940
941
  )
942
+ type_line_indent = None # Reset for new section
941
943
  continue
942
944
 
943
945
  # Non-admonition sections - only match actual section headers, not indented content
@@ -955,6 +957,7 @@ class DocstringChecker:
955
957
  current_section = next(
956
958
  (s for s in parentheses_sections if s.name.lower() == section_name), None
957
959
  )
960
+ type_line_indent = None # Reset for new section
958
961
  continue
959
962
  # If it doesn't match a known section, fall through to content processing
960
963
 
@@ -962,19 +965,66 @@ class DocstringChecker:
962
965
  if current_section and stripped_line and not stripped_line.startswith(("!", "?", "#")):
963
966
  # Look for parameter/type definitions
964
967
  if ":" in stripped_line:
968
+ # Calculate current line indentation
969
+ current_indent = len(line) - len(line.lstrip())
970
+
971
+ # Skip description lines that start with common description words
972
+ description_prefixes = [
973
+ "default:",
974
+ "note:",
975
+ "example:",
976
+ "see:",
977
+ "warning:",
978
+ "info:",
979
+ "tip:",
980
+ "returns:",
981
+ ]
982
+ is_description_line = any(
983
+ stripped_line.lower().startswith(prefix) for prefix in description_prefixes
984
+ )
985
+
986
+ # Skip lines that are clearly descriptions (containing "Default:", etc.)
987
+ if (
988
+ is_description_line
989
+ or "Default:" in stripped_line
990
+ or "Output format:" in stripped_line
991
+ or "Show examples:" in stripped_line
992
+ ):
993
+ continue
994
+
995
+ # For list_type sections, we need special handling
996
+ if current_section.type == "list_type":
997
+ # Check if this line has parentheses at the beginning
998
+ if re.search(r"^\s*\([^)]+\):", stripped_line):
999
+ # This is a valid type definition line, remember its indentation
1000
+ type_line_indent = current_indent
1001
+ continue
1002
+ else:
1003
+ # If no type definition has been found yet, allow lines with colons as possible descriptions
1004
+ if type_line_indent is None:
1005
+ continue
1006
+ # Check if this is a description line (more indented than type line)
1007
+ if current_indent > type_line_indent:
1008
+ # This is a description line, skip validation
1009
+ continue
1010
+ else:
1011
+ # This should be a type definition but doesn't have proper format
1012
+ errors.append(
1013
+ f"Section '{current_section.name}' (type: '{current_section.type}') requires "
1014
+ f"parenthesized types, see: '{stripped_line}'"
1015
+ )
965
1016
  # For list_name_and_type sections, check format like "name (type):" or "(type):"
966
- if current_section.type == "list_name_and_type":
1017
+ elif current_section.type == "list_name_and_type":
967
1018
  # Pattern: name (type): or (type):
968
- if not re.search(r"\([^)]+\):", stripped_line):
969
- errors.append(
970
- f"Section '{current_section.name}' (type: '{current_section.type}') requires "
971
- f"parenthesized types, see: '{stripped_line}'"
972
- )
1019
+ # But skip if it doesn't look like a parameter definition (e.g., has multiple words before the colon)
1020
+ colon_part = stripped_line.split(":")[0].strip()
1021
+ # Skip if it contains phrases that indicate it's a description, not a parameter
1022
+ if any(
1023
+ word in colon_part.lower() for word in ["default", "output", "format", "show", "example"]
1024
+ ):
1025
+ continue
973
1026
 
974
- # For list_type sections, check format like "(Type):"
975
- elif current_section.type == "list_type":
976
- # Pattern: (Type):
977
- if not re.search(r"^\s*\([^)]+\):", stripped_line):
1027
+ if not re.search(r"\([^)]+\):", stripped_line):
978
1028
  errors.append(
979
1029
  f"Section '{current_section.name}' (type: '{current_section.type}') requires "
980
1030
  f"parenthesized types, see: '{stripped_line}'"