Coverage for pylint_report/pylint_report.py: 99%
70 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 16:46 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-23 16:46 +0000
1#!/usr/bin/env python3
2"""Custom JSON reporter for pylint, and JSON to HTML export utility."""
3import argparse
4import html
5import json
6import sys
7from datetime import datetime
8from pathlib import Path
10import jinja2
11import pandas as pd
12from pylint.reporters.base_reporter import BaseReporter
14CURRENT_DIR = Path(__file__).resolve().parent
16TEMPLATE_FILE = "style/template.html.j2"
17DEFAULT_CSS_FILE = "style/pylint-report.css"
18COLS2KEEP = ["line", "column", "symbol", "type", "obj", "message"]
21def get_score(stats):
22 """Compute score.
24 Note
25 -----
26 https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#evaluation
28 """
29 f = stats.get("fatal", False)
30 e = stats.get("error", 0)
31 w = stats.get("warning", 0)
32 r = stats.get("refactor", 0)
33 c = stats.get("convention", 0)
34 s = stats.get("statement", 0)
36 if s == 0:
37 return None
38 return max(0, 0 if f else 10 * (1 - ((5 * e + w + r + c) / s)))
41def get_template():
42 """Return jinja2 template."""
43 return jinja2.Environment(
44 loader=jinja2.FileSystemLoader(CURRENT_DIR),
45 keep_trailing_newline=True,
46 undefined=jinja2.StrictUndefined,
47 ).get_template(TEMPLATE_FILE)
50def json2html(data, external_css):
51 """Generate an html file based on JSON data."""
53 if not external_css:
54 with open(CURRENT_DIR / DEFAULT_CSS_FILE, "r", encoding="utf-8") as h:
55 css = h.read()
56 else:
57 css = None
59 if data["messages"]:
60 msg = {
61 name: df.sort_values(["line", "column"]).reset_index(drop=True)
62 for name, df in pd.DataFrame(data["messages"]).groupby("module")
63 }
64 else:
65 msg = {}
67 score = get_score(data["stats"])
68 score = None if score is None else f"{score:0.2f}"
69 now = datetime.now()
70 context = dict(
71 cols2keep=COLS2KEEP,
72 date=now.strftime("%Y-%d-%m"),
73 time=now.strftime("%H:%M:%S"),
74 score=score,
75 external_css=external_css,
76 css=css,
77 modules=data["stats"]["by_module"],
78 msg=msg,
79 )
80 return get_template().render(context)
83class _SetEncoder(json.JSONEncoder):
84 """Handle sets when dumping to json.
86 Note
87 -----
88 See https://stackoverflow.com/a/8230505
90 """
92 def default(self, o):
93 if isinstance(o, set):
94 return list(o)
95 return json.JSONEncoder.default(self, o)
98class CustomJsonReporter(BaseReporter):
99 """Customize the default json reporter.
101 Note
102 -----
103 See ``pylint/reporters/json_reporter.py``
105 """
107 name = "custom json"
109 def __init__(self, output=None):
110 """Construct object."""
111 super().__init__(sys.stdout if output is None else output)
112 self.messages = []
114 def handle_message(self, msg):
115 """Manage message of different type and in the context of path."""
116 self.messages.append(
117 {
118 "type": msg.category,
119 "module": msg.module,
120 "obj": msg.obj,
121 "line": msg.line,
122 "column": msg.column,
123 "path": msg.path,
124 "symbol": msg.symbol,
125 "message": html.escape(msg.msg or "", quote=False),
126 "message-id": msg.msg_id,
127 }
128 )
130 def display_messages(self, layout):
131 """See ``pylint/reporters/base_reporter.py``."""
133 def display_reports(self, layout):
134 """See ``pylint/reporters/base_reporter.py``."""
136 def _display(self, layout):
137 """See ``pylint/reporters/base_reporter.py``."""
139 def on_close(self, stats, previous_stats):
140 """See ``pylint/reporters/base_reporter.py``."""
141 if not isinstance(stats, dict): # behavior from version 2.12.0 141 ↛ 154line 141 didn't jump to line 154 because the condition on line 141 was always true
142 stats = {
143 key: getattr(stats, key)
144 for key in [
145 "by_module",
146 "statement",
147 "error",
148 "warning",
149 "refactor",
150 "convention",
151 ]
152 }
154 print(
155 json.dumps(
156 {"messages": self.messages, "stats": stats}, cls=_SetEncoder, indent=2
157 ),
158 file=self.out,
159 )
162def register(linter):
163 """Register a reporter (required by :mod:`pylint`)."""
164 linter.register_reporter(CustomJsonReporter)
167def get_parser():
168 """Define cli parser."""
169 parser = argparse.ArgumentParser()
171 # see https://stackoverflow.com/a/11038508
172 parser.add_argument(
173 "json_file",
174 nargs="?",
175 type=argparse.FileType("r"),
176 default=sys.stdin,
177 help="JSON file/stdin generated by ``pylint``",
178 )
180 parser.add_argument(
181 "-o",
182 dest="html_file",
183 type=argparse.FileType("w"),
184 default=sys.stdout,
185 help="ame of html file to generate (send to stdout by default)",
186 )
188 parser.add_argument(
189 "-e",
190 "--external-css",
191 action="store_true",
192 help=(
193 "use external ``pylint-report.css`` file "
194 "(by default CSS styles are stored in the HTML)"
195 ),
196 )
198 return parser
201def main(argv=None):
202 """Main."""
203 args = get_parser().parse_args(argv)
205 with args.json_file as h:
206 json_data = json.load(h)
208 print(json2html(json_data, args.external_css), file=args.html_file)
211def sphinx_argparse_func(): # pragma: no cover
212 """Return a parser to use with sphinx-argparse."""
213 return get_parser()
216if __name__ == "__main__": # pragma: no cover
217 main()