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
« 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
4"""File wrangling."""
6from __future__ import annotations 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
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
18from coverage import env 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
19from coverage.exceptions import ConfigError 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
20from coverage.misc import human_sorted, isolate_module, join_regex 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
22os = isolate_module(os) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
25RELATIVE_DIR: str = "" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
26CANONICAL_FILENAME_CACHE: dict[str, str] = {} 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
29def set_relative_directory() -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
30 """Set the directory that `relative_filename` will be relative to."""
31 global RELATIVE_DIR, CANONICAL_FILENAME_CACHE
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
39 # The absolute path to our current directory.
40 RELATIVE_DIR = os.path.normcase(abs_curdir) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
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
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
52def relative_filename(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
53 """Return the relative form of `filename`.
55 The file name will be relative to the current directory when the
56 `set_relative_directory` was called.
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
65def canonical_filename(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
66 """Return a canonical file name for `filename`.
68 An absolute path with no redundant components and normalized case.
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
90def flat_rootname(filename: str) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
91 """A base for a flat file name to correspond to this file.
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.
97 For example, the file a/b/c.py will return 'z_86bbcbe134d28fd2_c_py'
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
113if env.WINDOWS: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
114 _ACTUAL_PATH_CACHE: dict[str, str] = {} 1abcdefghijklmnopqrs
115 _ACTUAL_PATH_LIST_CACHE: dict[str, list[str]] = {} 1abcdefghijklmnopqrs
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
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
149else:
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
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
161def zip_location(filename: str) -> tuple[str, str] | None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
162 """Split a filename into a zipfile / inner name pair.
164 Only return a pair if the zipfile exists. No check is made if the inner
165 name is in the zipfile.
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
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
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
187 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
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
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
200def prep_patterns(patterns: Iterable[str]) -> list[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
201 """Prepare the file patterns for use in a `GlobMatcher`.
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.
207 If `patterns` is None, an empty list is returned.
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
218class TreeMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
219 """A matcher for files in a tree.
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.
225 """
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
232 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
233 return f"<TreeMatcher {self.name} {self.original_paths!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
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
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
253class ModuleMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
254 """A matcher for modules in a tree."""
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
260 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
261 return f"<ModuleMatcher {self.name} {self.modules!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
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
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
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
280 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
283class GlobMatcher: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
284 """A matcher for files by file name pattern."""
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
291 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
292 return f"<GlobMatcher {self.name} {self.pats!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
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
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
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
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
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
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.
360 Slashes are always converted to match either slash or backslash, for
361 Windows support, even when running elsewhere.
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.
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.
370 Returns: a compiled regex object. Use the .match method to compare target
371 strings.
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
384class PathAliases: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
385 """A collection of aliases for paths.
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.
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.
394 """
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
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
413 def add(self, pattern: str, result: str) -> None: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
414 """Add the `pattern`/`result` pair to the list of aliases.
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.
422 `pattern` can't end with a wildcard component, since that would
423 match an entire tree, and not just its root.
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
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
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
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
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
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
452 def map(self, path: str, exists: Callable[[str], bool] = source_exists) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
453 """Map `path` through the aliases.
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.
460 The separator style in the result is made to match that of the result
461 in the alias.
463 `exists` is a function to determine if the resulting path actually
464 exists.
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.
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
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
496 # If we get here, no pattern matched.
498 if self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VW)XY9op5Z0!12#qr634s
499 path = relative_filename(path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ(kl7RS$TU'mn8VWXY9op5Z0!12#qr634s
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
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
521def find_python_files(dirname: str, include_namespace_packages: bool) -> Iterable[str]: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
522 """Yield all of the importable Python files in `dirname`, recursively.
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.
530 If `include_namespace_packages` is True, then the check for __init__.py
531 files is skipped.
533 Files with strange characters are skipped, since they couldn't have been
534 imported, and are probably editor side-files.
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
552# Globally set the relative directory.
553set_relative_directory() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s