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

1"""Pydoclint tool definition. 

2 

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. 

6 

7Configuration is read directly from [tool.pydoclint] in pyproject.toml. 

8See docs/tool-analysis/pydoclint-analysis.md for recommended settings. 

9""" 

10 

11from __future__ import annotations 

12 

13import subprocess # nosec B404 - used safely with shell disabled 

14from dataclasses import dataclass 

15 

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 

24 

25# Constants for Pydoclint configuration 

26PYDOCLINT_DEFAULT_TIMEOUT: int = 30 

27PYDOCLINT_DEFAULT_PRIORITY: int = 45 

28PYDOCLINT_FILE_PATTERNS: list[str] = ["*.py", "*.pyi"] 

29 

30 

31@register_tool 

32@dataclass 

33class PydoclintPlugin(BaseToolPlugin): 

34 """Pydoclint Python docstring linter plugin. 

35 

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 """ 

40 

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 ) 

63 

64 def _build_command(self) -> list[str]: 

65 """Build the pydoclint command. 

66 

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"] 

71 

72 if self.options.get("quiet", True): 

73 cmd.append("--quiet") 

74 

75 return cmd 

76 

77 def _process_single_file( 

78 self, 

79 file_path: str, 

80 timeout: int, 

81 ) -> FileProcessingResult: 

82 """Process a single Python file with pydoclint. 

83 

84 Args: 

85 file_path: Path to the Python file to process. 

86 timeout: Timeout in seconds for the pydoclint command. 

87 

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 ) 

114 

115 def doc_url(self, code: str) -> str | None: 

116 """Return pydoclint documentation URL. 

117 

118 Pydoclint uses a single configuration page for all rules. 

119 

120 Args: 

121 code: Pydoclint code (e.g., "DOC301"). 

122 

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 

129 

130 def check(self, paths: list[str], options: dict[str, object]) -> ToolResult: 

131 """Check files with pydoclint. 

132 

133 Args: 

134 paths: List of file or directory paths to check. 

135 options: Runtime options that override defaults. 

136 

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] 

144 

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 ) 

150 

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 ) 

158 

159 def fix(self, paths: list[str], options: dict[str, object]) -> ToolResult: 

160 """Pydoclint cannot fix issues, only report them. 

161 

162 Args: 

163 paths: List of file or directory paths to fix. 

164 options: Tool-specific options. 

165 

166 Returns: 

167 ToolResult: Never returns, always raises NotImplementedError. 

168 

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 )