Coverage for coverage / misc.py: 97.758%

169 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"""Miscellaneous stuff for coverage.py.""" 

5 

6from __future__ import annotations 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

7 

8import contextlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

9import datetime 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

10import errno 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

11import functools 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

12import hashlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

13import importlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

14import importlib.util 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

15import inspect 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

16import os 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

17import os.path 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

18import re 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

19import sys 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

20import types 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

21from collections.abc import Iterable, Iterator, Mapping, Sequence 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

22from types import ModuleType 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

23from typing import Any, NoReturn, TypeVar 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

24 

25# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of 

26# other packages were importing the exceptions from misc, so import them here. 

27# pylint: disable=unused-wildcard-import 

28from coverage.exceptions import * # pylint: disable=wildcard-import 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

29from coverage.exceptions import CoverageException 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

30from coverage.types import TArc 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

31 

32ISOLATED_MODULES: dict[ModuleType, ModuleType] = {} 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

33 

34 

35def isolate_module(mod: ModuleType) -> ModuleType: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

36 """Copy a module so that we are isolated from aggressive mocking. 

37 

38 If a test suite mocks os.path.exists (for example), and then we need to use 

39 it during the test, everything will get tangled up if we use their mock. 

40 Making a copy of the module when we import it will isolate coverage.py from 

41 those complications. 

42 """ 

43 if mod not in ISOLATED_MODULES: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

44 new_mod = types.ModuleType(mod.__name__) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

45 ISOLATED_MODULES[mod] = new_mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

46 for name in dir(mod): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

47 value = getattr(mod, name) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

48 if isinstance(value, types.ModuleType): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

49 value = isolate_module(value) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

50 setattr(new_mod, name, value) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

51 return ISOLATED_MODULES[mod] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

52 

53 

54os = isolate_module(os) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

55 

56 

57class SysModuleSaver: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

58 """Saves the contents of sys.modules, and removes new modules later.""" 

59 

60 def __init__(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

61 self.old_modules = set(sys.modules) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

62 

63 def restore(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

64 """Remove any modules imported since this object started.""" 

65 new_modules = set(sys.modules) - self.old_modules 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

66 for m in new_modules: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

67 del sys.modules[m] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

68 

69 

70@contextlib.contextmanager 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

71def sys_modules_saved() -> Iterator[None]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

72 """A context manager to remove any modules imported during a block.""" 

73 saver = SysModuleSaver() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

74 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

75 yield 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

76 finally: 

77 saver.restore() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

78 

79 

80def import_third_party(modname: str) -> tuple[ModuleType, bool]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

81 """Import a third-party module we need, but might not be installed. 

82 

83 This also cleans out the module after the import, so that coverage won't 

84 appear to have imported it. This lets the third party use coverage for 

85 their own tests. 

86 

87 Arguments: 

88 modname (str): the name of the module to import. 

89 

90 Returns: 

91 The imported module, and a boolean indicating if the module could be imported. 

92 

93 If the boolean is False, the module returned is not the one you want: don't use it. 

94 

95 """ 

96 with sys_modules_saved(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

97 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

98 return importlib.import_module(modname), True 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

99 except ImportError: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

100 return sys, False 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

101 

102 

103def nice_pair(pair: TArc) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

104 """Make a nice string representation of a pair of numbers. 

105 

106 If the numbers are equal, just return the number, otherwise return the pair 

107 with a dash between them, indicating the range. 

108 

109 """ 

110 start, end = pair 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

111 if start == end: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

112 return f"{start}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

113 else: 

114 return f"{start}-{end}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uVvW8wX9xY'yZ(z0!A1)234

115 

116 

117def bool_or_none(b: Any) -> bool | None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

118 """Return bool(b), but preserve None.""" 

119 if b is None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

120 return None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

121 else: 

122 return bool(b) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

123 

124 

125def join_regex(regexes: Iterable[str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

126 """Combine a series of regex strings into one that matches any of them.""" 

127 regexes = list(regexes) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

128 if len(regexes) == 1: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

129 return regexes[0] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

130 else: 

131 return "|".join(f"(?:{r})" for r in regexes) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

132 

133 

134def file_be_gone(path: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

135 """Remove a file, and don't get annoyed if it doesn't exist.""" 

136 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

137 os.remove(path) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

138 except OSError as e: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

139 if e.errno != errno.ENOENT: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

140 raise 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

141 

142 

143def ensure_dir(directory: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

144 """Make sure the directory exists. 

145 

146 If `directory` is None or empty, do nothing. 

147 """ 

148 if directory: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

149 os.makedirs(directory, exist_ok=True) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

150 

151 

152def ensure_dir_for_file(path: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

153 """Make sure the directory for the path exists.""" 

154 ensure_dir(os.path.dirname(path)) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

155 

156 

157class Hasher: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

158 """Hashes Python data for fingerprinting.""" 

159 

160 def __init__(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

161 self.hash = hashlib.new("sha3_256", usedforsecurity=False) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

162 

163 def update(self, v: Any) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

164 """Add `v` to the hash, recursively if needed.""" 

165 self.hash.update(str(type(v)).encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

166 match v: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

167 case None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

168 pass 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

169 case str(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

170 self.hash.update(v.encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

171 case bytes(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

172 self.hash.update(v) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

173 case int() | float(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

174 self.hash.update(str(v).encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

175 case tuple() | list(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

176 for e in v: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

177 self.update(e) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

178 case dict(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

179 keys = v.keys() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

180 for k in sorted(keys): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

181 self.update(k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtU5uVvWwXxYyZz0A1234

182 self.update(v[k]) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtU5uVvWwXxYyZz0A1234

183 case _: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

184 for k in dir(v): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

185 if k.startswith("__"): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

186 continue 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

187 a = getattr(v, k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

188 if inspect.isroutine(a): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

189 continue 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

190 self.update(k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

191 self.update(a) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

192 self.hash.update(b".") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

193 

194 def hexdigest(self) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

195 """Retrieve the hex digest of the hash.""" 

196 return self.hash.hexdigest()[:32] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

197 

198 

199def _needs_to_implement(that: Any, func_name: str) -> NoReturn: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

200 """Helper to raise NotImplementedError in interface stubs.""" 

201 if hasattr(that, "_coverage_plugin_name"): 201 ↛ 205line 201 didn't jump to line 205 because the condition on line 201 was always true1abcdefghijklmnopqrstuvwxyzA

202 thing = "Plugin" 1abcdefghijklmnopqrstuvwxyzA

203 name = that._coverage_plugin_name 1abcdefghijklmnopqrstuvwxyzA

204 else: 

205 thing = "Class" 

206 klass = that.__class__ 

207 name = f"{klass.__module__}.{klass.__name__}" 

208 

209 raise NotImplementedError( 1abcdefghijklmnopqrstuvwxyzA

210 f"{thing} {name!r} needs to implement {func_name}()", 

211 ) 

212 

213 

214class DefaultValue: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

215 """A sentinel object to use for unusual default-value needs. 

216 

217 Construct with a string that will be used as the repr, for display in help 

218 and Sphinx output. 

219 

220 """ 

221 

222 def __init__(self, display_as: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

223 self.display_as = display_as 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

224 

225 def __repr__(self) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

226 return self.display_as 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

227 

228 

229def substitute_variables(text: str, variables: Mapping[str, str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

230 """Substitute ``${VAR}`` variables in `text` with their values. 

231 

232 Variables in the text can take a number of shell-inspired forms:: 

233 

234 $VAR 

235 ${VAR} 

236 ${VAR?} strict: an error if VAR isn't defined. 

237 ${VAR-missing} defaulted: "missing" if VAR isn't defined. 

238 $$ just a dollar sign. 

239 

240 `variables` is a dictionary of variable values. 

241 

242 Returns the resulting text with values substituted. 

243 

244 """ 

245 dollar_pattern = r"""(?x) # Use extended regex syntax 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

246 \$ # A dollar sign, 

247 (?: # then 

248 (?P<dollar> \$ ) | # a dollar sign, or 

249 (?P<word1> \w+ ) | # a plain word, or 

250 \{ # a {-wrapped 

251 (?P<word2> \w+ ) # word, 

252 (?: # either 

253 (?P<strict> \? ) | # with a strict marker 

254 -(?P<defval> [^}]* ) # or a default value 

255 )? # maybe. 

256 } 

257 ) 

258 """ 

259 

260 dollar_groups = ("dollar", "word1", "word2") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

261 

262 def dollar_replace(match: re.Match[str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

263 """Called for each $replacement.""" 

264 # Only one of the dollar_groups will have matched, just get its text. 

265 word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

266 if word == "$": 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

267 return "$" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

268 elif word in variables: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

269 return variables[word] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

270 elif match["strict"]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

271 msg = f"Variable {word} is undefined: {text!r}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqR6rSsT7tU5uV#vW8wX9xYyZz0!A1234

272 raise CoverageException(msg) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqR6rSsT7tU5uV#vW8wX9xYyZz0!A1234

273 else: 

274 return match["defval"] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

275 

276 text = re.sub(dollar_pattern, dollar_replace, text) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

277 return text 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

278 

279 

280def format_local_datetime(dt: datetime.datetime) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

281 """Return a string with local timezone representing the date.""" 

282 return dt.astimezone().strftime("%Y-%m-%d %H:%M %z") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

283 

284 

285def import_local_file(modname: str, modfile: str | None = None) -> ModuleType: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

286 """Import a local file as a module. 

287 

288 Opens a file in the current directory named `modname`.py, imports it 

289 as `modname`, and returns the module object. `modfile` is the file to 

290 import if it isn't in the current directory. 

291 

292 """ 

293 if modfile is None: 293 ↛ 295line 293 didn't jump to line 295 because the condition on line 293 was always true1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

294 modfile = modname + ".py" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

295 spec = importlib.util.spec_from_file_location(modname, modfile) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

296 assert spec is not None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

297 mod = importlib.util.module_from_spec(spec) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

298 sys.modules[modname] = mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

299 assert spec.loader is not None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

300 spec.loader.exec_module(mod) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

301 

302 return mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

303 

304 

305@functools.cache 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

306def _human_key(s: str) -> tuple[list[str | int], str]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

307 """Turn a string into a list of string and number chunks. 

308 

309 "z23a" -> (["z", 23, "a"], "z23a") 

310 

311 The original string is appended as a last value to ensure the 

312 key is unique enough so that "x1y" and "x001y" can be distinguished. 

313 """ 

314 

315 def tryint(s: str) -> str | int: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

316 """If `s` is a number, return an int, else `s` unchanged.""" 

317 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

318 return int(s) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

319 except ValueError: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

320 return s 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

321 

322 return ([tryint(c) for c in re.split(r"(\d+)", s)], s) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

323 

324 

325def human_sorted(strings: Iterable[str]) -> list[str]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

326 """Sort the given iterable of strings the way that humans expect. 

327 

328 Numeric components in the strings are sorted as numbers. 

329 

330 Returns the sorted list. 

331 

332 """ 

333 return sorted(strings, key=_human_key) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

334 

335 

336SortableItem = TypeVar("SortableItem", bound=Sequence[Any]) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

337 

338 

339def human_sorted_items( 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1

340 items: Iterable[SortableItem], 

341 reverse: bool = False, 

342) -> list[SortableItem]: 

343 """Sort (string, ...) items the way humans expect. 

344 

345 The elements of `items` can be any tuple/list. They'll be sorted by the 

346 first element (a string), with ties broken by the remaining elements. 

347 

348 Returns the sorted list of items. 

349 """ 

350 return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

351 

352 

353def plural(n: int, thing: str = "", things: str = "") -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

354 """Pluralize a word. 

355 

356 If n is 1, return thing. Otherwise return things, or thing+s. 

357 """ 

358 if n == 1: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

359 return thing 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

360 else: 

361 return things or (thing + "s") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

362 

363 

364def stdout_link(text: str, url: str) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

365 """Format text+url as a clickable link for stdout. 

366 

367 If attached to a terminal, use escape sequences. Otherwise, just return 

368 the text. 

369 """ 

370 if hasattr(sys.stdout, "isatty") and sys.stdout.isatty(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234

371 return f"\033]8;;{url}\a{text}\033]8;;\a" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234

372 else: 

373 return text 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234