diff --git a/.cursor/skills/google-docstring-format/SKILL.md b/.cursor/skills/google-docstring-format/SKILL.md new file mode 100644 index 0000000..f64d067 --- /dev/null +++ b/.cursor/skills/google-docstring-format/SKILL.md @@ -0,0 +1,64 @@ +--- +name: google-docstring-format +description: Write and fix Google-style docstrings that pass the project's check-docstrings pre-commit hook. Use when writing docstrings, fixing check-docstrings failures, ReferenceFormatError, InvalidTypeAnnotationError, or when docstring validation fails. +--- + +# Google Docstring Format (Project) + +## Config (pyproject.toml) + +```toml +[tool.docstring_checker] +paths = ["google_docstring_parser", "tools"] +require_param_types = true +check_references = true +check_type_consistency = true +exclude_files = ["test_malformed_docstrings.py"] +``` + +## Rules + +### Args +- Every parameter **must** have a type: `param_name (type): description` +- Use `list[str]` not `list`; `dict[str, Any]` not `dict`. Bare collections fail validation. +- Types must match function annotations when `check_type_consistency` is true. + +### Returns +- Use `Returns:` (plural), not `Return:` or `return:` or `returns:` +- Must have type: `Returns:\n dict[str, Any]: Description` +- Or just `None` if no return value. + +### References +- **Single reference**: no leading dash + ``` + Reference: + Paper title: https://example.com/paper + ``` +- **Multiple references**: all must start with `-` + ``` + References: + - First paper: https://example.com/paper1 + - Second paper: https://example.com/paper2 + ``` +- Each reference needs non-empty `description` and `source` (colon-separated). + +### Type validation +- `dict`, `list`, `set`, `tuple`, etc. require brackets: `list[str]`, `dict[str, int]` +- No unclosed parentheses in param types +- Brackets must be balanced and matched + +## Fixing errors + +| Error | Fix | +|-------|-----| +| `Parameter 'x' is missing a type` | Add `(type)` after param name | +| `Collection 'list' must include element types` | Use `list[str]` not `list` | +| `missing_dash` / `dash_in_single` | Single ref: no dash. Multiple refs: all start with `-` | +| `Invalid section name 'return:'` | Use `Returns:` | +| `Returns section is missing type annotation` | Add type before colon in Returns | + +## Verify + +```bash +pre-commit run check-docstrings --all-files +``` diff --git a/.cursor/skills/pypi-release/SKILL.md b/.cursor/skills/pypi-release/SKILL.md new file mode 100644 index 0000000..1bf0363 --- /dev/null +++ b/.cursor/skills/pypi-release/SKILL.md @@ -0,0 +1,36 @@ +--- +name: pypi-release +description: Cut a PyPI release - bump version, build, upload. Use when releasing, publishing to PyPI, bumping version, or creating a new release. +--- + +# PyPI Release (Project) + +## Workflow + +1. **Bump version** in `pyproject.toml`: + ```toml + version = "0.0.10" # was 0.0.9 + ``` + +2. **Commit and push**, create GitHub release (tag + publish) + +3. **CI runs** `upload_to_pypi.yml` on `release: published`: + - Builds with `python -m build` + - Uploads with `twine upload dist/*` + - Uses `PYPI_API_TOKEN` secret + +## Manual build/upload (if needed) + +```bash +pip install build twine +python -m build +twine upload dist/* +``` + +Requires `TWINE_USERNAME=__token__` and `TWINE_PASSWORD` (PyPI token). + +## CI job (upload_to_pypi.yml) + +- Trigger: `release: types: [published]` +- Removes `tests` and `benchmark` before build +- Uses `secrets.PYPI_API_TOKEN` diff --git a/.cursor/skills/pytest-parametrize/SKILL.md b/.cursor/skills/pytest-parametrize/SKILL.md new file mode 100644 index 0000000..1a10bfa --- /dev/null +++ b/.cursor/skills/pytest-parametrize/SKILL.md @@ -0,0 +1,48 @@ +--- +name: pytest-parametrize +description: Write pytest tests using parametrize for similar cases, fixtures, and project test layout. Use when adding tests, writing test cases, parametrizing, or when asked to test new code. +--- + +# Pytest and Parametrize (Project) + +## Test layout + +- `tests/test_*.py` for top-level tests +- `tests/test_docstring_checker/` for checker-specific tests +- Use `pytest` and `@pytest.mark.parametrize` + +## Parametrize pattern + +```python +import pytest + +@pytest.mark.parametrize( + "docstring,expected", + [ + ( + """Description. + + Args: + x (int): Param x + """, + {"Description": "Description.", "Args": [{"name": "x", "type": "int", "description": "Param x"}]}, + ), + # More cases... + ], +) +def test_parse(docstring: str, expected: dict) -> None: + assert parse_google_docstring(docstring) == expected +``` + +## Guidelines + +- Use parametrize when testing multiple similar inputs/outputs (same structure, different values) +- Keep each case as a `(input, expected)` tuple for clarity +- Use fixtures for shared setup (e.g. sample docstrings, config) +- Follow project style: type hints on test functions, no `# type: ignore` unless necessary + +## Run tests + +```bash +pytest +``` diff --git a/.cursor/skills/python-pre-commit/SKILL.md b/.cursor/skills/python-pre-commit/SKILL.md new file mode 100644 index 0000000..24185d5 --- /dev/null +++ b/.cursor/skills/python-pre-commit/SKILL.md @@ -0,0 +1,52 @@ +--- +name: python-pre-commit +description: Work with pre-commit hooks, fix pre-commit failures, add or update hooks. Use when pre-commit fails, adding pre-commit hooks, or debugging ruff/mypy/check-docstrings issues. +--- + +# Python Pre-commit (Project) + +## Workflow + +**Do not run ruff or flake8 directly.** Use: + +```bash +pre-commit run --all-files +``` + +## Hooks (from .pre-commit-config.yaml) + +| Hook | Config | Notes | +|------|--------|-------| +| ruff | pyproject.toml | Lint + format. `args: [--fix]` | +| ruff-format | pyproject.toml | Formatter | +| mypy | pyproject.toml | `files: ^(google_docstring_parser\|tests)/` | +| check-docstrings | local | `python -m tools.check_docstrings` | +| pyproject-fmt | - | Formats pyproject.toml | +| codespell | - | Spell check | +| pre-commit-hooks | - | AST, TOML, JSON, etc. | + +## Lint rules + +**Do not disable checks that force refactoring for better code** (e.g. C901 complexity). Fix the code instead. + +## Config locations + +- **Ruff**: `[tool.ruff]` in pyproject.toml (line-length 120, py310, pydocstyle google) +- **Mypy**: `[tool.mypy]` in pyproject.toml (strict: disallow_untyped_defs, etc.) +- **Docstrings**: `[tool.docstring_checker]` in pyproject.toml + +## Fixing failures + +1. Run `pre-commit run --all-files` to see all errors +2. Ruff: fix lint/format, often auto-fixable with `--fix` +3. Mypy: add types, fix annotations +4. check-docstrings: see google-docstring-format skill + +## Add new hook + +Edit `.pre-commit-config.yaml`, then: + +```bash +pre-commit autoupdate # optional: update revs +pre-commit run --all-files +``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a952b2f..db8787e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,45 +12,25 @@ jobs: runs-on: ${{ matrix.operating-system }} strategy: matrix: - operating-system: [ubuntu-latest, windows-latest, macos-13] - python-version: ["3.10", "3.11", "3.12"] - include: - - operating-system: ubuntu-latest - path: ~/.cache/pip - - operating-system: windows-latest - path: ~\AppData\Local\pip\Cache - - operating-system: macos-13 - path: ~/Library/Caches/pip + operating-system: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] fail-fast: true steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv and set Python version ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v7 with: + enable-cache: true python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: | - requirements-dev.txt - - - name: Cache Python packages - uses: actions/cache@v4 - with: - path: ${{ matrix.path }} - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python-version }}- - ${{ runner.os }}-pip- - - - name: Install uv - run: pip install uv + activate-environment: true - name: Install dependencies run: | - uv pip install --system --upgrade pip wheel - uv pip install --system -r requirements-dev.txt - uv pip install --system . + uv pip install wheel + uv pip install . + uv pip install -r requirements-dev.txt - name: Run PyTest with coverage run: pytest @@ -65,22 +45,18 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Install uv and set Python version ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v7 with: + enable-cache: true python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: | - requirements-dev.txt - - - name: Install uv - run: pip install uv + activate-environment: true - name: Install requirements run: | - uv pip install --system --upgrade pip - uv pip install --system -r requirements-dev.txt - uv pip install --system . + uv pip install wheel + uv pip install . + uv pip install -r requirements-dev.txt - name: Run checks run: pre-commit run --all-files diff --git a/.gitignore b/.gitignore index 0a19790..c3fa66d 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,5 @@ cython_debug/ # PyPI configuration file .pypirc + +uv.lock diff --git a/README.md b/README.md index 34ce2c1..8915b19 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ Add a `[tool.docstring_checker]` section to your pyproject.toml: paths = ["src", "tests"] # Directories or files to scan require_param_types = true # Require parameter types in docstrings check_references = true # Check references for proper format +check_type_consistency = true # Compare docstring types with annotations exclude_files = ["conftest.py", "__init__.py"] # Files to exclude from checks +min_short_description_length = 10 # Minimum summary length; set to 0 to disable verbose = false # Enable verbose output ``` diff --git a/google_docstring_parser/google_docstring_parser.py b/google_docstring_parser/google_docstring_parser.py index ba81b5e..57efedd 100644 --- a/google_docstring_parser/google_docstring_parser.py +++ b/google_docstring_parser/google_docstring_parser.py @@ -40,7 +40,7 @@ class ReferenceFormatError(ValueError): - """Error raised when a reference format is invalid. + """Error raised when a reference format is invalid or malformed. Args: code (str): Error code identifying the specific format issue @@ -88,7 +88,7 @@ def __init__(self, line: str) -> None: def _extract_sections(docstring: str) -> dict[str, str]: - """Extract sections from a docstring. + """Extract named sections from a Google-style docstring. Args: docstring (str): The docstring to extract sections from @@ -170,7 +170,7 @@ def _find_separator_colon(content: str) -> int: def _parse_reference_line(line: str, *, is_single: bool = False) -> dict[str, str]: - """Parse a single reference line. + """Parse a single reference line into description and source. Args: line (str): The line to parse @@ -252,7 +252,7 @@ def _identify_main_reference_lines(lines: list[str]) -> list[str]: def _process_single_reference(main_line: str, all_lines: list[str]) -> dict[str, str]: - """Process a single reference entry. + """Process a single reference entry from the References section. Args: main_line (str): The main reference line @@ -285,7 +285,7 @@ def _process_single_reference(main_line: str, all_lines: list[str]) -> dict[str, def _process_multiple_references(lines: list[str]) -> list[dict[str, str]]: - """Process multiple reference entries. + """Process multiple reference entries from the References section. Args: lines (list[str]): Lines containing multiple references @@ -338,7 +338,7 @@ def _process_multiple_references(lines: list[str]) -> list[dict[str, str]]: def _parse_references(reference_content: str) -> list[dict[str, str]]: - """Parse references section content. + """Parse references section content into structured reference entries. Args: reference_content (str): Content of the references section @@ -373,7 +373,7 @@ def _parse_references(reference_content: str) -> list[dict[str, str]]: def _validate_type_with_error_handling(type_str: str, result: dict[str, Any], collect_errors: bool) -> None: - """Validate a type annotation and handle any errors. + """Validate a type annotation and handle any validation errors. This function validates type annotations and handles errors differently based on the collect_errors flag: - When collect_errors is True: Errors are added to result["errors"] list instead of being raised @@ -408,7 +408,7 @@ def _process_args_with_validation( validate_types: bool, collect_errors: bool, ) -> None: - """Process the Args section with type validation. + """Process the Args section with type validation and error collection. Args: sections (dict[str, str]): The sections dictionary @@ -439,7 +439,7 @@ def _process_args_with_validation( def _parse_returns_section(sections: dict[str, str], *, validate_types: bool) -> dict[str, str] | str: - """Process the Returns section of a docstring. + """Process the Returns section of a docstring into type and description. Args: sections (dict[str, str]): The sections dictionary @@ -482,7 +482,7 @@ def _process_returns_with_validation( validate_types: bool, collect_errors: bool, ) -> None: - """Process the Returns section with type validation. + """Process the Returns section with type validation and error handling. Args: sections (dict[str, str]): The sections dictionary @@ -506,7 +506,7 @@ def _process_returns_with_validation( def _process_references_section(sections: dict[str, str], result: dict[str, Any]) -> None: - """Process the References section. + """Process the References section into structured reference entries. Args: sections (dict[str, str]): The sections dictionary @@ -527,7 +527,7 @@ def parse_google_docstring( validate_types: bool = True, collect_errors: bool = True, ) -> dict[str, Any]: - """Parse a Google-style docstring. + """Parse a Google-style docstring into a structured dictionary. Args: docstring (str): The docstring to parse diff --git a/google_docstring_parser/type_validation.py b/google_docstring_parser/type_validation.py index 62ce373..83db1d3 100644 --- a/google_docstring_parser/type_validation.py +++ b/google_docstring_parser/type_validation.py @@ -89,7 +89,7 @@ class InvalidTypeAnnotationError(ValueError): - """Error raised when a type annotation is invalid. + """Error raised when a type annotation is invalid or malformed. Args: message (str): The error message. @@ -100,7 +100,7 @@ class InvalidTypeAnnotationError(ValueError): INVALID_NESTED_TYPE = "Invalid nested type: {}" def __init__(self, message: str) -> None: - """Initialize the error with a message. + """Initialize the error instance with a descriptive message. Args: message (str): The error message. @@ -125,7 +125,7 @@ class BracketValidationError(ValueError): WRONG_BRACKET_TYPE = "Collection '{}' must use square brackets for type arguments, not '{}'" def __init__(self, error_type: str) -> None: - """Initialize with a specific error type. + """Initialize the error with a specific bracket validation type. Args: error_type (str): One of the predefined error types. @@ -137,7 +137,7 @@ def __init__(self, error_type: str) -> None: def is_collection_type(type_name: str) -> bool: - """Check if a type name is a known collection type. + """Check if a type name is a known collection type (list, dict, etc). Args: type_name (str): The type name to check. @@ -261,7 +261,7 @@ def _is_within_string_literal(text: str, position: int) -> bool: def _looks_like_type_annotation(text: str) -> bool: - """Check if text looks like a type annotation. + """Check if text looks like a type annotation using heuristics. Args: text (str): The text to check @@ -277,7 +277,7 @@ def _looks_like_type_annotation(text: str) -> bool: def _process_string_literals(text: str) -> tuple[str, list[str]]: - """Process string literals in text. + """Process string literals in text by replacing them with placeholders. Args: text (str): The text to process @@ -386,7 +386,7 @@ def _check_for_opening_bracket( bracket_stack: list[str], collection_stack: list[tuple[str, str]], ) -> None: - """Check for opening bracket in type declaration. + """Check for opening bracket in type declaration and update stacks. Args: tokens (list[str]): List of tokens @@ -409,7 +409,7 @@ def _check_for_opening_bracket( def _check_for_closing_bracket(token: str, bracket_stack: list[str], collection_stack: list[tuple[str, str]]) -> None: - """Check for closing bracket in type declaration. + """Check for closing bracket in type declaration and validate pairing. Args: token (str): Current token @@ -440,7 +440,7 @@ def _check_for_closing_bracket(token: str, bracket_stack: list[str], collection_ def _check_for_bare_collection(tokens: list[str], i: int, token: str) -> None: - """Check for bare collection type usage. + """Check for bare collection type usage without type arguments. Args: tokens (list[str]): List of tokens @@ -487,7 +487,7 @@ def _is_bare_collection_in_nested_type(token: str, tokens: list[str], i: int, br def _check_tokens_for_collection_type_usage(tokens: list[str]) -> None: - """Check tokens for proper collection type usage. + """Check tokens for proper collection type usage and brackets. Args: tokens (list[str]): List of tokens to check @@ -539,7 +539,7 @@ def _check_tokens_for_collection_type_usage(tokens: list[str]) -> None: def _validate_type_declaration(declaration: str) -> None: - """Validate a type declaration. + """Validate a type declaration for syntax and collection usage. Args: declaration (str): The type declaration to validate diff --git a/pyproject.toml b/pyproject.toml index 9138f38..30ff06b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools>=45", "wheel" ] [project] name = "google-docstring-parser" -version = "0.0.9" +version = "0.0.10" description = "A lightweight, efficient parser for Google-style Python docstrings that converts them into structured dictionaries." readme = "README.md" @@ -119,6 +119,7 @@ lint.per-file-ignores = { "__init__.py" = [ "BLE001", "FBT002", "ANN201", + "PLR0913", ] } lint.fixable = [ "ALL" ] @@ -146,5 +147,6 @@ paths = [ "google_docstring_parser", "tools" ] require_param_types = true check_references = true check_type_consistency = true +min_short_description_length = 50 exclude_files = [ "test_malformed_docstrings.py" ] verbose = false diff --git a/tests/test_docstring_checker/test_check_docstrings.py b/tests/test_docstring_checker/test_check_docstrings.py index 6f55e65..665e2c0 100644 --- a/tests/test_docstring_checker/test_check_docstrings.py +++ b/tests/test_docstring_checker/test_check_docstrings.py @@ -4,9 +4,41 @@ import subprocess import sys from pathlib import Path +from typing import Any import pytest +from tools.check_docstrings import ( + _config_keys_match, + check_short_description_length, +) + + +def test_config_keys_match() -> None: + """Test that DEFAULT_CONFIG and _CONFIG_KEYS stay in sync.""" + assert _config_keys_match(), "DEFAULT_CONFIG and _CONFIG_KEYS must have the same keys" + + +def test_mutually_exclusive_type_consistency_flags(tmp_path: Path) -> None: + """Test that --check-type-consistency and --no-check-type-consistency cannot be used together.""" + test_file = tmp_path / "test.py" + test_file.write_text('"""Module."""\ndef foo(): pass\n') + + result = subprocess.run( + [ + sys.executable, + "-m", + "tools.check_docstrings", + str(test_file), + "--check-type-consistency", + "--no-check-type-consistency", + ], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "not allowed with" in result.stderr + def test_valid_docstrings() -> None: """Test that valid docstrings pass the checker.""" @@ -21,7 +53,9 @@ def test_valid_docstrings() -> None: "tools.check_docstrings", str(valid_dir), "--exclude-files", - "test_malformed_docstrings.py,test_check_docstrings.py" + "test_malformed_docstrings.py,test_check_docstrings.py", + "--min-short-description-length", + "0", ], capture_output=True, text=True, @@ -126,6 +160,9 @@ def test_config_from_pyproject_toml() -> None: # Check that it reads exclude_files from pyproject.toml assert "Exclude files: ['test_malformed_docstrings.py']" in result.stdout, "Should read exclude_files from pyproject.toml" + # Check that it reads min_short_description_length from pyproject.toml + assert "Min short description length: 50" in result.stdout, "Should read min_short_description_length from pyproject.toml" + def test_missing_param_types_in_real_code() -> None: """Test that the checker detects missing parameter types in a real file with missing types.""" @@ -249,6 +286,8 @@ def test_error_count_reporting(code: str, expected_count: int, expected_message: "tools.check_docstrings", str(temp_file), "--require-param-types", + "--min-short-description-length", + "0", ], capture_output=True, text=True, @@ -347,6 +386,8 @@ def test_returns_validation(code: str, expected_returncode: int, expected_output "tools.check_docstrings", str(temp_file), "--verbose", + "--min-short-description-length", + "0", ], capture_output=True, text=True, @@ -545,3 +586,276 @@ def missing_docstring_function(param1): # Check that it shows the empty paths in the configuration output assert "Paths: []" in result.stdout, "Should show empty paths list in configuration" + + +@pytest.mark.parametrize( + "parsed,min_length,expected_errors", + [ + ( + {"Description": "Short."}, + 50, + ["Short description too short (6 chars, min 50): 'Short.'"], + ), + ( + {"Description": "A" * 49}, + 50, + ["Short description too short (49 chars, min 50): 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'"], + ), + ({"Description": "A" * 50}, 50, []), + ({"Description": "A" * 60}, 50, []), + ({"Description": ""}, 50, []), + ({}, 50, []), + ({"Description": "Short."}, 0, []), + ({"Description": "Short."}, 5, []), + ({"Description": "Short."}, 6, []), + ( + {"Description": "Short."}, + 7, + ["Short description too short (6 chars, min 7): 'Short.'"], + ), + ( + {"Description": "First line.\n\nSecond paragraph."}, + 50, + ["Short description too short (11 chars, min 50): 'First line.'"], + ), + ], +) +def test_check_short_description_length( + parsed: dict[str, Any], min_length: int, expected_errors: list[str] +) -> None: + """Test that check_short_description_length validates correctly. + + Args: + parsed (dict): Parsed docstring dict with Description key + min_length (int): Minimum length threshold + expected_errors (list[str]): Expected error messages + """ + result = check_short_description_length(parsed, min_length) + assert result == expected_errors + + +@pytest.mark.parametrize( + "code,expected_returncode,expected_in_output", + [ + # Param type match - no error + ( + ''' +"""Test module with matching types.""" + +def foo(x: int) -> str: + """Function with matching docstring types. + + Args: + x (int): Param x + + Returns: + str: Result + """ + return str(x) +''', + 0, + "", + ), + # Param type mismatch - error + ( + ''' +"""Test module with param type mismatch.""" + +def foo(x: int) -> str: + """Function with wrong docstring type. + + Args: + x (str): Docstring says str, annotation says int + + Returns: + str: Result + """ + return str(x) +''', + 1, + "docstring says 'str' but annotation says 'int'", + ), + # Return type mismatch - error + ( + ''' +"""Test module with return type mismatch.""" + +def foo(x: int) -> str: + """Function with wrong return type in docstring. + + Args: + x (int): Param x + + Returns: + int: Docstring says int, annotation says str + """ + return str(x) +''', + 1, + "Returns: docstring says 'int' but annotation says 'str'", + ), + # Missing annotation in source - skip (no error) + ( + ''' +"""Test module with no annotations.""" + +def foo(x): + """Function with no type annotations in source. + + Args: + x (int): Param x + + Returns: + str: Result + """ + return str(x) +''', + 0, + "", + ), + # self skipped - method with self, only x is compared + ( + ''' +"""Test module with self param.""" + +def method(self, x: int) -> None: + """Method with self. + + Args: + x (int): Param x + + Returns: + None + """ + pass +''', + 0, + "", + ), + # pos-only param type mismatch + ( + ''' +"""Test module with pos-only param mismatch.""" + +def foo(x: int, /, y: str) -> None: + """Function with pos-only param. + + Args: + x (str): Docstring says str, annotation says int + y (str): Correct + + Returns: + None + """ + pass +''', + 1, + "docstring says 'str' but annotation says 'int'", + ), + # kw-only param type mismatch + ( + ''' +"""Test module with kw-only param mismatch.""" + +def foo(x: int, *, y: str) -> None: + """Function with kw-only param. + + Args: + x (int): Correct + y (int): Docstring says int, annotation says str + + Returns: + None + """ + pass +''', + 1, + "docstring says 'int' but annotation says 'str'", + ), + # *args type mismatch + ( + ''' +"""Test module with *args type mismatch.""" + +def foo(*args: int) -> None: + """Function with varargs. + + Args: + args (str): Docstring says str, annotation says int + + Returns: + None + """ + pass +''', + 1, + "docstring says 'str' but annotation says 'int'", + ), + # Returns string "None" vs annotation str - mismatch detected + ( + ''' +"""Test module with Returns as string None.""" + +def foo() -> str: + """Function returning str but docstring says None. + + Returns: + None + """ + return "x" +''', + 1, + "Returns: docstring says 'None' but annotation says 'str'", + ), + # Whitespace normalization: tuple[int, str] vs tuple[int,str] - no error + ( + ''' +"""Test module with whitespace in type.""" + +def foo(x: tuple[int, str]) -> int | None: + """Function with types that may have different whitespace. + + Args: + x (tuple[int, str]): Param with spaces in docstring + + Returns: + int | None: Union with spaces + """ + return None +''', + 0, + "", + ), + ], +) +def test_check_type_consistency( + code: str, expected_returncode: int, expected_in_output: str, tmp_path: Path +) -> None: + """Test that check_type_consistency compares docstring types with annotations. + + Args: + code (str): Python code to test + expected_returncode (int): Expected return code + expected_in_output (str): Expected substring in output (empty for success) + tmp_path (Path): Temporary directory fixture + """ + temp_file = tmp_path / "test_file.py" + temp_file.write_text(code) + + result = subprocess.run( + [ + sys.executable, + "-m", + "tools.check_docstrings", + str(temp_file), + "--check-type-consistency", + "--min-short-description-length", + "0", + ], + capture_output=True, + text=True, + ) + + assert result.returncode == expected_returncode + if expected_in_output: + assert expected_in_output in result.stdout diff --git a/tests/test_docstring_checker/test_example_files.py b/tests/test_docstring_checker/test_example_files.py index 58146f3..7a3b341 100644 --- a/tests/test_docstring_checker/test_example_files.py +++ b/tests/test_docstring_checker/test_example_files.py @@ -11,14 +11,24 @@ def test_valid_docstrings_file() -> None: """Test that the valid docstrings file passes the checker.""" valid_file = Path(__file__).parent / "test_valid_docstrings.py" - errors = check_file(valid_file, require_param_types=False, verbose=True) + errors = check_file( + valid_file, + require_param_types=False, + verbose=True, + min_short_description_length=0, + ) assert not errors, f"Found errors in valid docstrings file: {errors}" def test_malformed_docstrings_file() -> None: """Test that the malformed docstrings file fails the checker.""" malformed_file = Path(__file__).parent / "test_malformed_docstrings.py" - errors = check_file(malformed_file, require_param_types=False, verbose=True) + errors = check_file( + malformed_file, + require_param_types=False, + verbose=True, + min_short_description_length=0, + ) # Check that we found the expected errors assert errors, "No errors found in malformed docstrings file" @@ -33,10 +43,20 @@ def test_require_param_types_on_malformed_file() -> None: malformed_file = Path(__file__).parent / "test_malformed_docstrings.py" # First check without requiring types - errors_without_types = check_file(malformed_file, require_param_types=False, verbose=True) + errors_without_types = check_file( + malformed_file, + require_param_types=False, + verbose=True, + min_short_description_length=0, + ) # Then check with requiring types - errors_with_types = check_file(malformed_file, require_param_types=True, verbose=True) + errors_with_types = check_file( + malformed_file, + require_param_types=True, + verbose=True, + min_short_description_length=0, + ) # Should find more errors when requiring types assert len(errors_with_types) > len(errors_without_types) @@ -56,6 +76,7 @@ def test_scan_directory() -> None: exclude_files=["test_malformed_docstrings.py"], require_param_types=False, verbose=True, + min_short_description_length=0, ) # Scan without excluding the malformed file @@ -64,6 +85,7 @@ def test_scan_directory() -> None: exclude_files=[], require_param_types=False, verbose=True, + min_short_description_length=0, ) # Should find more errors when including the malformed file @@ -82,7 +104,12 @@ def test_scan_directory() -> None: def test_parametrized_file_checks(filename: str, require_types: bool, expected_error_count: int) -> None: """Test checking different files with different settings.""" file_path = Path(__file__).parent / filename - errors = check_file(file_path, require_param_types=require_types, verbose=True) + errors = check_file( + file_path, + require_param_types=require_types, + verbose=True, + min_short_description_length=0, + ) # Check that we found at least the expected number of errors assert len(errors) >= expected_error_count, ( diff --git a/tests/test_docstring_checker/test_valid_docstrings.py b/tests/test_docstring_checker/test_valid_docstrings.py index 4625247..7a02bc6 100644 --- a/tests/test_docstring_checker/test_valid_docstrings.py +++ b/tests/test_docstring_checker/test_valid_docstrings.py @@ -2,7 +2,7 @@ This file contains properly formatted docstrings to test the docstring checker. """ -from typing import Dict, List, Any, Union +from typing import Any def simple_function() -> bool: @@ -23,14 +23,14 @@ def function_with_args(param1: int, param2: str) -> bool: return True -def function_with_sections(param: Dict[str, Any]) -> List[Any]: +def function_with_sections(param: dict[str, Any]) -> list[Any]: """Function with multiple sections. Args: param (dict[str, Any]): A dictionary parameter Returns: - list[str]: A list of items + list[Any]: A list of items Raises: ValueError: If param is empty diff --git a/tools/README.md b/tools/README.md index 89eeee1..9720e28 100644 --- a/tools/README.md +++ b/tools/README.md @@ -43,6 +43,12 @@ require_param_types = true # Whether to check references for proper format check_references = true +# Whether to compare docstring types with function annotations +check_type_consistency = true + +# Minimum short description length (0 to disable) +min_short_description_length = 0 + # List of filenames to exclude from checks # These can be just filenames (e.g., "conftest.py") or paths ending with the filename exclude_files = ["conftest.py", "__init__.py", "tests/fixtures/bad_docstrings.py"] @@ -57,6 +63,10 @@ verbose = false When `require_param_types = true`, the hook will check if all parameters in docstrings have their types specified. This helps ensure consistent documentation across your codebase. +#### Type Consistency Checking + +When `check_type_consistency = true`, the hook compares docstring types (Args, Returns) with Python function annotations. Mismatches are reported (e.g., docstring says `int` but annotation says `str`). Use Python 3.10+ style: `list[str]`, `dict[str, Any]`, `tuple[int, str]`, `X | Y` — not `List`, `Dict`, `Tuple`, `Union`. + #### Reference Checking When `check_references = true`, the hook will validate the References section in docstrings for proper formatting. It checks for: @@ -91,6 +101,8 @@ References: paths = ["src", "tests"] require_param_types = true check_references = true +check_type_consistency = true +min_short_description_length = 0 exclude_files = ["conftest.py", "__init__.py"] verbose = false ``` @@ -116,5 +128,8 @@ Command line options: - `--require-param-types`: Require parameter types in docstrings - `--check-references`: Check references for proper format - `--no-check-references`: Skip reference checking +- `--check-type-consistency`: Compare docstring types with function annotations +- `--no-check-type-consistency`: Skip type consistency checking +- `--min-short-description-length N`: Minimum short description length (0 to disable) - `--exclude-files`: Comma-separated list of filenames to exclude - `-v, --verbose`: Enable verbose output diff --git a/tools/check_docstrings.py b/tools/check_docstrings.py index 677e939..f577fd5 100755 --- a/tools/check_docstrings.py +++ b/tools/check_docstrings.py @@ -20,18 +20,23 @@ parse_google_docstring, ) +# Preview length for short description error messages +SHORT_DESC_PREVIEW_LENGTH = 50 + # Default configuration DEFAULT_CONFIG = { "paths": [], # Empty by default, so no directories are scanned unless explicitly specified "require_param_types": False, "check_references": True, + "check_type_consistency": False, + "min_short_description_length": 0, "exclude_files": [], "verbose": False, } class DocstringContext(NamedTuple): - """Context for docstring processing. + """Context for docstring processing, validation, and error reporting. Args: file_path (Path): Path to the file @@ -40,6 +45,9 @@ class DocstringContext(NamedTuple): verbose (bool): Whether to print verbose output require_param_types (bool): Whether parameter types are required check_references (bool): Whether to check references for errors + check_type_consistency (bool): Whether to compare docstring types with annotations + min_short_description_length (int): Minimum length for short description + node (ast.AST | None): AST node for the function or class Returns: DocstringContext: A named tuple containing docstring processing context @@ -51,6 +59,33 @@ class DocstringContext(NamedTuple): verbose: bool require_param_types: bool = False check_references: bool = True + check_type_consistency: bool = False + min_short_description_length: int = 0 + node: ast.AST | None = None + + +_CONFIG_KEYS: dict[str, tuple[str, type]] = { + "paths": ("paths", list), + "require_param_types": ("require_param_types", bool), + "check_references": ("check_references", bool), + "check_type_consistency": ("check_type_consistency", bool), + "min_short_description_length": ("min_short_description_length", int), + "exclude_files": ("exclude_files", list), + "verbose": ("verbose", bool), +} + + +def _config_keys_match() -> bool: + """Return True if DEFAULT_CONFIG and _CONFIG_KEYS have the same keys.""" + return set(DEFAULT_CONFIG.keys()) == set(_CONFIG_KEYS.keys()) + + +def _apply_tool_config(config: dict[str, Any], tool_config: dict[str, Any]) -> None: + """Apply tool_config values to config. Modifies config in place.""" + for key, (config_key, converter) in _CONFIG_KEYS.items(): + if key in tool_config: + raw = tool_config[key] + config[config_key] = raw if converter is list else converter(raw) def load_pyproject_config() -> dict[str, Any]: @@ -60,8 +95,6 @@ def load_pyproject_config() -> dict[str, Any]: dict[str, Any]: Dictionary with configuration values """ config = DEFAULT_CONFIG.copy() - - # Look for pyproject.toml in the current directory pyproject_path = Path("pyproject.toml") if not pyproject_path.is_file(): return config @@ -69,24 +102,10 @@ def load_pyproject_config() -> dict[str, Any]: try: with pyproject_path.open("rb") as f: pyproject_data = tomli.load(f) - - # Check if our tool is configured tool_config = pyproject_data.get("tool", {}).get("docstring_checker", {}) if not tool_config: return config - - # Update config with values from pyproject.toml - if "paths" in tool_config: - config["paths"] = tool_config["paths"] - if "require_param_types" in tool_config: - config["require_param_types"] = bool(tool_config["require_param_types"]) - if "check_references" in tool_config: - config["check_references"] = bool(tool_config["check_references"]) - if "exclude_files" in tool_config: - config["exclude_files"] = tool_config["exclude_files"] - if "verbose" in tool_config: - config["verbose"] = bool(tool_config["verbose"]) - + _apply_tool_config(config, tool_config) except Exception as e: print(f"Warning: Failed to load configuration from pyproject.toml: {e}") @@ -94,7 +113,7 @@ def load_pyproject_config() -> dict[str, Any]: def get_docstrings(file_path: Path) -> list[tuple[str, int, str | None, ast.AST | None]]: - """Extract docstrings from a Python file. + """Extract docstrings from a Python file using AST parsing. Args: file_path (Path): Path to the Python file @@ -128,7 +147,7 @@ def get_docstrings(file_path: Path) -> list[tuple[str, int, str | None, ast.AST def check_param_types(docstring_dict: dict[str, Any], require_types: bool) -> list[str]: - """Check if all parameters have types if required. + """Check if all parameters have types when types are required. Args: docstring_dict (dict[str, Any]): Parsed docstring dictionary @@ -150,6 +169,111 @@ def check_param_types(docstring_dict: dict[str, Any], require_types: bool) -> li return errors +def _normalize_type(type_str: str) -> str: + """Normalize type string for comparison (quotes and whitespace only). + + Python 3.10+ typing uses list, dict, tuple, X|Y - no List, Dict, Tuple, Union. + We do not normalize those; mismatches will be reported. + Internal whitespace differences (e.g., around commas, |, or brackets) are ignored. + + Args: + type_str (str): Type string to normalize + + Returns: + str: Normalized type string + """ + normalized = type_str.strip().strip("'\"") + return re.sub(r"\s+", "", normalized) + + +def _annotation_to_str(annotation: ast.expr | None) -> str | None: + """Extract the type string from an AST annotation node. + + Args: + annotation (ast.expr | None): AST annotation node + + Returns: + str | None: Type string or None if no annotation + """ + if annotation is None: + return None + if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str): + return annotation.value + return ast.unparse(annotation) + + +def _get_ast_param_types(node: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, str]: + """Extract parameter names and type strings from function AST. + + Args: + node (ast.FunctionDef | ast.AsyncFunctionDef): Function AST node + + Returns: + dict[str, str]: Map of param name to annotation string (skips self/cls) + """ + ast_params: dict[str, str] = {} + all_args: list[ast.arg] = [] + all_args.extend(node.args.posonlyargs) + all_args.extend(node.args.args) + all_args.extend(node.args.kwonlyargs) + if node.args.vararg is not None: + all_args.append(node.args.vararg) + if node.args.kwarg is not None: + all_args.append(node.args.kwarg) + for arg in all_args: + if arg.arg in ("self", "cls"): + continue + if ann_str := _annotation_to_str(arg.annotation): + ast_params[arg.arg] = ann_str + return ast_params + + +def check_type_consistency( + parsed: dict[str, Any], + node: ast.FunctionDef | ast.AsyncFunctionDef, +) -> list[str]: + """Compare docstring types with function annotations. + + Args: + parsed (dict[str, Any]): Parsed docstring dictionary + node (ast.FunctionDef | ast.AsyncFunctionDef): Function AST node + + Returns: + list[str]: List of error messages for type mismatches + """ + errors = [] + ast_params = _get_ast_param_types(node) + + # Compare Args + for arg in parsed.get("Args", []): + doc_type = arg.get("type") + if not doc_type: + continue + param_name = arg.get("name") + if not param_name or param_name not in ast_params: + continue + ast_type = ast_params[param_name] + if _normalize_type(doc_type) != _normalize_type(ast_type): + errors.append( + f"Parameter '{param_name}': docstring says '{doc_type}' but annotation says '{ast_type}'", + ) + + # Compare Returns (handle both dict and string "None" from parse_google_docstring) + returns = parsed.get("Returns") + doc_ret: str | None = None + if isinstance(returns, dict): + doc_ret = returns.get("type") + elif isinstance(returns, str): + doc_ret = returns + ast_ret = _annotation_to_str(node.returns) + if doc_ret and ast_ret and _normalize_type(doc_ret) != _normalize_type(ast_ret): + errors.append( + f"Returns: docstring says '{doc_ret}' but annotation says '{ast_ret}'", + ) + + return errors + + def _check_reference_fields(reference: dict[str, Any], index: int) -> list[str]: """Check a single reference for missing or empty fields. @@ -177,7 +301,7 @@ def _check_reference_fields(reference: dict[str, Any], index: int) -> list[str]: def check_references(docstring_dict: dict[str, Any]) -> list[str]: - """Check references section for common errors. + """Check references section for common formatting errors. Args: docstring_dict (dict[str, Any]): Parsed docstring dictionary @@ -216,7 +340,7 @@ def check_references(docstring_dict: dict[str, Any]) -> list[str]: def validate_docstring(docstring: str) -> list[str]: - """Perform additional validation on a docstring. + """Perform additional validation on docstring format and structure. Args: docstring (str): The docstring to validate @@ -250,7 +374,7 @@ def validate_docstring(docstring: str) -> list[str]: def check_returns_section_name(docstring: str) -> list[str]: - """Check for incorrect Returns section names. + """Check for incorrect Returns section names (e.g. return vs Returns). Args: docstring (str): The docstring to check @@ -267,8 +391,37 @@ def check_returns_section_name(docstring: str) -> list[str]: return errors +def check_short_description_length(parsed: dict[str, Any], min_length: int) -> list[str]: + """Check that the short description meets the minimum length requirement. + + Args: + parsed (dict[str, Any]): Parsed docstring dictionary + min_length (int): Minimum length for short description (0 to disable) + + Returns: + list[str]: List of error messages for short descriptions that are too short + """ + if min_length <= 0: + return [] + + short_desc = (parsed.get("Description") or "").split("\n")[0].strip() + if not short_desc: + return [] + + if len(short_desc) < min_length: + preview = ( + short_desc[:SHORT_DESC_PREVIEW_LENGTH] + "..." + if len(short_desc) > SHORT_DESC_PREVIEW_LENGTH + else short_desc + ) + return [ + f"Short description too short ({len(short_desc)} chars, min {min_length}): '{preview}'", + ] + return [] + + def check_returns_type(docstring_dict: dict[str, Any]) -> list[str]: - """Check Returns type in a docstring.""" + """Check that the Returns section has proper type annotation.""" errors = [] if returns := docstring_dict.get("Returns"): # Special case: Returns section just contains "None" @@ -286,7 +439,7 @@ def check_returns_type(docstring_dict: dict[str, Any]) -> list[str]: def _format_error(context: DocstringContext, error: str) -> str: - """Format an error message consistently. + """Format an error message consistently with file, line, and name. Args: context (DocstringContext): Docstring context @@ -333,7 +486,7 @@ def safe_execute( def _check_returns_section(context: DocstringContext, docstring: str) -> list[str]: - """Check the Returns section name. + """Check the Returns section name for correct spelling. Args: context (DocstringContext): Docstring context @@ -352,7 +505,7 @@ def _check_returns_section(context: DocstringContext, docstring: str) -> list[st def _validate_docstring_format(context: DocstringContext, docstring: str) -> list[str]: - """Validate docstring format. + """Validate docstring format for common structural issues. Args: context (DocstringContext): Docstring context @@ -371,7 +524,7 @@ def _validate_docstring_format(context: DocstringContext, docstring: str) -> lis def _parse_and_check_returns(context: DocstringContext, docstring: str) -> tuple[list[str], dict[str, Any] | None]: - """Parse docstring and check returns type. + """Parse docstring and check that the Returns section has proper type. Args: context (DocstringContext): Docstring context @@ -408,7 +561,7 @@ def _parse_and_check_returns(context: DocstringContext, docstring: str) -> tuple def _check_additional_validations(context: DocstringContext, parsed: dict[str, Any]) -> list[str]: - """Run additional validations on parsed docstring. + """Run additional validations on the parsed docstring dictionary. Args: context (DocstringContext): Docstring context @@ -438,11 +591,35 @@ def _check_additional_validations(context: DocstringContext, parsed: dict[str, A ) errors.extend(ref_errors) + if context.min_short_description_length > 0: + length_errors, _ = safe_execute( + context, + check_short_description_length, + parsed, + context.min_short_description_length, + error_prefix="Error checking short description length", + ) + errors.extend(length_errors) + + if ( + context.check_type_consistency + and context.node is not None + and isinstance(context.node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ): + consistency_errors, _ = safe_execute( + context, + check_type_consistency, + parsed, + context.node, + error_prefix="Error checking type consistency", + ) + errors.extend(consistency_errors) + return errors def _process_docstring(context: DocstringContext, docstring: str) -> list[str]: - """Process a single docstring. + """Process a single docstring and collect all validation errors. Args: context (DocstringContext): Docstring context @@ -482,14 +659,18 @@ def check_file( require_param_types: bool = False, verbose: bool = False, check_references: bool = True, + check_type_consistency: bool = False, + min_short_description_length: int = 0, ) -> list[str]: - """Check docstrings in a file. + """Check docstrings in a Python file for parsing and validation errors. Args: file_path (Path): Path to the Python file require_param_types (bool): Whether parameter types are required verbose (bool): Whether to print verbose output check_references (bool): Whether to check references for errors + check_type_consistency (bool): Whether to compare docstring types with annotations + min_short_description_length (int): Minimum length for short description (0 to disable) Returns: list[str]: List of error messages @@ -508,7 +689,7 @@ def check_file( print(error_msg) return errors - for name, line_no, docstring, _ in docstrings: + for name, line_no, docstring, node in docstrings: context = DocstringContext( file_path=file_path, line_no=line_no, @@ -516,6 +697,9 @@ def check_file( verbose=verbose, require_param_types=require_param_types, check_references=check_references, + check_type_consistency=check_type_consistency, + min_short_description_length=min_short_description_length, + node=node, ) errors.extend(_process_docstring(context, docstring)) @@ -528,6 +712,8 @@ def scan_directory( require_param_types: bool = False, verbose: bool = False, check_references: bool = True, + check_type_consistency: bool = False, + min_short_description_length: int = 0, ) -> list[str]: """Scan a directory for Python files and check their docstrings. @@ -537,6 +723,8 @@ def scan_directory( require_param_types (bool): Whether parameter types are required verbose (bool): Whether to print verbose output check_references (bool): Whether to check references for errors + check_type_consistency (bool): Whether to compare docstring types with annotations + min_short_description_length (int): Minimum length for short description (0 to disable) Returns: list[str]: List of error messages @@ -559,12 +747,21 @@ def scan_directory( break if not should_exclude: - errors.extend(check_file(py_file, require_param_types, verbose, check_references)) + errors.extend( + check_file( + py_file, + require_param_types, + verbose, + check_references, + check_type_consistency, + min_short_description_length, + ), + ) return errors def _parse_args() -> argparse.Namespace: - """Parse command line arguments. + """Parse command line arguments for the docstring checker. Returns: argparse.Namespace: Parsed command line arguments @@ -582,29 +779,47 @@ def _parse_args() -> argparse.Namespace: action="store_true", help="Require parameter types in docstrings", ) - parser.add_argument( + ref_group = parser.add_mutually_exclusive_group() + ref_group.add_argument( "--check-references", action="store_true", help="Check references for errors", ) - parser.add_argument( + ref_group.add_argument( "--no-check-references", action="store_true", help="Skip reference checking", ) + type_consistency_group = parser.add_mutually_exclusive_group() + type_consistency_group.add_argument( + "--check-type-consistency", + action="store_true", + help="Compare docstring types with function annotations", + ) + type_consistency_group.add_argument( + "--no-check-type-consistency", + action="store_true", + help="Skip type consistency checking", + ) parser.add_argument( "--exclude-files", help="Comma-separated list of filenames to exclude", default="", ) parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") + parser.add_argument( + "--min-short-description-length", + type=int, + metavar="N", + help="Minimum length for short description (0 to disable)", + ) return parser.parse_args() def _get_config_values( args: argparse.Namespace, config: dict[str, Any], -) -> tuple[list[str], bool, bool, bool, list[str]]: +) -> tuple[list[str], bool, bool, bool, bool, int, list[str]]: """Get configuration values from command line arguments and config file. Args: @@ -612,11 +827,13 @@ def _get_config_values( config (dict[str, Any]): Configuration dictionary Returns: - tuple[list[str], bool, bool, bool, list[str]]: Tuple containing: + tuple[list[str], bool, bool, bool, bool, int, list[str]]: Tuple containing: - List of paths to check - Whether to require parameter types - - Whether to check references - Whether to enable verbose output + - Whether to check references + - Whether to check type consistency + - Minimum short description length - List of files to exclude """ # Get paths @@ -628,13 +845,20 @@ def _get_config_values( # Get verbose verbose = args.verbose or config["verbose"] - # Get check_references - handle both positive and negative flags + # Get check_references - handle both positive and negative flags (mutually exclusive) check_references = config["check_references"] if args.check_references: check_references = True if args.no_check_references: check_references = False + # Get check_type_consistency - handle both positive and negative flags (mutually exclusive) + check_type_consistency = config.get("check_type_consistency", False) + if args.check_type_consistency: + check_type_consistency = True + if args.no_check_type_consistency: + check_type_consistency = False + # Get exclude_files exclude_files = [] if args.exclude_files: @@ -644,7 +868,20 @@ def _get_config_values( if not exclude_files: exclude_files = config["exclude_files"] - return paths, require_param_types, verbose, check_references, exclude_files + # Get min_short_description_length - CLI overrides config + min_short_description_length = config.get("min_short_description_length", 0) + if args.min_short_description_length is not None: + min_short_description_length = args.min_short_description_length + + return ( + paths, + require_param_types, + verbose, + check_references, + check_type_consistency, + min_short_description_length, + exclude_files, + ) def _process_paths( @@ -653,8 +890,10 @@ def _process_paths( require_param_types: bool, verbose: bool, check_references: bool, + check_type_consistency: bool, + min_short_description_length: int, ) -> list[str]: - """Process paths and check docstrings. + """Process paths and check docstrings in each file or directory. Args: paths (list[str]): List of paths to check @@ -662,6 +901,8 @@ def _process_paths( require_param_types (bool): Whether parameter types are required verbose (bool): Whether to print verbose output check_references (bool): Whether to check references for errors + check_type_consistency (bool): Whether to compare docstring types with annotations + min_short_description_length (int): Minimum length for short description (0 to disable) Returns: list[str]: List of error messages @@ -670,10 +911,25 @@ def _process_paths( for path_str in paths: path = Path(path_str) if path.is_dir(): - errors = scan_directory(path, exclude_files, require_param_types, verbose, check_references) + errors = scan_directory( + path, + exclude_files, + require_param_types, + verbose, + check_references, + check_type_consistency, + min_short_description_length, + ) all_errors.extend(errors) elif path.is_file() and path.suffix == ".py": - errors = check_file(path, require_param_types, verbose, check_references) + errors = check_file( + path, + require_param_types, + verbose, + check_references, + check_type_consistency, + min_short_description_length, + ) all_errors.extend(errors) else: print(f"Error: {path} is not a directory or Python file") @@ -681,7 +937,7 @@ def _process_paths( def main() -> None: - """Run the docstring checker. + """Run the docstring checker and exit with appropriate status code. Returns: None @@ -693,7 +949,15 @@ def main() -> None: args = _parse_args() # Get configuration values - paths, require_param_types, verbose, check_references, exclude_files = _get_config_values(args, config) + ( + paths, + require_param_types, + verbose, + check_references, + check_type_consistency, + min_short_description_length, + exclude_files, + ) = _get_config_values(args, config) # Print configuration if verbose if verbose: @@ -701,6 +965,8 @@ def main() -> None: print(f" Paths: {paths}") print(f" Require parameter types: {require_param_types}") print(f" Check references: {check_references}") + print(f" Check type consistency: {check_type_consistency}") + print(f" Min short description length: {min_short_description_length}") print(f" Exclude files: {exclude_files}") # Check if paths is empty @@ -717,6 +983,8 @@ def main() -> None: require_param_types, verbose, check_references, + check_type_consistency, + min_short_description_length, ): for error in all_errors: print(error)