gh-133306: Use \z instead of \Z in fnmatch.translate() and glob.translate() (GH-133338)

This commit is contained in:
Serhiy Storchaka 2025-05-03 17:58:21 +03:00 committed by GitHub
parent cb3174113e
commit add0ca9ea0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 96 additions and 94 deletions

View file

@ -111,7 +111,7 @@ functions: :func:`fnmatch`, :func:`fnmatchcase`, :func:`.filter`.
>>> >>>
>>> regex = fnmatch.translate('*.txt') >>> regex = fnmatch.translate('*.txt')
>>> regex >>> regex
'(?s:.*\\.txt)\\Z' '(?s:.*\\.txt)\\z'
>>> reobj = re.compile(regex) >>> reobj = re.compile(regex)
>>> reobj.match('foobar.txt') >>> reobj.match('foobar.txt')
<re.Match object; span=(0, 10), match='foobar.txt'> <re.Match object; span=(0, 10), match='foobar.txt'>

View file

@ -134,7 +134,7 @@ The :mod:`glob` module defines the following functions:
>>> >>>
>>> regex = glob.translate('**/*.txt', recursive=True, include_hidden=True) >>> regex = glob.translate('**/*.txt', recursive=True, include_hidden=True)
>>> regex >>> regex
'(?s:(?:.+/)?[^/]*\\.txt)\\Z' '(?s:(?:.+/)?[^/]*\\.txt)\\z'
>>> reobj = re.compile(regex) >>> reobj = re.compile(regex)
>>> reobj.match('foo/bar/baz.txt') >>> reobj.match('foo/bar/baz.txt')
<re.Match object; span=(0, 15), match='foo/bar/baz.txt'> <re.Match object; span=(0, 15), match='foo/bar/baz.txt'>

View file

@ -185,7 +185,7 @@ def _translate(pat, star, question_mark):
def _join_translated_parts(parts, star_indices): def _join_translated_parts(parts, star_indices):
if not star_indices: if not star_indices:
return fr'(?s:{"".join(parts)})\Z' return fr'(?s:{"".join(parts)})\z'
iter_star_indices = iter(star_indices) iter_star_indices = iter(star_indices)
j = next(iter_star_indices) j = next(iter_star_indices)
buffer = parts[:j] # fixed pieces at the start buffer = parts[:j] # fixed pieces at the start
@ -206,4 +206,4 @@ def _join_translated_parts(parts, star_indices):
append('.*') append('.*')
extend(parts[i:]) extend(parts[i:])
res = ''.join(buffer) res = ''.join(buffer)
return fr'(?s:{res})\Z' return fr'(?s:{res})\z'

View file

@ -316,7 +316,7 @@ def translate(pat, *, recursive=False, include_hidden=False, seps=None):
if idx < last_part_idx: if idx < last_part_idx:
results.append(any_sep) results.append(any_sep)
res = ''.join(results) res = ''.join(results)
return fr'(?s:{res})\Z' return fr'(?s:{res})\z'
@functools.lru_cache(maxsize=512) @functools.lru_cache(maxsize=512)

View file

@ -218,24 +218,24 @@ class TranslateTestCase(unittest.TestCase):
def test_translate(self): def test_translate(self):
import re import re
self.assertEqual(translate('*'), r'(?s:.*)\Z') self.assertEqual(translate('*'), r'(?s:.*)\z')
self.assertEqual(translate('?'), r'(?s:.)\Z') self.assertEqual(translate('?'), r'(?s:.)\z')
self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\Z') self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\z')
self.assertEqual(translate('[abc]'), r'(?s:[abc])\Z') self.assertEqual(translate('[abc]'), r'(?s:[abc])\z')
self.assertEqual(translate('[]]'), r'(?s:[]])\Z') self.assertEqual(translate('[]]'), r'(?s:[]])\z')
self.assertEqual(translate('[!x]'), r'(?s:[^x])\Z') self.assertEqual(translate('[!x]'), r'(?s:[^x])\z')
self.assertEqual(translate('[^x]'), r'(?s:[\^x])\Z') self.assertEqual(translate('[^x]'), r'(?s:[\^x])\z')
self.assertEqual(translate('[x'), r'(?s:\[x)\Z') self.assertEqual(translate('[x'), r'(?s:\[x)\z')
# from the docs # from the docs
self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\Z') self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\z')
# squash consecutive stars # squash consecutive stars
self.assertEqual(translate('*********'), r'(?s:.*)\Z') self.assertEqual(translate('*********'), r'(?s:.*)\z')
self.assertEqual(translate('A*********'), r'(?s:A.*)\Z') self.assertEqual(translate('A*********'), r'(?s:A.*)\z')
self.assertEqual(translate('*********A'), r'(?s:.*A)\Z') self.assertEqual(translate('*********A'), r'(?s:.*A)\z')
self.assertEqual(translate('A*********?[?]?'), r'(?s:A.*.[?].)\Z') self.assertEqual(translate('A*********?[?]?'), r'(?s:A.*.[?].)\z')
# fancy translation to prevent exponential-time match failure # fancy translation to prevent exponential-time match failure
t = translate('**a*a****a') t = translate('**a*a****a')
self.assertEqual(t, r'(?s:(?>.*?a)(?>.*?a).*a)\Z') self.assertEqual(t, r'(?s:(?>.*?a)(?>.*?a).*a)\z')
# and try pasting multiple translate results - it's an undocumented # and try pasting multiple translate results - it's an undocumented
# feature that this works # feature that this works
r1 = translate('**a**a**a*') r1 = translate('**a**a**a*')
@ -249,27 +249,27 @@ class TranslateTestCase(unittest.TestCase):
def test_translate_wildcards(self): def test_translate_wildcards(self):
for pattern, expect in [ for pattern, expect in [
('ab*', r'(?s:ab.*)\Z'), ('ab*', r'(?s:ab.*)\z'),
('ab*cd', r'(?s:ab.*cd)\Z'), ('ab*cd', r'(?s:ab.*cd)\z'),
('ab*cd*', r'(?s:ab(?>.*?cd).*)\Z'), ('ab*cd*', r'(?s:ab(?>.*?cd).*)\z'),
('ab*cd*12', r'(?s:ab(?>.*?cd).*12)\Z'), ('ab*cd*12', r'(?s:ab(?>.*?cd).*12)\z'),
('ab*cd*12*', r'(?s:ab(?>.*?cd)(?>.*?12).*)\Z'), ('ab*cd*12*', r'(?s:ab(?>.*?cd)(?>.*?12).*)\z'),
('ab*cd*12*34', r'(?s:ab(?>.*?cd)(?>.*?12).*34)\Z'), ('ab*cd*12*34', r'(?s:ab(?>.*?cd)(?>.*?12).*34)\z'),
('ab*cd*12*34*', r'(?s:ab(?>.*?cd)(?>.*?12)(?>.*?34).*)\Z'), ('ab*cd*12*34*', r'(?s:ab(?>.*?cd)(?>.*?12)(?>.*?34).*)\z'),
]: ]:
with self.subTest(pattern): with self.subTest(pattern):
translated = translate(pattern) translated = translate(pattern)
self.assertEqual(translated, expect, pattern) self.assertEqual(translated, expect, pattern)
for pattern, expect in [ for pattern, expect in [
('*ab', r'(?s:.*ab)\Z'), ('*ab', r'(?s:.*ab)\z'),
('*ab*', r'(?s:(?>.*?ab).*)\Z'), ('*ab*', r'(?s:(?>.*?ab).*)\z'),
('*ab*cd', r'(?s:(?>.*?ab).*cd)\Z'), ('*ab*cd', r'(?s:(?>.*?ab).*cd)\z'),
('*ab*cd*', r'(?s:(?>.*?ab)(?>.*?cd).*)\Z'), ('*ab*cd*', r'(?s:(?>.*?ab)(?>.*?cd).*)\z'),
('*ab*cd*12', r'(?s:(?>.*?ab)(?>.*?cd).*12)\Z'), ('*ab*cd*12', r'(?s:(?>.*?ab)(?>.*?cd).*12)\z'),
('*ab*cd*12*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*)\Z'), ('*ab*cd*12*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*)\z'),
('*ab*cd*12*34', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*34)\Z'), ('*ab*cd*12*34', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*34)\z'),
('*ab*cd*12*34*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12)(?>.*?34).*)\Z'), ('*ab*cd*12*34*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12)(?>.*?34).*)\z'),
]: ]:
with self.subTest(pattern): with self.subTest(pattern):
translated = translate(pattern) translated = translate(pattern)
@ -277,28 +277,28 @@ class TranslateTestCase(unittest.TestCase):
def test_translate_expressions(self): def test_translate_expressions(self):
for pattern, expect in [ for pattern, expect in [
('[', r'(?s:\[)\Z'), ('[', r'(?s:\[)\z'),
('[!', r'(?s:\[!)\Z'), ('[!', r'(?s:\[!)\z'),
('[]', r'(?s:\[\])\Z'), ('[]', r'(?s:\[\])\z'),
('[abc', r'(?s:\[abc)\Z'), ('[abc', r'(?s:\[abc)\z'),
('[!abc', r'(?s:\[!abc)\Z'), ('[!abc', r'(?s:\[!abc)\z'),
('[abc]', r'(?s:[abc])\Z'), ('[abc]', r'(?s:[abc])\z'),
('[!abc]', r'(?s:[^abc])\Z'), ('[!abc]', r'(?s:[^abc])\z'),
('[!abc][!def]', r'(?s:[^abc][^def])\Z'), ('[!abc][!def]', r'(?s:[^abc][^def])\z'),
# with [[ # with [[
('[[', r'(?s:\[\[)\Z'), ('[[', r'(?s:\[\[)\z'),
('[[a', r'(?s:\[\[a)\Z'), ('[[a', r'(?s:\[\[a)\z'),
('[[]', r'(?s:[\[])\Z'), ('[[]', r'(?s:[\[])\z'),
('[[]a', r'(?s:[\[]a)\Z'), ('[[]a', r'(?s:[\[]a)\z'),
('[[]]', r'(?s:[\[]\])\Z'), ('[[]]', r'(?s:[\[]\])\z'),
('[[]a]', r'(?s:[\[]a\])\Z'), ('[[]a]', r'(?s:[\[]a\])\z'),
('[[a]', r'(?s:[\[a])\Z'), ('[[a]', r'(?s:[\[a])\z'),
('[[a]]', r'(?s:[\[a]\])\Z'), ('[[a]]', r'(?s:[\[a]\])\z'),
('[[a]b', r'(?s:[\[a]b)\Z'), ('[[a]b', r'(?s:[\[a]b)\z'),
# backslashes # backslashes
('[\\', r'(?s:\[\\)\Z'), ('[\\', r'(?s:\[\\)\z'),
(r'[\]', r'(?s:[\\])\Z'), (r'[\]', r'(?s:[\\])\z'),
(r'[\\]', r'(?s:[\\\\])\Z'), (r'[\\]', r'(?s:[\\\\])\z'),
]: ]:
with self.subTest(pattern): with self.subTest(pattern):
translated = translate(pattern) translated = translate(pattern)

View file

@ -459,59 +459,59 @@ class GlobTests(unittest.TestCase):
def test_translate(self): def test_translate(self):
def fn(pat): def fn(pat):
return glob.translate(pat, seps='/') return glob.translate(pat, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\Z') self.assertEqual(fn('foo'), r'(?s:foo)\z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\Z') self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\z')
self.assertEqual(fn('*'), r'(?s:[^/.][^/]*)\Z') self.assertEqual(fn('*'), r'(?s:[^/.][^/]*)\z')
self.assertEqual(fn('?'), r'(?s:(?!\.)[^/])\Z') self.assertEqual(fn('?'), r'(?s:(?!\.)[^/])\z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\Z') self.assertEqual(fn('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\Z') self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\Z') self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\z')
self.assertEqual(fn('?aa'), r'(?s:(?!\.)[^/]aa)\Z') self.assertEqual(fn('?aa'), r'(?s:(?!\.)[^/]aa)\z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\Z') self.assertEqual(fn('aa?'), r'(?s:aa[^/])\z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\Z') self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\Z') self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\Z') self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z') self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\Z') self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), self.assertEqual(fn('/**/*/*.*/**'),
r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\Z') r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\z')
def test_translate_include_hidden(self): def test_translate_include_hidden(self):
def fn(pat): def fn(pat):
return glob.translate(pat, include_hidden=True, seps='/') return glob.translate(pat, include_hidden=True, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\Z') self.assertEqual(fn('foo'), r'(?s:foo)\z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\Z') self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\z')
self.assertEqual(fn('*'), r'(?s:[^/]+)\Z') self.assertEqual(fn('*'), r'(?s:[^/]+)\z')
self.assertEqual(fn('?'), r'(?s:[^/])\Z') self.assertEqual(fn('?'), r'(?s:[^/])\z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\Z') self.assertEqual(fn('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:[^/]*a)\Z') self.assertEqual(fn('*a'), r'(?s:[^/]*a)\z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\Z') self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\z')
self.assertEqual(fn('?aa'), r'(?s:[^/]aa)\Z') self.assertEqual(fn('?aa'), r'(?s:[^/]aa)\z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\Z') self.assertEqual(fn('aa?'), r'(?s:aa[^/])\z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\Z') self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:[^/]*)\Z') self.assertEqual(fn('**'), r'(?s:[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\Z') self.assertEqual(fn('***'), r'(?s:[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z') self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\Z') self.assertEqual(fn('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\Z') self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\z')
def test_translate_recursive(self): def test_translate_recursive(self):
def fn(pat): def fn(pat):
return glob.translate(pat, recursive=True, include_hidden=True, seps='/') return glob.translate(pat, recursive=True, include_hidden=True, seps='/')
self.assertEqual(fn('*'), r'(?s:[^/]+)\Z') self.assertEqual(fn('*'), r'(?s:[^/]+)\z')
self.assertEqual(fn('?'), r'(?s:[^/])\Z') self.assertEqual(fn('?'), r'(?s:[^/])\z')
self.assertEqual(fn('**'), r'(?s:.*)\Z') self.assertEqual(fn('**'), r'(?s:.*)\z')
self.assertEqual(fn('**/**'), r'(?s:.*)\Z') self.assertEqual(fn('**/**'), r'(?s:.*)\z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\Z') self.assertEqual(fn('***'), r'(?s:[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z') self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\Z') self.assertEqual(fn('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/(?:.+/)?[^/]+/[^/]*\.[^/]*/.*)\Z') self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/(?:.+/)?[^/]+/[^/]*\.[^/]*/.*)\z')
def test_translate_seps(self): def test_translate_seps(self):
def fn(pat): def fn(pat):
return glob.translate(pat, recursive=True, include_hidden=True, seps=['/', '\\']) return glob.translate(pat, recursive=True, include_hidden=True, seps=['/', '\\'])
self.assertEqual(fn('foo/bar\\baz'), r'(?s:foo[/\\]bar[/\\]baz)\Z') self.assertEqual(fn('foo/bar\\baz'), r'(?s:foo[/\\]bar[/\\]baz)\z')
self.assertEqual(fn('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\Z') self.assertEqual(fn('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\z')
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -0,0 +1,2 @@
Use ``\z`` instead of ``\Z`` in :func:`fnmatch.translate` and
:func:`glob.translate`.