Coverage for /builds/svesch/cikit/src/cikit/macros/pylint.py: 98%

68 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-09 14:17 +0000

1# SPDX-License-Identifier: GPL-3.0-or-later 

2"""Pylint macro helpers for cikit. 

3 

4This module executes Pylint checks, stores lint metrics in cikit data, 

5generates badges and converts JSON lint reports into generated Sphinx 

6RST pages. 

7 

8Author: 

9 Sven Schlender 

10Date: 

11 2026-05-28 

12""" 

13 

14import json 

15from pathlib import Path 

16from tempfile import TemporaryDirectory 

17 

18from cikit.badge import write_badge 

19from cikit.command import Command, run_command 

20from cikit.artifacts import Artifact, ArtifactType 

21from cikit.env import write_file 

22from cikit.errors import CiToolsError 

23from cikit.lint_to_rst import LintIssue, read_lint_issues, write_lint_rst 

24from cikit.rst_tools import RstDocument, timeline_artifacts_block 

25 

26from .common import resolve_name_label 

27 

28 

29PYLINT_TIMEOUT = 300 

30RST_TOC_DEPTH = 2 

31 

32 

33def _get_pylint_check_file_path(env) -> Path: 

34 """Return the registered Pylint JSON report artifact. 

35 

36 Args: 

37 env: Resolved CI run environment. 

38 

39 Returns: 

40 Path to the Pylint JSON report file. 

41 """ 

42 return env.get_run_artifact("pylint.json", ArtifactType.LINT).path 

43 

44 

45def _build_command(env, rcfile: Path = None, codequality_file: Path = None) -> Command: 

46 """Build the Pylint command definition. 

47 

48 Args: 

49 env: Resolved CI run environment. 

50 rcfile: Optional Pylint configuration file. 

51 codequality_file: Optional GitLab codequality output file. 

52 

53 Returns: 

54 Configured Pylint command. 

55 """ 

56 common_args = ["--exit-zero"] 

57 if rcfile: 

58 common_args.append(f"--rcfile={env.resolve_path(rcfile)}") 

59 

60 cmd = Command("pylint", use_exec_tool=True, common_args=common_args) 

61 

62 if codequality_file is not None: 

63 cmd.add_subcommand("codequality", ["--output-format", 

64 "pylint_gitlab.GitlabCodeClimateReporter", 

65 "--output", str(codequality_file)]) 

66 

67 cmd.add_subcommand("json", ["--output-format", "json2"]) 

68 

69 return cmd 

70 

71 

72def _write_json_data(env, pylint_data: dict) -> Artifact: 

73 """Write normalized Pylint messages to the JSON artifact file. 

74 

75 Args: 

76 env: Resolved CI run environment. 

77 pylint_data: Raw Pylint JSON output. 

78 

79 Returns: 

80 Created and registered lint artifact. 

81 

82 Raises: 

83 CiToolsError: If writing the JSON file fails. 

84 """ 

85 messages = [ 

86 { 

87 "type": msg.get("type", ""), 

88 "symbol": msg.get("symbol", ""), 

89 "message": msg.get("message", ""), 

90 "message-id": msg.get("messageId", ""), 

91 "module": msg.get("module", ""), 

92 "line": msg.get("line", 0), 

93 "column": msg.get("column", 0), 

94 "path": msg.get("path", ""), 

95 } 

96 for msg in pylint_data.get("messages", []) 

97 ] 

98 

99 with TemporaryDirectory() as temp_dir: 

100 output_file = Path(temp_dir) / "pylint.json" 

101 write_file(output_file, json.dumps(messages, indent=4)) 

102 return env.copy_artifact(output_file, ArtifactType.LINT) 

103 

104 

105def pylint(env, paths: list, rcfile: Path = None): 

106 """Run Pylint checks. 

107 

108 The generated JSON report is registered as a lint artifact and the resulting 

109 score is stored in the current run data context. 

110 

111 Args: 

112 env: Resolved CI run environment. 

113 paths: List of paths to lint. 

114 rcfile: Optional pylint configuration file. 

115 

116 Raises: 

117 CiToolsError: If a lint path does not exist or Pylint output cannot 

118 be parsed. 

119 """ 

120 for path in paths: 

121 if not Path(path).exists(): 

122 raise CiToolsError(f"Path '{path}' to lint does not exist!") 

123 

124 cmd = _build_command(env, rcfile) 

125 argv = env.argv(cmd, "json", extra_args=paths) 

126 result = run_command(argv, live_output=True, cwd=env["PROJECT_ROOT"], timeout=PYLINT_TIMEOUT, 

127 check_exit_code=False) 

128 

129 try: 

130 pylint_data = json.loads(result.stdout or "{}") 

131 except json.JSONDecodeError as exp: 

132 raise CiToolsError(f"Error while parsing output returned by pylint: {exp}") from exp 

133 

134 stats = pylint_data.get("statistics", {}) 

135 type_count = stats.get("messageTypeCount", {}) 

136 

137 _write_json_data(env, pylint_data) 

138 

139 result = { 

140 "score": round(stats.get("score", 0.0), 2), 

141 "modules_checked": stats.get("modulesLinted", 0), 

142 "statement_count": len(pylint_data.get("messages", [])), 

143 "fatal": type_count.get("fatal", 0), 

144 "error": type_count.get("error", 0), 

145 "warning": type_count.get("warning", 0), 

146 "refactoring": type_count.get("refactor", 0), 

147 "convention": type_count.get("convention", 0), 

148 "info": type_count.get("info", 0), 

149 } 

150 

151 env.data_update(result) 

152 

153 

154def gitlab_code_quality(env, paths, rcfile: Path = None): 

155 """Generate GitLab Code Quality output for Pylint. 

156 

157 Args: 

158 env: Resolved CI run environment. 

159 paths: Paths passed to Pylint. 

160 rcfile: Optional Pylint configuration file. 

161 """ 

162 with TemporaryDirectory() as temp_dir: 

163 output_file = Path(temp_dir) / "pylint-codequality.json" 

164 cmd = _build_command(env, rcfile, output_file) 

165 argv = env.argv(cmd, "codequality", extra_args=paths) 

166 run_command(argv, live_output=True, cwd=env["PROJECT_ROOT"], timeout=PYLINT_TIMEOUT, 

167 check_exit_code=False) 

168 env.copy_artifact(output_file, ArtifactType.LINT) 

169 

170 

171def badge(env, name: str = None, label: str = None) -> Artifact: 

172 """Generate the Pylint score badge. 

173 

174 Args: 

175 env: Resolved CI run environment. 

176 name: Basename of the badge file. If omitted, the run name is used. 

177 label: Label of the badge. If omitted, the run name is used. 

178 

179 Returns: 

180 Created and registered badge artifact. 

181 """ 

182 name, label = resolve_name_label(env, name, label) 

183 

184 return write_badge(env=env, name=name, label=label, value=env.data.get("score", 0.0), 

185 thresholds={4: "red", 6: "orange", 8: "yellow", 10: "green"}) 

186 

187 

188def _pylint_issue_from_dict(entry: dict) -> LintIssue: 

189 """Convert one Pylint JSON entry into a LintIssue. 

190 

191 Args: 

192 entry: Pylint JSON issue entry. 

193 

194 Returns: 

195 Converted lint issue. 

196 """ 

197 return LintIssue(code=entry.get("symbol") or entry.get("message-id") or "unknown", 

198 file_path=Path(entry.get("path")).resolve(), 

199 line=entry.get("line") or 0, 

200 message=entry.get("message") or "", 

201 severity=entry.get("type") or "info") 

202 

203 

204def write_rst(env, name: str = None, report_title: str = None, filter_code: str = None) -> Artifact: 

205 """Create an RST report from Pylint JSON output. 

206 

207 Args: 

208 env: Resolved CI run environment. 

209 name: Output file basename without extension. If omitted, the run name 

210 is used. 

211 report_title: Title of the RST page. If omitted, it is derived from the 

212 current Pylint score. 

213 filter_code: Optional Pylint message code filter. Filtered reports do 

214 not include timeline charts to avoid duplicating overview content. 

215 

216 Returns: 

217 Created and registered RST artifact. 

218 """ 

219 if name is None: 

220 name = env.name 

221 

222 if report_title is None: 

223 report_title = f"Pylint Report (Score {env.data.get('score')})" 

224 

225 issues = read_lint_issues(_get_pylint_check_file_path(env), _pylint_issue_from_dict) 

226 

227 document = RstDocument(report_title, toc_depth=RST_TOC_DEPTH) 

228 

229 if not filter_code: 

230 document.add_block(timeline_artifacts_block(env.get_run_artifacts(ArtifactType.TIMELINE))) 

231 

232 return write_lint_rst(env, document, issues, name, filter_code)