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.
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/PKG-INFO +1 -1
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/pyproject.toml +4 -2
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/cli.py +1 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/config.py +2 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/core.py +190 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/README.md +0 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/__init__.py +0 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/utils/__init__.py +0 -0
- {docstring_format_checker-1.5.0 → docstring_format_checker-1.6.0}/src/docstring_format_checker/utils/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docstring-format-checker
|
|
3
|
-
Version: 1.
|
|
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.
|
|
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 =
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|