codekingpro/portable-devtools
114k
1import os2import re3 4from .compat.py313 import legacy_end_marker5 6_default_seps = os.sep + str(os.altsep) * bool(os.altsep)7 8 9class Translator:10 """11 >>> Translator('xyz')12 Traceback (most recent call last):13 ...14 AssertionError: Invalid separators15 16 >>> Translator('')17 Traceback (most recent call last):18 ...19 AssertionError: Invalid separators20 """21 22 seps: str23 24 def __init__(self, seps: str = _default_seps):25 assert seps and set(seps) <= set(_default_seps), "Invalid separators"26 self.seps = seps27 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)))33 34 @legacy_end_marker35 def extend(self, pattern):36 r"""37 Extend regex for pattern-wide concerns.38 39 Apply '(?s:)' to create a non-matching group that40 matches newlines (valid on Unix).41 42 Append '\z' to imply fullmatch even when match is used.43 """44 return rf'(?s:{pattern})\z'45 46 def match_dirs(self, pattern):47 """48 Ensure that zipfile.Path directory names are matched.49 50 zipfile.Path directory names always end in a slash.51 """52 return rf'{pattern}[/]?'53 54 def translate_core(self, pattern):55 r"""56 Given a glob pattern, produce a regex that matches it.57 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))))68 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 )79 80 def restrict_rglob(self, pattern):81 """82 Raise ValueError if ** appears in anything but a full path segment.83 84 >>> Translator().translate('**foo')85 Traceback (most recent call last):86 ...87 ValueError: ** must appear alone in a path segment88 """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")93 94 def star_not_empty(self, pattern):95 """96 Ensure that * will not match an empty segment.97 """98 99 def handle_segment(match):100 segment = match.group(0)101 return '?*' if segment == '*' else segment102 103 not_seps_pattern = rf'[^{re.escape(self.seps)}]+'104 return re.sub(not_seps_pattern, handle_segment, pattern)105 106 107def separate(pattern):108 """109 Separate out character sets to avoid translating their contents.110 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)117 