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

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 

9 

10import jinja2 

11import pandas as pd 

12from pylint.reporters.base_reporter import BaseReporter 

13 

14CURRENT_DIR = Path(__file__).resolve().parent 

15 

16TEMPLATE_FILE = "style/template.html.j2" 

17DEFAULT_CSS_FILE = "style/pylint-report.css" 

18COLS2KEEP = ["line", "column", "symbol", "type", "obj", "message"] 

19 

20 

21def get_score(stats): 

22 """Compute score. 

23 

24 Note 

25 ----- 

26 https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#evaluation 

27 

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) 

35 

36 if s == 0: 

37 return None 

38 return max(0, 0 if f else 10 * (1 - ((5 * e + w + r + c) / s))) 

39 

40 

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) 

48 

49 

50def json2html(data, external_css): 

51 """Generate an html file based on JSON data.""" 

52 

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 

58 

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 = {} 

66 

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) 

81 

82 

83class _SetEncoder(json.JSONEncoder): 

84 """Handle sets when dumping to json. 

85 

86 Note 

87 ----- 

88 See https://stackoverflow.com/a/8230505 

89 

90 """ 

91 

92 def default(self, o): 

93 if isinstance(o, set): 

94 return list(o) 

95 return json.JSONEncoder.default(self, o) 

96 

97 

98class CustomJsonReporter(BaseReporter): 

99 """Customize the default json reporter. 

100 

101 Note 

102 ----- 

103 See ``pylint/reporters/json_reporter.py`` 

104 

105 """ 

106 

107 name = "custom json" 

108 

109 def __init__(self, output=None): 

110 """Construct object.""" 

111 super().__init__(sys.stdout if output is None else output) 

112 self.messages = [] 

113 

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 ) 

129 

130 def display_messages(self, layout): 

131 """See ``pylint/reporters/base_reporter.py``.""" 

132 

133 def display_reports(self, layout): 

134 """See ``pylint/reporters/base_reporter.py``.""" 

135 

136 def _display(self, layout): 

137 """See ``pylint/reporters/base_reporter.py``.""" 

138 

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 } 

153 

154 print( 

155 json.dumps( 

156 {"messages": self.messages, "stats": stats}, cls=_SetEncoder, indent=2 

157 ), 

158 file=self.out, 

159 ) 

160 

161 

162def register(linter): 

163 """Register a reporter (required by :mod:`pylint`).""" 

164 linter.register_reporter(CustomJsonReporter) 

165 

166 

167def get_parser(): 

168 """Define cli parser.""" 

169 parser = argparse.ArgumentParser() 

170 

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 ) 

179 

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 ) 

187 

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 ) 

197 

198 return parser 

199 

200 

201def main(argv=None): 

202 """Main.""" 

203 args = get_parser().parse_args(argv) 

204 

205 with args.json_file as h: 

206 json_data = json.load(h) 

207 

208 print(json2html(json_data, args.external_css), file=args.html_file) 

209 

210 

211def sphinx_argparse_func(): # pragma: no cover 

212 """Return a parser to use with sphinx-argparse.""" 

213 return get_parser() 

214 

215 

216if __name__ == "__main__": # pragma: no cover 

217 main()