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
« 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
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#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
39 # The absolute path to our current directory.
40 RELATIVE_DIR = os.path.normcase(abs_curdir) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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#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
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#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
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#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
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: 1abcdefghijkl7mn6op8qr5s
120 return _ACTUAL_PATH_CACHE[path] 1abcdefghijkl7mn6op8qr5s
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
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(XY)Z0912'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#mn6VW(XY)op8Z0912'qr534s
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#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
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
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
187 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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#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
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#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
232 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
233 return f"<TreeMatcher {self.name} {self.original_paths!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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#mn6VW(XY)op8Z0912'qr534s
258 self.name = name 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
260 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
261 return f"<ModuleMatcher {self.name} {self.modules!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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
280 return False 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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#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
291 def __repr__(self) -> str: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s
292 return f"<GlobMatcher {self.name} {self.pats!r}>" 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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
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#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
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#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
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#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
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
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#mn6VW(XY)op8Z0912'qr534s
427 pattern_sep = sep(pattern) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
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
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
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
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
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
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#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
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
496 # If we get here, no pattern matched.
498 if self.relative: 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VW(XY)op8Z0912'qr534s
499 path = relative_filename(path) 1tuvwabxyzAcdBCDEefFGHIghJKLMijNO%PQ!kl7RS$TU#mn6VWXYop8Z0912'qr534s
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
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
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#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
552# Globally set the relative directory.
553set_relative_directory() 1tuvwabxyzAcdBCDEefFGHIghJKLMijNOPQklRSTUmnVWXYopZ012qr34s