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
« 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.
4This module executes Pylint checks, stores lint metrics in cikit data,
5generates badges and converts JSON lint reports into generated Sphinx
6RST pages.
8Author:
9 Sven Schlender
10Date:
11 2026-05-28
12"""
14import json
15from pathlib import Path
16from tempfile import TemporaryDirectory
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
26from .common import resolve_name_label
29PYLINT_TIMEOUT = 300
30RST_TOC_DEPTH = 2
33def _get_pylint_check_file_path(env) -> Path:
34 """Return the registered Pylint JSON report artifact.
36 Args:
37 env: Resolved CI run environment.
39 Returns:
40 Path to the Pylint JSON report file.
41 """
42 return env.get_run_artifact("pylint.json", ArtifactType.LINT).path
45def _build_command(env, rcfile: Path = None, codequality_file: Path = None) -> Command:
46 """Build the Pylint command definition.
48 Args:
49 env: Resolved CI run environment.
50 rcfile: Optional Pylint configuration file.
51 codequality_file: Optional GitLab codequality output file.
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)}")
60 cmd = Command("pylint", use_exec_tool=True, common_args=common_args)
62 if codequality_file is not None:
63 cmd.add_subcommand("codequality", ["--output-format",
64 "pylint_gitlab.GitlabCodeClimateReporter",
65 "--output", str(codequality_file)])
67 cmd.add_subcommand("json", ["--output-format", "json2"])
69 return cmd
72def _write_json_data(env, pylint_data: dict) -> Artifact:
73 """Write normalized Pylint messages to the JSON artifact file.
75 Args:
76 env: Resolved CI run environment.
77 pylint_data: Raw Pylint JSON output.
79 Returns:
80 Created and registered lint artifact.
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 ]
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)
105def pylint(env, paths: list, rcfile: Path = None):
106 """Run Pylint checks.
108 The generated JSON report is registered as a lint artifact and the resulting
109 score is stored in the current run data context.
111 Args:
112 env: Resolved CI run environment.
113 paths: List of paths to lint.
114 rcfile: Optional pylint configuration file.
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!")
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)
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
134 stats = pylint_data.get("statistics", {})
135 type_count = stats.get("messageTypeCount", {})
137 _write_json_data(env, pylint_data)
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 }
151 env.data_update(result)
154def gitlab_code_quality(env, paths, rcfile: Path = None):
155 """Generate GitLab Code Quality output for Pylint.
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)
171def badge(env, name: str = None, label: str = None) -> Artifact:
172 """Generate the Pylint score badge.
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.
179 Returns:
180 Created and registered badge artifact.
181 """
182 name, label = resolve_name_label(env, name, label)
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"})
188def _pylint_issue_from_dict(entry: dict) -> LintIssue:
189 """Convert one Pylint JSON entry into a LintIssue.
191 Args:
192 entry: Pylint JSON issue entry.
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")
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.
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.
216 Returns:
217 Created and registered RST artifact.
218 """
219 if name is None:
220 name = env.name
222 if report_title is None:
223 report_title = f"Pylint Report (Score {env.data.get('score')})"
225 issues = read_lint_issues(_get_pylint_check_file_path(env), _pylint_issue_from_dict)
227 document = RstDocument(report_title, toc_depth=RST_TOC_DEPTH)
229 if not filter_code:
230 document.add_block(timeline_artifacts_block(env.get_run_artifacts(ArtifactType.TIMELINE)))
232 return write_lint_rst(env, document, issues, name, filter_code)