diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index d9b46df507dfd7..209410d716a07d 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -862,6 +862,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(col_offset));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(command));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(comment_factory));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compile));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(compile_mode));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(consts));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(context));
@@ -915,6 +916,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(entrypoint));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(env));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(errors));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(escape));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(event));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(eventmask));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exc_type));
@@ -943,6 +945,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(filter));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(filters));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(final));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(find));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(find_class));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fix_imports));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(flags));
@@ -1086,6 +1089,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(n_unnamed_fields));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(name));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(name_from));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(names));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(namespace_separator));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(namespaces));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(narg));
@@ -1128,6 +1132,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pages));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(parent));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(password));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pat));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(path));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pattern));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(peek));
@@ -1225,6 +1230,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strict));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strict_mode));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(string));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sub));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(sub_key));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(symmetric_difference_update));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(tabsize));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index 10773d7a6c7e3f..5431ba18bf4b24 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -351,6 +351,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(col_offset)
STRUCT_FOR_ID(command)
STRUCT_FOR_ID(comment_factory)
+ STRUCT_FOR_ID(compile)
STRUCT_FOR_ID(compile_mode)
STRUCT_FOR_ID(consts)
STRUCT_FOR_ID(context)
@@ -404,6 +405,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(entrypoint)
STRUCT_FOR_ID(env)
STRUCT_FOR_ID(errors)
+ STRUCT_FOR_ID(escape)
STRUCT_FOR_ID(event)
STRUCT_FOR_ID(eventmask)
STRUCT_FOR_ID(exc_type)
@@ -432,6 +434,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(filter)
STRUCT_FOR_ID(filters)
STRUCT_FOR_ID(final)
+ STRUCT_FOR_ID(find)
STRUCT_FOR_ID(find_class)
STRUCT_FOR_ID(fix_imports)
STRUCT_FOR_ID(flags)
@@ -575,6 +578,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(n_unnamed_fields)
STRUCT_FOR_ID(name)
STRUCT_FOR_ID(name_from)
+ STRUCT_FOR_ID(names)
STRUCT_FOR_ID(namespace_separator)
STRUCT_FOR_ID(namespaces)
STRUCT_FOR_ID(narg)
@@ -617,6 +621,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(pages)
STRUCT_FOR_ID(parent)
STRUCT_FOR_ID(password)
+ STRUCT_FOR_ID(pat)
STRUCT_FOR_ID(path)
STRUCT_FOR_ID(pattern)
STRUCT_FOR_ID(peek)
@@ -714,6 +719,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(strict)
STRUCT_FOR_ID(strict_mode)
STRUCT_FOR_ID(string)
+ STRUCT_FOR_ID(sub)
STRUCT_FOR_ID(sub_key)
STRUCT_FOR_ID(symmetric_difference_update)
STRUCT_FOR_ID(tabsize)
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index 618f8d0a36b6c3..f3e8d4c5fab26d 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -860,6 +860,7 @@ extern "C" {
INIT_ID(col_offset), \
INIT_ID(command), \
INIT_ID(comment_factory), \
+ INIT_ID(compile), \
INIT_ID(compile_mode), \
INIT_ID(consts), \
INIT_ID(context), \
@@ -913,6 +914,7 @@ extern "C" {
INIT_ID(entrypoint), \
INIT_ID(env), \
INIT_ID(errors), \
+ INIT_ID(escape), \
INIT_ID(event), \
INIT_ID(eventmask), \
INIT_ID(exc_type), \
@@ -941,6 +943,7 @@ extern "C" {
INIT_ID(filter), \
INIT_ID(filters), \
INIT_ID(final), \
+ INIT_ID(find), \
INIT_ID(find_class), \
INIT_ID(fix_imports), \
INIT_ID(flags), \
@@ -1084,6 +1087,7 @@ extern "C" {
INIT_ID(n_unnamed_fields), \
INIT_ID(name), \
INIT_ID(name_from), \
+ INIT_ID(names), \
INIT_ID(namespace_separator), \
INIT_ID(namespaces), \
INIT_ID(narg), \
@@ -1126,6 +1130,7 @@ extern "C" {
INIT_ID(pages), \
INIT_ID(parent), \
INIT_ID(password), \
+ INIT_ID(pat), \
INIT_ID(path), \
INIT_ID(pattern), \
INIT_ID(peek), \
@@ -1223,6 +1228,7 @@ extern "C" {
INIT_ID(strict), \
INIT_ID(strict_mode), \
INIT_ID(string), \
+ INIT_ID(sub), \
INIT_ID(sub_key), \
INIT_ID(symmetric_difference_update), \
INIT_ID(tabsize), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index f848a002c3b5d1..2a494149e6143a 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -1204,6 +1204,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(compile);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(compile_mode);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
@@ -1416,6 +1420,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(escape);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(event);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
@@ -1528,6 +1536,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(find);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(find_class);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
@@ -2100,6 +2112,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(names);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(namespace_separator);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
@@ -2268,6 +2284,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(pat);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(path);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
@@ -2656,6 +2676,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(sub);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(sub_key);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/fnmatch.py b/Lib/fnmatch.py
index 73acb1fe8d4106..0a1dc7c5196597 100644
--- a/Lib/fnmatch.py
+++ b/Lib/fnmatch.py
@@ -70,16 +70,18 @@ def fnmatchcase(name, pat):
match = _compile_pattern(pat)
return match(name) is not None
-
-def translate(pat):
- """Translate a shell PATTERN to a regular expression.
-
- There is no way to quote meta-characters.
- """
-
- STAR = object()
- parts = _translate(pat, STAR, '.')
- return _join_translated_parts(parts, STAR)
+try:
+ from _fnmatch import translate
+except ImportError:
+ def translate(pat):
+ """Translate a shell PATTERN to a regular expression.
+
+ There is no way to quote meta-characters.
+ """
+
+ STAR = object()
+ parts = _translate(pat, STAR, '.')
+ return _join_translated_parts(parts, STAR)
def _translate(pat, STAR, QUESTION_MARK):
diff --git a/Lib/test/test_fnmatch.py b/Lib/test/test_fnmatch.py
index 10ed496d4e2f37..186f4eb81dee83 100644
--- a/Lib/test/test_fnmatch.py
+++ b/Lib/test/test_fnmatch.py
@@ -1,23 +1,29 @@
"""Test cases for the fnmatch module."""
-import unittest
+import itertools
import os
import string
+import unittest
import warnings
-from fnmatch import fnmatch, fnmatchcase, translate, filter
+import test.support.import_helper
+
+c_fnmatch = test.support.import_helper.import_fresh_module("_fnmatch")
+py_fnmatch = test.support.import_helper.import_fresh_module("fnmatch", blocked=["_fnmatch"])
+
+class FnmatchTestCaseMixin:
+ fnmatch = None
-class FnmatchTestCase(unittest.TestCase):
+ def check_match(self, filename, pattern, should_match=True, func=None):
+ if func is None:
+ func = self.fnmatch.fnmatch
- def check_match(self, filename, pattern, should_match=True, fn=fnmatch):
- if should_match:
- self.assertTrue(fn(filename, pattern),
- "expected %r to match pattern %r"
- % (filename, pattern))
- else:
- self.assertFalse(fn(filename, pattern),
- "expected %r not to match pattern %r"
- % (filename, pattern))
+ with self.subTest(fn=func, name=filename, pattern=pattern):
+ res = func(filename, pattern)
+ if should_match:
+ self.assertTrue(res, f"expected {filename!r} to match pattern {pattern!r}")
+ else:
+ self.assertFalse(res, f"expected {filename!r} not to match pattern {pattern!r}")
def test_fnmatch(self):
check = self.check_match
@@ -54,13 +60,17 @@ def test_slow_fnmatch(self):
check('a' * 50 + 'b', '*a*a*a*a*a*a*a*a*a*a', False)
def test_mix_bytes_str(self):
+ fnmatch = self.fnmatch.fnmatch
self.assertRaises(TypeError, fnmatch, 'test', b'*')
self.assertRaises(TypeError, fnmatch, b'test', '*')
+
+ fnmatchcase = self.fnmatch.fnmatchcase
self.assertRaises(TypeError, fnmatchcase, 'test', b'*')
self.assertRaises(TypeError, fnmatchcase, b'test', '*')
def test_fnmatchcase(self):
check = self.check_match
+ fnmatchcase = self.fnmatch.fnmatchcase
check('abc', 'abc', True, fnmatchcase)
check('AbC', 'abc', False, fnmatchcase)
check('abc', 'AbC', False, fnmatchcase)
@@ -216,11 +226,15 @@ def test_warnings(self):
check(',', '[a-z+--A-Z]')
check('.', '[a-z--/A-Z]')
+class PurePythonFnmatchTestCase(FnmatchTestCaseMixin, unittest.TestCase):
+ fnmatch = py_fnmatch
-class TranslateTestCase(unittest.TestCase):
+class TranslateTestCaseMixin:
+ fnmatch = None
def test_translate(self):
import re
+ translate = self.fnmatch.translate
self.assertEqual(translate('*'), r'(?s:.*)\Z')
self.assertEqual(translate('?'), r'(?s:.)\Z')
self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\Z')
@@ -250,32 +264,135 @@ def test_translate(self):
self.assertTrue(re.match(fatre, 'cbabcaxc'))
self.assertFalse(re.match(fatre, 'dabccbad'))
-class FilterTestCase(unittest.TestCase):
+ def test_translate_wildcards(self):
+ for pattern, expect in [
+ ('', r'(?s:)\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'),
+ ]:
+ translated = self.fnmatch.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'),
+ ]:
+ translated = self.fnmatch.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'),
+ # 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'),
+ # backslashes
+ ('[\\', r'(?s:\[\\)\Z'),
+ (r'[\]', r'(?s:[\\])\Z'),
+ (r'[\\]', r'(?s:[\\\\])\Z'),
+ ]:
+ translated = self.fnmatch.translate(pattern)
+ self.assertEqual(translated, expect, pattern)
+
+class PurePythonTranslateTestCase(TranslateTestCaseMixin, unittest.TestCase):
+ fnmatch = py_fnmatch
+
+class CPythonTranslateTestCase(TranslateTestCaseMixin, unittest.TestCase):
+ fnmatch = c_fnmatch
+
+ @staticmethod
+ def translate_func(pattern):
+ # Pure Python implementation of translate()
+ STAR = object()
+ parts = py_fnmatch._translate(pattern, STAR, '.')
+ return py_fnmatch._join_translated_parts(parts, STAR)
+
+ def test_translate(self):
+ # We want to check that the C implementation is EXACTLY the same
+ # as the Python implementation. For that, we will need to cover
+ # a lot of cases.
+ translate = self.fnmatch.translate
+
+ for choice in itertools.combinations_with_replacement('*?.', 5):
+ for suffix in ['', '!']:
+ pat = suffix + ''.join(choice)
+ with self.subTest(pattern=pat):
+ self.assertEqual(translate(pat), self.translate_func(pat))
+
+ for pat in [
+ '',
+ '!!a*', '!\\!a*', '!a*', '*', '**', '*******?', '*******c', '*****??', '**/',
+ '*.js', '*/man*/bash.*', '*???', '?', '?*****??', '?*****?c', '?***?****',
+ '?***?****?', '?***?****c', '?*?', '??', '???', '???*', '[!\\]',
+ '\\**', '\\*\\*', 'a*', 'a*****?c', 'a****c**?**??*****', 'a***c',
+ 'a**?**cd**?**??***k', 'a**?**cd**?**??***k**', 'a**?**cd**?**??k',
+ 'a**?**cd**?**??k***', 'a*[^c]',
+ 'a*cd**?**??k', 'a/*', 'a/**', 'a/**/b',
+ 'a/**/b/**/c', 'a/.*/c', 'a/?', 'a/??', 'a[X-]b', 'a[\\.]c',
+ 'a[\\b]c', 'a[bc', 'a\\*?/*', 'a\\*b/*',
+ 'ab[!de]', 'ab[cd]', 'ab[cd]ef', 'abc', 'b*/', 'foo*',
+ 'man/man1/bash.1'
+ ]:
+ with self.subTest(pattern=pat):
+ self.assertEqual(translate(pat), self.translate_func(pat))
+
+class FilterTestCaseMixin:
+ fnmatch = None
def test_filter(self):
+ filter = self.fnmatch.filter
self.assertEqual(filter(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*'),
['Python', 'Perl'])
self.assertEqual(filter([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*'),
[b'Python', b'Perl'])
def test_mix_bytes_str(self):
+ filter = self.fnmatch.filter
self.assertRaises(TypeError, filter, ['test'], b'*')
self.assertRaises(TypeError, filter, [b'test'], '*')
def test_case(self):
ignorecase = os.path.normcase('P') == os.path.normcase('p')
+ filter = self.fnmatch.filter
self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'),
['Test.py', 'Test.PL'] if ignorecase else ['Test.py'])
self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.P*'),
['Test.py', 'Test.PL'] if ignorecase else ['Test.PL'])
def test_sep(self):
+ filter = self.fnmatch.filter
normsep = os.path.normcase('\\') == os.path.normcase('/')
self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr/*'),
['usr/bin', 'usr\\lib'] if normsep else ['usr/bin'])
self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr\\*'),
['usr/bin', 'usr\\lib'] if normsep else ['usr\\lib'])
+class PurePythonFilterTestCase(FilterTestCaseMixin, unittest.TestCase):
+ fnmatch = py_fnmatch
if __name__ == "__main__":
unittest.main()
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 9ea7bc49be316c..497e854e10edcf 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -3139,6 +3139,7 @@ MODULE__CTYPES_TEST_DEPS=$(srcdir)/Modules/_ctypes/_ctypes_test_generated.c.h
MODULE__CTYPES_MALLOC_CLOSURE=@MODULE__CTYPES_MALLOC_CLOSURE@
MODULE__DECIMAL_DEPS=$(srcdir)/Modules/_decimal/docstrings.h @LIBMPDEC_INTERNAL@
MODULE__ELEMENTTREE_DEPS=$(srcdir)/Modules/pyexpat.c @LIBEXPAT_INTERNAL@
+MODULE__FNMATCH_DEPS=$(srcdir)/Modules/_fnmatch/macros.h $(srcdir)/Modules/_fnmatch/util.h
MODULE__HASHLIB_DEPS=$(srcdir)/Modules/hashlib.h
MODULE__IO_DEPS=$(srcdir)/Modules/_io/_iomodule.h
MODULE__MD5_DEPS=$(srcdir)/Modules/hashlib.h $(LIBHACL_HEADERS) Modules/_hacl/Hacl_Hash_MD5.h Modules/_hacl/Hacl_Hash_MD5.c
diff --git a/Misc/NEWS.d/next/Library/2024-07-12-09-24-38.gh-issue-121445.KYtNOZ.rst b/Misc/NEWS.d/next/Library/2024-07-12-09-24-38.gh-issue-121445.KYtNOZ.rst
new file mode 100644
index 00000000000000..e310ca0a76bc0d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-12-09-24-38.gh-issue-121445.KYtNOZ.rst
@@ -0,0 +1,2 @@
+Improve the performances of :func:`fnmatch.translate` by a factor 7.
+Patch by Bénédikt Tran.
diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in
index dfc75077650df8..8195b7c75c2aa8 100644
--- a/Modules/Setup.stdlib.in
+++ b/Modules/Setup.stdlib.in
@@ -33,6 +33,7 @@
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
@MODULE__CONTEXTVARS_TRUE@_contextvars _contextvarsmodule.c
@MODULE__CSV_TRUE@_csv _csv.c
+@MODULE__FNMATCH_TRUE@_fnmatch _fnmatch/_fnmatchmodule.c _fnmatch/translate.c
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
@MODULE__JSON_TRUE@_json _json.c
@MODULE__LSPROF_TRUE@_lsprof _lsprof.c rotatingtree.c
diff --git a/Modules/_fnmatch/_fnmatchmodule.c b/Modules/_fnmatch/_fnmatchmodule.c
new file mode 100644
index 00000000000000..9b3413cf3f233a
--- /dev/null
+++ b/Modules/_fnmatch/_fnmatchmodule.c
@@ -0,0 +1,215 @@
+/*
+ * C accelerator for the 'fnmatch' module.
+ */
+
+#ifndef Py_BUILD_CORE_BUILTIN
+# define Py_BUILD_CORE_MODULE 1
+#endif
+
+#include "macros.h"
+#include "util.h" // prototypes
+
+#include "pycore_runtime.h" // for _Py_ID()
+
+#include "clinic/_fnmatchmodule.c.h"
+
+#define LRU_CACHE_SIZE 32768
+#define INVALID_PATTERN_TYPE "pattern must be a string or a bytes object"
+
+// ==== Cached re.escape() unit ===============================================
+
+/* Create an LRU-cached function for re.escape(). */
+static int
+fnmatchmodule_load_escapefunc(fnmatchmodule_state *st)
+{
+ // make sure that this function is called once
+ assert(st->re_escape == NULL);
+ PyObject *maxsize = PyLong_FromLong(LRU_CACHE_SIZE);
+ CHECK_NOT_NULL_OR_ABORT(maxsize);
+ PyObject *cache = _PyImport_GetModuleAttrString("functools", "lru_cache");
+ if (cache == NULL) {
+ Py_DECREF(maxsize);
+ return -1;
+ }
+ PyObject *wrapper = PyObject_CallOneArg(cache, maxsize);
+ Py_DECREF(maxsize);
+ Py_DECREF(cache);
+ CHECK_NOT_NULL_OR_ABORT(wrapper);
+ PyObject *wrapped = _PyImport_GetModuleAttrString("re", "escape");
+ if (wrapped == NULL) {
+ Py_DECREF(wrapper);
+ return -1;
+ }
+ st->re_escape = PyObject_CallOneArg(wrapper, wrapped);
+ Py_DECREF(wrapped);
+ Py_DECREF(wrapper);
+ CHECK_NOT_NULL_OR_ABORT(st->re_escape);
+ return 0;
+abort:
+ return -1;
+}
+
+// ==== Cached re.sub() unit for set operation tokens =========================
+
+/* Store a reference to re.compile('([&~|])').sub(). */
+static int
+fnmatchmodule_load_setops_re_sub(fnmatchmodule_state *st)
+{
+ // make sure that this function is called once
+ assert(st->setops_re_subfn == NULL);
+ PyObject *pattern = PyUnicode_FromStringAndSize("([&~|])", 7);
+ CHECK_NOT_NULL_OR_ABORT(pattern);
+ PyObject *re_compile = _PyImport_GetModuleAttrString("re", "compile");
+ if (re_compile == NULL) {
+ Py_DECREF(pattern);
+ return -1;
+ }
+ PyObject *compiled = PyObject_CallOneArg(re_compile, pattern);
+ Py_DECREF(re_compile);
+ Py_DECREF(pattern);
+ CHECK_NOT_NULL_OR_ABORT(compiled);
+ st->setops_re_subfn = PyObject_GetAttr(compiled, &_Py_ID(sub));
+ Py_DECREF(compiled);
+ CHECK_NOT_NULL_OR_ABORT(st->setops_re_subfn);
+ return 0;
+abort:
+ return -1;
+}
+
+// ==== Module state functions ================================================
+
+static int
+fnmatchmodule_exec(PyObject *module)
+{
+ // ---- def local macros --------------------------------------------------
+ /* Intern a literal STRING and store it in 'STATE->ATTRIBUTE'. */
+#define INTERN_STRING(STATE, ATTRIBUTE, STRING) \
+ do { \
+ STATE->ATTRIBUTE = PyUnicode_InternFromString((STRING)); \
+ CHECK_NOT_NULL_OR_ABORT(STATE->ATTRIBUTE); \
+ } while (0)
+ // ------------------------------------------------------------------------
+ fnmatchmodule_state *st = get_fnmatchmodule_state(module);
+ CHECK_RET_CODE_OR_ABORT(fnmatchmodule_load_escapefunc(st));
+ INTERN_STRING(st, hyphen_str, "-");
+ INTERN_STRING(st, hyphen_esc_str, "\\-");
+ INTERN_STRING(st, backslash_str, "\\");
+ INTERN_STRING(st, backslash_esc_str, "\\\\");
+ CHECK_RET_CODE_OR_ABORT(fnmatchmodule_load_setops_re_sub(st));
+ INTERN_STRING(st, setops_repl_str, "\\\\\\1");
+#undef INTERN_STRING
+ return 0;
+abort:
+ return -1;
+}
+
+static int
+fnmatchmodule_traverse(PyObject *m, visitproc visit, void *arg)
+{
+ fnmatchmodule_state *st = get_fnmatchmodule_state(m);
+ Py_VISIT(st->setops_repl_str);
+ Py_VISIT(st->setops_re_subfn);
+ Py_VISIT(st->backslash_esc_str);
+ Py_VISIT(st->backslash_str);
+ Py_VISIT(st->hyphen_esc_str);
+ Py_VISIT(st->hyphen_str);
+ Py_VISIT(st->re_escape);
+ return 0;
+}
+
+static int
+fnmatchmodule_clear(PyObject *m)
+{
+ fnmatchmodule_state *st = get_fnmatchmodule_state(m);
+ Py_CLEAR(st->setops_repl_str);
+ Py_CLEAR(st->setops_re_subfn);
+ Py_CLEAR(st->backslash_esc_str);
+ Py_CLEAR(st->backslash_str);
+ Py_CLEAR(st->hyphen_esc_str);
+ Py_CLEAR(st->hyphen_str);
+ Py_CLEAR(st->re_escape);
+ return 0;
+}
+
+static inline void
+fnmatchmodule_free(void *m)
+{
+ (void)fnmatchmodule_clear((PyObject *)m);
+}
+
+/*[clinic input]
+module fnmatch
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=797aa965370a9ef2]*/
+
+/*[clinic input]
+fnmatch.translate -> object
+
+ pat as pattern: object
+
+Translate a shell pattern *pat* to a regular expression.
+
+There is no way to quote meta-characters.
+[clinic start generated code]*/
+
+static PyObject *
+fnmatch_translate_impl(PyObject *module, PyObject *pattern)
+/*[clinic end generated code: output=77e0f5de9fbb59bd input=2cc1203a34c571fd]*/
+{
+ if (PyBytes_Check(pattern)) {
+ PyObject *decoded = PyUnicode_DecodeLatin1(PyBytes_AS_STRING(pattern),
+ PyBytes_GET_SIZE(pattern),
+ "strict");
+ CHECK_NOT_NULL_OR_ABORT(decoded);
+ PyObject *translated = _Py_fnmatch_translate(module, decoded);
+ Py_DECREF(decoded);
+ CHECK_NOT_NULL_OR_ABORT(translated);
+ PyObject *res = PyUnicode_AsLatin1String(translated);
+ Py_DECREF(translated);
+ return res;
+ }
+ else if (PyUnicode_Check(pattern)) {
+ return _Py_fnmatch_translate(module, pattern);
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError, INVALID_PATTERN_TYPE);
+ return NULL;
+ }
+abort:
+ return NULL;
+}
+
+// ==== Module specs ==========================================================
+
+static PyMethodDef fnmatchmodule_methods[] = {
+ FNMATCH_TRANSLATE_METHODDEF
+ {NULL, NULL}
+};
+
+static struct PyModuleDef_Slot fnmatchmodule_slots[] = {
+ {Py_mod_exec, fnmatchmodule_exec},
+ {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
+ {Py_mod_gil, Py_MOD_GIL_NOT_USED},
+ {0, NULL},
+};
+
+static struct PyModuleDef _fnmatchmodule = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "_fnmatch",
+ .m_doc = NULL,
+ .m_size = sizeof(fnmatchmodule_state),
+ .m_methods = fnmatchmodule_methods,
+ .m_slots = fnmatchmodule_slots,
+ .m_traverse = fnmatchmodule_traverse,
+ .m_clear = fnmatchmodule_clear,
+ .m_free = fnmatchmodule_free,
+};
+
+PyMODINIT_FUNC
+PyInit__fnmatch(void)
+{
+ return PyModuleDef_Init(&_fnmatchmodule);
+}
+
+#undef INVALID_PATTERN_TYPE
+#undef LRU_CACHE_SIZE
diff --git a/Modules/_fnmatch/clinic/_fnmatchmodule.c.h b/Modules/_fnmatch/clinic/_fnmatchmodule.c.h
new file mode 100644
index 00000000000000..38129540d37433
--- /dev/null
+++ b/Modules/_fnmatch/clinic/_fnmatchmodule.c.h
@@ -0,0 +1,67 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+# include "pycore_gc.h" // PyGC_Head
+# include "pycore_runtime.h" // _Py_ID()
+#endif
+#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
+
+PyDoc_STRVAR(fnmatch_translate__doc__,
+"translate($module, /, pat)\n"
+"--\n"
+"\n"
+"Translate a shell pattern *pat* to a regular expression.\n"
+"\n"
+"There is no way to quote meta-characters.");
+
+#define FNMATCH_TRANSLATE_METHODDEF \
+ {"translate", _PyCFunction_CAST(fnmatch_translate), METH_FASTCALL|METH_KEYWORDS, fnmatch_translate__doc__},
+
+static PyObject *
+fnmatch_translate_impl(PyObject *module, PyObject *pattern);
+
+static PyObject *
+fnmatch_translate(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 1
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_item = { &_Py_ID(pat), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"pat", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "translate",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[1];
+ PyObject *pattern;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ pattern = args[0];
+ return_value = fnmatch_translate_impl(module, pattern);
+
+exit:
+ return return_value;
+}
+/*[clinic end generated code: output=eab39d3bb9f3a13d input=a9049054013a1b77]*/
diff --git a/Modules/_fnmatch/macros.h b/Modules/_fnmatch/macros.h
new file mode 100644
index 00000000000000..2363e1b8051ff8
--- /dev/null
+++ b/Modules/_fnmatch/macros.h
@@ -0,0 +1,126 @@
+/*
+ * This file contains various macro definitions in order to reduce the
+ * number of lines in '_fnmatch'. Do not use them for something else.
+ */
+
+#ifndef _FNMATCH_MACROS_H
+#define _FNMATCH_MACROS_H
+
+/*
+ * Check that STATUS is >= 0 or execute 'goto abort'.
+ *
+ * This macro is provided for convenience and should be
+ * carefully used if more resources should be released
+ * before jumping to the 'abort' label.
+ */
+#define CHECK_RET_CODE_OR_ABORT(STATUS) \
+ do { \
+ if ((STATUS) < 0) { \
+ assert(PyErr_Occurred()); \
+ goto abort; \
+ } \
+ } while (0)
+
+/*
+ * Identical to CHECK_RET_CODE_OR_ABORT but where the
+ * argument is semantically used as a positive integer.
+ */
+#define CHECK_UNSIGNED_INT_OR_ABORT CHECK_RET_CODE_OR_ABORT
+
+/*
+ * Check that OBJ is not NULL or execute 'goto abort'.
+ *
+ * This macro is provided for convenience and should be
+ * carefully used if more resources should be released
+ * before jumping to the 'abort' label.
+ */
+#define CHECK_NOT_NULL_OR_ABORT(OBJ) \
+ do { \
+ if ((OBJ) == NULL) { \
+ goto abort; \
+ } \
+ } while (0)
+
+// The following _WRITE_* and _WRITE_*_OR macros do NOT check their inputs
+// since they directly delegate to the _PyUnicodeWriter_Write* underlying
+// function. In particular, the caller is responsible for type safety.
+
+/* Cast WRITER and call _PyUnicodeWriter_WriteChar(). */
+#define _WRITE_CHAR(WRITER, CHAR) \
+ _PyUnicodeWriter_WriteChar((_PyUnicodeWriter *)(WRITER), (CHAR))
+
+/* Cast WRITER and call _PyUnicodeWriter_WriteASCIIString(). */
+#define _WRITE_ASCII(WRITER, STRING, LENGTH) \
+ _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter *)(WRITER), \
+ (STRING), (LENGTH))
+
+/* Cast WRITER and call _PyUnicodeWriter_WriteStr(). */
+#define _WRITE_STRING(WRITER, STRING) \
+ _PyUnicodeWriter_WriteStr((_PyUnicodeWriter *)(WRITER), (STRING))
+
+/* Cast WRITER and call _PyUnicodeWriter_WriteSubstring(). */
+#define _WRITE_SUBSTRING(WRITER, STRING, START, STOP) \
+ _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter *)(WRITER), \
+ (STRING), (START), (STOP))
+
+// ----------------------------------------------------------------------------
+
+/* Write the character CHAR or jump to the 'abort' label on failure. */
+#define WRITE_CHAR_OR_ABORT(WRITER, CHAR) \
+ CHECK_RET_CODE_OR_ABORT(_WRITE_CHAR((WRITER), (CHAR)))
+
+/*
+ * Write an ASCII string STRING of given length LENGTH,
+ * or jump to the 'abort' label on failure.
+ */
+#define WRITE_ASCII_OR_ABORT(WRITER, ASCII, LENGTH) \
+ CHECK_RET_CODE_OR_ABORT(_WRITE_ASCII((WRITER), (ASCII), (LENGTH)))
+
+/* Write the string STRING or jump to the 'abort' label on failure. */
+#define WRITE_STRING_OR_ABORT(WRITER, STRING) \
+ CHECK_RET_CODE_OR_ABORT(_WRITE_STRING((WRITER), (STRING)))
+
+/*
+ * Write the substring STRING[START:STOP] (no-op if empty)
+ * or jump to the 'abort' label on failure.
+ */
+#define WRITE_SUBSTRING_OR_ABORT(WRITER, STRING, START, STOP) \
+ do { \
+ const Py_ssize_t _START = (START); \
+ const Py_ssize_t _STOP = (STOP); \
+ int _RC = _WRITE_SUBSTRING((WRITER), (STRING), _START, _STOP); \
+ CHECK_RET_CODE_OR_ABORT(_RC); \
+ } while (0)
+
+// ----------------------------------------------------------------------------
+
+/* Replace backslashes in STRING by escaped backslashes. */
+#define BACKSLASH_REPLACE(STATE, STRING) \
+ PyObject_CallMethodObjArgs( \
+ (STRING), \
+ &_Py_ID(replace), \
+ (STATE)->backslash_str, \
+ (STATE)->backslash_esc_str, \
+ NULL \
+ )
+
+/* Replace hyphens in STRING by escaped hyphens. */
+#define HYPHEN_REPLACE(STATE, STRING) \
+ PyObject_CallMethodObjArgs( \
+ (STRING), \
+ &_Py_ID(replace), \
+ (STATE)->hyphen_str, \
+ (STATE)->hyphen_esc_str, \
+ NULL \
+ )
+
+/* Escape set operations in STRING using re.sub(). */
+#define SETOPS_REPLACE(STATE, STRING) \
+ PyObject_CallFunctionObjArgs( \
+ (STATE)->setops_re_subfn, \
+ (STATE)->setops_repl_str, \
+ (STRING), \
+ NULL \
+ )
+
+#endif // _FNMATCH_MACROS_H
diff --git a/Modules/_fnmatch/translate.c b/Modules/_fnmatch/translate.c
new file mode 100644
index 00000000000000..ef2d2e43f4b3b1
--- /dev/null
+++ b/Modules/_fnmatch/translate.c
@@ -0,0 +1,504 @@
+/*
+ * C accelerator for the translation function from UNIX shell patterns
+ * to RE patterns.
+ */
+
+#ifndef Py_BUILD_CORE_BUILTIN
+# define Py_BUILD_CORE_MODULE 1
+#endif
+
+#include "macros.h"
+#include "util.h" // for get_fnmatchmodulestate_state()
+
+#include "pycore_runtime.h" // for _Py_ID()
+
+// ==== Helper declarations ===================================================
+
+/*
+ * Write re.escape(ch).
+ *
+ * This returns the number of written characters, or -1 if an error occurred.
+ */
+static Py_ssize_t
+escape_char(fnmatchmodule_state *state, PyUnicodeWriter *writer, Py_UCS4 ch);
+
+/*
+ * Construct a regular expression out of a UNIX-style expression.
+ *
+ * The expression to translate is the content of an '[(BLOCK)]' expression,
+ * which contains single unicode characters or character ranges (e.g., 'a-z').
+ *
+ * By convention, 'start' and 'stop' represent the INCLUSIVE start index
+ * and EXCLUSIVE stop index of BLOCK in 'pattern'. Stated otherwise:
+ *
+ * pattern[start] == BLOCK[0]
+ * pattern[stop] == ']'
+ *
+ * For instance, for "ab[c-f]g[!1-5]", the values of 'start' and 'stop'
+ * for the sub-pattern '[c-f]' are 3 and 6 respectively, while their
+ * values for '[!1-5]' are 9 and 13 respectively.
+ *
+ * The 'pattern_str_find_meth' argument is a reference to pattern.find().
+ */
+static PyObject *
+translate_expression(fnmatchmodule_state *state,
+ PyObject *pattern, Py_ssize_t start, Py_ssize_t stop,
+ PyObject *pattern_str_find_meth);
+
+/*
+ * Write the translated pattern obtained by translate_expression().
+ *
+ * This returns the number of written characters, or -1 if an error occurred.
+ */
+static Py_ssize_t
+write_expression(fnmatchmodule_state *state,
+ PyUnicodeWriter *writer, PyObject *expression);
+
+/*
+ * Build the final regular expression by processing the wildcards.
+ *
+ * The position of each wildcard in 'pattern' is given by 'indices'.
+ */
+static PyObject *
+process_wildcards(PyObject *pattern, PyObject *indices);
+
+// ==== API implementation ====================================================
+
+PyObject *
+_Py_fnmatch_translate(PyObject *module, PyObject *pattern)
+{
+ assert(PyUnicode_Check(pattern));
+ fnmatchmodule_state *state = get_fnmatchmodule_state(module);
+ const Py_ssize_t maxind = PyUnicode_GET_LENGTH(pattern);
+ PyUnicodeWriter *writer = PyUnicodeWriter_Create(maxind);
+ if (writer == NULL) {
+ return NULL;
+ }
+ // ---- decl local objects ------------------------------------------------
+ PyObject *wildcard_indices = NULL; // positions of stars
+ PyObject *pattern_str_find_meth = NULL; // cached pattern.find()
+ // ---- def local objects -------------------------------------------------
+ wildcard_indices = PyList_New(0);
+ CHECK_NOT_NULL_OR_ABORT(wildcard_indices);
+ pattern_str_find_meth = PyObject_GetAttr(pattern, &_Py_ID(find));
+ CHECK_NOT_NULL_OR_ABORT(pattern_str_find_meth);
+ // ------------------------------------------------------------------------
+ const unsigned int pattern_kind = PyUnicode_KIND(pattern);
+ const void *const pattern_data = PyUnicode_DATA(pattern);
+ // ---- def local macros --------------------------------------------------
+#define READ_CHAR(IND) PyUnicode_READ(pattern_kind, pattern_data, IND)
+ /* advance IND if the character is CHAR */
+#define ADVANCE_IF_CHAR_IS(CHAR, IND, MAXIND) \
+ do { \
+ if ((IND) < (MAXIND) && READ_CHAR(IND) == (CHAR)) { \
+ ++IND; \
+ } \
+ } while (0)
+ // ------------------------------------------------------------------------
+ Py_ssize_t i = 0; // current index
+ Py_ssize_t written = 0; // number of characters written
+ while (i < maxind) {
+ Py_UCS4 chr = READ_CHAR(i++);
+ switch (chr) {
+ case '*': {
+ // translate wildcard '*' (fnmatch) into optional '.' (regex)
+ WRITE_CHAR_OR_ABORT(writer, '*');
+ // skip duplicated '*'
+ for (; i < maxind && READ_CHAR(i) == '*'; ++i);
+ // store the position of the wildcard
+ PyObject *wildcard_index = PyLong_FromSsize_t(written++);
+ CHECK_NOT_NULL_OR_ABORT(wildcard_index);
+ int rc = PyList_Append(wildcard_indices, wildcard_index);
+ Py_DECREF(wildcard_index);
+ CHECK_RET_CODE_OR_ABORT(rc);
+ break;
+ }
+ case '?': {
+ // translate optional '?' (fnmatch) into optional '.' (regex)
+ WRITE_CHAR_OR_ABORT(writer, '.');
+ ++written; // increase the expected result's length
+ break;
+ }
+ case '[': {
+ assert(READ_CHAR(i - 1) == '[');
+ Py_ssize_t j = i;
+ ADVANCE_IF_CHAR_IS('!', j, maxind); // [!
+ ADVANCE_IF_CHAR_IS(']', j, maxind); // [!] or []
+ for (; j < maxind && READ_CHAR(j) != ']'; ++j); // locate ']'
+ if (j >= maxind) {
+ WRITE_ASCII_OR_ABORT(writer, "\\[", 2);
+ written += 2; // we just wrote 2 characters
+ break; // explicit early break for clarity
+ }
+ else {
+ assert(READ_CHAR(j) == ']');
+ Py_ssize_t pos = PyUnicode_FindChar(pattern, '-', i, j, 1);
+ if (pos == -2) {
+ goto abort;
+ }
+ PyObject *expr = NULL;
+ if (pos == -1) {
+ PyObject *tmp = PyUnicode_Substring(pattern, i, j);
+ CHECK_NOT_NULL_OR_ABORT(tmp);
+ expr = BACKSLASH_REPLACE(state, tmp);
+ Py_DECREF(tmp);
+ }
+ else {
+ expr = translate_expression(state, pattern, i, j,
+ pattern_str_find_meth);
+ }
+ CHECK_NOT_NULL_OR_ABORT(expr);
+ Py_ssize_t expr_len = write_expression(state, writer, expr);
+ Py_DECREF(expr);
+ CHECK_UNSIGNED_INT_OR_ABORT(expr_len);
+ written += expr_len;
+ i = j + 1; // jump to the character after ']'
+ break; // explicit early break for clarity
+ }
+ }
+ default: {
+ Py_ssize_t t = escape_char(state, writer, chr);
+ CHECK_UNSIGNED_INT_OR_ABORT(t);
+ written += t;
+ break;
+ }
+ }
+ }
+#undef ADVANCE_IF_CHAR_IS
+#undef READ_CHAR
+ Py_DECREF(pattern_str_find_meth);
+ PyObject *translated = PyUnicodeWriter_Finish(writer);
+ if (translated == NULL) {
+ Py_DECREF(wildcard_indices);
+ return NULL;
+ }
+ PyObject *res = process_wildcards(translated, wildcard_indices);
+ Py_DECREF(translated);
+ Py_DECREF(wildcard_indices);
+ return res;
+abort:
+ Py_XDECREF(pattern_str_find_meth);
+ Py_XDECREF(wildcard_indices);
+ PyUnicodeWriter_Discard(writer);
+ return NULL;
+}
+
+// ==== Helper implementations ================================================
+
+/* taken from unicodeobject.c */
+static inline PyObject *
+unicode_char(Py_UCS4 ch)
+{
+#define MAX_UNICODE 0x10ffff
+ assert(ch <= MAX_UNICODE);
+#undef MAX_UNICODE
+ if (ch < 256) {
+ return _Py_LATIN1_CHR(ch);
+ }
+ PyObject *unicode = PyUnicode_New(1, ch);
+ if (unicode == NULL) {
+ return NULL;
+ }
+ assert(PyUnicode_KIND(unicode) != PyUnicode_1BYTE_KIND);
+ if (PyUnicode_KIND(unicode) == PyUnicode_2BYTE_KIND) {
+ PyUnicode_2BYTE_DATA(unicode)[0] = (Py_UCS2)ch;
+ }
+ else {
+ assert(PyUnicode_KIND(unicode) == PyUnicode_4BYTE_KIND);
+ PyUnicode_4BYTE_DATA(unicode)[0] = ch;
+ }
+ assert(_PyUnicode_CheckConsistency(unicode, 1));
+ return unicode;
+}
+
+static Py_ssize_t
+escape_char(fnmatchmodule_state *state, PyUnicodeWriter *writer, Py_UCS4 ch)
+{
+ PyObject *str = unicode_char(ch);
+ CHECK_NOT_NULL_OR_ABORT(str);
+ PyObject *escaped = PyObject_CallOneArg(state->re_escape, str);
+ Py_DECREF(str);
+ CHECK_NOT_NULL_OR_ABORT(escaped);
+ Py_ssize_t written = PyUnicode_GET_LENGTH(escaped);
+ int rc = _WRITE_STRING(writer, escaped);
+ Py_DECREF(escaped);
+ CHECK_RET_CODE_OR_ABORT(rc);
+ return written;
+abort:
+ return -1;
+}
+
+/*
+ * Extract a list of chunks from the pattern group described by start and stop.
+ *
+ * For instance, the chunks for [a-z0-9] or [!a-z0-9] are ['a', 'z0', '9'].
+ */
+static PyObject *
+split_expression(fnmatchmodule_state *state,
+ PyObject *pattern, Py_ssize_t start, Py_ssize_t stop,
+ PyObject *str_find_func)
+{
+ // ---- decl local objects ------------------------------------------------
+ PyObject *chunks = NULL, *maxind = NULL;
+ PyObject *hyphen = state->hyphen_str;
+ // ---- def local objects -------------------------------------------------
+ chunks = PyList_New(0);
+ CHECK_NOT_NULL_OR_ABORT(chunks);
+ maxind = PyLong_FromSsize_t(stop);
+ CHECK_NOT_NULL_OR_ABORT(maxind);
+ // ---- def local macros --------------------------------------------------
+ /* add pattern[START:STOP] to the list of chunks */
+#define ADD_CHUNK(START, STOP) \
+ do { \
+ PyObject *chunk = PyUnicode_Substring(pattern, (START), (STOP)); \
+ CHECK_NOT_NULL_OR_ABORT(chunk); \
+ int rc = PyList_Append(chunks, chunk); \
+ Py_DECREF(chunk); \
+ CHECK_RET_CODE_OR_ABORT(rc); \
+ } while (0)
+ // ------------------------------------------------------------------------
+ Py_ssize_t chunk_start = start;
+ bool is_complement = PyUnicode_READ_CHAR(pattern, start) == '!';
+ // skip '!' character (it is handled separately in write_expression())
+ Py_ssize_t ind = is_complement ? start + 2 : start + 1;
+ while (ind < stop) {
+ PyObject *p_chunk_stop = PyObject_CallFunction(str_find_func, "OnO",
+ hyphen, ind, maxind);
+ CHECK_NOT_NULL_OR_ABORT(p_chunk_stop);
+ Py_ssize_t chunk_stop = PyLong_AsSsize_t(p_chunk_stop);
+ Py_DECREF(p_chunk_stop);
+ if (chunk_stop < 0) {
+ if (PyErr_Occurred()) {
+ goto abort;
+ }
+ // -1 here means that '-' was not found
+ assert(chunk_stop == -1);
+ break;
+ }
+ ADD_CHUNK(chunk_start, chunk_stop);
+ chunk_start = chunk_stop + 1; // jump after '-'
+ ind = chunk_stop + 3; // ensure a non-empty next chunk
+ }
+ if (chunk_start < stop) {
+ ADD_CHUNK(chunk_start, stop);
+ }
+ else {
+ Py_ssize_t chunkscount = PyList_GET_SIZE(chunks);
+ assert(chunkscount > 0);
+ PyObject *chunk = PyList_GET_ITEM(chunks, chunkscount - 1);
+ PyObject *str = PyUnicode_Concat(chunk, hyphen);
+ if (str == NULL || PyList_SetItem(chunks, chunkscount - 1, str) < 0) {
+ Py_XDECREF(str);
+ goto abort;
+ }
+ }
+#undef ADD_CHUNK
+ Py_DECREF(maxind);
+ return chunks;
+abort:
+ Py_XDECREF(maxind);
+ Py_XDECREF(chunks);
+ return NULL;
+}
+
+/* Remove empty ranges (they are invalid in RE). */
+static int
+simplify_expression(PyObject *chunks)
+{
+ // for k in range(len(chunks) - 1, 0, -1):
+ for (Py_ssize_t k = PyList_GET_SIZE(chunks) - 1; k > 0; --k) {
+ PyObject *c1 = PyList_GET_ITEM(chunks, k - 1);
+ Py_ssize_t c1len = PyUnicode_GET_LENGTH(c1);
+
+ PyObject *c2 = PyList_GET_ITEM(chunks, k);
+ Py_ssize_t c2len = PyUnicode_GET_LENGTH(c2);
+
+ if (PyUnicode_READ_CHAR(c1, c1len - 1) > PyUnicode_READ_CHAR(c2, 0)) {
+ Py_ssize_t olen = c1len + c2len - 2;
+ assert(olen >= 0);
+ PyObject *str = NULL;
+ if (olen == 0) { // c1[:1] + c2[1:] == ''
+ str = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
+ }
+ else if (c1len == 1) { // c1[:1] + c2[1:] == c2[1:]
+ str = PyUnicode_Substring(c2, 1, c2len);
+ }
+ else if (c2len == 1) { // c1[:1] + c2[1:] == c1[:1]
+ str = PyUnicode_Substring(c1, 0, c1len - 1);
+ }
+ else {
+ PyUnicodeWriter *writer = PyUnicodeWriter_Create(olen);
+ CHECK_NOT_NULL_OR_ABORT(writer);
+ // all but the last character in the first chunk
+ if (_WRITE_SUBSTRING(writer, c1, 0, c1len - 1) < 0) {
+ PyUnicodeWriter_Discard(writer);
+ goto abort;
+ }
+ // all but the first character in the second chunk
+ if (_WRITE_SUBSTRING(writer, c2, 1, c2len) < 0) {
+ PyUnicodeWriter_Discard(writer);
+ goto abort;
+ }
+ str = PyUnicodeWriter_Finish(writer);
+ }
+ if (str == NULL || PyList_SetItem(chunks, k - 1, str) < 0) {
+ Py_XDECREF(str);
+ goto abort;
+ }
+ CHECK_RET_CODE_OR_ABORT(PySequence_DelItem(chunks, k));
+ }
+ }
+ return 0;
+abort:
+ return -1;
+}
+
+/* Escape backslashes and hyphens for set difference (--). */
+static int
+escape_expression(fnmatchmodule_state *state, PyObject *chunks)
+{
+ for (Py_ssize_t i = 0; i < PyList_GET_SIZE(chunks); ++i) {
+ PyObject *chunk = PyList_GET_ITEM(chunks, i);
+ PyObject *s1 = BACKSLASH_REPLACE(state, chunk);
+ CHECK_NOT_NULL_OR_ABORT(s1);
+ PyObject *s2 = HYPHEN_REPLACE(state, s1);
+ Py_DECREF(s1);
+ if (s2 == NULL || PyList_SetItem(chunks, i, s2) < 0) {
+ Py_XDECREF(s2);
+ goto abort;
+ }
+ }
+ return 0;
+abort:
+ return -1;
+}
+
+static PyObject *
+translate_expression(fnmatchmodule_state *state,
+ PyObject *pattern, Py_ssize_t start, Py_ssize_t stop,
+ PyObject *pattern_str_find_meth)
+{
+ PyObject *chunks = split_expression(state, pattern, start, stop,
+ pattern_str_find_meth);
+ CHECK_NOT_NULL_OR_ABORT(chunks);
+ CHECK_RET_CODE_OR_ABORT(simplify_expression(chunks));
+ CHECK_RET_CODE_OR_ABORT(escape_expression(state, chunks));
+ PyObject *res = PyUnicode_Join(state->hyphen_str, chunks);
+ Py_DECREF(chunks);
+ return res;
+abort:
+ Py_XDECREF(chunks);
+ return NULL;
+}
+
+static Py_ssize_t
+write_expression(fnmatchmodule_state *state,
+ PyUnicodeWriter *writer, PyObject *expression)
+{
+ PyObject *safe_expression = NULL; // for the 'goto abort' statements
+ Py_ssize_t grouplen = PyUnicode_GET_LENGTH(expression);
+ if (grouplen == 0) {
+ // empty range: never match
+ WRITE_ASCII_OR_ABORT(writer, "(?!)", 4);
+ return 4;
+ }
+ Py_UCS4 token = PyUnicode_READ_CHAR(expression, 0);
+ if (grouplen == 1 && token == '!') {
+ // negated empty range: match any character
+ WRITE_CHAR_OR_ABORT(writer, '.');
+ return 1;
+ }
+ Py_ssize_t extra = 2; // '[' and ']'
+ WRITE_CHAR_OR_ABORT(writer, '[');
+ // escape set operations as late as possible
+ safe_expression = SETOPS_REPLACE(state, expression);
+ CHECK_NOT_NULL_OR_ABORT(safe_expression);
+ switch (token) {
+ case '!': {
+ WRITE_CHAR_OR_ABORT(writer, '^'); // replace '!' by '^'
+ WRITE_SUBSTRING_OR_ABORT(writer, safe_expression, 1, grouplen);
+ break;
+ }
+ case '^':
+ case '[': {
+ WRITE_CHAR_OR_ABORT(writer, '\\');
+ ++extra; // because we wrote '\\'
+ WRITE_STRING_OR_ABORT(writer, safe_expression);
+ break;
+ }
+ default: {
+ WRITE_STRING_OR_ABORT(writer, safe_expression);
+ break;
+ }
+ }
+ Py_DECREF(safe_expression);
+ WRITE_CHAR_OR_ABORT(writer, ']');
+ return grouplen + extra;
+abort:
+ Py_XDECREF(safe_expression);
+ return -1;
+}
+
+static PyObject *
+process_wildcards(PyObject *pattern, PyObject *indices)
+{
+ const Py_ssize_t n = PyUnicode_GET_LENGTH(pattern);
+ const Py_ssize_t m = PyList_GET_SIZE(indices);
+ // Let m = len(indices) and n = len(pattern). By construction,
+ //
+ // pattern = [PREFIX] [[(* INNER) ... (* INNER)] (*) [OUTER]]
+ //
+ // where [...] is an optional group and (...) is a required group.
+ //
+ // The algorithm is as follows:
+ //
+ // - Write "(?s:".
+ // - Write the optional PREFIX.
+ // - Write an INNER group (* INNER) as "(?>.*?" + INNER + ")".
+ // - Write ".*" instead of the last wildcard.
+ // - Write an optional OUTER string normally.
+ // - Write ")\\Z".
+ //
+ // If m = 0, the writer needs n + 7 characters. Otherwise, it requires
+ // exactly n + 6(m-1) + 1 + 7 = n + 6m + 2 characters, where the "+1"
+ // is due to the fact that writing ".*" instead of "*" only increases
+ // the total length of the pattern by 1 (and not by 2).
+ const Py_ssize_t reslen = m == 0 ? (n + 7) : (n + 6 * m + 2);
+ PyUnicodeWriter *writer = PyUnicodeWriter_Create(reslen);
+ if (writer == NULL) {
+ return NULL;
+ }
+ WRITE_ASCII_OR_ABORT(writer, "(?s:", 4);
+ if (m == 0) {
+ WRITE_STRING_OR_ABORT(writer, pattern);
+ }
+ else {
+ Py_ssize_t i = 0;
+ // process the optional PREFIX
+ Py_ssize_t j = PyLong_AsSsize_t(PyList_GET_ITEM(indices, 0));
+ CHECK_UNSIGNED_INT_OR_ABORT(j);
+ WRITE_SUBSTRING_OR_ABORT(writer, pattern, i, j);
+ i = j + 1;
+ for (Py_ssize_t k = 1; k < m; ++k) {
+ // process the (* INNER) groups
+ j = PyLong_AsSsize_t(PyList_GET_ITEM(indices, k));
+ CHECK_UNSIGNED_INT_OR_ABORT(j);
+ assert(i < j);
+ // write the atomic RE group '(?>.*?' + INNER + ')'
+ WRITE_ASCII_OR_ABORT(writer, "(?>.*?", 6);
+ WRITE_SUBSTRING_OR_ABORT(writer, pattern, i, j);
+ WRITE_CHAR_OR_ABORT(writer, ')');
+ i = j + 1;
+ }
+ // handle the (*) [OUTER] part
+ WRITE_ASCII_OR_ABORT(writer, ".*", 2);
+ WRITE_SUBSTRING_OR_ABORT(writer, pattern, i, n);
+ }
+ WRITE_ASCII_OR_ABORT(writer, ")\\Z", 3);
+ PyObject *res = PyUnicodeWriter_Finish(writer);
+ assert(res == NULL || PyUnicode_GET_LENGTH(res) == reslen);
+ return res;
+abort:
+ PyUnicodeWriter_Discard(writer);
+ return NULL;
+}
diff --git a/Modules/_fnmatch/util.h b/Modules/_fnmatch/util.h
new file mode 100644
index 00000000000000..8f598fa66f7c1d
--- /dev/null
+++ b/Modules/_fnmatch/util.h
@@ -0,0 +1,50 @@
+/*
+ * This file contains helper prototypes and structures.
+ */
+
+#ifndef _FNMATCH_UTIL_H
+#define _FNMATCH_UTIL_H
+
+#include "Python.h"
+
+typedef struct {
+ PyObject *re_escape; // LRU-cached re.escape() function
+
+ // strings used by translate.c
+ PyObject *hyphen_str; // hyphen '-'
+ PyObject *hyphen_esc_str; // escaped hyphen '\\-'
+
+ PyObject *backslash_str; // backslash '\\'
+ PyObject *backslash_esc_str; // escaped backslash '\\\\'
+
+ /* set operation tokens (&&, ~~ and ||) are not supported in regex */
+ PyObject *setops_re_subfn; // cached re.compile('([&~|])').sub()
+ PyObject *setops_repl_str; // replacement pattern '\\\\\\1'
+} fnmatchmodule_state;
+
+static inline fnmatchmodule_state *
+get_fnmatchmodule_state(PyObject *module)
+{
+ void *state = PyModule_GetState(module);
+ assert(state != NULL);
+ return (fnmatchmodule_state *)state;
+}
+
+// ==== Helper prototypes =====================================================
+
+/*
+ * C accelerator for translating UNIX shell patterns into RE patterns.
+ *
+ * Parameters
+ *
+ * module A module with a state given by get_fnmatchmodule_state().
+ * pattern A Unicode object to translate.
+ *
+ * Returns
+ *
+ * A translated unicode RE pattern.
+ */
+extern PyObject *
+_Py_fnmatch_translate(PyObject *module, PyObject *pattern);
+
+#endif // _FNMATCH_UTIL_H
diff --git a/Modules/clinic/_fnmatchmodule.c.h b/Modules/clinic/_fnmatchmodule.c.h
new file mode 100644
index 00000000000000..4b12f33113d3fb
--- /dev/null
+++ b/Modules/clinic/_fnmatchmodule.c.h
@@ -0,0 +1,185 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+# include "pycore_gc.h" // PyGC_Head
+# include "pycore_runtime.h" // _Py_ID()
+#endif
+#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
+
+PyDoc_STRVAR(_fnmatch_filter__doc__,
+"filter($module, /, names, pat)\n"
+"--\n"
+"\n");
+
+#define _FNMATCH_FILTER_METHODDEF \
+ {"filter", _PyCFunction_CAST(_fnmatch_filter), METH_FASTCALL|METH_KEYWORDS, _fnmatch_filter__doc__},
+
+static PyObject *
+_fnmatch_filter_impl(PyObject *module, PyObject *names, PyObject *pat);
+
+static PyObject *
+_fnmatch_filter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 2
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_item = { &_Py_ID(names), &_Py_ID(pat), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"names", "pat", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "filter",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[2];
+ PyObject *names;
+ PyObject *pat;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ names = args[0];
+ pat = args[1];
+ return_value = _fnmatch_filter_impl(module, names, pat);
+
+exit:
+ return return_value;
+}
+
+PyDoc_STRVAR(_fnmatch_fnmatchcase__doc__,
+"fnmatchcase($module, /, name, pat)\n"
+"--\n"
+"\n"
+"Test whether `name` matches `pattern`, including case.\n"
+"\n"
+"This is a version of fnmatch() which doesn\'t case-normalize\n"
+"its arguments.");
+
+#define _FNMATCH_FNMATCHCASE_METHODDEF \
+ {"fnmatchcase", _PyCFunction_CAST(_fnmatch_fnmatchcase), METH_FASTCALL|METH_KEYWORDS, _fnmatch_fnmatchcase__doc__},
+
+static int
+_fnmatch_fnmatchcase_impl(PyObject *module, PyObject *name, PyObject *pat);
+
+static PyObject *
+_fnmatch_fnmatchcase(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 2
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_item = { &_Py_ID(name), &_Py_ID(pat), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"name", "pat", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "fnmatchcase",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[2];
+ PyObject *name;
+ PyObject *pat;
+ int _return_value;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ name = args[0];
+ pat = args[1];
+ _return_value = _fnmatch_fnmatchcase_impl(module, name, pat);
+ if ((_return_value == -1) && PyErr_Occurred()) {
+ goto exit;
+ }
+ return_value = PyBool_FromLong((long)_return_value);
+
+exit:
+ return return_value;
+}
+
+PyDoc_STRVAR(_fnmatch_translate__doc__,
+"translate($module, /, pat)\n"
+"--\n"
+"\n");
+
+#define _FNMATCH_TRANSLATE_METHODDEF \
+ {"translate", _PyCFunction_CAST(_fnmatch_translate), METH_FASTCALL|METH_KEYWORDS, _fnmatch_translate__doc__},
+
+static PyObject *
+_fnmatch_translate_impl(PyObject *module, PyObject *pattern);
+
+static PyObject *
+_fnmatch_translate(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 1
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_item = { &_Py_ID(pat), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"pat", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "translate",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[1];
+ PyObject *pattern;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ pattern = args[0];
+ return_value = _fnmatch_translate_impl(module, pattern);
+
+exit:
+ return return_value;
+}
+/*[clinic end generated code: output=b0366b259b101bdf input=a9049054013a1b77]*/
diff --git a/PC/config.c b/PC/config.c
index b744f711b0d636..7c7c2540118cf2 100644
--- a/PC/config.c
+++ b/PC/config.c
@@ -10,6 +10,7 @@ extern PyObject* PyInit_array(void);
extern PyObject* PyInit_binascii(void);
extern PyObject* PyInit_cmath(void);
extern PyObject* PyInit_errno(void);
+extern PyObject* PyInit__fnmatch(void);
extern PyObject* PyInit_faulthandler(void);
extern PyObject* PyInit__tracemalloc(void);
extern PyObject* PyInit_gc(void);
@@ -91,6 +92,7 @@ struct _inittab _PyImport_Inittab[] = {
{"binascii", PyInit_binascii},
{"cmath", PyInit_cmath},
{"errno", PyInit_errno},
+ {"_fnmatch", PyInit__fnmatch},
{"faulthandler", PyInit_faulthandler},
{"gc", PyInit_gc},
{"math", PyInit_math},
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 9e3af689f4a288..20141f370bc7a4 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -369,6 +369,8 @@
+
+
@@ -474,6 +476,8 @@
+
+
diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters
index 31f7971bda845d..94de5f38778401 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -252,6 +252,12 @@
Modules
+
+ Modules\_fnmatch
+
+
+ Modules\_fnmatch
+
Modules\_io
@@ -1061,6 +1067,12 @@
Modules
+
+ Modules\_fnmatch
+
+
+ Modules\_fnmatch
+
Modules\_io
diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h
index 4d595d98445a05..269c7b34e9d477 100644
--- a/Python/stdlib_module_names.h
+++ b/Python/stdlib_module_names.h
@@ -32,6 +32,7 @@ static const char* _Py_stdlib_module_names[] = {
"_dbm",
"_decimal",
"_elementtree",
+"_fnmatch",
"_frozen_importlib",
"_frozen_importlib_external",
"_functools",
diff --git a/configure b/configure
index 52988f77f6d926..60ebd04f3735d7 100755
--- a/configure
+++ b/configure
@@ -801,6 +801,8 @@ MODULE__JSON_FALSE
MODULE__JSON_TRUE
MODULE__HEAPQ_FALSE
MODULE__HEAPQ_TRUE
+MODULE__FNMATCH_FALSE
+MODULE__FNMATCH_TRUE
MODULE__CSV_FALSE
MODULE__CSV_TRUE
MODULE__CONTEXTVARS_FALSE
@@ -27749,6 +27751,7 @@ SRCDIRS="\
Modules/_ctypes \
Modules/_decimal \
Modules/_decimal/libmpdec \
+ Modules/_fnmatch \
Modules/_hacl \
Modules/_io \
Modules/_multiprocessing \
@@ -29233,6 +29236,28 @@ then :
+fi
+
+
+ if test "$py_cv_module__fnmatch" != "n/a"
+then :
+ py_cv_module__fnmatch=yes
+fi
+ if test "$py_cv_module__fnmatch" = yes; then
+ MODULE__FNMATCH_TRUE=
+ MODULE__FNMATCH_FALSE='#'
+else
+ MODULE__FNMATCH_TRUE='#'
+ MODULE__FNMATCH_FALSE=
+fi
+
+ as_fn_append MODULE_BLOCK "MODULE__FNMATCH_STATE=$py_cv_module__fnmatch$as_nl"
+ if test "x$py_cv_module__fnmatch" = xyes
+then :
+
+ as_fn_append MODULE_BLOCK "MODULE__FNMATCH_CFLAGS=-I\$(srcdir)/Modules/_fnmatch$as_nl"
+
+
fi
@@ -31818,6 +31843,10 @@ if test -z "${MODULE__CSV_TRUE}" && test -z "${MODULE__CSV_FALSE}"; then
as_fn_error $? "conditional \"MODULE__CSV\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
+if test -z "${MODULE__FNMATCH_TRUE}" && test -z "${MODULE__FNMATCH_FALSE}"; then
+ as_fn_error $? "conditional \"MODULE__FNMATCH\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
if test -z "${MODULE__HEAPQ_TRUE}" && test -z "${MODULE__HEAPQ_FALSE}"; then
as_fn_error $? "conditional \"MODULE__HEAPQ\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
diff --git a/configure.ac b/configure.ac
index 5bde6803cd5a7b..2183ae2a590daa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -7009,6 +7009,7 @@ SRCDIRS="\
Modules/_ctypes \
Modules/_decimal \
Modules/_decimal/libmpdec \
+ Modules/_fnmatch \
Modules/_hacl \
Modules/_io \
Modules/_multiprocessing \
@@ -7694,6 +7695,7 @@ PY_STDLIB_MOD_SIMPLE([_asyncio])
PY_STDLIB_MOD_SIMPLE([_bisect])
PY_STDLIB_MOD_SIMPLE([_contextvars])
PY_STDLIB_MOD_SIMPLE([_csv])
+PY_STDLIB_MOD_SIMPLE([_fnmatch], [-I\$(srcdir)/Modules/_fnmatch], [])
PY_STDLIB_MOD_SIMPLE([_heapq])
PY_STDLIB_MOD_SIMPLE([_json])
PY_STDLIB_MOD_SIMPLE([_lsprof])