Coverage for coverage / files.py: 97.838%

250 statements  

« prev     ^ index     » next       coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +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"""File wrangling.""" 

5 

6from __future__ import annotations 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

7 

8import hashlib 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

9import ntpath 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

10import os 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

11import os.path 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

12import posixpath 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

13import re 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

14import sys 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

15from collections.abc import Iterable 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

16from typing import Callable 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

17 

18from coverage import env 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

19from coverage.exceptions import ConfigError 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

20from coverage.misc import human_sorted, isolate_module, join_regex 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

21 

22os = isolate_module(os) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

23 

24 

25RELATIVE_DIR: str = "" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

26CANONICAL_FILENAME_CACHE: dict[str, str] = {} 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

27 

28 

29def set_relative_directory() -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

30 """Set the directory that `relative_filename` will be relative to.""" 

31 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE 

32 

33 # The current directory 

34 abs_curdir = abs_file(os.curdir) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

35 if not abs_curdir.endswith(os.sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

36 # Suffix with separator only if not at the system root 

37 abs_curdir = abs_curdir + os.sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

38 

39 # The absolute path to our current directory. 

40 RELATIVE_DIR = os.path.normcase(abs_curdir) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

41 

42 # Cache of results of calling the canonical_filename() method, to 

43 # avoid duplicating work. 

44 CANONICAL_FILENAME_CACHE = {} 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

45 

46 

47def relative_directory() -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

48 """Return the directory that `relative_filename` is relative to.""" 

49 return RELATIVE_DIR 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

50 

51 

52def relative_filename(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

53 """Return the relative form of `filename`. 

54 

55 The file name will be relative to the current directory when the 

56 `set_relative_directory` was called. 

57 

58 """ 

59 fnorm = os.path.normcase(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

60 if fnorm.startswith(RELATIVE_DIR): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

61 filename = filename[len(RELATIVE_DIR) :] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

62 return filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

63 

64 

65def canonical_filename(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

66 """Return a canonical file name for `filename`. 

67 

68 An absolute path with no redundant components and normalized case. 

69 

70 """ 

71 if filename not in CANONICAL_FILENAME_CACHE: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

72 cf = filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

73 if not os.path.isabs(filename): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

74 for path in [os.curdir] + sys.path: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

75 if path is None: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

76 continue # type: ignore[unreachable] 

77 f = os.path.join(path, filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

78 try: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

79 exists = os.path.exists(f) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

80 except UnicodeError: 

81 exists = False 

82 if exists: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

83 cf = f 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

84 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

85 cf = abs_file(cf) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

86 CANONICAL_FILENAME_CACHE[filename] = cf 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

87 return CANONICAL_FILENAME_CACHE[filename] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

88 

89 

90def flat_rootname(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

91 """A base for a flat file name to correspond to this file. 

92 

93 Useful for writing files about the code where you want all the files in 

94 the same directory, but need to differentiate same-named files from 

95 different directories. 

96 

97 For example, the file a/b/c.py will return 'z_86bbcbe134d28fd2_c_py' 

98 

99 """ 

100 dirname, basename = ntpath.split(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

101 if dirname: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

102 fp = hashlib.new( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

103 "sha3_256", 

104 dirname.encode("UTF-8"), 

105 usedforsecurity=False, 

106 ).hexdigest()[:16] 

107 prefix = f"z_{fp}_" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

108 else: 

109 prefix = "" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

110 return prefix + basename.replace(".", "_") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

111 

112 

113if env.WINDOWS: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

114 _ACTUAL_PATH_CACHE: dict[str, str] = {} 1abcdefghijklmnopqrs

115 _ACTUAL_PATH_LIST_CACHE: dict[str, list[str]] = {} 1abcdefghijklmnopqrs

116 

117 def actual_path(path: str) -> str: 1abcdefghijklmnopqr34s

118 """Get the actual path of `path`, including the correct case.""" 

119 if path in _ACTUAL_PATH_CACHE: 1abcdefghijkl7mn6op8qr5s

120 return _ACTUAL_PATH_CACHE[path] 1abcdefghijkl7mn6op8qr5s

121 

122 head, tail = os.path.split(path) 1abcdefghijkl7mn6op8qr5s

123 if not tail: 1abcdefghijkl7mn6op8qr5s

124 # This means head is the drive spec: normalize it. 

125 actpath = head.upper() 1abcdefghijklmnopqrs

126 elif not head: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true1abcdefghijkl7mn6op8qr5s

127 actpath = tail 

128 else: 

129 head = actual_path(head) 1abcdefghijkl7mn6op8qr5s

130 if head in _ACTUAL_PATH_LIST_CACHE: 1abcdefghijkl7mn6op8qr5s

131 files = _ACTUAL_PATH_LIST_CACHE[head] 1abcdefghijkl7mn6op8qr5s

132 else: 

133 try: 1abcdefghijkl7mn6op8qr5s

134 files = os.listdir(head) 1abcdefghijkl7mn6op8qr5s

135 except Exception: 1abcdefghijklmnopqrs

136 # This will raise OSError, or this bizarre TypeError: 

137 # https://bugs.python.org/issue1776160 

138 files = [] 1abcdefghijklmnopqrs

139 _ACTUAL_PATH_LIST_CACHE[head] = files 1abcdefghijkl7mn6op8qr5s

140 normtail = os.path.normcase(tail) 1abcdefghijkl7mn6op8qr5s

141 for f in files: 1abcdefghijkl7mn6op8qr5s

142 if os.path.normcase(f) == normtail: 1abcdefghijkl7mn6op8qr5s

143 tail = f 1abcdefghijkl7mn6op8qr5s

144 break 1abcdefghijkl7mn6op8qr5s

145 actpath = os.path.join(head, tail) 1abcdefghijkl7mn6op8qr5s

146 _ACTUAL_PATH_CACHE[path] = actpath 1abcdefghijkl7mn6op8qr5s

147 return actpath 1abcdefghijkl7mn6op8qr5s

148 

149else: 

150 

151 def actual_path(path: str) -> str: 1tuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

152 """The actual path for non-Windows platforms.""" 

153 return path 1tuvwxyzABCDEFGHIJKLMNO%PQ!RS$TU#VW(XY)Z0912'34

154 

155 

156def abs_file(path: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

157 """Return the absolute normalized form of `path`.""" 

158 return actual_path(os.path.abspath(os.path.realpath(path))) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

159 

160 

161def zip_location(filename: str) -> tuple[str, str] | None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

162 """Split a filename into a zipfile / inner name pair. 

163 

164 Only return a pair if the zipfile exists. No check is made if the inner 

165 name is in the zipfile. 

166 

167 """ 

168 for ext in [".zip", ".whl", ".egg", ".pex", ".par"]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

169 zipbase, extension, inner = filename.partition(ext + sep(filename)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

170 if extension: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

171 zipfile = zipbase + ext 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

172 if os.path.exists(zipfile): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

173 return zipfile, inner 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

174 return None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

175 

176 

177def source_exists(path: str) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

178 """Determine if a source file path exists.""" 

179 if os.path.exists(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

180 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

181 

182 if zip_location(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

183 # If zip_location returns anything, then it's a zipfile that 

184 # exists. That's good enough for us. 

185 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mnVW(XY)opZ0912'qr534s

186 

187 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

188 

189 

190def python_reported_file(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

191 """Return the string as Python would describe this file name.""" 

192 return os.path.abspath(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

193 

194 

195def isabs_anywhere(filename: str) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

196 """Is `filename` an absolute path on any OS?""" 

197 return ntpath.isabs(filename) or posixpath.isabs(filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

198 

199 

200def prep_patterns(patterns: Iterable[str]) -> list[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

201 """Prepare the file patterns for use in a `GlobMatcher`. 

202 

203 If a pattern starts with a wildcard, it is used as a pattern 

204 as-is. If it does not start with a wildcard, then it is made 

205 absolute with the current directory. 

206 

207 If `patterns` is None, an empty list is returned. 

208 

209 """ 

210 prepped = [] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

211 for p in patterns or []: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

212 prepped.append(p) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

213 if not p.startswith(("*", "?")): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

214 prepped.append(abs_file(p)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

215 return prepped 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

216 

217 

218class TreeMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

219 """A matcher for files in a tree. 

220 

221 Construct with a list of paths, either files or directories. Paths match 

222 with the `match` method if they are one of the files, or if they are 

223 somewhere in a subtree rooted at one of the directories. 

224 

225 """ 

226 

227 def __init__(self, paths: Iterable[str], name: str = "unknown") -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

228 self.original_paths: list[str] = human_sorted(paths) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

229 self.paths = [os.path.normcase(p) for p in paths] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

230 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

231 

232 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

233 return f"<TreeMatcher {self.name} {self.original_paths!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

234 

235 def info(self) -> list[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

236 """A list of strings for displaying when dumping state.""" 

237 return self.original_paths 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

238 

239 def match(self, fpath: str) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

240 """Does `fpath` indicate a file in one of our trees?""" 

241 fpath = os.path.normcase(fpath) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

242 for p in self.paths: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

243 if fpath.startswith(p): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

244 if fpath == p: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQklRSTUmnVW(XYopZ0912qr34s

245 # This is the same file! 

246 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQklRSTUmnVW(XYopZ0912qr34s

247 if fpath[len(p)] == os.sep: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQklRSTUmnVW(XYopZ0912qr34s

248 # This is a file in the directory 

249 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQklRSTUmnVW(XYopZ0912qr34s

250 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

251 

252 

253class ModuleMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

254 """A matcher for modules in a tree.""" 

255 

256 def __init__(self, module_names: Iterable[str], name: str = "unknown") -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

257 self.modules = list(module_names) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

258 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

259 

260 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

261 return f"<ModuleMatcher {self.name} {self.modules!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

262 

263 def info(self) -> list[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

264 """A list of strings for displaying when dumping state.""" 

265 return self.modules 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

266 

267 def match(self, module_name: str) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

268 """Does `module_name` indicate a module in one of our packages?""" 

269 if not module_name: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

270 return False 

271 

272 for m in self.modules: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

273 if module_name.startswith(m): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

274 if module_name == m: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

275 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

276 if module_name[len(m)] == ".": 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

277 # This is a module in the package 

278 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

279 

280 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

281 

282 

283class GlobMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

284 """A matcher for files by file name pattern.""" 

285 

286 def __init__(self, pats: Iterable[str], name: str = "unknown") -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

287 self.pats = list(pats) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

288 self.re = globs_to_regex(self.pats, case_insensitive=env.WINDOWS) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

289 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

290 

291 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

292 return f"<GlobMatcher {self.name} {self.pats!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

293 

294 def info(self) -> list[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

295 """A list of strings for displaying when dumping state.""" 

296 return self.pats 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

297 

298 def match(self, fpath: str) -> bool: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

299 """Does `fpath` match one of our file name patterns?""" 

300 return self.re.match(fpath) is not None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

301 

302 

303def sep(s: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

304 """Find the path separator used in this string, or os.sep if none.""" 

305 if sep_match := re.search(r"[\\/]", s): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

306 the_sep = sep_match[0] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

307 else: 

308 the_sep = os.sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

309 return the_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

310 

311 

312# Tokenizer for _glob_to_regex. 

313# None as a sub means disallowed. 

314# fmt: off 

315G2RX_TOKENS = [(re.compile(rx), sub) for rx, sub in [ 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

316 (r"\*\*\*+", None), # Can't have *** 

317 (r"[^/]+\*\*+", None), # Can't have x** 

318 (r"\*\*+[^/]+", None), # Can't have **x 

319 (r"\*\*/\*\*", None), # Can't have **/** 

320 (r"^\*+/", r"(.*[/\\\\])?"), # ^*/ matches any prefix-slash, or nothing. 

321 (r"/\*+$", r"[/\\\\].*"), # /*$ matches any slash-suffix. 

322 (r"\*\*/", r"(.*[/\\\\])?"), # **/ matches any subdirs, including none 

323 (r"/", r"[/\\\\]"), # / matches either slash or backslash 

324 (r"\*", r"[^/\\\\]*"), # * matches any number of non slash-likes 

325 (r"\?", r"[^/\\\\]"), # ? matches one non slash-like 

326 (r"\[.*?\]", r"\g<0>"), # [a-f] matches [a-f] 

327 (r"[a-zA-Z0-9_-]+", r"\g<0>"), # word chars match themselves 

328 (r"[\[\]]", None), # Can't have single square brackets 

329 (r".", r"\\\g<0>"), # Anything else is escaped to be safe 

330]] 

331# fmt: on 

332 

333 

334def _glob_to_regex(pattern: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

335 """Convert a file-path glob pattern into a regex.""" 

336 # Turn all backslashes into slashes to simplify the tokenizer. 

337 pattern = pattern.replace("\\", "/") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

338 if "/" not in pattern: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

339 pattern = f"**/{pattern}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

340 path_rx = [] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

341 pos = 0 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

342 while pos < len(pattern): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

343 for rx, sub in G2RX_TOKENS: # pragma: always breaks 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

344 if m := rx.match(pattern, pos=pos): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

345 if sub is None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

346 raise ConfigError(f"File pattern can't include {m[0]!r}") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!klRS$TU#mnVW(XY)opZ0912'qr34s

347 path_rx.append(m.expand(sub)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

348 pos = m.end() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

349 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

350 return "".join(path_rx) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

351 

352 

353def globs_to_regex( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr

354 patterns: Iterable[str], 

355 case_insensitive: bool = False, 

356 partial: bool = False, 

357) -> re.Pattern[str]: 

358 """Convert glob patterns to a compiled regex that matches any of them. 

359 

360 Slashes are always converted to match either slash or backslash, for 

361 Windows support, even when running elsewhere. 

362 

363 If the pattern has no slash or backslash, then it is interpreted as 

364 matching a file name anywhere it appears in the tree. Otherwise, the glob 

365 pattern must match the whole file path. 

366 

367 If `partial` is true, then the pattern will match if the target string 

368 starts with the pattern. Otherwise, it must match the entire string. 

369 

370 Returns: a compiled regex object. Use the .match method to compare target 

371 strings. 

372 

373 """ 

374 flags = 0 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

375 if case_insensitive: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

376 flags |= re.IGNORECASE 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

377 rx = join_regex(map(_glob_to_regex, patterns)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

378 if not partial: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

379 rx = rf"(?:{rx})\Z" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

380 compiled = re.compile(rx, flags=flags) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

381 return compiled 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

382 

383 

384class PathAliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

385 """A collection of aliases for paths. 

386 

387 When combining data files from remote machines, often the paths to source 

388 code are different, for example, due to OS differences, or because of 

389 serialized checkouts on continuous integration machines. 

390 

391 A `PathAliases` object tracks a list of pattern/result pairs, and can 

392 map a path through those aliases to produce a unified path. 

393 

394 """ 

395 

396 def __init__( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr

397 self, 

398 debugfn: Callable[[str], None] | None = None, 

399 relative: bool = False, 

400 ) -> None: 

401 # A list of (original_pattern, regex, result) 

402 self.aliases: list[tuple[str, re.Pattern[str], str]] = [] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

403 self.debugfn = debugfn or (lambda msg: 0) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

404 self.relative = relative 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

405 self.pprinted = False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

406 

407 def pprint(self) -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

408 """Dump the important parts of the PathAliases, for debugging.""" 

409 self.debugfn(f"Aliases (relative={self.relative}):") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

410 for original_pattern, regex, result in self.aliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

411 self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

412 

413 def add(self, pattern: str, result: str) -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

414 """Add the `pattern`/`result` pair to the list of aliases. 

415 

416 `pattern` is an `glob`-style pattern. `result` is a simple 

417 string. When mapping paths, if a path starts with a match against 

418 `pattern`, then that match is replaced with `result`. This models 

419 isomorphic source trees being rooted at different places on two 

420 different machines. 

421 

422 `pattern` can't end with a wildcard component, since that would 

423 match an entire tree, and not just its root. 

424 

425 """ 

426 original_pattern = pattern 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

427 pattern_sep = sep(pattern) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

428 

429 if len(pattern) > 1: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

430 pattern = pattern.rstrip(r"\/") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

431 

432 # The pattern can't end with a wildcard component. 

433 if pattern.endswith("*"): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

434 raise ConfigError("Pattern must not end with wildcards.") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TUmn6VWXY)opZ0912'qr534s

435 

436 # The pattern is meant to match a file path. Let's make it absolute 

437 # unless it already is, or is meant to match any prefix. 

438 if not self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

439 if not pattern.startswith("*") and not isabs_anywhere(pattern + pattern_sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

440 pattern = abs_file(pattern) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

441 if not pattern.endswith(pattern_sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

442 pattern += pattern_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

443 

444 # Make a regex from the pattern. 

445 regex = globs_to_regex([pattern], case_insensitive=True, partial=True) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

446 

447 # Normalize the result: it must end with a path separator. 

448 result_sep = sep(result) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

449 result = result.rstrip(r"\/") + result_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

450 self.aliases.append((original_pattern, regex, result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

451 

452 def map(self, path: str, exists: Callable[[str], bool] = source_exists) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

453 """Map `path` through the aliases. 

454 

455 `path` is checked against all of the patterns. The first pattern to 

456 match is used to replace the root of the path with the result root. 

457 Only one pattern is ever used. If no patterns match, `path` is 

458 returned unchanged. 

459 

460 The separator style in the result is made to match that of the result 

461 in the alias. 

462 

463 `exists` is a function to determine if the resulting path actually 

464 exists. 

465 

466 Returns the mapped path. If a mapping has happened, this is a 

467 canonical path. If no mapping has happened, it is the original value 

468 of `path` unchanged. 

469 

470 """ 

471 if not self.pprinted: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

472 self.pprint() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

473 self.pprinted = True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

474 

475 for original_pattern, regex, result in self.aliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

476 if m := regex.match(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

477 new = path.replace(m[0], result) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

478 new = new.replace(sep(path), sep(result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

479 if not self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

480 new = canonical_filename(new) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

481 dot_start = result.startswith(("./", ".\\")) and len(result) > 2 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

482 if new.startswith(("./", ".\\")) and not dot_start: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

483 new = new[2:] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXY)opZ012'qr34s

484 if not exists(new): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

485 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!klRSTU#mn6VWXY)op8Z0912'qr534s

486 f"Rule {original_pattern!r} changed {path!r} to {new!r} " 

487 + "which doesn't exist, continuing", 

488 ) 

489 continue 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!klRSTU#mn6VWXY)op8Z0912'qr534s

490 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

491 f"Matched path {path!r} to rule {original_pattern!r} -> {result!r}, " 

492 + f"producing {new!r}", 

493 ) 

494 return new 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

495 

496 # If we get here, no pattern matched. 

497 

498 if self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

499 path = relative_filename(path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VWXYop8Z0912'qr534s

500 

501 if self.relative and not isabs_anywhere(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

502 # Auto-generate a pattern to implicitly match relative files 

503 parts = re.split(r"[/\\]", path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

504 if len(parts) > 1: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

505 dir1 = parts[0] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

506 pattern = f"*/{dir1}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

507 regex_pat = rf"^(.*[\\/])?{re.escape(dir1)}[\\/]" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

508 result = f"{dir1}{os.sep}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

509 # Only add a new pattern if we don't already have this pattern. 

510 if not any(p == pattern for p, _, _ in self.aliases): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

511 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

512 f"Generating rule: {pattern!r} -> {result!r} using regex {regex_pat!r}", 

513 ) 

514 self.aliases.append((pattern, re.compile(regex_pat), result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

515 return self.map(path, exists=exists) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQ!kl7RS$TU#mn6VWXYop8Z0912qr534s

516 

517 self.debugfn(f"No rules match, path {path!r} is unchanged") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

518 return path 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

519 

520 

521def find_python_files(dirname: str, include_namespace_packages: bool) -> Iterable[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

522 """Yield all of the importable Python files in `dirname`, recursively. 

523 

524 To be importable, the files have to be in a directory with a __init__.py, 

525 except for `dirname` itself, which isn't required to have one. The 

526 assumption is that `dirname` was specified directly, so the user knows 

527 best, but sub-directories are checked for a __init__.py to be sure we only 

528 find the importable files. 

529 

530 If `include_namespace_packages` is True, then the check for __init__.py 

531 files is skipped. 

532 

533 Files with strange characters are skipped, since they couldn't have been 

534 imported, and are probably editor side-files. 

535 

536 """ 

537 for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

538 if not include_namespace_packages: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

539 if i > 0 and "__init__.py" not in filenames: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

540 # If a directory doesn't have __init__.py, then it isn't 

541 # importable and neither are its files 

542 del dirnames[:] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

543 continue 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

544 for filename in filenames: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

545 # We're only interested in files that look like reasonable Python 

546 # files: Must end with .py or .pyw, and must not have certain funny 

547 # characters that probably mean they are editor junk. 

548 if re.match(r"^[^.#~!$@%^&*()+=,]+\.pyw?$", filename): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

549 yield os.path.join(dirpath, filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s

550 

551 

552# Globally set the relative directory. 

553set_relative_directory() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s