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
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
1import fnmatch
2import os
3import re
5from .compat.py313 import legacy_end_marker
7_default_seps = os.sep + str(os.altsep) * bool(os.altsep)
10class Translator:
11 """
12 >>> Translator('xyz')
13 Traceback (most recent call last):
14 ...
15 AssertionError: Invalid separators
17 >>> Translator('')
18 Traceback (most recent call last):
19 ...
20 AssertionError: Invalid separators
21 """
23 seps: str
25 def __init__(self, seps: str = _default_seps):
26 assert seps and set(seps) <= set(_default_seps), "Invalid separators"
27 self.seps = seps
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)))
35 @legacy_end_marker
36 def extend(self, pattern):
37 r"""
38 Extend regex for pattern-wide concerns.
40 Apply '(?s:)' to create a non-matching group that
41 matches newlines (valid on Unix).
43 Append '\z' to imply fullmatch even when match is used.
44 """
45 return rf'(?s:{pattern})\z'
47 def match_dirs(self, pattern):
48 """
49 Ensure that zipfile.Path directory names are matched.
51 zipfile.Path directory names always end in a slash.
52 """
53 return rf'{pattern}[/]?'
55 def translate_core(self, pattern):
56 r"""
57 Given a glob pattern, produce a regex that matches it.
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))))
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))
77 def _replace_set(self, character_set):
78 r"""
79 Translate a character set using shell-style semantics.
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.
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}'
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 )
105 def restrict_rglob(self, pattern):
106 """
107 Raise ValueError if ** appears in anything but a full path segment.
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")
119 def star_not_empty(self, pattern):
120 """
121 Ensure that * will not match an empty segment.
122 """
124 def handle_segment(match):
125 segment = match.group(0)
126 return '?*' if segment == '*' else segment
128 not_seps_pattern = rf'[^{re.escape(self.seps)}]+'
129 return re.sub(not_seps_pattern, handle_segment, pattern)
132def separate(pattern):
133 """
134 Separate out character sets to avoid translating their contents.
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)
144def _strip_fnmatch(translated):
145 r"""
146 Strip fnmatch's outer group and end marker.
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)