Coverage for coverage / files.py: 97.838%

250 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"""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'mn8VW)XY9op5Z0!12#qr634s

35 if not abs_curdir.endswith(os.sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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

37 abs_curdir = abs_curdir + os.sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

38 

39 # The absolute path to our current directory. 

40 RELATIVE_DIR = os.path.normcase(abs_curdir) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

41 

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

43 # avoid duplicating work. 

44 CANONICAL_FILENAME_CACHE = {} 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

60 if fnorm.startswith(RELATIVE_DIR): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

61 filename = filename[len(RELATIVE_DIR) :] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

62 return filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

72 cf = filename 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

73 if not os.path.isabs(filename): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

74 for path in [os.curdir] + sys.path: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

76 continue # type: ignore[unreachable] 

77 f = os.path.join(path, filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

78 try: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

79 exists = os.path.exists(f) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

80 except UnicodeError: 

81 exists = False 

82 if exists: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

83 cf = f 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

84 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

85 cf = abs_file(cf) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

86 CANONICAL_FILENAME_CACHE[filename] = cf 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

87 return CANONICAL_FILENAME_CACHE[filename] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

101 if dirname: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

102 fp = hashlib.new( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

103 "sha3_256", 

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

105 usedforsecurity=False, 

106 ).hexdigest()[:16] 

107 prefix = f"z_{fp}_" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

108 else: 

109 prefix = "" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

110 return prefix + basename.replace(".", "_") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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: 1abcdefghijkl7mn8op5qr6s

120 return _ACTUAL_PATH_CACHE[path] 1abcdefghijkl7mn8op5qr6s

121 

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

123 if not tail: 1abcdefghijkl7mn8op5qr6s

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 true1abcdefghijkl7mn8op5qr6s

127 actpath = tail 

128 else: 

129 head = actual_path(head) 1abcdefghijkl7mn8op5qr6s

130 if head in _ACTUAL_PATH_LIST_CACHE: 1abcdefghijkl7mn8op5qr6s

131 files = _ACTUAL_PATH_LIST_CACHE[head] 1abcdefghijkl7mn8op5qr6s

132 else: 

133 try: 1abcdefghijkl7mn8op5qr6s

134 files = os.listdir(head) 1abcdefghijkl7mn8op5qr6s

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 1abcdefghijkl7mn8op5qr6s

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

141 for f in files: 1abcdefghijkl7mn8op5qr6s

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

143 tail = f 1abcdefghijkl7mn8op5qr6s

144 break 1abcdefghijkl7mn8op5qr6s

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

146 _ACTUAL_PATH_CACHE[path] = actpath 1abcdefghijkl7mn8op5qr6s

147 return actpath 1abcdefghijkl7mn8op5qr6s

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)XY9Z0!12#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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

169 zipbase, extension, inner = filename.partition(ext + sep(filename)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

170 if extension: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

171 zipfile = zipbase + ext 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

172 if os.path.exists(zipfile): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

173 return zipfile, inner 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

174 return None 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

180 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

181 

182 if zip_location(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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(klRS$TU'mnVW)XY9opZ0!12#qr34s

186 

187 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

211 for p in patterns or []: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

212 prepped.append(p) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

213 if not p.startswith(("*", "?")): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

214 prepped.append(abs_file(p)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

215 return prepped 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

229 self.paths = [os.path.normcase(p) for p in paths] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

230 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

231 

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

233 return f"<TreeMatcher {self.name} {self.original_paths!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

242 for p in self.paths: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

243 if fpath.startswith(p): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

244 if fpath == p: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

245 # This is the same file! 

246 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s

247 if fpath[len(p)] == os.sep: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

248 # This is a file in the directory 

249 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

250 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

258 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

259 

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

261 return f"<ModuleMatcher {self.name} {self.modules!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

270 return False 

271 

272 for m in self.modules: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

273 if module_name.startswith(m): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

274 if module_name == m: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

275 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

276 if module_name[len(m)] == ".": 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

277 # This is a module in the package 

278 return True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

279 

280 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

288 self.re = globs_to_regex(self.pats, case_insensitive=env.WINDOWS) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

289 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

290 

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

292 return f"<GlobMatcher {self.name} {self.pats!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

306 the_sep = sep_match[0] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

307 else: 

308 the_sep = os.sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

309 return the_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

338 if "/" not in pattern: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

339 pattern = f"**/{pattern}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

340 path_rx = [] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

341 pos = 0 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

342 while pos < len(pattern): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

343 for rx, sub in G2RX_TOKENS: # pragma: always breaks 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

344 if m := rx.match(pattern, pos=pos): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

345 if sub is None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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

347 path_rx.append(m.expand(sub)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

348 pos = m.end() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

349 break 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

350 return "".join(path_rx) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

375 if case_insensitive: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

376 flags |= re.IGNORECASE 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

377 rx = join_regex(map(_glob_to_regex, patterns)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

378 if not partial: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

379 rx = rf"(?:{rx})\Z" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

380 compiled = re.compile(rx, flags=flags) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

381 return compiled 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

403 self.debugfn = debugfn or (lambda msg: 0) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

404 self.relative = relative 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

405 self.pprinted = False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

410 for original_pattern, regex, result in self.aliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

411 self.debugfn(f" Rule: {original_pattern!r} -> {result!r} using regex {regex.pattern!r}") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

427 pattern_sep = sep(pattern) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

428 

429 if len(pattern) > 1: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

430 pattern = pattern.rstrip(r"\/") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

431 

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

433 if pattern.endswith("*"): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

434 raise ConfigError("Pattern must not end with wildcards.") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQklRS$TU'mnVWXY9opZ0!12#qr34s

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'mn8VW)XY9op5Z0!12#qr634s

439 if not pattern.startswith("*") and not isabs_anywhere(pattern + pattern_sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

440 pattern = abs_file(pattern) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

441 if not pattern.endswith(pattern_sep): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

442 pattern += pattern_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

443 

444 # Make a regex from the pattern. 

445 regex = globs_to_regex([pattern], case_insensitive=True, partial=True) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

446 

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

448 result_sep = sep(result) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

449 result = result.rstrip(r"\/") + result_sep 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

450 self.aliases.append((original_pattern, regex, result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

472 self.pprint() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

473 self.pprinted = True 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

474 

475 for original_pattern, regex, result in self.aliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

476 if m := regex.match(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

477 new = path.replace(m[0], result) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

478 new = new.replace(sep(path), sep(result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

479 if not self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

480 new = canonical_filename(new) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

481 dot_start = result.startswith(("./", ".\\")) and len(result) > 2 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

482 if new.startswith(("./", ".\\")) and not dot_start: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

483 new = new[2:] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRS$TUmnVWXY9opZ0!12qr34s

484 if not exists(new): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

485 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXY9op5Z012#qr634s

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

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

488 ) 

489 continue 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXY9op5Z012#qr634s

490 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

495 

496 # If we get here, no pattern matched. 

497 

498 if self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

499 path = relative_filename(path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VWXY9op5Z0!12#qr634s

500 

501 if self.relative and not isabs_anywhere(path): 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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

503 parts = re.split(r"[/\\]", path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmn8VWXY9op5Z0!12qr634s

504 if len(parts) > 1: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmn8VWXY9op5Z0!12qr634s

505 dir1 = parts[0] 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

506 pattern = f"*/{dir1}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

507 regex_pat = rf"^(.*[\\/])?{re.escape(dir1)}[\\/]" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

508 result = f"{dir1}{os.sep}" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

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'mn8VW)XY9op5Z0!12#qr634s

511 self.debugfn( 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

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

513 ) 

514 self.aliases.append((pattern, re.compile(regex_pat), result)) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

515 return self.map(path, exists=exists) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQkl7RSTUmnVWXY9op5Z0!12qr634s

516 

517 self.debugfn(f"No rules match, path {path!r} is unchanged") 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

518 return path 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

538 if not include_namespace_packages: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

539 if i > 0 and "__init__.py" not in filenames: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

543 continue 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

544 for filename in filenames: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

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'mn8VW)XY9op5Z0!12#qr634s

549 yield os.path.join(dirpath, filename) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s

550 

551 

552# Globally set the relative directory. 

553set_relative_directory() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s