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 os
2import re
4from .compat.py313 import legacy_end_marker
6_default_seps = os.sep + str(os.altsep) * bool(os.altsep)
9class Translator:
10 """
11 >>> Translator('xyz')
12 Traceback (most recent call last):
13 ...
14 AssertionError: Invalid separators
16 >>> Translator('')
17 Traceback (most recent call last):
18 ...
19 AssertionError: Invalid separators
20 """
22 seps: str
24 def __init__(self, seps: str = _default_seps):
25 assert seps and set(seps) <= set(_default_seps), "Invalid separators"
26 self.seps = seps
28 def translate(self, pattern):
29 """
30 Given a glob pattern, produce a regex that matches it.
31 """
32 return self.extend(self.match_dirs(self.translate_core(pattern)))
34 @legacy_end_marker
35 def extend(self, pattern):
36 r"""
37 Extend regex for pattern-wide concerns.
39 Apply '(?s:)' to create a non-matching group that
40 matches newlines (valid on Unix).
42 Append '\z' to imply fullmatch even when match is used.
43 """
44 return rf'(?s:{pattern})\z'
46 def match_dirs(self, pattern):
47 """
48 Ensure that zipfile.Path directory names are matched.
50 zipfile.Path directory names always end in a slash.
51 """
52 return rf'{pattern}[/]?'
54 def translate_core(self, pattern):
55 r"""
56 Given a glob pattern, produce a regex that matches it.
58 >>> t = Translator()
59 >>> t.translate_core('*.txt').replace('\\\\', '')
60 '[^/]*\\.txt'
61 >>> t.translate_core('a?txt')
62 'a[^/]txt'
63 >>> t.translate_core('**/*').replace('\\\\', '')
64 '.*/[^/][^/]*'
65 """
66 self.restrict_rglob(pattern)
67 return ''.join(map(self.replace, separate(self.star_not_empty(pattern))))
69 def replace(self, match):
70 """
71 Perform the replacements for a match from :func:`separate`.
72 """
73 return match.group('set') or (
74 re.escape(match.group(0))
75 .replace('\\*\\*', r'.*')
76 .replace('\\*', rf'[^{re.escape(self.seps)}]*')
77 .replace('\\?', r'[^/]')
78 )
80 def restrict_rglob(self, pattern):
81 """
82 Raise ValueError if ** appears in anything but a full path segment.
84 >>> Translator().translate('**foo')
85 Traceback (most recent call last):
86 ...
87 ValueError: ** must appear alone in a path segment
88 """
89 seps_pattern = rf'[{re.escape(self.seps)}]+'
90 segments = re.split(seps_pattern, pattern)
91 if any('**' in segment and segment != '**' for segment in segments):
92 raise ValueError("** must appear alone in a path segment")
94 def star_not_empty(self, pattern):
95 """
96 Ensure that * will not match an empty segment.
97 """
99 def handle_segment(match):
100 segment = match.group(0)
101 return '?*' if segment == '*' else segment
103 not_seps_pattern = rf'[^{re.escape(self.seps)}]+'
104 return re.sub(not_seps_pattern, handle_segment, pattern)
107def separate(pattern):
108 """
109 Separate out character sets to avoid translating their contents.
111 >>> [m.group(0) for m in separate('*.txt')]
112 ['*.txt']
113 >>> [m.group(0) for m in separate('a[?]txt')]
114 ['a', '[?]', 'txt']
115 """
116 return re.finditer(r'([^\[]+)|(?P<set>[\[].*?[\]])|([\[][^\]]*$)', pattern)