Coverage for lintro / tools / definitions / pydoclint.py: 96%
47 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Pydoclint tool definition.
3Pydoclint is a Python docstring linter that validates docstrings match
4function signatures. It checks for missing, extra, or incorrectly documented
5parameters, return values, and raised exceptions.
7Configuration is read directly from [tool.pydoclint] in pyproject.toml.
8See docs/tool-analysis/pydoclint-analysis.md for recommended settings.
9"""
11from __future__ import annotations
13import subprocess # nosec B404 - used safely with shell disabled
14from dataclasses import dataclass
16from lintro.enums.doc_url_template import DocUrlTemplate
17from lintro.enums.tool_type import ToolType
18from lintro.models.core.tool_result import ToolResult
19from lintro.parsers.pydoclint.pydoclint_parser import parse_pydoclint_output
20from lintro.plugins.base import BaseToolPlugin
21from lintro.plugins.file_processor import FileProcessingResult
22from lintro.plugins.protocol import ToolDefinition
23from lintro.plugins.registry import register_tool
25# Constants for Pydoclint configuration
26PYDOCLINT_DEFAULT_TIMEOUT: int = 30
27PYDOCLINT_DEFAULT_PRIORITY: int = 45
28PYDOCLINT_FILE_PATTERNS: list[str] = ["*.py", "*.pyi"]
31@register_tool
32@dataclass
33class PydoclintPlugin(BaseToolPlugin):
34 """Pydoclint Python docstring linter plugin.
36 This plugin integrates pydoclint with Lintro for validating Python
37 docstrings match function signatures. Pydoclint reads its configuration
38 directly from [tool.pydoclint] in pyproject.toml.
39 """
41 @property
42 def definition(self) -> ToolDefinition:
43 """Return the tool definition."""
44 return ToolDefinition(
45 name="pydoclint",
46 description=(
47 "Python docstring linter that validates docstrings match "
48 "function signatures"
49 ),
50 can_fix=False,
51 tool_type=ToolType.LINTER | ToolType.DOCUMENTATION,
52 file_patterns=PYDOCLINT_FILE_PATTERNS,
53 priority=PYDOCLINT_DEFAULT_PRIORITY,
54 conflicts_with=[],
55 native_configs=["pyproject.toml", ".pydoclint.toml"],
56 version_command=["pydoclint", "--version"],
57 default_options={
58 "timeout": PYDOCLINT_DEFAULT_TIMEOUT,
59 "quiet": True,
60 },
61 default_timeout=PYDOCLINT_DEFAULT_TIMEOUT,
62 )
64 def _build_command(self) -> list[str]:
65 """Build the pydoclint command.
67 pydoclint reads most options from [tool.pydoclint] in pyproject.toml.
68 We only add --quiet for cleaner lintro output.
69 """
70 cmd: list[str] = ["pydoclint"]
72 if self.options.get("quiet", True):
73 cmd.append("--quiet")
75 return cmd
77 def _process_single_file(
78 self,
79 file_path: str,
80 timeout: int,
81 ) -> FileProcessingResult:
82 """Process a single Python file with pydoclint.
84 Args:
85 file_path: Path to the Python file to process.
86 timeout: Timeout in seconds for the pydoclint command.
88 Returns:
89 FileProcessingResult with processing outcome.
90 """
91 cmd = self._build_command() + [str(file_path)]
92 try:
93 success, output = self._run_subprocess(cmd=cmd, timeout=timeout)
94 issues = parse_pydoclint_output(output=output)
95 return FileProcessingResult(
96 success=success and len(issues) == 0,
97 output=output,
98 issues=issues,
99 )
100 except subprocess.TimeoutExpired:
101 return FileProcessingResult(
102 success=False,
103 output="",
104 issues=[],
105 skipped=True,
106 )
107 except (OSError, ValueError, RuntimeError) as e:
108 return FileProcessingResult(
109 success=False,
110 output="",
111 issues=[],
112 error=str(e),
113 )
115 def doc_url(self, code: str) -> str | None:
116 """Return pydoclint documentation URL.
118 Pydoclint uses a single configuration page for all rules.
120 Args:
121 code: Pydoclint code (e.g., "DOC301").
123 Returns:
124 URL to the pydoclint documentation, or None if code is empty.
125 """
126 if not code:
127 return None
128 return DocUrlTemplate.PYDOCLINT
130 def check(self, paths: list[str], options: dict[str, object]) -> ToolResult:
131 """Check files with pydoclint.
133 Args:
134 paths: List of file or directory paths to check.
135 options: Runtime options that override defaults.
137 Returns:
138 ToolResult with check results.
139 """
140 ctx = self._prepare_execution(paths=paths, options=options)
141 if ctx.should_skip:
142 # early_result is guaranteed to be ToolResult when should_skip=True
143 return ctx.early_result # type: ignore[return-value]
145 result = self._process_files_with_progress(
146 files=ctx.files,
147 processor=lambda f: self._process_single_file(f, ctx.timeout),
148 timeout=ctx.timeout,
149 )
151 return ToolResult(
152 name=self.definition.name,
153 success=result.all_success and result.total_issues == 0,
154 output=result.build_output(timeout=ctx.timeout),
155 issues_count=result.total_issues,
156 issues=result.all_issues,
157 )
159 def fix(self, paths: list[str], options: dict[str, object]) -> ToolResult:
160 """Pydoclint cannot fix issues, only report them.
162 Args:
163 paths: List of file or directory paths to fix.
164 options: Tool-specific options.
166 Returns:
167 ToolResult: Never returns, always raises NotImplementedError.
169 Raises:
170 NotImplementedError: Pydoclint does not support fixing issues.
171 """
172 raise NotImplementedError(
173 "Pydoclint cannot automatically fix issues. Run 'lintro check' to see "
174 "issues.",
175 )