docstring-format-checker 1.6.0__tar.gz → 1.6.1__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.6.0
3
+ Version: 1.6.1
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 = "1.6.0"
3
+ version = "1.6.1"
4
4
  description = "A CLI tool to check and validate Python docstring formatting and completeness"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -543,8 +543,19 @@ def _format_error_output(error: DocstringError) -> list[str]:
543
543
  """
544
544
  lines: list[str] = [_create_error_header(error)]
545
545
  individual_errors: list[str] = _split_error_messages(error.message)
546
+
546
547
  for individual_error in individual_errors:
547
- lines.append(f" - {individual_error}")
548
+ # Check if this error has multi-line content (e.g., parameter type mismatches)
549
+ if "\n" in individual_error:
550
+ # Split by newlines and add 4 spaces of extra indentation to each line
551
+ error_lines = individual_error.split("\n")
552
+ lines.append(f" - {error_lines[0]}") # First line gets the bullet
553
+ for sub_line in error_lines[1:]:
554
+ if sub_line.strip(): # Only add non-empty lines
555
+ lines.append(f" {sub_line}") # Continuation lines get 4 spaces
556
+ else:
557
+ lines.append(f" - {individual_error}")
558
+
548
559
  return lines
549
560
 
550
561
 
@@ -559,10 +559,14 @@ class DocstringChecker:
559
559
 
560
560
  errors: list[str] = []
561
561
 
562
- # Validate required sections
562
+ # Validate required sections are present
563
563
  required_section_errors: list[str] = self._validate_all_required_sections(docstring, item)
564
564
  errors.extend(required_section_errors)
565
565
 
566
+ # Validate all existing sections (required or not)
567
+ existing_section_errors: list[str] = self._validate_all_existing_sections(docstring, item)
568
+ errors.extend(existing_section_errors)
569
+
566
570
  # Perform comprehensive validation checks
567
571
  comprehensive_errors: list[str] = self._perform_comprehensive_validation(docstring)
568
572
  errors.extend(comprehensive_errors)
@@ -578,10 +582,33 @@ class DocstringChecker:
578
582
  item_type=item.item_type,
579
583
  )
580
584
 
585
+ def _is_params_section_required(self, item: FunctionAndClassDetails) -> bool:
586
+ """
587
+ !!! note "Summary"
588
+ Check if params section is required for this item.
589
+
590
+ Params:
591
+ item (FunctionAndClassDetails):
592
+ The function or class details.
593
+
594
+ Returns:
595
+ (bool):
596
+ True if params section is required, False otherwise.
597
+ """
598
+
599
+ # For classes, params section not required (attributes handled differently)
600
+ if isinstance(item.node, ast.ClassDef):
601
+ return False
602
+
603
+ # For functions, only required if function has parameters (excluding self/cls)
604
+ # item.node is guaranteed to be FunctionDef or AsyncFunctionDef due to type constraints
605
+ params = [arg.arg for arg in item.node.args.args if arg.arg not in ("self", "cls")]
606
+ return len(params) > 0
607
+
581
608
  def _validate_all_required_sections(self, docstring: str, item: FunctionAndClassDetails) -> list[str]:
582
609
  """
583
610
  !!! note "Summary"
584
- Validate all required sections are present and valid.
611
+ Validate all required sections are present.
585
612
 
586
613
  Params:
587
614
  docstring (str):
@@ -591,115 +618,127 @@ class DocstringChecker:
591
618
 
592
619
  Returns:
593
620
  (list[str]):
594
- List of validation error messages.
621
+ List of validation error messages for missing required sections.
595
622
  """
596
623
 
597
624
  errors: list[str] = []
598
625
  for section in self.required_sections:
599
- section_error = self._validate_single_required_section(docstring, section, item)
600
- if section_error:
601
- errors.append(section_error)
626
+ # Special handling for params section - only required if function/class has parameters
627
+ if section.name.lower() == "params":
628
+ if not self._is_params_section_required(item):
629
+ continue
630
+
631
+ # Only check if the section exists, don't validate content yet
632
+ if not self._section_exists(docstring, section):
633
+ errors.append(f"Missing required section: {section.name}")
602
634
  return errors
603
635
 
604
- def _validate_single_required_section(
605
- self, docstring: str, section: SectionConfig, item: FunctionAndClassDetails
606
- ) -> Optional[str]:
636
+ def _validate_all_existing_sections(self, docstring: str, item: FunctionAndClassDetails) -> list[str]:
607
637
  """
608
638
  !!! note "Summary"
609
- Validate a single required section based on its type.
639
+ Validate content of all existing sections (required or not).
610
640
 
611
641
  Params:
612
642
  docstring (str):
613
643
  The docstring to validate.
614
- section (SectionConfig):
615
- The section configuration to validate against.
616
644
  item (FunctionAndClassDetails):
617
645
  The function or class details.
618
646
 
619
647
  Returns:
620
- (Optional[str]):
621
- Error message if validation fails, None otherwise.
648
+ (list[str]):
649
+ List of validation error messages for invalid section content.
622
650
  """
623
651
 
624
- if section.type == "free_text":
625
- return self._validate_free_text_section(docstring, section)
626
- elif section.type == "list_name_and_type":
627
- return self._validate_list_name_and_type_section(docstring, section, item)
628
- elif section.type == "list_type":
629
- return self._validate_list_type_section(docstring, section)
630
- elif section.type == "list_name":
631
- return self._validate_list_name_section(docstring, section)
632
- return None
652
+ errors: list[str] = []
653
+ for section in self.config.sections:
654
+ # Only validate if the section actually exists in the docstring
655
+ if self._section_exists(docstring, section):
656
+ section_error = self._validate_single_section_content(docstring, section, item)
657
+ if section_error:
658
+ errors.append(section_error)
659
+ return errors
633
660
 
634
- def _validate_free_text_section(self, docstring: str, section: SectionConfig) -> Optional[str]:
661
+ def _section_exists(self, docstring: str, section: SectionConfig) -> bool:
635
662
  """
636
663
  !!! note "Summary"
637
- Validate free text sections.
664
+ Check if a section exists in the docstring.
638
665
 
639
666
  Params:
640
667
  docstring (str):
641
- The docstring to validate.
668
+ The docstring to check.
642
669
  section (SectionConfig):
643
670
  The section configuration.
644
671
 
645
672
  Returns:
646
- (Optional[str]):
647
- Error message if section is missing, None otherwise.
673
+ (bool):
674
+ `True` if section exists, `False` otherwise.
648
675
  """
649
676
 
650
- if not self._check_free_text_section(docstring, section):
651
- return f"Missing required section: {section.name}"
652
- return None
677
+ section_name: str = section.name.lower()
653
678
 
654
- def _validate_list_name_and_type_section(
679
+ # For free text sections, use the existing logic from _check_free_text_section
680
+ if section.type == "free_text":
681
+ return self._check_free_text_section(docstring, section)
682
+
683
+ # Check for admonition style sections (for non-free-text types)
684
+ if section.admonition and isinstance(section.admonition, str):
685
+ if section.prefix and isinstance(section.prefix, str):
686
+ # e.g., "!!! note" or "???+ abstract"
687
+ pattern: str = rf"{re.escape(section.prefix)}\s+{re.escape(section.admonition)}"
688
+ if re.search(pattern, docstring, re.IGNORECASE):
689
+ return True
690
+
691
+ # Check for standard sections with colons (e.g., "Params:", "Returns:")
692
+ pattern = rf"^[ \t]*{re.escape(section_name)}:[ \t]*$"
693
+ if re.search(pattern, docstring, re.IGNORECASE | re.MULTILINE):
694
+ return True
695
+
696
+ return False
697
+
698
+ def _validate_single_section_content(
655
699
  self, docstring: str, section: SectionConfig, item: FunctionAndClassDetails
656
700
  ) -> Optional[str]:
657
701
  """
658
702
  !!! note "Summary"
659
- Validate list_name_and_type sections (params, returns).
703
+ Validate the content of a single section based on its type.
660
704
 
661
705
  Params:
662
706
  docstring (str):
663
707
  The docstring to validate.
664
708
  section (SectionConfig):
665
- The section configuration.
709
+ The section configuration to validate against.
666
710
  item (FunctionAndClassDetails):
667
711
  The function or class details.
668
712
 
669
713
  Returns:
670
714
  (Optional[str]):
671
- Error message if section is invalid, None otherwise.
715
+ Error message if validation fails, None otherwise.
672
716
  """
673
717
 
674
- section_name: str = section.name.lower()
675
-
676
- if section_name == "params" and isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
677
- # Check params section exists and is properly formatted
678
- if not self._check_params_section(docstring, item.node):
679
- return "Missing or invalid Params section"
680
-
681
- # If validate_param_types is enabled, validate type annotations match
682
- if self.config.global_config.validate_param_types:
683
- type_error: Optional[str] = self._validate_param_types(docstring, item.node)
684
- if type_error:
685
- return type_error
718
+ if section.type == "list_name_and_type":
719
+ return self._validate_list_name_and_type_section(docstring, section, item)
686
720
 
687
- elif section_name in ["returns", "return"]:
688
- if not self._check_returns_section(docstring):
689
- return "Missing or invalid Returns section"
721
+ if section.type == "list_name":
722
+ return self._validate_list_name_section(docstring, section)
690
723
 
724
+ # For `section.type in ("free_text", "list_type")`
725
+ # these sections do not need content validation beyond existence
691
726
  return None
692
727
 
693
- def _validate_list_type_section(self, docstring: str, section: SectionConfig) -> Optional[str]:
728
+ def _validate_list_name_and_type_section(
729
+ self, docstring: str, section: SectionConfig, item: FunctionAndClassDetails
730
+ ) -> Optional[str]:
694
731
  """
695
732
  !!! note "Summary"
696
- Validate list_type sections (raises, yields).
733
+ Validate list_name_and_type sections (params, returns).
697
734
 
698
735
  Params:
699
736
  docstring (str):
700
737
  The docstring to validate.
701
738
  section (SectionConfig):
702
739
  The section configuration.
740
+ item (FunctionAndClassDetails):
741
+ The function or class details.
703
742
 
704
743
  Returns:
705
744
  (Optional[str]):
@@ -708,12 +747,19 @@ class DocstringChecker:
708
747
 
709
748
  section_name: str = section.name.lower()
710
749
 
711
- if section_name in ["raises", "raise"]:
712
- if not self._check_raises_section(docstring):
713
- return "Missing or invalid Raises section"
714
- elif section_name in ["yields", "yield"]:
715
- if not self._check_yields_section(docstring):
716
- return "Missing or invalid Yields section"
750
+ if section_name == "params" and isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
751
+ # Check params section exists and is properly formatted
752
+ if not self._check_params_section(docstring, item.node):
753
+ return "Missing or invalid Params section"
754
+
755
+ # If validate_param_types is enabled, validate type annotations match
756
+ if self.config.global_config.validate_param_types:
757
+ type_error: Optional[str] = self._validate_param_types(docstring, item.node)
758
+ if type_error:
759
+ return type_error
760
+
761
+ # For returns/return sections, no additional validation beyond existence
762
+ # The _section_exists check already verified the section is present
717
763
 
718
764
  return None
719
765
 
@@ -732,8 +778,8 @@ class DocstringChecker:
732
778
  (Optional[str]):
733
779
  Error message if section is missing, None otherwise.
734
780
  """
735
- if not self._check_simple_section(docstring, section.name):
736
- return f"Missing required section: {section.name}"
781
+ # No additional validation beyond existence
782
+ # The _section_exists check already verified the section is present
737
783
  return None
738
784
 
739
785
  def _perform_comprehensive_validation(self, docstring: str) -> list[str]:
@@ -943,7 +989,8 @@ class DocstringChecker:
943
989
  continue
944
990
 
945
991
  # Check if we've left the Params section (next section starts)
946
- if in_params_section and re.match(r"^\s*[A-Z]\w+:", line):
992
+ # Section headers have minimal indentation (0-4 spaces), not deep indentation like param descriptions
993
+ if in_params_section and re.match(r"^[ ]{0,4}[A-Z]\w+:", line):
947
994
  break
948
995
 
949
996
  # Extract parameter name and type
@@ -1053,45 +1100,22 @@ class DocstringChecker:
1053
1100
  mismatches: list[tuple[str, str, str]] = self._compare_param_types(signature_types, docstring_types)
1054
1101
 
1055
1102
  if mismatches:
1056
- mismatch_details: list[str] = [
1057
- f"'{name}': signature has '{sig_type}', docstring has '{doc_type}'"
1058
- for name, sig_type, doc_type in mismatches
1059
- ]
1060
- return f"Parameter type mismatch: {'; '.join(mismatch_details)}"
1061
-
1062
- return None
1063
-
1064
- def _check_returns_section(self, docstring: str) -> bool:
1065
- """
1066
- !!! note "Summary"
1067
- Check if the Returns section exists.
1103
+ # Format each mismatch with parameter name on one line, signature and docstring indented below
1104
+ # Use 2 spaces for params, 4 for sig/doc (suitable for table output)
1105
+ # List output will add additional indentation via CLI formatting
1106
+ mismatch_blocks: list[str] = []
1107
+ for name, sig_type, doc_type in mismatches:
1108
+ sig_type: str = sig_type.replace("'", '"')
1109
+ doc_type: str = doc_type.replace("'", '"')
1110
+ param_block: str = f"""'{name}':\n - signature: '{sig_type}'\n - docstring: '{doc_type}' """
1111
+ mismatch_blocks.append(param_block)
1068
1112
 
1069
- Params:
1070
- docstring (str):
1071
- The docstring to check.
1113
+ # Join all parameter blocks with proper indentation
1114
+ formatted_details: str = "\n - ".join([""] + mismatch_blocks)
1072
1115
 
1073
- Returns:
1074
- (bool):
1075
- `True` if the section exists, `False` otherwise.
1076
- """
1077
-
1078
- return bool(re.search(r"Returns:", docstring))
1079
-
1080
- def _check_raises_section(self, docstring: str) -> bool:
1081
- """
1082
- !!! note "Summary"
1083
- Check if the Raises section exists.
1084
-
1085
- Params:
1086
- docstring (str):
1087
- The docstring to check.
1116
+ return f"Parameter type mismatch:{formatted_details}"
1088
1117
 
1089
- Returns:
1090
- (bool):
1091
- `True` if the section exists, `False` otherwise.
1092
- """
1093
-
1094
- return bool(re.search(r"Raises:", docstring))
1118
+ return None
1095
1119
 
1096
1120
  def _has_both_returns_and_yields(self, docstring: str) -> bool:
1097
1121
  """
@@ -1237,41 +1261,6 @@ class DocstringChecker:
1237
1261
 
1238
1262
  return errors
1239
1263
 
1240
- def _check_yields_section(self, docstring: str) -> bool:
1241
- """
1242
- !!! note "Summary"
1243
- Check if the Yields section exists.
1244
-
1245
- Params:
1246
- docstring (str):
1247
- The docstring to check.
1248
-
1249
- Returns:
1250
- (bool):
1251
- `True` if the section exists, `False` otherwise.
1252
- """
1253
-
1254
- return bool(re.search(r"Yields:", docstring))
1255
-
1256
- def _check_simple_section(self, docstring: str, section_name: str) -> bool:
1257
- """
1258
- !!! note "Summary"
1259
- Check if a simple named section exists.
1260
-
1261
- Params:
1262
- docstring (str):
1263
- The docstring to check.
1264
- section_name (str):
1265
- The name of the section to check for.
1266
-
1267
- Returns:
1268
- (bool):
1269
- `True` if the section exists, `False` otherwise.
1270
- """
1271
-
1272
- pattern: str = rf"{re.escape(section_name)}:"
1273
- return bool(re.search(pattern, docstring, re.IGNORECASE))
1274
-
1275
1264
  def _normalize_section_name(self, section_name: str) -> str:
1276
1265
  """
1277
1266
  !!! note "Summary"