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

92 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-13 02:35 -0700

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 = dict( 

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 

138 if val is not None: 

139 Color._colorize = val 

140 

141 if isinstance(val, dict): 

142 unknown = [] 

143 for k in val: 

144 if k in Color.categories: 

145 if val[k] in Color.colors: 

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

147 else: 

148 print("Unknown colour %s for category %s" % (val[k], k), file=sys.stderr) 

149 else: 

150 unknown.append(k) 

151 

152 if unknown: 

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

154 

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

156 

157 def __str__(self): 

158 if not self.colorize(): 

159 return self.rawText 

160 

161 base = "\033[" 

162 

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

164 suffix = base + "m" 

165 

166 return prefix + self.rawText + suffix 

167 

168 

169def _colorize(text, category): 

170 text = Color(text, category) 

171 return str(text) 

172 

173 

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

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

176 configuration field. 

177 

178 Parameters 

179 ---------- 

180 config : `lsst.pex.config.Config` 

181 A configuration instance. 

182 name : `str`, optional 

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

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

185 writeSourceLine : `bool`, optional 

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

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

188 prefix : `str`, optional 

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

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

191 verbose : `bool`, optional 

192 Default is `False`. 

193 """ 

194 

195 if name is None: 

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

197 if i > 0: 

198 print() 

199 print(format(config, name)) 

200 

201 outputs = [] 

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

203 output = [] 

204 for frame in stack: 

205 if frame.function in ( 

206 "__new__", 

207 "__set__", 

208 "__setattr__", 

209 "execfile", 

210 "wrapper", 

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

212 if not verbose: 

213 continue 

214 

215 line = [] 

216 if writeSourceLine: 

217 line.append( 

218 [ 

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

220 "FILE", 

221 ] 

222 ) 

223 

224 line.append( 

225 [ 

226 frame.content, 

227 "TEXT", 

228 ] 

229 ) 

230 if False: 

231 line.append( 

232 [ 

233 frame.function, 

234 "FUNCTION_NAME", 

235 ] 

236 ) 

237 

238 output.append(line) 

239 

240 outputs.append([value, output]) 

241 

242 if outputs: 

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

244 if writeSourceLine: 

245 sourceLengths = [] 

246 for value, output in outputs: 

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

248 sourceLength = max(sourceLengths) 

249 

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

251 

252 # Generate the config history content. 

253 msg = [] 

254 fullname = "%s.%s" % (config._name, name) if config._name is not None else name 

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

256 for value, output in outputs: 

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

258 for i, vt in enumerate(output): 

259 if writeSourceLine: 

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

261 

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

263 

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

265 msg.append(line) 

266 

267 return "\n".join(msg)