docstring-format-checker 1.6.2__tar.gz → 1.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.
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/PKG-INFO +1 -1
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/pyproject.toml +5 -2
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/src/docstring_format_checker/cli.py +7 -2
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/src/docstring_format_checker/config.py +10 -0
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/src/docstring_format_checker/core.py +153 -15
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/README.md +0 -0
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/src/docstring_format_checker/__init__.py +0 -0
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.0}/src/docstring_format_checker/utils/__init__.py +0 -0
- {docstring_format_checker-1.6.2 → docstring_format_checker-1.7.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.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 = "1.
|
|
3
|
+
version = "1.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"
|
|
@@ -75,7 +75,8 @@ docs = [
|
|
|
75
75
|
"pygithub==2.*",
|
|
76
76
|
]
|
|
77
77
|
test = [
|
|
78
|
-
"
|
|
78
|
+
"ty==0.*",
|
|
79
|
+
# "mypy==1.*,!=1.17.*",
|
|
79
80
|
"parameterized==0.*",
|
|
80
81
|
"pytest==8.*",
|
|
81
82
|
"pytest-clarity==1.*",
|
|
@@ -154,6 +155,7 @@ disable = [
|
|
|
154
155
|
"C0103", # invalid-name
|
|
155
156
|
"C0301", # line-too-long
|
|
156
157
|
"C0302", # too-many-lines
|
|
158
|
+
"R0912", # too-many-branches
|
|
157
159
|
"R0913", # too-many-arguments
|
|
158
160
|
"R0914", # too-many-locals
|
|
159
161
|
"R0915", # too-many-statements
|
|
@@ -188,6 +190,7 @@ allow_undefined_sections = false
|
|
|
188
190
|
require_docstrings = true
|
|
189
191
|
check_private = true
|
|
190
192
|
validate_param_types = true
|
|
193
|
+
optional_style = "silent" #<-- optional: "silent", "validate", or "strict"
|
|
191
194
|
sections = [
|
|
192
195
|
{ order=1, name="summary", type="free_text", required=true, admonition="note", prefix="!!!" },
|
|
193
196
|
{ order=2, name="details", type="free_text", required=false, admonition="abstract", prefix="???+" },
|
|
@@ -52,6 +52,7 @@ from typing import Optional
|
|
|
52
52
|
# ## Python Third Party Imports ----
|
|
53
53
|
import pyfiglet
|
|
54
54
|
from rich.console import Console
|
|
55
|
+
from rich.markup import escape
|
|
55
56
|
from rich.panel import Panel
|
|
56
57
|
from rich.table import Table
|
|
57
58
|
from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo
|
|
@@ -270,6 +271,7 @@ def _show_config_example_callback() -> None:
|
|
|
270
271
|
[blue]require_docstrings = true[/blue]
|
|
271
272
|
[blue]check_private = true[/blue]
|
|
272
273
|
[blue]validate_param_types = true[/blue]
|
|
274
|
+
[blue]optional_style = "validate"[/blue] [green]# "silent", "validate", or "strict"[/green]
|
|
273
275
|
[blue]sections = [[/blue]
|
|
274
276
|
[blue]{ order = 1, name = "summary", type = "free_text", required = true, admonition = "note", prefix = "!!!" },[/blue]
|
|
275
277
|
[blue]{ order = 2, name = "details", type = "free_text", required = false, admonition = "abstract", prefix = "???+" },[/blue]
|
|
@@ -485,7 +487,7 @@ def _display_table_output(results: dict[str, list[DocstringError]]) -> None:
|
|
|
485
487
|
str(error.line_number) if error.line_number > 0 else "",
|
|
486
488
|
error.item_name,
|
|
487
489
|
error.item_type,
|
|
488
|
-
formatted_error_message,
|
|
490
|
+
f"[red]{formatted_error_message}[/red]",
|
|
489
491
|
)
|
|
490
492
|
console.print(table)
|
|
491
493
|
|
|
@@ -545,10 +547,13 @@ def _format_error_output(error: DocstringError) -> list[str]:
|
|
|
545
547
|
individual_errors: list[str] = _split_error_messages(error.message)
|
|
546
548
|
|
|
547
549
|
for individual_error in individual_errors:
|
|
550
|
+
# Escape square brackets for Rich markup using Rich's escape function
|
|
551
|
+
individual_error: str = escape(individual_error)
|
|
552
|
+
|
|
548
553
|
# Check if this error has multi-line content (e.g., parameter type mismatches)
|
|
549
554
|
if "\n" in individual_error:
|
|
550
555
|
# Split by newlines and add 4 spaces of extra indentation to each line
|
|
551
|
-
error_lines = individual_error.split("\n")
|
|
556
|
+
error_lines: list[str] = individual_error.split("\n")
|
|
552
557
|
lines.append(f" - {error_lines[0]}") # First line gets the bullet
|
|
553
558
|
for sub_line in error_lines[1:]:
|
|
554
559
|
if sub_line.strip(): # Only add non-empty lines
|
|
@@ -115,6 +115,7 @@ class GlobalConfig:
|
|
|
115
115
|
require_docstrings: bool = True
|
|
116
116
|
check_private: bool = False
|
|
117
117
|
validate_param_types: bool = True
|
|
118
|
+
optional_style: Literal["silent", "validate", "strict"] = "validate"
|
|
118
119
|
|
|
119
120
|
|
|
120
121
|
## --------------------------------------------------------------------------- #
|
|
@@ -447,11 +448,20 @@ def _parse_global_config(tool_config: dict[str, Any]) -> GlobalConfig:
|
|
|
447
448
|
(GlobalConfig):
|
|
448
449
|
Parsed global configuration object.
|
|
449
450
|
"""
|
|
451
|
+
# Validate optional_style if provided
|
|
452
|
+
optional_style: str = tool_config.get("optional_style", "validate")
|
|
453
|
+
valid_styles: tuple[str, str, str] = ("silent", "validate", "strict")
|
|
454
|
+
if optional_style not in valid_styles:
|
|
455
|
+
raise InvalidConfigError(
|
|
456
|
+
f"Invalid optional_style: '{optional_style}'. Must be one of: {', '.join(valid_styles)}"
|
|
457
|
+
)
|
|
458
|
+
|
|
450
459
|
return GlobalConfig(
|
|
451
460
|
allow_undefined_sections=tool_config.get("allow_undefined_sections", False),
|
|
452
461
|
require_docstrings=tool_config.get("require_docstrings", True),
|
|
453
462
|
check_private=tool_config.get("check_private", False),
|
|
454
463
|
validate_param_types=tool_config.get("validate_param_types", True),
|
|
464
|
+
optional_style=optional_style, # type:ignore
|
|
455
465
|
)
|
|
456
466
|
|
|
457
467
|
|
|
@@ -1124,9 +1124,14 @@ class DocstringChecker:
|
|
|
1124
1124
|
(str):
|
|
1125
1125
|
Normalized type string.
|
|
1126
1126
|
"""
|
|
1127
|
+
|
|
1127
1128
|
# Remove whitespace
|
|
1128
1129
|
normalized: str = re.sub(r"\s+", "", type_str)
|
|
1129
1130
|
|
|
1131
|
+
# Normalize quotes: ast.unparse() uses single quotes but docstrings typically use double quotes
|
|
1132
|
+
# Convert all quotes to single quotes for consistent comparison
|
|
1133
|
+
normalized = normalized.replace('"', "'")
|
|
1134
|
+
|
|
1130
1135
|
# Make case-insensitive for basic types
|
|
1131
1136
|
# But preserve case for complex types to avoid breaking things like Optional
|
|
1132
1137
|
return normalized
|
|
@@ -1168,6 +1173,123 @@ class DocstringChecker:
|
|
|
1168
1173
|
|
|
1169
1174
|
return mismatches
|
|
1170
1175
|
|
|
1176
|
+
def _get_params_with_defaults(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> set[str]:
|
|
1177
|
+
"""
|
|
1178
|
+
!!! note "Summary"
|
|
1179
|
+
Get set of parameter names that have default values.
|
|
1180
|
+
|
|
1181
|
+
Params:
|
|
1182
|
+
node (Union[ast.FunctionDef, ast.AsyncFunctionDef]):
|
|
1183
|
+
The function node to analyse.
|
|
1184
|
+
|
|
1185
|
+
Returns:
|
|
1186
|
+
(set[str]):
|
|
1187
|
+
Set of parameter names that have default values.
|
|
1188
|
+
"""
|
|
1189
|
+
params_with_defaults: set[str] = set()
|
|
1190
|
+
args = node.args
|
|
1191
|
+
|
|
1192
|
+
# Regular args with defaults
|
|
1193
|
+
num_defaults = len(args.defaults)
|
|
1194
|
+
if num_defaults > 0:
|
|
1195
|
+
# Defaults apply to the last n arguments
|
|
1196
|
+
num_args = len(args.args)
|
|
1197
|
+
for i in range(num_args - num_defaults, num_args):
|
|
1198
|
+
if args.args[i].arg not in ("self", "cls"):
|
|
1199
|
+
params_with_defaults.add(args.args[i].arg)
|
|
1200
|
+
|
|
1201
|
+
# Keyword-only args with defaults
|
|
1202
|
+
for i, arg in enumerate(args.kwonlyargs):
|
|
1203
|
+
if args.kw_defaults[i] is not None:
|
|
1204
|
+
params_with_defaults.add(arg.arg)
|
|
1205
|
+
|
|
1206
|
+
return params_with_defaults
|
|
1207
|
+
|
|
1208
|
+
def _process_optional_suffix(
|
|
1209
|
+
self,
|
|
1210
|
+
param_name: str,
|
|
1211
|
+
doc_type: str,
|
|
1212
|
+
params_with_defaults: set[str],
|
|
1213
|
+
optional_style: str,
|
|
1214
|
+
) -> tuple[str, Optional[str]]:
|
|
1215
|
+
"""
|
|
1216
|
+
!!! note "Summary"
|
|
1217
|
+
Process the ', optional' suffix based on the optional_style mode.
|
|
1218
|
+
|
|
1219
|
+
Params:
|
|
1220
|
+
param_name (str):
|
|
1221
|
+
Name of the parameter.
|
|
1222
|
+
doc_type (str):
|
|
1223
|
+
Docstring type including potential ', optional' suffix.
|
|
1224
|
+
params_with_defaults (set[str]):
|
|
1225
|
+
Set of parameters that have default values.
|
|
1226
|
+
optional_style (str):
|
|
1227
|
+
The validation mode: 'silent', 'validate', or 'strict'.
|
|
1228
|
+
|
|
1229
|
+
Returns:
|
|
1230
|
+
(tuple[str, Optional[str]]):
|
|
1231
|
+
Tuple of (cleaned_type, error_message).
|
|
1232
|
+
"""
|
|
1233
|
+
has_optional_suffix: bool = bool(re.search(r",\s*optional$", doc_type, flags=re.IGNORECASE))
|
|
1234
|
+
clean_type: str = re.sub(r",\s*optional$", "", doc_type, flags=re.IGNORECASE).strip()
|
|
1235
|
+
error_message: Optional[str] = None
|
|
1236
|
+
|
|
1237
|
+
if optional_style == "validate":
|
|
1238
|
+
if has_optional_suffix and param_name not in params_with_defaults:
|
|
1239
|
+
error_message = f"Parameter '{param_name}' has ', optional' suffix but no default value in signature"
|
|
1240
|
+
elif optional_style == "strict":
|
|
1241
|
+
if param_name in params_with_defaults and not has_optional_suffix:
|
|
1242
|
+
error_message = (
|
|
1243
|
+
f"Parameter '{param_name}' has default value but missing ', optional' suffix in docstring"
|
|
1244
|
+
)
|
|
1245
|
+
elif has_optional_suffix and param_name not in params_with_defaults:
|
|
1246
|
+
error_message = f"Parameter '{param_name}' has ', optional' suffix but no default value in signature"
|
|
1247
|
+
|
|
1248
|
+
return clean_type, error_message
|
|
1249
|
+
|
|
1250
|
+
def _format_optional_errors(self, errors: list[str]) -> str:
|
|
1251
|
+
"""
|
|
1252
|
+
!!! note "Summary"
|
|
1253
|
+
Format multiple optional suffix validation errors.
|
|
1254
|
+
|
|
1255
|
+
Params:
|
|
1256
|
+
errors (list[str]):
|
|
1257
|
+
List of error messages.
|
|
1258
|
+
|
|
1259
|
+
Returns:
|
|
1260
|
+
(str):
|
|
1261
|
+
Formatted error message.
|
|
1262
|
+
"""
|
|
1263
|
+
if len(errors) == 1:
|
|
1264
|
+
return errors[0]
|
|
1265
|
+
formatted_errors: str = "\n - ".join([""] + errors)
|
|
1266
|
+
return f"Optional suffix validation errors:{formatted_errors}"
|
|
1267
|
+
|
|
1268
|
+
def _format_type_mismatches(self, mismatches: list[tuple[str, str, str]]) -> str:
|
|
1269
|
+
"""
|
|
1270
|
+
!!! note "Summary"
|
|
1271
|
+
Format parameter type mismatches for error output.
|
|
1272
|
+
|
|
1273
|
+
Params:
|
|
1274
|
+
mismatches (list[tuple[str, str, str]]):
|
|
1275
|
+
List of (param_name, sig_type, doc_type) tuples.
|
|
1276
|
+
|
|
1277
|
+
Returns:
|
|
1278
|
+
(str):
|
|
1279
|
+
Formatted error message.
|
|
1280
|
+
"""
|
|
1281
|
+
mismatch_blocks: list[str] = []
|
|
1282
|
+
for name, sig_type, doc_type in mismatches:
|
|
1283
|
+
sig_type_clean: str = sig_type.replace("'", '"')
|
|
1284
|
+
doc_type_clean: str = doc_type.replace("'", '"')
|
|
1285
|
+
param_block: str = (
|
|
1286
|
+
f"""'{name}':\n - signature: '{sig_type_clean}'\n - docstring: '{doc_type_clean}'"""
|
|
1287
|
+
)
|
|
1288
|
+
mismatch_blocks.append(param_block)
|
|
1289
|
+
|
|
1290
|
+
formatted_details: str = "\n - ".join([""] + mismatch_blocks)
|
|
1291
|
+
return f"Parameter type mismatch:{formatted_details}"
|
|
1292
|
+
|
|
1171
1293
|
def _validate_param_types(
|
|
1172
1294
|
self, docstring: str, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
|
|
1173
1295
|
) -> Optional[str]:
|
|
@@ -1175,6 +1297,13 @@ class DocstringChecker:
|
|
|
1175
1297
|
!!! note "Summary"
|
|
1176
1298
|
Validate that parameter types in docstring match the signature.
|
|
1177
1299
|
|
|
1300
|
+
???+ abstract "Details"
|
|
1301
|
+
Implements three validation modes based on `optional_style` configuration:
|
|
1302
|
+
|
|
1303
|
+
- **`"silent"`**: Strip `, optional` from docstring types before comparison.
|
|
1304
|
+
- **`"validate"`**: Error if `, optional` appears on required parameters.
|
|
1305
|
+
- **`"strict"`**: Require `, optional` for parameters with defaults, error if on required parameters.
|
|
1306
|
+
|
|
1178
1307
|
Params:
|
|
1179
1308
|
docstring (str):
|
|
1180
1309
|
The docstring to validate.
|
|
@@ -1187,11 +1316,33 @@ class DocstringChecker:
|
|
|
1187
1316
|
"""
|
|
1188
1317
|
# Extract types from both sources
|
|
1189
1318
|
signature_types: dict[str, str] = self._extract_param_types(node)
|
|
1190
|
-
|
|
1319
|
+
docstring_types_raw: dict[str, str] = self._extract_param_types_from_docstring(docstring)
|
|
1320
|
+
|
|
1321
|
+
# Get parameters with default values
|
|
1322
|
+
params_with_defaults: set[str] = self._get_params_with_defaults(node)
|
|
1191
1323
|
|
|
1192
1324
|
# Get all parameter names (excluding self/cls)
|
|
1193
1325
|
all_params: list[str] = [arg.arg for arg in node.args.args if arg.arg not in ("self", "cls")]
|
|
1194
1326
|
|
|
1327
|
+
# Get the optional_style mode
|
|
1328
|
+
optional_style: str = self.config.global_config.optional_style
|
|
1329
|
+
|
|
1330
|
+
# Process docstring types based on optional_style mode
|
|
1331
|
+
docstring_types: dict[str, str] = {}
|
|
1332
|
+
optional_errors: list[str] = []
|
|
1333
|
+
|
|
1334
|
+
for param_name, doc_type in docstring_types_raw.items():
|
|
1335
|
+
clean_type, error_message = self._process_optional_suffix(
|
|
1336
|
+
param_name, doc_type, params_with_defaults, optional_style
|
|
1337
|
+
)
|
|
1338
|
+
docstring_types[param_name] = clean_type
|
|
1339
|
+
if error_message:
|
|
1340
|
+
optional_errors.append(error_message)
|
|
1341
|
+
|
|
1342
|
+
# Return optional_style errors first if any
|
|
1343
|
+
if optional_errors:
|
|
1344
|
+
return self._format_optional_errors(optional_errors)
|
|
1345
|
+
|
|
1195
1346
|
# Check for parameters documented with type in docstring but missing annotation in signature
|
|
1196
1347
|
for param_name in all_params:
|
|
1197
1348
|
if param_name in docstring_types and param_name not in signature_types:
|
|
@@ -1208,20 +1359,7 @@ class DocstringChecker:
|
|
|
1208
1359
|
mismatches: list[tuple[str, str, str]] = self._compare_param_types(signature_types, docstring_types)
|
|
1209
1360
|
|
|
1210
1361
|
if mismatches:
|
|
1211
|
-
|
|
1212
|
-
# Use 2 spaces for params, 4 for sig/doc (suitable for table output)
|
|
1213
|
-
# List output will add additional indentation via CLI formatting
|
|
1214
|
-
mismatch_blocks: list[str] = []
|
|
1215
|
-
for name, sig_type, doc_type in mismatches:
|
|
1216
|
-
sig_type: str = sig_type.replace("'", '"')
|
|
1217
|
-
doc_type: str = doc_type.replace("'", '"')
|
|
1218
|
-
param_block: str = f"""'{name}':\n - signature: '{sig_type}'\n - docstring: '{doc_type}' """
|
|
1219
|
-
mismatch_blocks.append(param_block)
|
|
1220
|
-
|
|
1221
|
-
# Join all parameter blocks with proper indentation
|
|
1222
|
-
formatted_details: str = "\n - ".join([""] + mismatch_blocks)
|
|
1223
|
-
|
|
1224
|
-
return f"Parameter type mismatch:{formatted_details}"
|
|
1362
|
+
return self._format_type_mismatches(mismatches)
|
|
1225
1363
|
|
|
1226
1364
|
return None
|
|
1227
1365
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|