docstring-format-checker 1.5.0__tar.gz → 1.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: 1.5.0
3
+ Version: 1.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 = "1.5.0"
3
+ version = "1.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"
@@ -170,9 +170,10 @@ disable = [
170
170
 
171
171
  [tool.complexipy]
172
172
  paths = "src/docstring_format_checker"
173
- max-complexity-allowed = 15
173
+ max-complexity-allowed = 13
174
174
  quiet = false
175
175
  ignore-complexity = false
176
+ details = "low"
176
177
  sort = "asc"
177
178
 
178
179
  [tool.bump_version.replacements]
@@ -186,6 +187,7 @@ files = [
186
187
  allow_undefined_sections = false
187
188
  require_docstrings = true
188
189
  check_private = true
190
+ validate_param_types = true
189
191
  sections = [
190
192
  { order=1, name="summary", type="free_text", required=true, admonition="note", prefix="!!!" },
191
193
  { order=2, name="details", type="free_text", required=false, admonition="abstract", prefix="???+" },
@@ -269,6 +269,7 @@ def _show_config_example_callback() -> None:
269
269
  [blue]allow_undefined_sections = false[/blue]
270
270
  [blue]require_docstrings = true[/blue]
271
271
  [blue]check_private = true[/blue]
272
+ [blue]validate_param_types = true[/blue]
272
273
  [blue]sections = [[/blue]
273
274
  [blue]{ order = 1, name = "summary", type = "free_text", required = true, admonition = "note", prefix = "!!!" },[/blue]
274
275
  [blue]{ order = 2, name = "details", type = "free_text", required = false, admonition = "abstract", prefix = "???+" },[/blue]
@@ -114,6 +114,7 @@ class GlobalConfig:
114
114
  allow_undefined_sections: bool = False
115
115
  require_docstrings: bool = True
116
116
  check_private: bool = False
117
+ validate_param_types: bool = True
117
118
 
118
119
 
119
120
  ## --------------------------------------------------------------------------- #
@@ -450,6 +451,7 @@ def _parse_global_config(tool_config: dict[str, Any]) -> GlobalConfig:
450
451
  allow_undefined_sections=tool_config.get("allow_undefined_sections", False),
451
452
  require_docstrings=tool_config.get("require_docstrings", True),
452
453
  check_private=tool_config.get("check_private", False),
454
+ validate_param_types=tool_config.get("validate_param_types", True),
453
455
  )
454
456
 
455
457
 
@@ -674,8 +674,16 @@ class DocstringChecker:
674
674
  section_name: str = section.name.lower()
675
675
 
676
676
  if section_name == "params" and isinstance(item.node, (ast.FunctionDef, ast.AsyncFunctionDef)):
677
+ # Check params section exists and is properly formatted
677
678
  if not self._check_params_section(docstring, item.node):
678
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
686
+
679
687
  elif section_name in ["returns", "return"]:
680
688
  if not self._check_returns_section(docstring):
681
689
  return "Missing or invalid Returns section"
@@ -871,6 +879,188 @@ class DocstringChecker:
871
879
 
872
880
  return True
873
881
 
882
+ def _extract_param_types(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> dict[str, str]:
883
+ """
884
+ !!! note "Summary"
885
+ Extract parameter names and their type annotations from function signature.
886
+
887
+ Params:
888
+ node (Union[ast.FunctionDef, ast.AsyncFunctionDef]):
889
+ The function AST node.
890
+
891
+ Returns:
892
+ (dict[str, str]):
893
+ Dictionary mapping parameter names to their type annotation strings.
894
+ """
895
+ param_types: dict[str, str] = {}
896
+
897
+ for arg in node.args.args:
898
+ # Skip 'self' and 'cls' parameters
899
+ if arg.arg in ("self", "cls"):
900
+ continue
901
+
902
+ # Extract type annotation if present
903
+ if arg.annotation:
904
+ type_str: str = ast.unparse(arg.annotation)
905
+ param_types[arg.arg] = type_str
906
+
907
+ return param_types
908
+
909
+ def _extract_param_types_from_docstring(self, docstring: str) -> dict[str, str]:
910
+ """
911
+ !!! note "Summary"
912
+ Extract parameter types from the Params section of docstring.
913
+
914
+ Params:
915
+ docstring (str):
916
+ The docstring to parse.
917
+
918
+ Returns:
919
+ (dict[str, str]):
920
+ Dictionary mapping parameter names to their documented types.
921
+ """
922
+ param_types: dict[str, str] = {}
923
+
924
+ # Find the Params section
925
+ if not re.search(r"Params:", docstring):
926
+ return param_types
927
+
928
+ # Pattern to match parameter documentation: name (type):
929
+ # Handles variations like:
930
+ # - name (str):
931
+ # - name (Optional[str]):
932
+ # - name (Union[str, int]):
933
+ # - name (list[str]):
934
+ pattern: str = r"^\s*(\w+)\s*\(([^)]+)\)\s*:"
935
+
936
+ lines: list[str] = docstring.split("\n")
937
+ in_params_section: bool = False
938
+
939
+ for line in lines:
940
+ # Check if we've entered the Params section
941
+ if "Params:" in line:
942
+ in_params_section = True
943
+ continue
944
+
945
+ # 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):
947
+ break
948
+
949
+ # Extract parameter name and type
950
+ if in_params_section:
951
+ match = re.match(pattern, line)
952
+ if match:
953
+ param_name: str = match.group(1)
954
+ param_type: str = match.group(2)
955
+ param_types[param_name] = param_type
956
+
957
+ return param_types
958
+
959
+ def _normalize_type_string(self, type_str: str) -> str:
960
+ """
961
+ !!! note "Summary"
962
+ Normalize a type string for comparison.
963
+
964
+ Params:
965
+ type_str (str):
966
+ The type string to normalize.
967
+
968
+ Returns:
969
+ (str):
970
+ Normalized type string.
971
+ """
972
+ # Remove whitespace
973
+ normalized: str = re.sub(r"\s+", "", type_str)
974
+
975
+ # Make case-insensitive for basic types
976
+ # But preserve case for complex types to avoid breaking things like Optional
977
+ return normalized
978
+
979
+ def _compare_param_types(
980
+ self, signature_types: dict[str, str], docstring_types: dict[str, str]
981
+ ) -> list[tuple[str, str, str]]:
982
+ """
983
+ !!! note "Summary"
984
+ Compare parameter types from signature and docstring.
985
+
986
+ Params:
987
+ signature_types (dict[str, str]):
988
+ Parameter types from function signature.
989
+ docstring_types (dict[str, str]):
990
+ Parameter types from docstring.
991
+
992
+ Returns:
993
+ (list[tuple[str, str, str]]):
994
+ List of mismatches as (param_name, signature_type, docstring_type).
995
+ """
996
+ mismatches: list[tuple[str, str, str]] = []
997
+
998
+ for param_name, sig_type in signature_types.items():
999
+ # Check if parameter is documented in docstring
1000
+ if param_name not in docstring_types:
1001
+ # Parameter not documented - this is handled by other validation
1002
+ continue
1003
+
1004
+ doc_type: str = docstring_types[param_name]
1005
+
1006
+ # Normalize both types for comparison
1007
+ normalized_sig: str = self._normalize_type_string(sig_type)
1008
+ normalized_doc: str = self._normalize_type_string(doc_type)
1009
+
1010
+ # Case-insensitive comparison
1011
+ if normalized_sig.lower() != normalized_doc.lower():
1012
+ mismatches.append((param_name, sig_type, doc_type))
1013
+
1014
+ return mismatches
1015
+
1016
+ def _validate_param_types(
1017
+ self, docstring: str, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
1018
+ ) -> Optional[str]:
1019
+ """
1020
+ !!! note "Summary"
1021
+ Validate that parameter types in docstring match the signature.
1022
+
1023
+ Params:
1024
+ docstring (str):
1025
+ The docstring to validate.
1026
+ node (Union[ast.FunctionDef, ast.AsyncFunctionDef]):
1027
+ The function node with type annotations.
1028
+
1029
+ Returns:
1030
+ (Optional[str]):
1031
+ Error message if validation fails, None otherwise.
1032
+ """
1033
+ # Extract types from both sources
1034
+ signature_types: dict[str, str] = self._extract_param_types(node)
1035
+ docstring_types: dict[str, str] = self._extract_param_types_from_docstring(docstring)
1036
+
1037
+ # Get all parameter names (excluding self/cls)
1038
+ all_params: list[str] = [arg.arg for arg in node.args.args if arg.arg not in ("self", "cls")]
1039
+
1040
+ # Check for parameters documented with type in docstring but missing annotation in signature
1041
+ for param_name in all_params:
1042
+ if param_name in docstring_types and param_name not in signature_types:
1043
+ return f"Parameter '{param_name}' has type in docstring but no type annotation in signature"
1044
+
1045
+ # Check for parameters with annotations but no type in docstring
1046
+ for param_name, sig_type in signature_types.items():
1047
+ if param_name not in docstring_types:
1048
+ return (
1049
+ f"Parameter '{param_name}' has type annotation '{sig_type}' in signature but no type in docstring"
1050
+ )
1051
+
1052
+ # Compare types
1053
+ mismatches: list[tuple[str, str, str]] = self._compare_param_types(signature_types, docstring_types)
1054
+
1055
+ 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
+
874
1064
  def _check_returns_section(self, docstring: str) -> bool:
875
1065
  """
876
1066
  !!! note "Summary"