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

126 statements  

1"""Filename matching with shell patterns. 

2 

3fnmatch(FILENAME, PATTERN) matches according to the local convention. 

4fnmatchcase(FILENAME, PATTERN) always takes case in account. 

5 

6The functions operate by translating the pattern into a regular 

7expression. They cache the compiled regular expressions for speed. 

8 

9The function translate(PATTERN) returns a regular expression 

10corresponding to PATTERN. (It does not compile it.) 

11""" 

12 

13import functools 

14import itertools 

15import os 

16import posixpath 

17import re 

18 

19__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"] 

20 

21 

22def fnmatch(name, pat): 

23 """Test whether FILENAME matches PATTERN. 

24 

25 Patterns are Unix shell style: 

26 

27 * matches everything 

28 ? matches any single character 

29 [seq] matches any character in seq 

30 [!seq] matches any char not in seq 

31 

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) 

40 

41 

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 

51 

52 

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 

68 

69 

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)) 

77 

78 result = [] 

79 for name in names: 

80 if match(os.path.normcase(name)) is None: 

81 result.append(name) 

82 return result 

83 

84 

85def fnmatchcase(name, pat): 

86 """Test whether FILENAME matches PATTERN, including case. 

87 

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 

93 

94 

95def translate(pat): 

96 """Translate a shell PATTERN to a regular expression. 

97 

98 There is no way to quote meta-characters. 

99 """ 

100 

101 parts, star_indices = _translate(pat, '*', '.') 

102 return _join_translated_parts(parts, star_indices) 

103 

104 

105_re_setops_sub = re.compile(r'([&~|])').sub 

106_re_escape = functools.lru_cache(maxsize=512)(re.escape) 

107 

108 

109def _translate(pat, star, question_mark): 

110 res = [] 

111 add = res.append 

112 star_indices = [] 

113 

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 

184 

185 

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'