10000 gh-133306: Use \z instead of \Z in fnmatch.translate() and glob.translate() by serhiy-storchaka · Pull Request #133338 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Doc/library/fnmatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ functions: :func:`fnmatch`, :func:`fnmatchcase`, :func:`.filter`.
>>>
>>> regex = fnmatch.translate('*.txt')
>>> regex
'(?s:.*\\.txt)\\Z'
'(?s:.*\\.txt)\\z'
>>> reobj = re.compile(regex)
>>> reobj.match('foobar.txt')
<re.Match object; span=(0, 10), match='foobar.txt'>
Expand Down
2 changes: 1 addition & 1 deletion Doc/library/glob.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The :mod:`glob` module defines the following functions:
>>>
>>> regex = glob.translate('**/*.txt', recursive=True, include_hidden=True)
>>> regex
'(?s:(?:.+/)?[^/]*\\.txt)\\Z'
'(?s:(?:.+/)?[^/]*\\.txt)\\z'
>>> reobj = re.compile(regex)
>>> reobj.match('foo/bar/baz.txt')
<re.Match object; span=(0, 15), match='foo/bar/baz.txt'>
Expand Down
4 changes: 2 additions & 2 deletions Lib/fnmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def _translate(pat, star, question_mark):

def _join_translated_parts(parts, star_indices):
if not star_indices:
return fr'(?s:{"".join(parts)})\Z'
return fr'(?s:{"".join(parts)})\z'
iter_star_indices = iter(star_indices)
j = next(iter_star_indices)
buffer = parts[:j] # fixed pieces at the start
Expand All @@ -206,4 +206,4 @@ def _join_translated_parts(parts, star_indices):
append('.*')
extend(parts[i:])
res = ''.join(buffer)
return fr'(?s:{res})\Z'
return fr'(?s:{res})\z'
2 changes: 1 addition & 1 deletion Lib/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def translate(pat, *, recursive=False, include_hidden=False, seps=None):
if idx < last_part_idx:
results.append(any_sep)
res = ''.join(results)
return fr'(?s:{res})\Z'
return fr'(?s:{res})\z'


@functools.lru_cache(maxsize=512)
Expand Down
98 changes: 49 additions & 49 deletions Lib/test/test_fnmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,24 @@ class TranslateTestCase(unittest.TestCase):

def test_translate(self):
import re
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('[abc]'), r'(?s:[abc])\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('*'), r'(?s:.*)\z')
self.assertEqual(translate('?'), r'(?s:.)\z')
self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\z')
self.assertEqual(translate('[abc]'), r'(?s:[abc])\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')
# from the docs
self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\Z')
self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\z')
# squash consecutive stars
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('*********'), 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')
# fancy translation to prevent exponential-time match failure
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
# feature that this works
r1 = translate('**a**a**a*')
Expand All @@ -249,56 +249,56 @@ def test_translate(self):

def test_translate_wildcards(self):
for pattern, expect in [
('ab*', r'(?s:ab.*)\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*34', r'(?s:ab(?>.*?cd)(?>.*?12).*34)\Z'),
('ab*cd*12*34*', r'(?s:ab(?>.*?cd)(?>.*?12)(?>.*?34).*)\Z'),
('ab*', r'(?s:ab.*)\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*34', r'(?s:ab(?>.*?cd)(?>.*?12).*34)\z'),
('ab*cd*12*34*', r'(?s:ab(?>.*?cd)(?>.*?12)(?>.*?34).*)\z'),
]:
with self.subTest(pattern):
translated = translate(pattern)
self.assertEqual(translated, expect, pattern)

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*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', 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*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'),
]:
with self.subTest(pattern):
translated = translate(pattern)
self.assertEqual(translated, expect, pattern)

def test_translate_expressions(self):
for pattern, expect in [
('[', 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][!def]', r'(?s:[^abc][^def])\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][!def]', r'(?s:[^abc][^def])\z'),
# with [[
('[[', r'(?s:\[\[)\Z'),
('[[a', r'(?s:\[\[a)\Z'),
('[[]', r'(?s:[\[])\Z'),
('[[]a', r'(?s:[\[]a)\Z'),
('[[]]', r'(?s:[\[]\])\Z'),
('[[]a]', r'(?s:[\[]a\])\Z'),
('[[a]', r'(?s:[\[a])\Z'),
('[[a]]', r'(?s:[\[a]\])\Z'),
('[[a]b', r'(?s:[\[a]b)\Z'),
('[[', r'(?s:\[\[)\z'),
('[[a', r'(?s:\[\[a)\z'),
('[[]', r'(?s:[\[])\z'),
('[[]a', r'(?s:[\[]a)\z'),
('[[]]', r'(?s:[\[]\])\z'),
('[[]a]', r'(?s:[\[]a\])\z'),
('[[a]', r'(?s:[\[a])\z'),
('[[a]]', r'(?s:[\[a]\])\z'),
('[[a]b', r'(?s:[\[a]b)\z'),
# backslashes
('[\\', r'(?s:\[\\)\Z'),
(r'[\]', r'(?s:[\\])\Z'),
(r'[\\]', r'(?s:[\\\\])\Z'),
('[\\', r'(?s:\[\\)\z'),
(r'[\]', r'(?s:[\\])\z'),
(r'[\\]', r'(?s:[\\\\])\z'),
]:
with self.subTest(pattern):
translated = translate(pattern)
Expand Down
80 changes: 40 additions & 40 deletions Lib/test/test_glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,59 +459,59 @@ def test_translate_matching(self):
def test_translate(self):
def fn(pat):
return glob.translate(pat, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\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('a*'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\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[ab]'), r'(?s:aa[ab])\Z')
self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\Z')
self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\Z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\Z')
self.assertEqual(fn('foo'), r'(?s:foo)\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('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\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[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'),
r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\Z')
r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\z')

def test_translate_include_hidden(self):
def fn(pat):
return glob.translate(pat, include_hidden=True, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\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('a*'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('*a'), r'(?s:[^/]*a)\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[ab]'), r'(?s:aa[ab])\Z')
self.assertEqual(fn('**'), r'(?s:[^/]*)\Z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\Z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\Z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\Z')
self.assertEqual(fn('foo'), r'(?s:foo)\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('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:[^/]*a)\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[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\z')

def test_translate_recursive(self):
def fn(pat):
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('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\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('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/(?:.+/)?[^/]+/[^/]*\.[^/]*/.*)\z')

def test_translate_seps(self):
def fn(pat):
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('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\Z')
self.assertEqual(fn('foo/bar\\baz'), r'(?s:foo[/\\]bar[/\\]baz)\z')
self.assertEqual(fn('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\z')


if __name__ == "__main__":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Use ``\z`` instead of ``\Z`` in :func:`fnmatch.translate` and
:func:`glob.translate`.
Loading
0