Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/zipp/glob.py: 47%

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

43 statements  

1import fnmatch 

2import os 

3import re 

4 

5from .compat.py313 import legacy_end_marker 

6 

7_default_seps = os.sep + str(os.altsep) * bool(os.altsep) 

8 

9 

10class Translator: 

11 """ 

12 >>> Translator('xyz') 

13 Traceback (most recent call last): 

14 ... 

15 AssertionError: Invalid separators 

16 

17 >>> Translator('') 

18 Traceback (most recent call last): 

19 ... 

20 AssertionError: Invalid separators 

21 """ 

22 

23 seps: str 

24 

25 def __init__(self, seps: str = _default_seps): 

26 assert seps and set(seps) <= set(_default_seps), "Invalid separators" 

27 self.seps = seps 

28 

29 def translate(self, pattern): 

30 """ 

31 Given a glob pattern, produce a regex that matches it. 

32 """ 

33 return self.extend(self.match_dirs(self.translate_core(pattern))) 

34 

35 @legacy_end_marker 

36 def extend(self, pattern): 

37 r""" 

38 Extend regex for pattern-wide concerns. 

39 

40 Apply '(?s:)' to create a non-matching group that 

41 matches newlines (valid on Unix). 

42 

43 Append '\z' to imply fullmatch even when match is used. 

44 """ 

45 return rf'(?s:{pattern})\z' 

46 

47 def match_dirs(self, pattern): 

48 """ 

49 Ensure that zipfile.Path directory names are matched. 

50 

51 zipfile.Path directory names always end in a slash. 

52 """ 

53 return rf'{pattern}[/]?' 

54 

55 def translate_core(self, pattern): 

56 r""" 

57 Given a glob pattern, produce a regex that matches it. 

58 

59 >>> t = Translator() 

60 >>> t.translate_core('*.txt').replace('\\\\', '') 

61 '[^/]*\\.txt' 

62 >>> t.translate_core('a?txt') 

63 'a[^/]txt' 

64 >>> t.translate_core('**/*').replace('\\\\', '') 

65 '.*/[^/][^/]*' 

66 """ 

67 self.restrict_rglob(pattern) 

68 return ''.join(map(self.replace, separate(self.star_not_empty(pattern)))) 

69 

70 def replace(self, match): 

71 """ 

72 Perform the replacements for a match from :func:`separate`. 

73 """ 

74 handler = self._replace_set if match.group('set') else self._replace_plain 

75 return handler(match.group(0)) 

76 

77 def _replace_set(self, character_set): 

78 r""" 

79 Translate a character set using shell-style semantics. 

80 

81 Glob character sets treat a leading ``!`` as negation and ``^`` 

82 as a literal (unlike regex), so defer to fnmatch, but guard 

83 against matching a separator. 

84 

85 >>> Translator('/')._replace_set('[!a]') 

86 '(?![/])[^a]' 

87 >>> Translator('/')._replace_set('[^a]') 

88 '(?![/])[\\^a]' 

89 """ 

90 translated = _strip_fnmatch(fnmatch.translate(character_set)) 

91 return rf'(?![{re.escape(self.seps)}]){translated}' 

92 

93 def _replace_plain(self, text): 

94 """ 

95 Escape the text, translating any wildcards. 

96 """ 

97 return ( 

98 re 

99 .escape(text) 

100 .replace('\\*\\*', r'.*') 

101 .replace('\\*', rf'[^{re.escape(self.seps)}]*') 

102 .replace('\\?', r'[^/]') 

103 ) 

104 

105 def restrict_rglob(self, pattern): 

106 """ 

107 Raise ValueError if ** appears in anything but a full path segment. 

108 

109 >>> Translator().translate('**foo') 

110 Traceback (most recent call last): 

111 ... 

112 ValueError: ** must appear alone in a path segment 

113 """ 

114 seps_pattern = rf'[{re.escape(self.seps)}]+' 

115 segments = re.split(seps_pattern, pattern) 

116 if any('**' in segment and segment != '**' for segment in segments): 

117 raise ValueError("** must appear alone in a path segment") 

118 

119 def star_not_empty(self, pattern): 

120 """ 

121 Ensure that * will not match an empty segment. 

122 """ 

123 

124 def handle_segment(match): 

125 segment = match.group(0) 

126 return '?*' if segment == '*' else segment 

127 

128 not_seps_pattern = rf'[^{re.escape(self.seps)}]+' 

129 return re.sub(not_seps_pattern, handle_segment, pattern) 

130 

131 

132def separate(pattern): 

133 """ 

134 Separate out character sets to avoid translating their contents. 

135 

136 >>> [m.group(0) for m in separate('*.txt')] 

137 ['*.txt'] 

138 >>> [m.group(0) for m in separate('a[?]txt')] 

139 ['a', '[?]', 'txt'] 

140 """ 

141 return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern) 

142 

143 

144def _strip_fnmatch(translated): 

145 r""" 

146 Strip fnmatch's outer group and end marker. 

147 

148 >>> _strip_fnmatch(r'(?s:[^a])\z') 

149 '[^a]' 

150 >>> _strip_fnmatch(r'(?s:[^a])\Z') 

151 '[^a]' 

152 """ 

153 return re.fullmatch(r'\(\?s:(.*)\)\\[zZ]', translated, re.DOTALL).group(1)