Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pathlib_abc/_fnmatch.py: 14%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Filename matching with shell patterns.
3fnmatch(FILENAME, PATTERN) matches according to the local convention.
4fnmatchcase(FILENAME, PATTERN) always takes case in account.
6The functions operate by translating the pattern into a regular
7expression. They cache the compiled regular expressions for speed.
9The function translate(PATTERN) returns a regular expression
10corresponding to PATTERN. (It does not compile it.)
11"""
13import functools
14import itertools
15import os
16import posixpath
17import re
19__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"]
22def fnmatch(name, pat):
23 """Test whether FILENAME matches PATTERN.
25 Patterns are Unix shell style:
27 * matches everything
28 ? matches any single character
29 [seq] matches any character in seq
30 [!seq] matches any char not in seq
32 An initial period in FILENAME is not special.
33 Both FILENAME and PATTERN are first case-normalized
34 if the operating system requires it.
35 If you don't want this, use fnmatchcase(FILENAME, PATTERN).
36 """
37 name = os.path.normcase(name)
38 pat = os.path.normcase(pat)
39 return fnmatchcase(name, pat)
42@functools.lru_cache(maxsize=32768, typed=True)
43def _compile_pattern(pat):
44 if isinstance(pat, bytes):
45 pat_str = str(pat, 'ISO-8859-1')
46 res_str = translate(pat_str)
47 res = bytes(res_str, 'ISO-8859-1')
48 else:
49 res = translate(pat)
50 return re.compile(res).match
53def filter(names, pat):
54 """Construct a list from those elements of the iterable NAMES that match PAT."""
55 result = []
56 pat = os.path.normcase(pat)
57 match = _compile_pattern(pat)
58 if os.path is posixpath:
59 # normcase on posix is NOP. Optimize it away from the loop.
60 for name in names:
61 if match(name):
62 result.append(name)
63 else:
64 for name in names:
65 if match(os.path.normcase(name)):
66 result.append(name)
67 return result
70def filterfalse(names, pat):
71 """Construct a list from those elements of the iterable NAMES that do not match PAT."""
72 pat = os.path.normcase(pat)
73 match = _compile_pattern(pat)
74 if os.path is posixpath:
75 # normcase on posix is NOP. Optimize it away from the loop.
76 return list(itertools.filterfalse(match, names))
78 result = []
79 for name in names:
80 if match(os.path.normcase(name)) is None:
81 result.append(name)
82 return result
85def fnmatchcase(name, pat):
86 """Test whether FILENAME matches PATTERN, including case.
88 This is a version of fnmatch() which doesn't case-normalize
89 its arguments.
90 """
91 match = _compile_pattern(pat)
92 return match(name) is not None
95def translate(pat):
96 """Translate a shell PATTERN to a regular expression.
98 There is no way to quote meta-characters.
99 """
101 parts, star_indices = _translate(pat, '*', '.')
102 return _join_translated_parts(parts, star_indices)
105_re_setops_sub = re.compile(r'([&~|])').sub
106_re_escape = functools.lru_cache(maxsize=512)(re.escape)
109def _translate(pat, star, question_mark):
110 res = []
111 add = res.append
112 star_indices = []
114 i, n = 0, len(pat)
115 while i < n:
116 c = pat[i]
117 i = i+1
118 if c == '*':
119 # store the position of the wildcard
120 star_indices.append(len(res))
121 add(star)
122 # compress consecutive `*` into one
123 while i < n and pat[i] == '*':
124 i += 1
125 elif c == '?':
126 add(question_mark)
127 elif c == '[':
128 j = i
129 if j < n and pat[j] == '!':
130 j = j+1
131 if j < n and pat[j] == ']':
132 j = j+1
133 while j < n and pat[j] != ']':
134 j = j+1
135 if j >= n:
136 add('\\[')
137 else:
138 stuff = pat[i:j]
139 if '-' not in stuff:
140 stuff = stuff.replace('\\', r'\\')
141 else:
142 chunks = []
143 k = i+2 if pat[i] == '!' else i+1
144 while True:
145 k = pat.find('-', k, j)
146 if k < 0:
147 break
148 chunks.append(pat[i:k])
149 i = k+1
150 k = k+3
151 chunk = pat[i:j]
152 if chunk:
153 chunks.append(chunk)
154 else:
155 chunks[-1] += '-'
156 # Remove empty ranges -- invalid in RE.
157 for k in range(len(chunks)-1, 0, -1):
158 if chunks[k-1][-1] > chunks[k][0]:
159 chunks[k-1] = chunks[k-1][:-1] + chunks[k][1:]
160 del chunks[k]
161 # Escape backslashes and hyphens for set difference (--).
162 # Hyphens that create ranges shouldn't be escaped.
163 stuff = '-'.join(s.replace('\\', r'\\').replace('-', r'\-')
164 for s in chunks)
165 i = j+1
166 if not stuff:
167 # Empty range: never match.
168 add('(?!)')
169 elif stuff == '!':
170 # Negated empty range: match any character.
171 add('.')
172 else:
173 # Escape set operations (&&, ~~ and ||).
174 stuff = _re_setops_sub(r'\\\1', stuff)
175 if stuff[0] == '!':
176 stuff = '^' + stuff[1:]
177 elif stuff[0] in ('^', '['):
178 stuff = '\\' + stuff
179 add(f'[{stuff}]')
180 else:
181 add(_re_escape(c))
182 assert i == n
183 return res, star_indices
186def _join_translated_parts(parts, star_indices):
187 if not star_indices:
188 return fr'(?s:{"".join(parts)})\Z'
189 iter_star_indices = iter(star_indices)
190 j = next(iter_star_indices)
191 buffer = parts[:j] # fixed pieces at the start
192 append, extend = buffer.append, buffer.extend
193 i = j + 1
194 for j in iter_star_indices:
195 # Now deal with STAR fixed STAR fixed ...
196 # For an interior `STAR fixed` pairing, we want to do a minimal
197 # .*? match followed by `fixed`, with no possibility of backtracking.
198 # Atomic groups ("(?>...)") allow us to spell that directly.
199 # Note: people rely on the undocumented ability to join multiple
200 # translate() results together via "|" to build large regexps matching
201 # "one of many" shell patterns.
202 append('(?>.*?')
203 extend(parts[i:j])
204 append(')')
205 i = j + 1
206 append('.*')
207 extend(parts[i:])
208 res = ''.join(buffer)
209 return fr'(?s:{res})\Z'