docstring-format-checker 0.4.0__tar.gz → 0.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-0.4.0 → docstring_format_checker-0.6.0}/PKG-INFO +1 -1
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/pyproject.toml +1 -1
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/src/docstring_format_checker/__init__.py +1 -1
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/src/docstring_format_checker/cli.py +270 -319
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/src/docstring_format_checker/core.py +34 -11
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/README.md +0 -0
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/src/docstring_format_checker/config.py +0 -0
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.6.0}/src/docstring_format_checker/utils/__init__.py +0 -0
- {docstring_format_checker-0.4.0 → docstring_format_checker-0.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: 0.
|
|
3
|
+
Version: 0.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>
|
|
@@ -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.
|
|
7
|
+
__version__ = "v0.6.0"
|
|
8
8
|
__author__ = "Chris Mahoney"
|
|
9
9
|
__email__ = "docstring-format-checker@data-science-extensions.com"
|
|
10
10
|
|
|
@@ -43,25 +43,16 @@
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
# ## Python StdLib Imports ----
|
|
46
|
+
from functools import partial
|
|
46
47
|
from pathlib import Path
|
|
47
48
|
from textwrap import dedent
|
|
48
|
-
from typing import Optional
|
|
49
|
+
from typing import Optional
|
|
49
50
|
|
|
50
51
|
# ## Python Third Party Imports ----
|
|
51
52
|
from rich.console import Console
|
|
52
53
|
from rich.panel import Panel
|
|
53
54
|
from rich.table import Table
|
|
54
|
-
from
|
|
55
|
-
from typer import (
|
|
56
|
-
Argument,
|
|
57
|
-
BadParameter,
|
|
58
|
-
CallbackParam,
|
|
59
|
-
Context,
|
|
60
|
-
Exit,
|
|
61
|
-
Option,
|
|
62
|
-
Typer,
|
|
63
|
-
echo,
|
|
64
|
-
)
|
|
55
|
+
from typer import Argument, CallbackParam, Context, Exit, Option, Typer, echo
|
|
65
56
|
|
|
66
57
|
# ## Local First Party Imports ----
|
|
67
58
|
from docstring_format_checker import __version__
|
|
@@ -76,8 +67,6 @@ from docstring_format_checker.core import DocstringChecker, DocstringError
|
|
|
76
67
|
|
|
77
68
|
__all__: list[str] = [
|
|
78
69
|
"main",
|
|
79
|
-
"config_example",
|
|
80
|
-
"check",
|
|
81
70
|
"entry_point",
|
|
82
71
|
]
|
|
83
72
|
|
|
@@ -90,6 +79,22 @@ __all__: list[str] = [
|
|
|
90
79
|
NEW_LINE = "\n"
|
|
91
80
|
|
|
92
81
|
|
|
82
|
+
## --------------------------------------------------------------------------- #
|
|
83
|
+
## Helpers ####
|
|
84
|
+
## --------------------------------------------------------------------------- #
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
### Colours ----
|
|
88
|
+
def _colour(text: str, colour: str) -> str:
|
|
89
|
+
return f"[{colour}]{text}[/{colour}]"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_green = partial(_colour, colour="green")
|
|
93
|
+
_red = partial(_colour, colour="red")
|
|
94
|
+
_cyan = partial(_colour, colour="cyan")
|
|
95
|
+
_blue = partial(_colour, colour="blue")
|
|
96
|
+
|
|
97
|
+
|
|
93
98
|
# ---------------------------------------------------------------------------- #
|
|
94
99
|
# #
|
|
95
100
|
# Main Application ####
|
|
@@ -136,7 +141,7 @@ def _version_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
|
|
|
136
141
|
raise Exit()
|
|
137
142
|
|
|
138
143
|
|
|
139
|
-
def
|
|
144
|
+
def _help_callback_main(ctx: Context, param: CallbackParam, value: bool) -> None:
|
|
140
145
|
"""
|
|
141
146
|
!!! note "Summary"
|
|
142
147
|
Show help and exit.
|
|
@@ -159,10 +164,10 @@ def _help_callback(ctx: Context, param: CallbackParam, value: bool) -> None:
|
|
|
159
164
|
raise Exit()
|
|
160
165
|
|
|
161
166
|
|
|
162
|
-
def
|
|
167
|
+
def _example_callback(ctx: Context, param: CallbackParam, value: Optional[str]) -> None:
|
|
163
168
|
"""
|
|
164
169
|
!!! note "Summary"
|
|
165
|
-
|
|
170
|
+
Handle example flag and show appropriate example content.
|
|
166
171
|
|
|
167
172
|
Params:
|
|
168
173
|
ctx (Context):
|
|
@@ -170,51 +175,26 @@ def _parse_boolean_flag(ctx: Context, param: CallbackParam, value: Optional[str]
|
|
|
170
175
|
param (CallbackParam):
|
|
171
176
|
The parameter object.
|
|
172
177
|
value (Optional[str]):
|
|
173
|
-
The
|
|
178
|
+
The example type to show: 'config' or 'usage'.
|
|
174
179
|
|
|
175
180
|
Returns:
|
|
176
|
-
(
|
|
177
|
-
|
|
178
|
-
"""
|
|
179
|
-
|
|
180
|
-
# Handle the case where the flag is provided without a value (e.g., just --recursive or -r)
|
|
181
|
-
# In this case, Typer doesn't call the callback, so we need to handle it differently
|
|
182
|
-
if value is None:
|
|
183
|
-
# This means the flag wasn't provided at all, use default
|
|
184
|
-
return True
|
|
185
|
-
|
|
186
|
-
# If value is an empty string, it means the flag was provided without a value
|
|
187
|
-
if value == "":
|
|
188
|
-
return True
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
return strtobool(value)
|
|
192
|
-
except ValueError as e:
|
|
193
|
-
raise BadParameter(
|
|
194
|
-
message=(
|
|
195
|
-
f"Invalid boolean value: '{value}'.{NEW_LINE}"
|
|
196
|
-
"Use one of: true/false, t/f, yes/no, y/n, 1/0, or on/off."
|
|
197
|
-
)
|
|
198
|
-
) from e
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _parse_recursive_flag(value: str) -> bool:
|
|
181
|
+
(None):
|
|
182
|
+
Nothing is returned.
|
|
202
183
|
"""
|
|
203
|
-
!!! note "Summary"
|
|
204
|
-
Parse recursive flag using `strtobool()` utility.
|
|
205
184
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
The string value of the flag.
|
|
185
|
+
if not value or ctx.resilient_parsing:
|
|
186
|
+
return
|
|
209
187
|
|
|
210
|
-
|
|
211
|
-
(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
188
|
+
if value == "config":
|
|
189
|
+
_show_config_example_callback()
|
|
190
|
+
elif value == "usage":
|
|
191
|
+
_show_usage_examples_callback()
|
|
192
|
+
else:
|
|
193
|
+
console.print(_red(f"Error: Invalid example type '{value}'. Use 'config' or 'usage'."))
|
|
194
|
+
raise Exit(1)
|
|
215
195
|
|
|
216
196
|
|
|
217
|
-
def
|
|
197
|
+
def _show_usage_examples_callback() -> None:
|
|
218
198
|
"""
|
|
219
199
|
!!! note "Summary"
|
|
220
200
|
Show examples and exit.
|
|
@@ -232,17 +212,19 @@ def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) ->
|
|
|
232
212
|
Nothing is returned.
|
|
233
213
|
"""
|
|
234
214
|
|
|
235
|
-
if not value or ctx.resilient_parsing:
|
|
236
|
-
return
|
|
237
|
-
|
|
238
215
|
examples_content: str = dedent(
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
216
|
+
f"""
|
|
217
|
+
{_green("dfc myfile.py")} Check a single Python file (list output)
|
|
218
|
+
{_green("dfc src/")} Check all Python files in src/ directory
|
|
219
|
+
{_green("dfc --output=table myfile.py")} Check with table output format
|
|
220
|
+
{_green("dfc -o list myfile.py")} Check with list output format (default)
|
|
221
|
+
{_green("dfc --check myfile.py")} Check and exit with error if issues found
|
|
222
|
+
{_green("dfc --quiet myfile.py")} Check quietly, only show pass/fail
|
|
223
|
+
{_green("dfc --quiet --check myfile.py")} Check quietly and exit with error if issues found
|
|
224
|
+
{_green("dfc . --exclude '*/tests/*'")} Check current directory, excluding tests
|
|
225
|
+
{_green("dfc . -c custom.toml")} Use custom configuration file
|
|
226
|
+
{_green("dfc --example=config")} Show example configuration
|
|
227
|
+
{_green("dfc -e usage")} Show usage examples (this help)
|
|
246
228
|
"""
|
|
247
229
|
).strip()
|
|
248
230
|
|
|
@@ -258,10 +240,10 @@ def _show_examples_callback(ctx: Context, param: CallbackParam, value: bool) ->
|
|
|
258
240
|
raise Exit()
|
|
259
241
|
|
|
260
242
|
|
|
261
|
-
def
|
|
243
|
+
def _show_config_example_callback() -> None:
|
|
262
244
|
"""
|
|
263
245
|
!!! note "Summary"
|
|
264
|
-
Show
|
|
246
|
+
Show configuration example and exit.
|
|
265
247
|
|
|
266
248
|
Params:
|
|
267
249
|
ctx (Context):
|
|
@@ -276,29 +258,74 @@ def _show_check_examples_callback(ctx: Context, param: CallbackParam, value: boo
|
|
|
276
258
|
Nothing is returned.
|
|
277
259
|
"""
|
|
278
260
|
|
|
279
|
-
|
|
280
|
-
return
|
|
281
|
-
|
|
282
|
-
examples_content: str = dedent(
|
|
283
|
-
"""
|
|
284
|
-
[green]dfc check myfile.py[/green] Check a single Python file
|
|
285
|
-
[green]dfc check src/[/green] Check all Python files in src/ directory
|
|
286
|
-
[green]dfc check . --exclude "*/tests/*"[/green] Check current directory, excluding tests
|
|
287
|
-
[green]dfc check . --config custom.toml[/green] Use custom configuration file
|
|
288
|
-
[green]dfc check . --verbose --recursive[/green] Show detailed output for all subdirectories
|
|
289
|
-
[green]dfc check . --quiet[/green] Only show errors, suppress success messages
|
|
261
|
+
example_config: str = dedent(
|
|
290
262
|
"""
|
|
291
|
-
|
|
263
|
+
# Example configuration for docstring-format-checker
|
|
264
|
+
# Place this in your pyproject.toml file
|
|
292
265
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
title="Check Command Examples",
|
|
296
|
-
title_align="left",
|
|
297
|
-
border_style="dim",
|
|
298
|
-
padding=(0, 1),
|
|
299
|
-
)
|
|
266
|
+
[tool.dfc]
|
|
267
|
+
# or [tool.docstring-format-checker]
|
|
300
268
|
|
|
301
|
-
|
|
269
|
+
[[tool.dfc.sections]]
|
|
270
|
+
order = 1
|
|
271
|
+
name = "summary"
|
|
272
|
+
type = "free_text"
|
|
273
|
+
admonition = "note"
|
|
274
|
+
prefix = "!!!"
|
|
275
|
+
required = true
|
|
276
|
+
|
|
277
|
+
[[tool.dfc.sections]]
|
|
278
|
+
order = 2
|
|
279
|
+
name = "details"
|
|
280
|
+
type = "free_text"
|
|
281
|
+
admonition = "info"
|
|
282
|
+
prefix = "???+"
|
|
283
|
+
required = false
|
|
284
|
+
|
|
285
|
+
[[tool.dfc.sections]]
|
|
286
|
+
order = 3
|
|
287
|
+
name = "params"
|
|
288
|
+
type = "list_name_and_type"
|
|
289
|
+
required = true
|
|
290
|
+
|
|
291
|
+
[[tool.dfc.sections]]
|
|
292
|
+
order = 4
|
|
293
|
+
name = "returns"
|
|
294
|
+
type = "list_name_and_type"
|
|
295
|
+
required = false
|
|
296
|
+
|
|
297
|
+
[[tool.dfc.sections]]
|
|
298
|
+
order = 5
|
|
299
|
+
name = "yields"
|
|
300
|
+
type = "list_type"
|
|
301
|
+
required = false
|
|
302
|
+
|
|
303
|
+
[[tool.dfc.sections]]
|
|
304
|
+
order = 6
|
|
305
|
+
name = "raises"
|
|
306
|
+
type = "list_type"
|
|
307
|
+
required = false
|
|
308
|
+
|
|
309
|
+
[[tool.dfc.sections]]
|
|
310
|
+
order = 7
|
|
311
|
+
name = "examples"
|
|
312
|
+
type = "free_text"
|
|
313
|
+
admonition = "example"
|
|
314
|
+
prefix = "???+"
|
|
315
|
+
required = false
|
|
316
|
+
|
|
317
|
+
[[tool.dfc.sections]]
|
|
318
|
+
order = 8
|
|
319
|
+
name = "notes"
|
|
320
|
+
type = "free_text"
|
|
321
|
+
admonition = "note"
|
|
322
|
+
prefix = "???"
|
|
323
|
+
required = false
|
|
324
|
+
"""
|
|
325
|
+
).strip()
|
|
326
|
+
|
|
327
|
+
# Print without Rich markup processing to avoid bracket interpretation
|
|
328
|
+
console.print(example_config, markup=False)
|
|
302
329
|
raise Exit()
|
|
303
330
|
|
|
304
331
|
|
|
@@ -325,7 +352,7 @@ def _format_error_messages(error_message: str) -> str:
|
|
|
325
352
|
return f"- {error_message.strip()}."
|
|
326
353
|
|
|
327
354
|
|
|
328
|
-
def _display_results(results: dict[str, list[DocstringError]], quiet: bool,
|
|
355
|
+
def _display_results(results: dict[str, list[DocstringError]], quiet: bool, output: str, check: bool) -> int:
|
|
329
356
|
"""
|
|
330
357
|
!!! note "Summary"
|
|
331
358
|
Display the results of docstring checking.
|
|
@@ -334,9 +361,11 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
334
361
|
results (dict[str, list[DocstringError]]):
|
|
335
362
|
Dictionary mapping file paths to lists of errors
|
|
336
363
|
quiet (bool):
|
|
337
|
-
Whether to suppress success messages
|
|
338
|
-
|
|
339
|
-
|
|
364
|
+
Whether to suppress success messages and error details
|
|
365
|
+
output (str):
|
|
366
|
+
Output format: 'table' or 'list'
|
|
367
|
+
check (bool):
|
|
368
|
+
Whether this is a check run (affects quiet behavior)
|
|
340
369
|
|
|
341
370
|
Returns:
|
|
342
371
|
(int):
|
|
@@ -344,14 +373,42 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
344
373
|
"""
|
|
345
374
|
if not results:
|
|
346
375
|
if not quiet:
|
|
347
|
-
console.print("
|
|
376
|
+
console.print(_green("✓ All docstrings are valid!"))
|
|
348
377
|
return 0
|
|
349
378
|
|
|
350
|
-
# Count total errors
|
|
351
|
-
|
|
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
|
|
352
394
|
total_files: int = len(results)
|
|
353
395
|
|
|
354
|
-
if
|
|
396
|
+
if quiet:
|
|
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}"))
|
|
409
|
+
return 1
|
|
410
|
+
|
|
411
|
+
if output == "table":
|
|
355
412
|
# Show detailed table
|
|
356
413
|
table = Table(show_header=True, header_style="bold magenta")
|
|
357
414
|
table.add_column("File", style="cyan", no_wrap=False)
|
|
@@ -377,22 +434,43 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
377
434
|
console.print(table)
|
|
378
435
|
|
|
379
436
|
else:
|
|
380
|
-
# Show compact output
|
|
437
|
+
# Show compact output - each individual error on its own line
|
|
381
438
|
for file_path, errors in results.items():
|
|
382
|
-
console.print(f"{NEW_LINE}
|
|
439
|
+
console.print(f"{NEW_LINE}{_cyan(file_path)}")
|
|
383
440
|
for error in errors:
|
|
384
|
-
#
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
441
|
+
# Split error message into individual errors for list mode
|
|
442
|
+
if "; " in error.message:
|
|
443
|
+
individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
|
|
444
|
+
for individual_error in individual_errors:
|
|
445
|
+
formatted_error = f"- {individual_error}"
|
|
446
|
+
if error.line_number > 0:
|
|
447
|
+
console.print(
|
|
448
|
+
f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error}"
|
|
449
|
+
)
|
|
450
|
+
else:
|
|
451
|
+
console.print(f" {_red('Error')}: {formatted_error}")
|
|
391
452
|
else:
|
|
392
|
-
|
|
453
|
+
# Single error message
|
|
454
|
+
formatted_error = f"- {error.message.strip()}"
|
|
455
|
+
if error.line_number > 0:
|
|
456
|
+
console.print(
|
|
457
|
+
f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}': {formatted_error}"
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
console.print(f" {_red('Error')}: {formatted_error}")
|
|
461
|
+
|
|
462
|
+
# Summary - more descriptive message
|
|
463
|
+
if total_functions == 1:
|
|
464
|
+
functions_text = f"1 function"
|
|
465
|
+
else:
|
|
466
|
+
functions_text = f"{total_functions} functions"
|
|
393
467
|
|
|
394
|
-
|
|
395
|
-
|
|
468
|
+
if total_files == 1:
|
|
469
|
+
files_text = f"1 file"
|
|
470
|
+
else:
|
|
471
|
+
files_text = f"{total_files} files"
|
|
472
|
+
|
|
473
|
+
console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
|
|
396
474
|
|
|
397
475
|
return 1
|
|
398
476
|
|
|
@@ -405,13 +483,13 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, verb
|
|
|
405
483
|
|
|
406
484
|
|
|
407
485
|
# This will be the default behavior when no command is specified
|
|
408
|
-
def
|
|
486
|
+
def check_docstrings(
|
|
409
487
|
path: str,
|
|
410
488
|
config: Optional[str] = None,
|
|
411
|
-
recursive: bool = True,
|
|
412
489
|
exclude: Optional[list[str]] = None,
|
|
413
490
|
quiet: bool = False,
|
|
414
|
-
|
|
491
|
+
output: str = "list",
|
|
492
|
+
check: bool = False,
|
|
415
493
|
) -> None:
|
|
416
494
|
"""
|
|
417
495
|
!!! note "Summary"
|
|
@@ -422,14 +500,19 @@ def _check_docstrings(
|
|
|
422
500
|
The path to the file or directory to check.
|
|
423
501
|
config (Optional[str]):
|
|
424
502
|
The path to the configuration file.
|
|
425
|
-
|
|
426
|
-
Whether to check files recursively.
|
|
503
|
+
Default: `None`.
|
|
427
504
|
exclude (Optional[list[str]]):
|
|
428
505
|
List of glob patterns to exclude from checking.
|
|
506
|
+
Default: `None`.
|
|
429
507
|
quiet (bool):
|
|
430
508
|
Whether to suppress output.
|
|
431
|
-
|
|
432
|
-
|
|
509
|
+
Default: `False`.
|
|
510
|
+
output (str):
|
|
511
|
+
Output format: 'table' or 'list'.
|
|
512
|
+
Default: `'list'`.
|
|
513
|
+
check (bool):
|
|
514
|
+
Whether to throw error if issues are found.
|
|
515
|
+
Default: `False`.
|
|
433
516
|
|
|
434
517
|
Returns:
|
|
435
518
|
(None):
|
|
@@ -440,7 +523,7 @@ def _check_docstrings(
|
|
|
440
523
|
|
|
441
524
|
# Validate target path
|
|
442
525
|
if not target_path.exists():
|
|
443
|
-
console.print(f"
|
|
526
|
+
console.print(_red(f"Error: Path does not exist: '{path}'"))
|
|
444
527
|
raise Exit(1)
|
|
445
528
|
|
|
446
529
|
# Load configuration
|
|
@@ -448,25 +531,19 @@ def _check_docstrings(
|
|
|
448
531
|
if config:
|
|
449
532
|
config_path = Path(config)
|
|
450
533
|
if not config_path.exists():
|
|
451
|
-
console.print(f"
|
|
534
|
+
console.print(_red(f"Error: Configuration file does not exist: {config}"))
|
|
452
535
|
raise Exit(1)
|
|
453
536
|
sections_config = load_config(config_path)
|
|
454
537
|
else:
|
|
455
538
|
# Try to find config file automatically
|
|
456
|
-
found_config:
|
|
457
|
-
target_path if target_path.is_dir() else target_path.parent
|
|
458
|
-
)
|
|
539
|
+
found_config: Optional[Path] = find_config_file(target_path if target_path.is_dir() else target_path.parent)
|
|
459
540
|
if found_config:
|
|
460
|
-
if verbose:
|
|
461
|
-
console.print(f"[blue]Using configuration from: {found_config}[/blue]")
|
|
462
541
|
sections_config: list[SectionConfig] = load_config(found_config)
|
|
463
542
|
else:
|
|
464
|
-
if verbose:
|
|
465
|
-
console.print("[blue]Using default configuration[/blue]")
|
|
466
543
|
sections_config: list[SectionConfig] = load_config()
|
|
467
544
|
|
|
468
545
|
except Exception as e:
|
|
469
|
-
console.print(f"
|
|
546
|
+
console.print(_red(f"Error loading configuration: {e}"))
|
|
470
547
|
raise Exit(1)
|
|
471
548
|
|
|
472
549
|
# Initialize checker
|
|
@@ -475,23 +552,18 @@ def _check_docstrings(
|
|
|
475
552
|
# Check files
|
|
476
553
|
try:
|
|
477
554
|
if target_path.is_file():
|
|
478
|
-
if verbose:
|
|
479
|
-
console.print(f"[blue]Checking file: {target_path}[/blue]")
|
|
480
555
|
errors: list[DocstringError] = checker.check_file(target_path)
|
|
481
556
|
results: dict[str, list[DocstringError]] = {str(target_path): errors} if errors else {}
|
|
482
557
|
else:
|
|
483
|
-
|
|
484
|
-
console.print(f"[blue]Checking directory: {target_path} (recursive={recursive})[/blue]")
|
|
485
|
-
results: dict[str, list[DocstringError]] = checker.check_directory(
|
|
486
|
-
target_path, recursive=recursive, exclude_patterns=exclude
|
|
487
|
-
)
|
|
558
|
+
results: dict[str, list[DocstringError]] = checker.check_directory(target_path, exclude_patterns=exclude)
|
|
488
559
|
except Exception as e:
|
|
489
|
-
console.print(f"
|
|
560
|
+
console.print(_red(f"Error during checking: {e}"))
|
|
490
561
|
raise Exit(1)
|
|
491
562
|
|
|
492
563
|
# Display results
|
|
493
|
-
exit_code: int = _display_results(results, quiet,
|
|
564
|
+
exit_code: int = _display_results(results, quiet, output, check)
|
|
494
565
|
|
|
566
|
+
# Always exit with error code if issues are found, regardless of check flag
|
|
495
567
|
if exit_code != 0:
|
|
496
568
|
raise Exit(exit_code)
|
|
497
569
|
|
|
@@ -507,6 +579,41 @@ def _check_docstrings(
|
|
|
507
579
|
@app.callback(invoke_without_command=True)
|
|
508
580
|
def main(
|
|
509
581
|
ctx: Context,
|
|
582
|
+
path: Optional[str] = Argument(None, help="Path to Python file or directory to check"),
|
|
583
|
+
config: Optional[str] = Option(None, "--config", "-f", help="Path to configuration file (TOML format)"),
|
|
584
|
+
exclude: Optional[list[str]] = Option(
|
|
585
|
+
None,
|
|
586
|
+
"--exclude",
|
|
587
|
+
"-x",
|
|
588
|
+
help="Glob patterns to exclude (can be used multiple times)",
|
|
589
|
+
),
|
|
590
|
+
output: str = Option(
|
|
591
|
+
"list",
|
|
592
|
+
"--output",
|
|
593
|
+
"-o",
|
|
594
|
+
help="Output format: 'table' or 'list'",
|
|
595
|
+
show_default=True,
|
|
596
|
+
),
|
|
597
|
+
check: bool = Option(
|
|
598
|
+
False,
|
|
599
|
+
"--check",
|
|
600
|
+
"-c",
|
|
601
|
+
help="Throw error (exit 1) if any issues are found",
|
|
602
|
+
),
|
|
603
|
+
quiet: bool = Option(
|
|
604
|
+
False,
|
|
605
|
+
"--quiet",
|
|
606
|
+
"-q",
|
|
607
|
+
help="Only output pass/fail confirmation, suppress errors unless failing",
|
|
608
|
+
),
|
|
609
|
+
example: Optional[str] = Option(
|
|
610
|
+
None,
|
|
611
|
+
"--example",
|
|
612
|
+
"-e",
|
|
613
|
+
callback=_example_callback,
|
|
614
|
+
is_eager=True,
|
|
615
|
+
help="Show examples: 'config' for configuration example, 'usage' for usage examples",
|
|
616
|
+
),
|
|
510
617
|
version: Optional[bool] = Option(
|
|
511
618
|
None,
|
|
512
619
|
"--version",
|
|
@@ -515,19 +622,11 @@ def main(
|
|
|
515
622
|
is_eager=True,
|
|
516
623
|
help="Show version and exit",
|
|
517
624
|
),
|
|
518
|
-
examples: Optional[bool] = Option(
|
|
519
|
-
None,
|
|
520
|
-
"--examples",
|
|
521
|
-
"-e",
|
|
522
|
-
callback=_show_examples_callback,
|
|
523
|
-
is_eager=True,
|
|
524
|
-
help="Show usage examples and exit",
|
|
525
|
-
),
|
|
526
625
|
help_flag: Optional[bool] = Option(
|
|
527
626
|
None,
|
|
528
627
|
"--help",
|
|
529
628
|
"-h",
|
|
530
|
-
callback=
|
|
629
|
+
callback=_help_callback_main,
|
|
531
630
|
is_eager=True,
|
|
532
631
|
help="Show this message and exit",
|
|
533
632
|
),
|
|
@@ -542,122 +641,22 @@ def main(
|
|
|
542
641
|
Params:
|
|
543
642
|
ctx (Context):
|
|
544
643
|
The context object for the command.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
examples (Optional[bool]):
|
|
548
|
-
Show usage examples and exit.
|
|
549
|
-
help_flag (Optional[bool]):
|
|
550
|
-
Show help message and exit.
|
|
551
|
-
|
|
552
|
-
Returns:
|
|
553
|
-
(None):
|
|
554
|
-
Nothing is returned.
|
|
555
|
-
"""
|
|
556
|
-
# If no subcommand is provided, show help
|
|
557
|
-
if ctx.invoked_subcommand is None:
|
|
558
|
-
echo(ctx.get_help())
|
|
559
|
-
raise Exit()
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
@app.command(
|
|
563
|
-
rich_help_panel="Commands",
|
|
564
|
-
add_help_option=False, # Disable automatic help so we can add our own with -h
|
|
565
|
-
)
|
|
566
|
-
def check(
|
|
567
|
-
path: str = Argument(..., help="Path to Python file or directory to check"),
|
|
568
|
-
config: Optional[str] = Option(None, "--config", "-c", help="Path to configuration file (TOML format)"),
|
|
569
|
-
recursive: str = Option(
|
|
570
|
-
"true",
|
|
571
|
-
"--recursive",
|
|
572
|
-
"-r",
|
|
573
|
-
help="Check directories recursively (default: true). Accepts: true/false, t/f, yes/no, y/n, 1/0, on/off",
|
|
574
|
-
),
|
|
575
|
-
exclude: Optional[list[str]] = Option(
|
|
576
|
-
None,
|
|
577
|
-
"--exclude",
|
|
578
|
-
"-x",
|
|
579
|
-
help="Glob patterns to exclude (can be used multiple times)",
|
|
580
|
-
),
|
|
581
|
-
quiet: bool = Option(False, "--quiet", "-q", help="Only show errors, no success messages"),
|
|
582
|
-
verbose: bool = Option(False, "--verbose", "-n", help="Show detailed output"),
|
|
583
|
-
examples: Optional[bool] = Option(
|
|
584
|
-
None,
|
|
585
|
-
"--examples",
|
|
586
|
-
"-e",
|
|
587
|
-
callback=_show_check_examples_callback,
|
|
588
|
-
is_eager=True,
|
|
589
|
-
help="Show usage examples and exit",
|
|
590
|
-
),
|
|
591
|
-
help_flag: Optional[bool] = Option(
|
|
592
|
-
None,
|
|
593
|
-
"--help",
|
|
594
|
-
"-h",
|
|
595
|
-
callback=_help_callback,
|
|
596
|
-
is_eager=True,
|
|
597
|
-
help="Show this message and exit",
|
|
598
|
-
),
|
|
599
|
-
) -> None:
|
|
600
|
-
"""
|
|
601
|
-
!!! note "Summary"
|
|
602
|
-
Check docstrings in Python files.
|
|
603
|
-
|
|
604
|
-
???+ abstract "Details"
|
|
605
|
-
This command checks the docstrings in the specified Python file or directory.
|
|
606
|
-
|
|
607
|
-
Params:
|
|
608
|
-
path (str):
|
|
609
|
-
The path to the Python file or directory to check.
|
|
644
|
+
path (Optional[str]):
|
|
645
|
+
Path to Python file or directory to check.
|
|
610
646
|
config (Optional[str]):
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
647
|
+
Path to configuration file (TOML format).
|
|
648
|
+
exclude (Optional[list[str]]):
|
|
649
|
+
Glob patterns to exclude.
|
|
650
|
+
output (str):
|
|
651
|
+
Output format: 'table' or 'list'.
|
|
652
|
+
check (bool):
|
|
653
|
+
Throw error if any issues are found.
|
|
616
654
|
quiet (bool):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
Show
|
|
622
|
-
help_flag (Optional[bool]):
|
|
623
|
-
Show help message and exit.
|
|
624
|
-
|
|
625
|
-
Returns:
|
|
626
|
-
(None):
|
|
627
|
-
Nothing is returned.
|
|
628
|
-
"""
|
|
629
|
-
# Parse the recursive string value into a boolean
|
|
630
|
-
try:
|
|
631
|
-
recursive_bool: bool = _parse_recursive_flag(recursive)
|
|
632
|
-
except ValueError as e:
|
|
633
|
-
raise BadParameter(
|
|
634
|
-
message=(
|
|
635
|
-
f"Invalid value for --recursive: '{recursive}'.{NEW_LINE}"
|
|
636
|
-
"Use one of: true/false, t/f, yes/no, y/n, 1/0, or on/off."
|
|
637
|
-
)
|
|
638
|
-
) from e
|
|
639
|
-
_check_docstrings(path, config, recursive_bool, exclude, quiet, verbose)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
@app.command(
|
|
643
|
-
rich_help_panel="Commands",
|
|
644
|
-
add_help_option=False, # Disable automatic help so we can add our own with -h
|
|
645
|
-
)
|
|
646
|
-
def config_example(
|
|
647
|
-
help_flag: Optional[bool] = Option(
|
|
648
|
-
None,
|
|
649
|
-
"--help",
|
|
650
|
-
"-h",
|
|
651
|
-
callback=_help_callback,
|
|
652
|
-
is_eager=True,
|
|
653
|
-
help="Show this message and exit",
|
|
654
|
-
),
|
|
655
|
-
) -> None:
|
|
656
|
-
"""
|
|
657
|
-
!!! note "Summary"
|
|
658
|
-
Show example configuration file.
|
|
659
|
-
|
|
660
|
-
Params:
|
|
655
|
+
Only output pass/fail confirmation.
|
|
656
|
+
example (Optional[str]):
|
|
657
|
+
Show examples: 'config' or 'usage'.
|
|
658
|
+
version (Optional[bool]):
|
|
659
|
+
Show version and exit.
|
|
661
660
|
help_flag (Optional[bool]):
|
|
662
661
|
Show help message and exit.
|
|
663
662
|
|
|
@@ -665,74 +664,26 @@ def config_example(
|
|
|
665
664
|
(None):
|
|
666
665
|
Nothing is returned.
|
|
667
666
|
"""
|
|
668
|
-
example_config: str = dedent(
|
|
669
|
-
"""
|
|
670
|
-
# Example configuration for docstring-format-checker
|
|
671
|
-
# Place this in your pyproject.toml file
|
|
672
|
-
|
|
673
|
-
[tool.dfc]
|
|
674
|
-
# or [tool.docstring-format-checker]
|
|
675
|
-
|
|
676
|
-
[[tool.dfc.sections]]
|
|
677
|
-
order = 1
|
|
678
|
-
name = "summary"
|
|
679
|
-
type = "free_text"
|
|
680
|
-
admonition = "note"
|
|
681
|
-
prefix = "!!!"
|
|
682
|
-
required = true
|
|
683
|
-
|
|
684
|
-
[[tool.dfc.sections]]
|
|
685
|
-
order = 2
|
|
686
|
-
name = "details"
|
|
687
|
-
type = "free_text"
|
|
688
|
-
admonition = "info"
|
|
689
|
-
prefix = "???+"
|
|
690
|
-
required = false
|
|
691
|
-
|
|
692
|
-
[[tool.dfc.sections]]
|
|
693
|
-
order = 3
|
|
694
|
-
name = "params"
|
|
695
|
-
type = "list_name_and_type"
|
|
696
|
-
required = true
|
|
697
|
-
|
|
698
|
-
[[tool.dfc.sections]]
|
|
699
|
-
order = 4
|
|
700
|
-
name = "returns"
|
|
701
|
-
type = "list_name_and_type"
|
|
702
|
-
required = false
|
|
703
667
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
required = false
|
|
709
|
-
|
|
710
|
-
[[tool.dfc.sections]]
|
|
711
|
-
order = 6
|
|
712
|
-
name = "raises"
|
|
713
|
-
type = "list_type"
|
|
714
|
-
required = false
|
|
668
|
+
# If no path is provided, show help
|
|
669
|
+
if path is None:
|
|
670
|
+
echo(ctx.get_help())
|
|
671
|
+
raise Exit(0)
|
|
715
672
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
admonition = "example"
|
|
721
|
-
prefix = "???+"
|
|
722
|
-
required = false
|
|
673
|
+
# Validate output format
|
|
674
|
+
if output not in ["table", "list"]:
|
|
675
|
+
console.print(_red(f"Error: Invalid output format '{output}'. Use 'table' or 'list'."))
|
|
676
|
+
raise Exit(1)
|
|
723
677
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
""".strip()
|
|
678
|
+
check_docstrings(
|
|
679
|
+
path=path,
|
|
680
|
+
config=config,
|
|
681
|
+
exclude=exclude,
|
|
682
|
+
quiet=quiet,
|
|
683
|
+
output=output,
|
|
684
|
+
check=check,
|
|
732
685
|
)
|
|
733
686
|
|
|
734
|
-
print(example_config)
|
|
735
|
-
|
|
736
687
|
|
|
737
688
|
def entry_point() -> None:
|
|
738
689
|
"""
|
|
@@ -167,18 +167,15 @@ class DocstringChecker:
|
|
|
167
167
|
def check_directory(
|
|
168
168
|
self,
|
|
169
169
|
directory_path: Union[str, Path],
|
|
170
|
-
recursive: bool = True,
|
|
171
170
|
exclude_patterns: Optional[list[str]] = None,
|
|
172
171
|
) -> dict[str, list[DocstringError]]:
|
|
173
172
|
"""
|
|
174
173
|
!!! note "Summary"
|
|
175
|
-
Check docstrings in all Python files in a directory.
|
|
174
|
+
Check docstrings in all Python files in a directory recursively.
|
|
176
175
|
|
|
177
176
|
Params:
|
|
178
177
|
directory_path (Union[str, Path]):
|
|
179
178
|
Path to the directory to check.
|
|
180
|
-
recursive (bool):
|
|
181
|
-
Whether to check subdirectories recursively.
|
|
182
179
|
exclude_patterns (Optional[list[str]]):
|
|
183
180
|
List of glob patterns to exclude.
|
|
184
181
|
|
|
@@ -200,13 +197,7 @@ class DocstringChecker:
|
|
|
200
197
|
if not directory_path.is_dir():
|
|
201
198
|
raise DirectoryNotFoundError(f"Path is not a directory: {directory_path}")
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
if recursive:
|
|
205
|
-
pattern = "**/*.py"
|
|
206
|
-
else:
|
|
207
|
-
pattern = "*.py"
|
|
208
|
-
|
|
209
|
-
python_files: list[Path] = list(directory_path.glob(pattern))
|
|
200
|
+
python_files: list[Path] = list(directory_path.glob("**/*.py"))
|
|
210
201
|
|
|
211
202
|
# Filter out excluded patterns
|
|
212
203
|
if exclude_patterns:
|
|
@@ -971,9 +962,41 @@ class DocstringChecker:
|
|
|
971
962
|
if current_section and stripped_line and not stripped_line.startswith(("!", "?", "#")):
|
|
972
963
|
# Look for parameter/type definitions
|
|
973
964
|
if ":" in stripped_line:
|
|
965
|
+
# Skip description lines that start with common description words
|
|
966
|
+
description_prefixes = [
|
|
967
|
+
"default:",
|
|
968
|
+
"note:",
|
|
969
|
+
"example:",
|
|
970
|
+
"see:",
|
|
971
|
+
"warning:",
|
|
972
|
+
"info:",
|
|
973
|
+
"tip:",
|
|
974
|
+
"returns:",
|
|
975
|
+
]
|
|
976
|
+
is_description_line = any(
|
|
977
|
+
stripped_line.lower().startswith(prefix) for prefix in description_prefixes
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# Skip lines that are clearly descriptions (containing "Default:", etc.)
|
|
981
|
+
if (
|
|
982
|
+
is_description_line
|
|
983
|
+
or "Default:" in stripped_line
|
|
984
|
+
or "Output format:" in stripped_line
|
|
985
|
+
or "Show examples:" in stripped_line
|
|
986
|
+
):
|
|
987
|
+
continue
|
|
988
|
+
|
|
974
989
|
# For list_name_and_type sections, check format like "name (type):" or "(type):"
|
|
975
990
|
if current_section.type == "list_name_and_type":
|
|
976
991
|
# Pattern: name (type): or (type):
|
|
992
|
+
# But skip if it doesn't look like a parameter definition (e.g., has multiple words before the colon)
|
|
993
|
+
colon_part = stripped_line.split(":")[0].strip()
|
|
994
|
+
# Skip if it contains phrases that indicate it's a description, not a parameter
|
|
995
|
+
if any(
|
|
996
|
+
word in colon_part.lower() for word in ["default", "output", "format", "show", "example"]
|
|
997
|
+
):
|
|
998
|
+
continue
|
|
999
|
+
|
|
977
1000
|
if not re.search(r"\([^)]+\):", stripped_line):
|
|
978
1001
|
errors.append(
|
|
979
1002
|
f"Section '{current_section.name}' (type: '{current_section.type}') requires "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|