Coverage for python/lsst/pex/config/history.py: 12%

92 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-03-20 11:16 +0000

1# This file is part of pex_config. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28__all__ = ("Color", "format") 

29 

30import os 

31import re 

32import sys 

33 

34 

35class Color: 

36 """A controller that determines whether strings should be colored. 

37 

38 Parameters 

39 ---------- 

40 text : `str` 

41 Text content to print to a terminal. 

42 category : `str` 

43 Semantic category of the ``text``. See `categories` for possible 

44 values. 

45 

46 Raises 

47 ------ 

48 RuntimeError 

49 Raised when the ``category`` is not a key of ``Color.categories``. 

50 

51 Notes 

52 ----- 

53 The usual usage is ``Color(string, category)`` which returns a string that 

54 may be printed; categories are given by the keys of `Color.categories`. 

55 

56 `Color.colorize` may be used to set or retrieve whether the user wants 

57 color. It always returns `False` when `sys.stdout` is not attached to a 

58 terminal. 

59 """ 

60 

61 categories = { 

62 "NAME": "blue", 

63 "VALUE": "yellow", 

64 "FILE": "green", 

65 "TEXT": "red", 

66 "FUNCTION_NAME": "blue", 

67 } 

68 """Mapping of semantic labels to color names (`dict`). 

69 

70 Notes 

71 ----- 

72 The default categories are: 

73 

74 - ``'NAME'`` 

75 - ``'VALUE'`` 

76 - ``'FILE'`` 

77 - ``'TEXT'`` 

78 - ``'FUNCTION_NAME'`` 

79 """ 

80 

81 colors = { 

82 "black": 0, 

83 "red": 1, 

84 "green": 2, 

85 "yellow": 3, 

86 "blue": 4, 

87 "magenta": 5, 

88 "cyan": 6, 

89 "white": 7, 

90 } 

91 """Mapping of color names to terminal color codes (`dict`). 

92 """ 

93 

94 _colorize = True 

95 

96 def __init__(self, text, category): 

97 try: 

98 color = Color.categories[category] 

99 except KeyError: 

100 raise RuntimeError("Unknown category: %s" % category) 

101 

102 self.rawText = str(text) 

103 x = color.lower().split(";") 

104 self.color, bold = x.pop(0), False 

105 if x: 

106 props = x.pop(0) 

107 if props in ("bold",): 

108 bold = True 

109 

110 try: 

111 self._code = "%s" % (30 + Color.colors[self.color]) 

112 except KeyError: 

113 raise RuntimeError("Unknown colour: %s" % self.color) 

114 

115 if bold: 

116 self._code += ";1" 

117 

118 @staticmethod 

119 def colorize(val=None): 

120 """Get or set whether the string should be colorized. 

121 

122 Parameters 

123 ---------- 

124 val : `bool` or `dict`, optional 

125 The value is usually a bool, but it may be a dict which is used 

126 to modify `Color.categories`. 

127 

128 Returns 

129 ------- 

130 shouldColorize : `bool` 

131 If `True`, the string should be colorized. A string **will not** be 

132 colorized if standard output or standard error are not attached to 

133 a terminal or if the ``val`` argument was `False`. 

134 

135 Only strings written to a terminal are colorized. 

136 """ 

137 if val is not None: 

138 Color._colorize = val 

139 

140 if isinstance(val, dict): 

141 unknown = [] 

142 for k in val: 

143 if k in Color.categories: 

144 if val[k] in Color.colors: 

145 Color.categories[k] = val[k] 

146 else: 

147 print(f"Unknown colour {val[k]} for category {k}", file=sys.stderr) 

148 else: 

149 unknown.append(k) 

150 

151 if unknown: 

152 print("Unknown colourizing category: %s" % " ".join(unknown), file=sys.stderr) 

153 

154 return Color._colorize if sys.stdout.isatty() else False 

155 

156 def __str__(self): 

157 if not self.colorize(): 

158 return self.rawText 

159 

160 base = "\033[" 

161 

162 prefix = base + self._code + "m" 

163 suffix = base + "m" 

164 

165 return prefix + self.rawText + suffix 

166 

167 

168def _colorize(text, category): 

169 text = Color(text, category) 

170 return str(text) 

171 

172 

173def format(config, name=None, writeSourceLine=True, prefix="", verbose=False): 

174 """Format the history record for a configuration, or a specific 

175 configuration field. 

176 

177 Parameters 

178 ---------- 

179 config : `lsst.pex.config.Config` 

180 A configuration instance. 

181 name : `str`, optional 

182 The name of a configuration field to specifically format the history 

183 for. Otherwise the history of all configuration fields is printed. 

184 writeSourceLine : `bool`, optional 

185 If `True`, prefix each printout line with the code filename and line 

186 number where the configuration event occurred. Default is `True`. 

187 prefix : `str`, optional 

188 A prefix for to add to each printout line. This prefix occurs first, 

189 even before any source line. The default is an empty string. 

190 verbose : `bool`, optional 

191 Default is `False`. 

192 """ 

193 if name is None: 

194 for i, name in enumerate(config.history.keys()): 

195 if i > 0: 

196 print() 

197 print(format(config, name)) 

198 

199 outputs = [] 

200 for value, stack, label in config.history.get(name, []): 

201 output = [] 

202 for frame in stack: 

203 if frame.function in ( 

204 "__new__", 

205 "__set__", 

206 "__setattr__", 

207 "execfile", 

208 "wrapper", 

209 ) or os.path.split(frame.filename)[1] in ("argparse.py", "argumentParser.py"): 

210 if not verbose: 

211 continue 

212 

213 line = [] 

214 if writeSourceLine: 

215 line.append( 

216 [ 

217 "%s" % ("%s:%d" % (frame.filename, frame.lineno)), 

218 "FILE", 

219 ] 

220 ) 

221 

222 line.append( 

223 [ 

224 frame.content, 

225 "TEXT", 

226 ] 

227 ) 

228 if False: 

229 line.append( 

230 [ 

231 frame.function, 

232 "FUNCTION_NAME", 

233 ] 

234 ) 

235 

236 output.append(line) 

237 

238 outputs.append([value, output]) 

239 

240 if outputs: 

241 # Find the maximum widths of the value and file:lineNo fields. 

242 if writeSourceLine: 

243 sourceLengths = [] 

244 for value, output in outputs: 

245 sourceLengths.append(max([len(x[0][0]) for x in output])) 

246 sourceLength = max(sourceLengths) 

247 

248 valueLength = len(prefix) + max([len(str(value)) for value, output in outputs]) 

249 

250 # Generate the config history content. 

251 msg = [] 

252 fullname = f"{config._name}.{name}" if config._name is not None else name 

253 msg.append(_colorize(re.sub(r"^root\.", "", fullname), "NAME")) 

254 for value, output in outputs: 

255 line = prefix + _colorize("%-*s" % (valueLength, value), "VALUE") + " " 

256 for i, vt in enumerate(output): 

257 if writeSourceLine: 

258 vt[0][0] = "%-*s" % (sourceLength, vt[0][0]) 

259 

260 output[i] = " ".join([_colorize(v, t) for v, t in vt]) 

261 

262 line += ("\n%*s" % (valueLength + 1, "")).join(output) 

263 msg.append(line) 

264 

265 return "\n".join(msg)