Coverage for coverage / debug.py: 97.600%

277 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000

1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt 

3 

4"""Control of and utilities for debugging.""" 

5 

6from __future__ import annotations 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

7 

8import _thread 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

9import atexit 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

10import contextlib 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

11import datetime 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

12import functools 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

13import inspect 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

14import itertools 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

15import os 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

16import pprint 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

17import re 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

18import reprlib 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

19import sys 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

20import traceback 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

21import types 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

22from collections.abc import Iterable, Iterator, Mapping 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

23from typing import IO, Any, Callable, Final, overload 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

24 

25from coverage.misc import human_sorted_items, isolate_module 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

26from coverage.types import AnyCallable, TWritable 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

27 

28os = isolate_module(os) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

29 

30 

31# When debugging, it can be helpful to force some options, especially when 

32# debugging the configuration mechanisms you usually use to control debugging! 

33# This is a list of forced debugging options. 

34FORCED_DEBUG: list[str] = [] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

35FORCED_DEBUG_FILE = None 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

36 

37 

38class DebugControl: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

39 """Control and output for debugging.""" 

40 

41 show_repr_attr = False # For auto_repr 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

42 

43 def __init__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234

44 self, 

45 options: Iterable[str], 

46 output: IO[str] | None, 

47 file_name: str | None = None, 

48 ) -> None: 

49 """Configure the options and output file for debugging.""" 

50 self.options = list(options) + FORCED_DEBUG 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

51 self.suppress_callers = False 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

52 

53 filters = [] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

54 if self.should("process"): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

55 filters.append(CwdTracker().filter) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LMNO8PQ9RSTU!VW#XYZ0$12534EFG

56 filters.append(ProcessTracker().filter) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LMNO8PQ9RSTU!VW#XYZ0$12534EFG

57 if self.should("pytest"): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

58 filters.append(PytestTracker().filter) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

59 if self.should("pid"): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

60 filters.append(add_pid_and_tid) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LMNO8PQ9RSTU!VW#XYZ0$12534EFG

61 

62 self.output = DebugOutputFile.get_one( 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

63 output, 

64 file_name=file_name, 

65 filters=filters, 

66 ) 

67 self.raw_output = self.output.outfile 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

68 

69 def __repr__(self) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

70 return f"<DebugControl options={self.options!r} raw_output={self.raw_output!r}>" 

71 

72 def should(self, option: str) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

73 """Decide whether to output debug information in category `option`.""" 

74 if option == "callers" and self.suppress_callers: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

75 return False 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

76 return option in self.options 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

77 

78 @contextlib.contextmanager 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

79 def without_callers(self) -> Iterator[None]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

80 """A context manager to prevent call stacks from being logged.""" 

81 old = self.suppress_callers 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

82 self.suppress_callers = True 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

83 try: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

84 yield 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

85 finally: 

86 self.suppress_callers = old 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

87 

88 def write(self, msg: str, *, exc: BaseException | None = None) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

89 """Write a line of debug output. 

90 

91 `msg` is the line to write. A newline will be appended. 

92 

93 If `exc` is provided, a stack trace of the exception will be written 

94 after the message. 

95 

96 """ 

97 self.output.write(msg + "\n") 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

98 if exc is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

99 self.output.write("".join(traceback.format_exception(None, exc, exc.__traceback__))) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

100 if self.should("self"): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

101 caller_self = inspect.stack()[1][0].f_locals.get("self") 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

102 if caller_self is not None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

103 self.output.write(f"self: {caller_self!r}\n") 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

104 if self.should("callers"): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

105 dump_stack_frames(out=self.output, skip=1) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ012534EFG

106 self.output.flush() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

107 

108 

109class NoDebugging(DebugControl): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

110 """A replacement for DebugControl that will never try to do anything.""" 

111 

112 def __init__(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

113 # pylint: disable=super-init-not-called 

114 pass 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

115 

116 def should(self, option: str) -> bool: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

117 """Should we write debug messages? Never.""" 

118 return False 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

119 

120 @contextlib.contextmanager 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

121 def without_callers(self) -> Iterator[None]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

122 """A dummy context manager to satisfy the api.""" 

123 yield # pragma: never called 

124 

125 def write(self, msg: str, *, exc: BaseException | None = None) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

126 """This will never be called.""" 

127 raise AssertionError("NoDebugging.write should never be called.") 

128 

129 

130class DevNullDebug(NoDebugging): 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

131 """A DebugControl that won't write anywhere.""" 

132 

133 def write(self, msg: str, *, exc: BaseException | None = None) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

134 pass 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

135 

136 

137def info_header(label: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

138 """Make a nice header string.""" 

139 return "--{:-<60s}".format(" " + label + " ") 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

140 

141 

142def info_formatter(info: Iterable[tuple[str, Any]]) -> Iterable[str]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

143 """Produce a sequence of formatted lines from info. 

144 

145 `info` is a sequence of pairs (label, data). The produced lines are 

146 nicely formatted, ready to print. 

147 

148 """ 

149 info = list(info) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

150 if not info: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

151 return 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

152 LABEL_LEN = 30 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

153 assert all(len(l) < LABEL_LEN for l, _ in info) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

154 for label, data in info: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

155 if data == []: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

156 data = "-none-" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

157 prefix = f"{label:>{LABEL_LEN}}: " 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

158 match data: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

159 case tuple() if len(str(data)) < 30: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

160 yield f"{prefix}{data}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

161 case tuple() | list() | set(): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

162 for e in data: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

163 yield f"{prefix}{e}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

164 prefix = " " * (LABEL_LEN + 2) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

165 case _: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

166 yield f"{prefix}{data}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

167 

168 

169def write_formatted_info( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

170 write: Callable[[str], None], 

171 header: str, 

172 info: Iterable[tuple[str, Any]], 

173) -> None: 

174 """Write a sequence of (label,data) pairs nicely. 

175 

176 `write` is a function write(str) that accepts each line of output. 

177 `header` is a string to start the section. `info` is a sequence of 

178 (label, data) pairs, where label is a str, and data can be a single 

179 value, or a list/set/tuple. 

180 

181 """ 

182 write(info_header(header)) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

183 for line in info_formatter(info): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

184 write(f" {line}") 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

185 

186 

187def exc_one_line(exc: Exception) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

188 """Get a one-line summary of an exception, including class name and message.""" 

189 lines = traceback.format_exception_only(type(exc), exc) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

190 return "|".join(l.rstrip() for l in lines) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

191 

192 

193_FILENAME_REGEXES: list[tuple[str, str]] = [ 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

194 (r".*[/\\]pytest-of-.*[/\\]pytest-\d+([/\\]popen-gw\d+)?", "tmp:"), 

195] 

196_FILENAME_SUBS: list[tuple[str, str]] = [] 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

197 

198 

199@overload 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

200def short_filename(filename: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

201 pass 

202 

203 

204@overload 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

205def short_filename(filename: None) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

206 pass 

207 

208 

209def short_filename(filename: str | None) -> str | None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

210 """Shorten a file name. Directories are replaced by prefixes like 'syspath:'""" 

211 if not _FILENAME_SUBS: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

212 for pathdir in sys.path: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

213 _FILENAME_SUBS.append((pathdir, "syspath:")) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

214 import coverage 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

215 

216 _FILENAME_SUBS.append((os.path.dirname(coverage.__file__), "cov:")) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

217 _FILENAME_SUBS.sort(key=(lambda pair: len(pair[0])), reverse=True) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

218 if filename is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

219 for pat, sub in _FILENAME_REGEXES: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

220 filename = re.sub(pat, sub, filename) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

221 for before, after in _FILENAME_SUBS: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

222 filename = filename.replace(before, after) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

223 return filename 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

224 

225 

226def file_summary(filename: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

227 """A one-line summary of a file, for log messages.""" 

228 try: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

229 s = os.stat(filename) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

230 except FileNotFoundError: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

231 summary = "does not exist" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

232 except Exception as e: 

233 summary = f"error: {e}" 

234 else: 

235 mod = datetime.datetime.fromtimestamp(s.st_mtime) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

236 summary = f"{s.st_size} bytes, modified {mod}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

237 return summary 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

238 

239 

240def short_stack( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234

241 skip: int = 0, 

242 full: bool = False, 

243 frame_ids: bool = False, 

244 short_filenames: bool = False, 

245) -> str: 

246 """Return a string summarizing the call stack. 

247 

248 The string is multi-line, with one line per stack frame. Each line shows 

249 the function name, the file name, and the line number: 

250 

251 ... 

252 start_import_stop : /Users/ned/coverage/trunk/tests/coveragetest.py:95 

253 import_local_file : /Users/ned/coverage/trunk/tests/coveragetest.py:81 

254 import_local_file : /Users/ned/coverage/trunk/coverage/backward.py:159 

255 ... 

256 

257 `skip` is the number of closest immediate frames to skip, so that debugging 

258 functions can call this and not be included in the result. 

259 

260 If `full` is true, then include all frames. Otherwise, initial "boring" 

261 frames (ones in site-packages and earlier) are omitted. 

262 

263 `short_filenames` will shorten filenames using `short_filename`, to reduce 

264 the amount of repetitive noise in stack traces. 

265 

266 """ 

267 # Regexes in initial frames that we don't care about. 

268 # fmt: off 

269 BORING_PRELUDE = [ 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

270 "<string>", # pytest-xdist has string execution. 

271 r"\bigor.py$", # Our test runner. 

272 r"\bsite-packages\b", # pytest etc getting to our tests. 

273 ] 

274 # fmt: on 

275 

276 stack: Iterable[inspect.FrameInfo] = inspect.stack()[:skip:-1] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

277 if not full: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

278 for pat in BORING_PRELUDE: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

279 stack = itertools.dropwhile( 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

280 (lambda fi, pat=pat: re.search(pat, fi.filename)), # type: ignore[misc] 

281 stack, 

282 ) 

283 lines = [] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

284 for frame_info in stack: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

285 line = f"{frame_info.function:>30s} : " 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

286 if frame_ids: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

287 line += f"{id(frame_info.frame):#x} " 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

288 filename = frame_info.filename 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

289 if short_filenames: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

290 filename = short_filename(filename) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

291 line += f"{filename}:{frame_info.lineno}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

292 lines.append(line) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

293 return "\n".join(lines) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

294 

295 

296def dump_stack_frames(out: TWritable, skip: int = 0) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

297 """Print a summary of the stack to `out`.""" 

298 out.write(short_stack(skip=skip + 1) + "\n") 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

299 

300 

301def clipped_repr(text: str, numchars: int = 50) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

302 """`repr(text)`, but limited to `numchars`.""" 

303 r = reprlib.Repr() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

304 r.maxstring = numchars 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

305 return r.repr(text) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

306 

307 

308def short_id(id64: int) -> int: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

309 """Given a 64-bit id, make a shorter 16-bit one.""" 

310 id16 = 0 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

311 for offset in range(0, 64, 16): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

312 id16 ^= id64 >> offset 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

313 return id16 & 0xFFFF 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

314 

315 

316def add_pid_and_tid(text: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

317 """A filter to add pid and tid to debug messages.""" 

318 # Thread ids are useful, but too long. Make a shorter one. 

319 tid = f"{short_id(_thread.get_ident()):04x}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

320 text = f"{os.getpid():5d}.{tid}: {text}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

321 return text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

322 

323 

324AUTO_REPR_IGNORE = {"$coverage.object_id"} 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

325 

326 

327def auto_repr(self: Any) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

328 """A function implementing an automatic __repr__ for debugging.""" 

329 show_attrs = ( 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

330 (k, v) 

331 for k, v in self.__dict__.items() 

332 if getattr(v, "show_repr_attr", True) 

333 and not inspect.ismethod(v) 

334 and k not in AUTO_REPR_IGNORE 

335 ) 

336 return "<{klass} @{id:#x}{attrs}>".format( 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

337 klass=self.__class__.__name__, 

338 id=id(self), 

339 attrs="".join(f" {k}={v!r}" for k, v in show_attrs), 

340 ) 

341 

342 

343def simplify(v: Any) -> Any: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

344 """Turn things which are nearly dict/list/etc into dict/list/etc.""" 

345 if isinstance(v, dict): 

346 return {k: simplify(vv) for k, vv in v.items()} 

347 elif isinstance(v, (list, tuple)): 

348 return type(v)(simplify(vv) for vv in v) 

349 elif hasattr(v, "__dict__"): 

350 return simplify({"." + k: v for k, v in v.__dict__.items()}) 

351 else: 

352 return v 

353 

354 

355def ppformat(v: Any) -> str: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

356 """Debug helper to pretty-print data, including SimpleNamespace objects.""" 

357 return pprint.pformat(simplify(v), indent=4, compact=True, sort_dicts=True, width=140) 

358 

359 

360def pp(v: Any) -> None: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

361 """Debug helper to pretty-print data, including SimpleNamespace objects.""" 

362 print(ppformat(v)) 

363 

364 

365def filter_text(text: str, filters: Iterable[Callable[[str], str]]) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

366 """Run `text` through a series of filters. 

367 

368 `filters` is a list of functions. Each takes a string and returns a 

369 string. Each is run in turn. After each filter, the text is split into 

370 lines, and each line is passed through the next filter. 

371 

372 Returns: the final string that results after all of the filters have 

373 run. 

374 

375 """ 

376 clean_text = text.rstrip() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

377 ending = text[len(clean_text) :] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

378 text = clean_text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

379 for filter_fn in filters: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

380 lines = [] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

381 for line in text.splitlines(): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

382 lines.extend(filter_fn(line).splitlines()) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

383 text = "\n".join(lines) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

384 return text + ending 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

385 

386 

387class CwdTracker: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

388 """A class to add cwd info to debug messages.""" 

389 

390 def __init__(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

391 self.cwd: str | None = None 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

392 

393 def filter(self, text: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

394 """Add a cwd message for each new cwd.""" 

395 cwd = os.getcwd() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

396 if cwd != self.cwd: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

397 text = f"cwd is now {cwd!r}\n{text}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

398 self.cwd = cwd 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

399 return text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

400 

401 

402class ProcessTracker: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

403 """Track process creation for debug logging.""" 

404 

405 def __init__(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

406 self.pid: int = os.getpid() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

407 self.did_welcome = False 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

408 

409 def filter(self, text: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

410 """Add a message about how new processes came to be.""" 

411 welcome = "" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

412 pid = os.getpid() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

413 if self.pid != pid: 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

414 welcome = f"New process: forked {self.pid} -> {pid}\n" 

415 self.pid = pid 

416 elif not self.did_welcome: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

417 argv = getattr(sys, "argv", None) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

418 welcome = ( 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

419 f"New process: {pid=}, executable: {sys.executable!r}\n" 

420 + f"New process: cmd: {argv!r}\n" 

421 + f"New process parent pid: {os.getppid()!r}\n" 

422 ) 

423 

424 if welcome: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

425 self.did_welcome = True 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

426 return welcome + text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

427 else: 

428 return text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

429 

430 

431class PytestTracker: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

432 """Track the current pytest test name to add to debug messages.""" 

433 

434 def __init__(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

435 self.test_name: str | None = None 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

436 

437 def filter(self, text: str) -> str: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

438 """Add a message when the pytest test changes.""" 

439 test_name = os.getenv("PYTEST_CURRENT_TEST") 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

440 if test_name != self.test_name: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

441 text = f"Pytest context: {test_name}\n{text}" 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

442 self.test_name = test_name 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

443 return text 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

444 

445 

446class DebugOutputFile: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

447 """A file-like object that includes pid and cwd information.""" 

448 

449 def __init__( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

450 self, 

451 outfile: IO[str] | None, 

452 filters: Iterable[Callable[[str], str]], 

453 ): 

454 self.outfile = outfile 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

455 self.filters = list(filters) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

456 self.pid = os.getpid() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

457 

458 @classmethod 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

459 def get_one( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234

460 cls, 

461 fileobj: IO[str] | None = None, 

462 file_name: str | None = None, 

463 filters: Iterable[Callable[[str], str]] = (), 

464 interim: bool = False, 

465 ) -> DebugOutputFile: 

466 """Get a DebugOutputFile. 

467 

468 If `fileobj` is provided, then a new DebugOutputFile is made with it. 

469 

470 If `fileobj` isn't provided, then a file is chosen (`file_name` if 

471 provided, or COVERAGE_DEBUG_FILE, or stderr), and a process-wide 

472 singleton DebugOutputFile is made. 

473 

474 `filters` are the text filters to apply to the stream to annotate with 

475 pids, etc. 

476 

477 If `interim` is true, then a future `get_one` can replace this one. 

478 

479 """ 

480 if fileobj is not None: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

481 # Make DebugOutputFile around the fileobj passed. 

482 return cls(fileobj, filters) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

483 

484 the_one, is_interim = cls._get_singleton_data() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

485 if the_one is None or is_interim: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

486 if file_name is not None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

487 fileobj = open(file_name, "a", encoding="utf-8") 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

488 else: 

489 # $set_env.py: COVERAGE_DEBUG_FILE - Where to write debug output 

490 file_name = os.getenv("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

491 if file_name in ["stdout", "stderr"]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

492 fileobj = getattr(sys, file_name) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

493 elif file_name: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

494 fileobj = open(file_name, "a", encoding="utf-8") 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

495 atexit.register(fileobj.close) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

496 else: 

497 fileobj = sys.stderr 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

498 the_one = cls(fileobj, filters) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

499 cls._set_singleton_data(the_one, interim) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

500 

501 if not (the_one.filters): 501 ↛ 503line 501 didn't jump to line 503 because the condition on line 501 was always true1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

502 the_one.filters = list(filters) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

503 return the_one 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

504 

505 # Because of the way igor.py deletes and re-imports modules, 

506 # this class can be defined more than once. But we really want 

507 # a process-wide singleton. So stash it in sys.modules instead of 

508 # on a class attribute. Yes, this is aggressively gross. 

509 

510 SYS_MOD_NAME: Final[str] = "$coverage.debug.DebugOutputFile.the_one" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

511 SINGLETON_ATTR: Final[str] = "the_one_and_is_interim" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

512 

513 @classmethod 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

514 def _set_singleton_data(cls, the_one: DebugOutputFile, interim: bool) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

515 """Set the one DebugOutputFile to rule them all.""" 

516 singleton_module = types.ModuleType(cls.SYS_MOD_NAME) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

517 setattr(singleton_module, cls.SINGLETON_ATTR, (the_one, interim)) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

518 sys.modules[cls.SYS_MOD_NAME] = singleton_module 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

519 

520 @classmethod 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

521 def _get_singleton_data(cls) -> tuple[DebugOutputFile | None, bool]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

522 """Get the one DebugOutputFile.""" 

523 singleton_module = sys.modules.get(cls.SYS_MOD_NAME) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

524 return getattr(singleton_module, cls.SINGLETON_ATTR, (None, True)) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

525 

526 @classmethod 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

527 def _del_singleton_data(cls) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

528 """Delete the one DebugOutputFile, just for tests to use.""" 

529 if cls.SYS_MOD_NAME in sys.modules: 529 ↛ exitline 529 didn't return from function '_del_singleton_data' because the condition on line 529 was always true1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

530 del sys.modules[cls.SYS_MOD_NAME] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

531 

532 def write(self, text: str) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

533 """Just like file.write, but filter through all our filters.""" 

534 assert self.outfile is not None 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

535 if not self.outfile.closed: 535 ↛ exitline 535 didn't return from function 'write' because the condition on line 535 was always true1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

536 self.outfile.write(filter_text(text, self.filters)) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

537 self.outfile.flush() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

538 

539 def flush(self) -> None: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

540 """Flush our file.""" 

541 assert self.outfile is not None 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

542 if not self.outfile.closed: 542 ↛ exitline 542 didn't return from function 'flush' because the condition on line 542 was always true1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

543 self.outfile.flush() 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

544 

545 

546def log(msg: str, stack: bool = False) -> None: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

547 """Write a log message as forcefully as possible.""" 

548 out = DebugOutputFile.get_one(interim=True) 

549 out.write(msg + "\n") 

550 if stack: 

551 dump_stack_frames(out=out, skip=1) 

552 

553 

554def decorate_methods( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234

555 decorator: Callable[..., Any], 1abcdefghijklmnopqrEFG

556 butnot: Iterable[str] = (), 1stuvwxyzABCDabcdefghijklmnopqrEFG

557 private: bool = False, 1stuvwxyzABCDabcdefghijklmnopqrEFG

558) -> Callable[..., Any]: # pragma: debugging 1abcdefghijklmnopqrEFG

559 """A class decorator to apply a decorator to methods.""" 

560 

561 def _decorator(cls): # type: ignore[no-untyped-def] 

562 for name, meth in inspect.getmembers(cls, inspect.isroutine): 

563 if name not in cls.__dict__: 

564 continue 

565 if name != "__init__": 

566 if not private and name.startswith("_"): 

567 continue 

568 if name in butnot: 

569 continue 

570 setattr(cls, name, decorator(meth)) 

571 return cls 

572 

573 return _decorator 

574 

575 

576def break_in_pudb(func: AnyCallable) -> AnyCallable: # pragma: debugging 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

577 """A function decorator to stop in the debugger for each call.""" 

578 

579 @functools.wraps(func) 

580 def _wrapper(*args: Any, **kwargs: Any) -> Any: 

581 import pudb 

582 

583 sys.stdout = sys.__stdout__ 

584 pudb.set_trace() 

585 return func(*args, **kwargs) 

586 

587 return _wrapper 

588 

589 

590OBJ_IDS = itertools.count() 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

591CALLS = itertools.count() 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

592OBJ_ID_ATTR = "$coverage.object_id" 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

593 

594 

595def show_calls( 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234

596 show_args: bool = True, 1stuvwxyzABCDabcdefghijklmnopqrEFG

597 show_stack: bool = False, 1stuvwxyzABCDabcdefghijklmnopqrEFG

598 show_return: bool = False, 1stuvwxyzABCDabcdefghijklmnopqrEFG

599) -> Callable[..., Any]: # pragma: debugging 1abcdefghijklmnopqrEFG

600 """A method decorator to debug-log each call to the function.""" 

601 

602 def _decorator(func: AnyCallable) -> AnyCallable: 

603 @functools.wraps(func) 

604 def _wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: 

605 oid = getattr(self, OBJ_ID_ATTR, None) 

606 if oid is None: 

607 oid = f"{os.getpid():08d} {next(OBJ_IDS):04d}" 

608 setattr(self, OBJ_ID_ATTR, oid) 

609 extra = "" 

610 if show_args: 

611 eargs = ", ".join(map(repr, args)) 

612 ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items()) 

613 extra += "(" 

614 extra += eargs 

615 if eargs and ekwargs: 

616 extra += ", " 

617 extra += ekwargs 

618 extra += ")" 

619 if show_stack: 

620 extra += " @ " 

621 extra += "; ".join(short_stack(short_filenames=True).splitlines()) 

622 callid = next(CALLS) 

623 msg = f"{oid} {callid:04d} {func.__name__}{extra}\n" 

624 DebugOutputFile.get_one(interim=True).write(msg) 

625 ret = func(self, *args, **kwargs) 

626 if show_return: 

627 msg = f"{oid} {callid:04d} {func.__name__} return {ret!r}\n" 

628 DebugOutputFile.get_one(interim=True).write(msg) 

629 return ret 

630 

631 return _wrapper 

632 

633 return _decorator 

634 

635 

636def relevant_environment_display(env: Mapping[str, str]) -> list[tuple[str, str]]: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

637 """Filter environment variables for a debug display. 

638 

639 Select variables to display (with COV or PY in the name, or HOME, TEMP, or 

640 TMP), and also cloak sensitive values with asterisks. 

641 

642 Arguments: 

643 env: a dict of environment variable names and values. 

644 

645 Returns: 

646 A list of pairs (name, value) to show. 

647 

648 """ 

649 SLUGS = {"COV", "PY"} 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

650 INCLUDE = {"HOME", "TEMP", "TMP"} 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

651 CLOAK = {"API", "TOKEN", "KEY", "SECRET", "PASS", "SIGNATURE"} 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

652 TRUNCATE = {"COVERAGE_PROCESS_CONFIG"} 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

653 TRUNCATE_LEN = 60 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

654 

655 to_show = [] 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

656 for name, val in env.items(): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

657 show = False 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

658 if name in INCLUDE: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

659 show = True 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

660 elif any(slug in name for slug in SLUGS): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

661 show = True 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

662 if show: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

663 if any(slug in name for slug in CLOAK): 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

664 val = re.sub(r"\w", "*", val) 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

665 if name in TRUNCATE: 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

666 if len(val) > TRUNCATE_LEN: 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

667 val = val[: TRUNCATE_LEN - 3] + "..." 1stuvwxyzABCDabcdefghijklmnopqrHIJKLMNOPQRSTUVWXYZ01234EFG

668 to_show.append((name, val)) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG

669 return human_sorted_items(to_show) 1stuvwxyzABCDabcdefghijklmnopqrHI6JK7LM%NO8PQ9RS'TU!VW#XY(Z0$12534)EFG