docstring-format-checker 1.3.0__tar.gz → 1.4.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.3.0 → docstring_format_checker-1.4.0}/PKG-INFO +2 -1
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/pyproject.toml +31 -10
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/src/docstring_format_checker/cli.py +144 -109
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/src/docstring_format_checker/config.py +77 -44
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/src/docstring_format_checker/core.py +309 -185
- docstring_format_checker-1.4.0/src/docstring_format_checker/utils/exceptions.py +103 -0
- docstring_format_checker-1.3.0/src/docstring_format_checker/utils/exceptions.py +0 -69
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/README.md +0 -0
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/src/docstring_format_checker/__init__.py +0 -0
- {docstring_format_checker-1.3.0 → docstring_format_checker-1.4.0}/src/docstring_format_checker/utils/__init__.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.4.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>
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
21
|
Classifier: Intended Audience :: Developers
|
|
21
22
|
Classifier: Environment :: Console
|
|
22
23
|
Requires-Dist: typer>=0.9.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "docstring-format-checker"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.4.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"
|
|
@@ -23,6 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.11",
|
|
24
24
|
"Programming Language :: Python :: 3.12",
|
|
25
25
|
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
26
27
|
"Intended Audience :: Developers",
|
|
27
28
|
"Environment :: Console",
|
|
28
29
|
]
|
|
@@ -83,6 +84,7 @@ test = [
|
|
|
83
84
|
"pytest-sugar==1.*",
|
|
84
85
|
"pytest-xdist==3.*",
|
|
85
86
|
"requests==2.*",
|
|
87
|
+
"complexipy==4.*",
|
|
86
88
|
]
|
|
87
89
|
|
|
88
90
|
[tool.black]
|
|
@@ -147,6 +149,25 @@ lines_after_imports = 2
|
|
|
147
149
|
[tool.codespell]
|
|
148
150
|
ignore-words-list = "demog"
|
|
149
151
|
|
|
152
|
+
[tool.pylint.main]
|
|
153
|
+
disable = [
|
|
154
|
+
"C0103", # invalid-name
|
|
155
|
+
"C0301", # line-too-long
|
|
156
|
+
"C0302", # too-many-lines
|
|
157
|
+
"R0913", # too-many-arguments
|
|
158
|
+
"R0914", # too-many-locals
|
|
159
|
+
"R0915", # too-many-statements
|
|
160
|
+
"R0917", # too-many-positional-arguments
|
|
161
|
+
"R1705", # no-else-return
|
|
162
|
+
"R1724", # no-else-continue
|
|
163
|
+
"W0107", # unnecessary-pass
|
|
164
|
+
"W0212", # protected-access
|
|
165
|
+
"W0612", # unused-variable
|
|
166
|
+
"W0613", # unused-argument
|
|
167
|
+
"W0622", # redefined-builtin
|
|
168
|
+
"W0718", # broad-exception-caught
|
|
169
|
+
]
|
|
170
|
+
|
|
150
171
|
[tool.bump_version.replacements]
|
|
151
172
|
files = [
|
|
152
173
|
{ file = "src/docstring_format_checker/__init__.py", pattern = "__version__ = \"{VERSION}\"" },
|
|
@@ -157,16 +178,16 @@ files = [
|
|
|
157
178
|
[tool.dfc]
|
|
158
179
|
allow_undefined_sections = false
|
|
159
180
|
require_docstrings = true
|
|
160
|
-
check_private =
|
|
181
|
+
check_private = false
|
|
161
182
|
sections = [
|
|
162
|
-
{ order
|
|
163
|
-
{ order
|
|
164
|
-
{ order
|
|
165
|
-
{ order
|
|
166
|
-
{ order
|
|
167
|
-
{ order
|
|
168
|
-
{ order
|
|
169
|
-
{ order
|
|
183
|
+
{ order=1, name="summary", type="free_text", required=true, admonition="note", prefix="!!!" },
|
|
184
|
+
{ order=2, name="details", type="free_text", required=false, admonition="abstract", prefix="???+" },
|
|
185
|
+
{ order=3, name="params", type="list_name_and_type", required=false, admonition=false },
|
|
186
|
+
{ order=4, name="raises", type="list_type", required=false, admonition=false },
|
|
187
|
+
{ order=5, name="returns", type="list_name_and_type", required=false, admonition=false },
|
|
188
|
+
{ order=6, name="yields", type="list_type", required=false, admonition=false },
|
|
189
|
+
{ order=7, name="examples", type="free_text", required=false, admonition="example", prefix="???+" },
|
|
190
|
+
{ order=8, name="notes", type="free_text", required=false, admonition="note", prefix="???" },
|
|
170
191
|
]
|
|
171
192
|
|
|
172
193
|
[build-system]
|
|
@@ -360,12 +360,18 @@ def _format_error_messages(error_message: str) -> str:
|
|
|
360
360
|
errors: list[str] = error_message.split("; ")
|
|
361
361
|
formatted_errors: list[str] = [f"- {error.strip()}" for error in errors if error.strip()]
|
|
362
362
|
return ";\n".join(formatted_errors) + "."
|
|
363
|
+
|
|
364
|
+
# Single error message
|
|
363
365
|
else:
|
|
364
|
-
# Single error message
|
|
365
366
|
return f"- {error_message.strip()}."
|
|
366
367
|
|
|
367
368
|
|
|
368
|
-
def _display_results(
|
|
369
|
+
def _display_results(
|
|
370
|
+
results: dict[str, list[DocstringError]],
|
|
371
|
+
quiet: bool,
|
|
372
|
+
output: str,
|
|
373
|
+
check: bool,
|
|
374
|
+
) -> int:
|
|
369
375
|
"""
|
|
370
376
|
!!! note "Summary"
|
|
371
377
|
Display the results of docstring checking.
|
|
@@ -386,100 +392,106 @@ def _display_results(results: dict[str, list[DocstringError]], quiet: bool, outp
|
|
|
386
392
|
"""
|
|
387
393
|
if not results:
|
|
388
394
|
if not quiet:
|
|
389
|
-
console.print(_green("
|
|
395
|
+
console.print(_green("✅ All docstrings are valid!"))
|
|
390
396
|
return 0
|
|
391
397
|
|
|
392
|
-
# Count
|
|
398
|
+
# Count errors and generate summary statistics
|
|
399
|
+
error_stats = _count_errors_and_files(results)
|
|
400
|
+
|
|
401
|
+
if quiet:
|
|
402
|
+
_display_quiet_summary(error_stats)
|
|
403
|
+
return 1
|
|
404
|
+
|
|
405
|
+
# Display detailed results based on output format
|
|
406
|
+
if output == "table":
|
|
407
|
+
_display_table_output(results)
|
|
408
|
+
else:
|
|
409
|
+
_display_list_output(results)
|
|
410
|
+
|
|
411
|
+
# Display final summary
|
|
412
|
+
_display_final_summary(error_stats)
|
|
413
|
+
return 1
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _count_errors_and_files(results: dict[str, list[DocstringError]]) -> dict[str, int]:
|
|
417
|
+
"""Count total errors, functions, and files from results."""
|
|
393
418
|
total_individual_errors: int = 0
|
|
394
419
|
total_functions: int = 0
|
|
395
420
|
|
|
396
421
|
for errors in results.values():
|
|
397
|
-
total_functions += len(errors)
|
|
422
|
+
total_functions += len(errors)
|
|
398
423
|
for error in errors:
|
|
399
|
-
# Count individual error messages within each error object
|
|
400
424
|
if "; " in error.message:
|
|
401
425
|
individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
|
|
402
426
|
total_individual_errors += len(individual_errors)
|
|
403
427
|
else:
|
|
404
428
|
total_individual_errors += 1
|
|
405
429
|
|
|
406
|
-
total_errors:
|
|
407
|
-
total_files: int = len(results)
|
|
408
|
-
|
|
409
|
-
if quiet:
|
|
410
|
-
# In quiet mode, only show summary with improved format
|
|
411
|
-
if total_functions == 1:
|
|
412
|
-
functions_text = f"1 function"
|
|
413
|
-
else:
|
|
414
|
-
functions_text = f"{total_functions} functions"
|
|
415
|
-
|
|
416
|
-
if total_files == 1:
|
|
417
|
-
files_text = f"1 file"
|
|
418
|
-
else:
|
|
419
|
-
files_text = f"{total_files} files"
|
|
430
|
+
return {"total_errors": total_individual_errors, "total_functions": total_functions, "total_files": len(results)}
|
|
420
431
|
|
|
421
|
-
console.print(_red(f"{NEW_LINE}Found {total_errors} error(s) in {functions_text} over {files_text}"))
|
|
422
|
-
return 1
|
|
423
432
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
433
|
+
def _display_quiet_summary(error_stats: dict[str, int]) -> None:
|
|
434
|
+
"""Display summary in quiet mode."""
|
|
435
|
+
functions_text = (
|
|
436
|
+
"1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions"
|
|
437
|
+
)
|
|
438
|
+
files_text = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files"
|
|
439
|
+
|
|
440
|
+
console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}"))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _display_table_output(results: dict[str, list[DocstringError]]) -> None:
|
|
444
|
+
"""Display results in table format."""
|
|
445
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
446
|
+
table.add_column("File", style="cyan", no_wrap=False)
|
|
447
|
+
table.add_column("Line", justify="right", style="white")
|
|
448
|
+
table.add_column("Item", style="yellow")
|
|
449
|
+
table.add_column("Type", style="blue")
|
|
450
|
+
table.add_column("Error", style="red")
|
|
451
|
+
|
|
452
|
+
for file_path, errors in results.items():
|
|
453
|
+
for i, error in enumerate(errors):
|
|
454
|
+
file_display = file_path if i == 0 else ""
|
|
455
|
+
formatted_error_message = _format_error_messages(error.message)
|
|
456
|
+
|
|
457
|
+
table.add_row(
|
|
458
|
+
file_display,
|
|
459
|
+
str(error.line_number) if error.line_number > 0 else "",
|
|
460
|
+
error.item_name,
|
|
461
|
+
error.item_type,
|
|
462
|
+
formatted_error_message,
|
|
463
|
+
)
|
|
464
|
+
console.print(table)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _display_list_output(results: dict[str, list[DocstringError]]) -> None:
|
|
468
|
+
"""Display results in list format."""
|
|
469
|
+
for file_path, errors in results.items():
|
|
470
|
+
console.print(f"{NEW_LINE}{_cyan(file_path)}")
|
|
471
|
+
for error in errors:
|
|
472
|
+
# Print header with line number, item type and name
|
|
473
|
+
if error.line_number > 0:
|
|
474
|
+
console.print(f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':")
|
|
475
|
+
else:
|
|
476
|
+
console.print(f" {_red('Error')} - {error.item_type} '{error.item_name}':")
|
|
448
477
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
console.print(f" [red]Line {error.line_number}[/red] - {error.item_type} '{error.item_name}':")
|
|
457
|
-
else:
|
|
458
|
-
console.print(f" {_red('Error')} - {error.item_type} '{error.item_name}':")
|
|
459
|
-
|
|
460
|
-
# Split error message into individual errors and indent them
|
|
461
|
-
if "; " in error.message:
|
|
462
|
-
individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
|
|
463
|
-
for individual_error in individual_errors:
|
|
464
|
-
console.print(f" - {individual_error}")
|
|
465
|
-
else:
|
|
466
|
-
# Single error message
|
|
467
|
-
console.print(f" - {error.message.strip()}")
|
|
468
|
-
|
|
469
|
-
# Summary - more descriptive message
|
|
470
|
-
if total_functions == 1:
|
|
471
|
-
functions_text = f"1 function"
|
|
472
|
-
else:
|
|
473
|
-
functions_text = f"{total_functions} functions"
|
|
478
|
+
# Format and print error messages
|
|
479
|
+
if "; " in error.message:
|
|
480
|
+
individual_errors = [msg.strip() for msg in error.message.split("; ") if msg.strip()]
|
|
481
|
+
for individual_error in individual_errors:
|
|
482
|
+
console.print(f" - {individual_error}")
|
|
483
|
+
else:
|
|
484
|
+
console.print(f" - {error.message.strip()}")
|
|
474
485
|
|
|
475
|
-
if total_files == 1:
|
|
476
|
-
files_text = f"1 file"
|
|
477
|
-
else:
|
|
478
|
-
files_text = f"{total_files} files"
|
|
479
486
|
|
|
480
|
-
|
|
487
|
+
def _display_final_summary(error_stats: dict[str, int]) -> None:
|
|
488
|
+
"""Display the final summary line."""
|
|
489
|
+
functions_text = (
|
|
490
|
+
"1 function" if error_stats["total_functions"] == 1 else f"{error_stats['total_functions']} functions"
|
|
491
|
+
)
|
|
492
|
+
files_text = "1 file" if error_stats["total_files"] == 1 else f"{error_stats['total_files']} files"
|
|
481
493
|
|
|
482
|
-
|
|
494
|
+
console.print(_red(f"{NEW_LINE}Found {error_stats['total_errors']} error(s) in {functions_text} over {files_text}"))
|
|
483
495
|
|
|
484
496
|
|
|
485
497
|
# ---------------------------------------------------------------------------- #
|
|
@@ -525,69 +537,92 @@ def check_docstrings(
|
|
|
525
537
|
(None):
|
|
526
538
|
Nothing is returned.
|
|
527
539
|
"""
|
|
540
|
+
# Validate and process input paths
|
|
541
|
+
target_paths = _validate_and_process_paths(paths)
|
|
542
|
+
|
|
543
|
+
# Load and validate configuration
|
|
544
|
+
config_obj = _load_and_validate_config(config, target_paths)
|
|
528
545
|
|
|
529
|
-
#
|
|
546
|
+
# Initialize checker and process all paths
|
|
547
|
+
checker = DocstringChecker(config_obj)
|
|
548
|
+
all_results = _process_all_paths(checker, target_paths, exclude)
|
|
549
|
+
|
|
550
|
+
# Display results and handle exit
|
|
551
|
+
exit_code = _display_results(all_results, quiet, output, check)
|
|
552
|
+
if exit_code != 0:
|
|
553
|
+
raise Exit(exit_code)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _validate_and_process_paths(paths: list[str]) -> list[Path]:
|
|
557
|
+
"""Validate input paths and return valid paths."""
|
|
530
558
|
path_objs: list[Path] = [Path(path) for path in paths]
|
|
531
559
|
target_paths: list[Path] = [p for p in path_objs if p.exists()]
|
|
532
560
|
invalid_paths: list[Path] = [p for p in path_objs if not p.exists()]
|
|
533
561
|
|
|
534
|
-
if
|
|
562
|
+
if invalid_paths:
|
|
535
563
|
console.print(
|
|
536
|
-
_red(
|
|
564
|
+
_red("[bold]Error: Paths do not exist:[/bold]"),
|
|
537
565
|
NEW_LINE,
|
|
538
566
|
NEW_LINE.join([f"- '{invalid_path}'" for invalid_path in invalid_paths]),
|
|
539
567
|
)
|
|
540
568
|
raise Exit(1)
|
|
541
569
|
|
|
542
|
-
|
|
570
|
+
return target_paths
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _load_and_validate_config(config: Optional[str], target_paths: list[Path]) -> Config:
|
|
574
|
+
"""Load and validate configuration from file or auto-discovery."""
|
|
543
575
|
try:
|
|
544
576
|
if config:
|
|
545
|
-
|
|
546
|
-
if not config_path.exists():
|
|
547
|
-
console.print(_red(f"Error: Configuration file does not exist: {config}"))
|
|
548
|
-
raise Exit(1)
|
|
549
|
-
config_obj = load_config(config_path)
|
|
577
|
+
return _load_explicit_config(config)
|
|
550
578
|
else:
|
|
551
|
-
|
|
552
|
-
first_path: Path = target_paths[0]
|
|
553
|
-
found_config: Optional[Path] = find_config_file(first_path if first_path.is_dir() else first_path.parent)
|
|
554
|
-
if found_config:
|
|
555
|
-
config_obj: Config = load_config(found_config)
|
|
556
|
-
else:
|
|
557
|
-
config_obj: Config = load_config()
|
|
558
|
-
|
|
579
|
+
return _load_auto_discovered_config(target_paths)
|
|
559
580
|
except Exception as e:
|
|
560
581
|
console.print(_red(f"Error loading configuration: {e}"))
|
|
582
|
+
raise Exit(1) from e
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _load_explicit_config(config: str) -> Config:
|
|
586
|
+
"""Load configuration from explicitly specified path."""
|
|
587
|
+
config_path = Path(config)
|
|
588
|
+
if not config_path.exists():
|
|
589
|
+
console.print(_red(f"Error: Configuration file does not exist: {config}"))
|
|
561
590
|
raise Exit(1)
|
|
591
|
+
return load_config(config_path)
|
|
562
592
|
|
|
563
|
-
# Initialize checker
|
|
564
|
-
checker = DocstringChecker(config_obj)
|
|
565
593
|
|
|
566
|
-
|
|
594
|
+
def _load_auto_discovered_config(target_paths: list[Path]) -> Config:
|
|
595
|
+
"""Load configuration from auto-discovery or defaults."""
|
|
596
|
+
first_path: Path = target_paths[0]
|
|
597
|
+
search_path = first_path if first_path.is_dir() else first_path.parent
|
|
598
|
+
found_config: Optional[Path] = find_config_file(search_path)
|
|
599
|
+
|
|
600
|
+
if found_config:
|
|
601
|
+
return load_config(found_config)
|
|
602
|
+
else:
|
|
603
|
+
return load_config()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _process_all_paths(
|
|
607
|
+
checker: DocstringChecker, target_paths: list[Path], exclude: Optional[list[str]]
|
|
608
|
+
) -> dict[str, list[DocstringError]]:
|
|
609
|
+
"""Process all target paths and collect docstring errors."""
|
|
567
610
|
all_results: dict[str, list[DocstringError]] = {}
|
|
568
611
|
|
|
569
612
|
try:
|
|
570
613
|
for target_path in target_paths:
|
|
571
614
|
if target_path.is_file():
|
|
572
|
-
errors
|
|
615
|
+
errors = checker.check_file(target_path)
|
|
573
616
|
if errors:
|
|
574
617
|
all_results[str(target_path)] = errors
|
|
575
618
|
else:
|
|
576
|
-
directory_results
|
|
577
|
-
target_path, exclude_patterns=exclude
|
|
578
|
-
)
|
|
619
|
+
directory_results = checker.check_directory(target_path, exclude_patterns=exclude)
|
|
579
620
|
all_results.update(directory_results)
|
|
580
|
-
|
|
581
621
|
except Exception as e:
|
|
582
622
|
console.print(_red(f"Error during checking: {e}"))
|
|
583
|
-
raise Exit(1)
|
|
623
|
+
raise Exit(1) from e
|
|
584
624
|
|
|
585
|
-
|
|
586
|
-
exit_code: int = _display_results(all_results, quiet, output, check)
|
|
587
|
-
|
|
588
|
-
# Always exit with error code if issues are found, regardless of check flag
|
|
589
|
-
if exit_code != 0:
|
|
590
|
-
raise Exit(exit_code)
|
|
625
|
+
return all_results
|
|
591
626
|
|
|
592
627
|
|
|
593
628
|
# ---------------------------------------------------------------------------- #
|
|
@@ -331,78 +331,111 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> Config:
|
|
|
331
331
|
(Config):
|
|
332
332
|
Configuration object containing global settings and section definitions.
|
|
333
333
|
"""
|
|
334
|
+
# Resolve config file path
|
|
335
|
+
resolved_path = _resolve_config_path(config_path)
|
|
336
|
+
if resolved_path is None:
|
|
337
|
+
return DEFAULT_CONFIG
|
|
338
|
+
|
|
339
|
+
# Parse TOML configuration
|
|
340
|
+
config_data = _parse_toml_file(resolved_path)
|
|
341
|
+
|
|
342
|
+
# Extract tool configuration
|
|
343
|
+
tool_config = _extract_tool_config(config_data)
|
|
344
|
+
if tool_config is None:
|
|
345
|
+
return DEFAULT_CONFIG
|
|
346
|
+
|
|
347
|
+
# Parse configuration components
|
|
348
|
+
global_config = _parse_global_config(tool_config)
|
|
349
|
+
sections_config = _parse_sections_config(tool_config)
|
|
350
|
+
|
|
351
|
+
return Config(global_config=global_config, sections=sections_config)
|
|
352
|
+
|
|
334
353
|
|
|
354
|
+
def _resolve_config_path(config_path: Optional[Union[str, Path]]) -> Optional[Path]:
|
|
355
|
+
"""Resolve configuration file path."""
|
|
335
356
|
if config_path is None:
|
|
336
357
|
# Look for pyproject.toml in current directory
|
|
337
358
|
pyproject_path: Path = Path.cwd().joinpath("pyproject.toml")
|
|
338
359
|
if pyproject_path.exists():
|
|
339
|
-
|
|
360
|
+
return pyproject_path
|
|
340
361
|
else:
|
|
341
|
-
return
|
|
362
|
+
return None
|
|
342
363
|
|
|
343
364
|
# Convert to Path object and check existence
|
|
344
365
|
config_path = Path(config_path)
|
|
345
366
|
if not config_path.exists():
|
|
346
367
|
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
|
347
368
|
|
|
369
|
+
return config_path
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _parse_toml_file(config_path: Path) -> dict[str, Any]:
|
|
373
|
+
"""Parse TOML configuration file."""
|
|
348
374
|
try:
|
|
349
375
|
with open(config_path, "rb") as f:
|
|
350
|
-
|
|
376
|
+
return tomllib.load(f)
|
|
351
377
|
except Exception as e:
|
|
352
378
|
raise InvalidConfigError(f"Failed to parse TOML file {config_path}: {e}") from e
|
|
353
379
|
|
|
354
|
-
# Try to find configuration under [tool.dfc] or [tool.docstring-format-checker]
|
|
355
|
-
tool_config = None
|
|
356
|
-
if "tool" in config_data:
|
|
357
|
-
if "dfc" in config_data["tool"]:
|
|
358
|
-
tool_config = config_data["tool"]["dfc"]
|
|
359
|
-
elif "docstring-format-checker" in config_data["tool"]:
|
|
360
|
-
tool_config = config_data["tool"]["docstring-format-checker"]
|
|
361
380
|
|
|
362
|
-
|
|
363
|
-
|
|
381
|
+
def _extract_tool_config(config_data: dict[str, Any]) -> Optional[dict[str, Any]]:
|
|
382
|
+
"""Extract tool configuration from TOML data."""
|
|
383
|
+
if "tool" not in config_data:
|
|
384
|
+
return None
|
|
385
|
+
|
|
386
|
+
tool_section = config_data["tool"]
|
|
387
|
+
if "dfc" in tool_section:
|
|
388
|
+
return tool_section["dfc"]
|
|
389
|
+
elif "docstring-format-checker" in tool_section:
|
|
390
|
+
return tool_section["docstring-format-checker"]
|
|
364
391
|
|
|
365
|
-
|
|
366
|
-
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _parse_global_config(tool_config: dict[str, Any]) -> GlobalConfig:
|
|
396
|
+
"""Parse global configuration flags."""
|
|
397
|
+
return GlobalConfig(
|
|
367
398
|
allow_undefined_sections=tool_config.get("allow_undefined_sections", False),
|
|
368
399
|
require_docstrings=tool_config.get("require_docstrings", True),
|
|
369
400
|
check_private=tool_config.get("check_private", False),
|
|
370
401
|
)
|
|
371
402
|
|
|
372
|
-
|
|
403
|
+
|
|
404
|
+
def _parse_sections_config(tool_config: dict[str, Any]) -> list[SectionConfig]:
|
|
405
|
+
"""Parse sections configuration."""
|
|
406
|
+
if "sections" not in tool_config:
|
|
407
|
+
return DEFAULT_SECTIONS
|
|
408
|
+
|
|
373
409
|
sections_config: list[SectionConfig] = []
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# Use default sections if none provided, otherwise validate and sort
|
|
396
|
-
if not sections_config:
|
|
397
|
-
sections_config = DEFAULT_SECTIONS
|
|
398
|
-
else:
|
|
399
|
-
# Validate no duplicate order values
|
|
400
|
-
_validate_config_order(config_sections=sections_config)
|
|
410
|
+
sections_data = tool_config["sections"]
|
|
411
|
+
|
|
412
|
+
for section_data in sections_data:
|
|
413
|
+
try:
|
|
414
|
+
# Get admonition value with proper default handling
|
|
415
|
+
admonition_value: Union[str, bool] = section_data.get("admonition")
|
|
416
|
+
if admonition_value is None:
|
|
417
|
+
admonition_value = False # Use SectionConfig default
|
|
418
|
+
|
|
419
|
+
section = SectionConfig(
|
|
420
|
+
order=section_data.get("order", 0),
|
|
421
|
+
name=section_data.get("name", ""),
|
|
422
|
+
type=section_data.get("type", ""),
|
|
423
|
+
admonition=admonition_value,
|
|
424
|
+
prefix=section_data.get("prefix", ""),
|
|
425
|
+
required=section_data.get("required", False),
|
|
426
|
+
)
|
|
427
|
+
sections_config.append(section)
|
|
428
|
+
except (KeyError, TypeError, ValueError, InvalidTypeValuesError) as e:
|
|
429
|
+
raise InvalidConfigError(f"Invalid section configuration: {section_data}. Error: {e}") from e
|
|
401
430
|
|
|
402
|
-
|
|
431
|
+
# Validate and sort sections
|
|
432
|
+
if sections_config:
|
|
433
|
+
_validate_config_order(config_sections=sections_config)
|
|
403
434
|
sections_config.sort(key=lambda x: x.order)
|
|
435
|
+
else:
|
|
436
|
+
sections_config = DEFAULT_SECTIONS
|
|
404
437
|
|
|
405
|
-
return
|
|
438
|
+
return sections_config
|
|
406
439
|
|
|
407
440
|
|
|
408
441
|
def find_config_file(start_path: Optional[Path] = None) -> Optional[Path]:
|