From 46107ad9da0add7aa5c0a899e159d89c1376d6be Mon Sep 17 00:00:00 2001 From: Xiao Yuan Date: Mon, 15 Jun 2026 18:05:29 +0300 Subject: [PATCH 1/4] gh-92455: Respect case-sensitive mimetype suffixes (GH-148782) --- Doc/library/mimetypes.rst | 8 +++- Lib/mimetypes.py | 22 ++++++++-- Lib/test/test_mimetypes.py | 44 +++++++++++++++++++ ...6-04-20-01-24-22.gh-issue-92455.vXhmad.rst | 3 ++ 4 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst diff --git a/Doc/library/mimetypes.rst b/Doc/library/mimetypes.rst index f33098faf7d8a77..5c29fff146eef00 100644 --- a/Doc/library/mimetypes.rst +++ b/Doc/library/mimetypes.rst @@ -39,8 +39,8 @@ the information :func:`init` sets up. (e.g. :program:`compress` or :program:`gzip`). The encoding is suitable for use as a :mailheader:`Content-Encoding` header, **not** as a :mailheader:`Content-Transfer-Encoding` header. The mappings are table driven. - Encoding suffixes are case sensitive; type suffixes are first tried case - sensitively, then case insensitively. + Encoding suffixes are case-sensitive. Suffix mappings and type suffixes are + first tried case-sensitively, then case-insensitively. The optional *strict* argument is a flag specifying whether the list of known MIME types is limited to only the official types `registered with IANA @@ -131,6 +131,8 @@ behavior of the module. is already known the extension will be added to the list of known extensions. Valid extensions are empty or start with a ``'.'``. + Registered lower-case extensions are matched case-insensitively. + When *strict* is ``True`` (the default), the mapping will be added to the official MIME types, otherwise to the non-standard ones. @@ -312,6 +314,8 @@ than one MIME-type database; it provides an interface similar to the one of the extension is already known, the new type will replace the old one. When the type is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched case-insensitively. + When *strict* is ``True`` (the default), the mapping will be added to the official MIME types, otherwise to the non-standard ones. diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index 15e8c0a437bfd93..4339ef5a61397dd 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -86,6 +86,9 @@ def add_type(self, type, ext, strict=True): is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched + case-insensitively. + If strict is true, information will be added to list of standard types, else to the list of non-standard types. @@ -172,23 +175,33 @@ def guess_file_type(self, path, *, strict=True): def _guess_file_type(self, path, strict, splitext): base, ext = splitext(path) - while (ext_lower := ext.lower()) in self.suffix_map: - base, ext = splitext(base + self.suffix_map[ext_lower]) + while True: + if ext in self.suffix_map: + suffix = self.suffix_map[ext] + elif (ext_lower := ext.lower()) in self.suffix_map: + suffix = self.suffix_map[ext_lower] + else: + break + base, ext = splitext(base + suffix) # encodings_map is case sensitive if ext in self.encodings_map: encoding = self.encodings_map[ext] base, ext = splitext(base) else: encoding = None - ext = ext.lower() + ext_lower = ext.lower() types_map = self.types_map[True] if ext in types_map: return types_map[ext], encoding + if ext_lower in types_map: + return types_map[ext_lower], encoding elif strict: return None, encoding types_map = self.types_map[False] if ext in types_map: return types_map[ext], encoding + if ext_lower in types_map: + return types_map[ext_lower], encoding else: return None, encoding @@ -386,6 +399,9 @@ def add_type(type, ext, strict=True): is already known the extension will be added to the list of known extensions. + Registered lower-case extensions are matched + case-insensitively. + If strict is true, information will be added to list of standard types, else to the list of non-standard types. diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index 1a3b49b87b121f2..19983fa3fa7628d 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -287,6 +287,50 @@ def test_case_sensitivity(self): eq(self.db.guess_file_type("foobar.tar.z"), (None, None)) eq(self.db.guess_type("scheme:foobar.tar.z"), (None, None)) + def test_suffix_map_case_sensitive_preferred(self): + self.db.suffix_map[".TEST-SUFFIX"] = ".tar.gz" + self.db.suffix_map[".test-suffix"] = ".tar.xz" + self.assertEqual( + self.db.guess_file_type("example.TEST-SUFFIX"), + ("application/x-tar", "gzip"), + ) + self.assertEqual( + self.db.guess_file_type("example.test-suffix"), + ("application/x-tar", "xz"), + ) + + def test_added_types_case_sensitive_preferred(self): + self.db.add_type("text/x-test-uppercase-r", ".R") + self.db.add_type("text/x-test-lowercase-r", ".r") + self.assertEqual( + self.db.guess_file_type("example.R"), + ("text/x-test-uppercase-r", None), + ) + self.assertEqual( + self.db.guess_file_type("example.r"), + ("text/x-test-lowercase-r", None), + ) + self.db.add_type("text/x-test-uppercase-non-strict", + ".NON-STRICT-EXT", strict=False) + self.db.add_type("text/x-test-lowercase-non-strict", + ".non-strict-ext", strict=False) + self.assertEqual( + self.db.guess_file_type("example.NON-STRICT-EXT"), + (None, None), + ) + self.assertEqual( + self.db.guess_file_type("example.non-strict-ext"), + (None, None), + ) + self.assertEqual( + self.db.guess_file_type("example.NON-STRICT-EXT", strict=False), + ("text/x-test-uppercase-non-strict", None), + ) + self.assertEqual( + self.db.guess_file_type("example.non-strict-ext", strict=False), + ("text/x-test-lowercase-non-strict", None), + ) + def test_default_data(self): eq = self.assertEqual eq(self.db.guess_file_type("foo.html"), ("text/html", None)) diff --git a/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst new file mode 100644 index 000000000000000..8d2a11cb7761377 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-20-01-24-22.gh-issue-92455.vXhmad.rst @@ -0,0 +1,3 @@ +Fix :mod:`mimetypes` to prefer case-sensitive matches for suffix mappings and +MIME type suffixes before falling back to case-insensitive matches. +Contributed by Xiao Yuan. From 585e14c7d3fbe773ff54b4038567ca1159fb295a Mon Sep 17 00:00:00 2001 From: Weilin Du <1372449351@qq.com> Date: Tue, 16 Jun 2026 00:11:10 +0800 Subject: [PATCH 2/4] gh-140145: Use repr of the key in `zoneinfo` "No time zone found" error (#140433) Co-authored-by: Stan Ulbrych --- Lib/zoneinfo/_common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/zoneinfo/_common.py b/Lib/zoneinfo/_common.py index 98668c15d8bf94b..caa3a5b583bab32 100644 --- a/Lib/zoneinfo/_common.py +++ b/Lib/zoneinfo/_common.py @@ -26,7 +26,7 @@ def load_tzdata(key): # UnicodeEncodeError: If package_name or resource_name are not UTF-8, # such as keys containing a surrogate character. # IsADirectoryError: If package_name without a resource_name specified. - raise ZoneInfoNotFoundError(f"No time zone found with key {key}") + raise ZoneInfoNotFoundError(f"No time zone found with key {key!r}") def load_data(fobj): From e9d5280f6c040f859907eb3c04ec308f4918db9f Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Mon, 15 Jun 2026 22:09:49 +0530 Subject: [PATCH 3/4] gh-151223: fix tsan data races in load global specializations (#151393) --- Modules/_testinternalcapi/test_cases.c.h | 6 +++--- Python/bytecodes.c | 6 +++--- Python/executor_cases.c.h | 6 +++--- Python/generated_cases.c.h | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Modules/_testinternalcapi/test_cases.c.h b/Modules/_testinternalcapi/test_cases.c.h index 503f566c9ae86a5..62d08826a2faea4 100644 --- a/Modules/_testinternalcapi/test_cases.c.h +++ b/Modules/_testinternalcapi/test_cases.c.h @@ -8830,7 +8830,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); assert(index < FT_ATOMIC_LOAD_SSIZE_RELAXED(keys->dk_nentries)); PyDictUnicodeEntry *ep = DK_UNICODE_ENTRIES(keys) + index; - PyObject *attr_o = FT_ATOMIC_LOAD_PTR_RELAXED(ep->me_value); + PyObject *attr_o = FT_ATOMIC_LOAD_PTR_CONSUME(ep->me_value); if (attr_o == NULL) { UPDATE_MISS_STATS(LOAD_ATTR); assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); @@ -9707,7 +9707,7 @@ } assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UPDATE_MISS_STATS(LOAD_GLOBAL); assert(_PyOpcode_Deopt[opcode] == (LOAD_GLOBAL)); @@ -9774,7 +9774,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); assert(index < DK_SIZE(keys)); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UPDATE_MISS_STATS(LOAD_GLOBAL); assert(_PyOpcode_Deopt[opcode] == (LOAD_GLOBAL)); diff --git a/Python/bytecodes.c b/Python/bytecodes.c index e368092b300f864..beaf6752b87ea2d 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -2349,7 +2349,7 @@ dummy_func( assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); assert(index < DK_SIZE(keys)); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); DEOPT_IF(res_o == NULL); #if Py_GIL_DISABLED int increfed = _Py_TryIncrefCompareStackRef(&entries[index].me_value, res_o, &res); @@ -2368,7 +2368,7 @@ dummy_func( DEOPT_IF(FT_ATOMIC_LOAD_UINT32_RELAXED(keys->dk_version) != version); assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); DEOPT_IF(res_o == NULL); #if Py_GIL_DISABLED int increfed = _Py_TryIncrefCompareStackRef(&entries[index].me_value, res_o, &res); @@ -2958,7 +2958,7 @@ dummy_func( assert(keys->dk_kind == DICT_KEYS_UNICODE); assert(index < FT_ATOMIC_LOAD_SSIZE_RELAXED(keys->dk_nentries)); PyDictUnicodeEntry *ep = DK_UNICODE_ENTRIES(keys) + index; - PyObject *attr_o = FT_ATOMIC_LOAD_PTR_RELAXED(ep->me_value); + PyObject *attr_o = FT_ATOMIC_LOAD_PTR_CONSUME(ep->me_value); EXIT_IF(attr_o == NULL); #ifdef Py_GIL_DISABLED int increfed = _Py_TryIncrefCompareStackRef(&ep->me_value, attr_o, &attr); diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h index 7973c75e1a60ad2..d5bfe60cd234737 100644 --- a/Python/executor_cases.c.h +++ b/Python/executor_cases.c.h @@ -10301,7 +10301,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); assert(index < DK_SIZE(keys)); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); @@ -10346,7 +10346,7 @@ } assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UOP_STAT_INC(uopcode, miss); SET_CURRENT_CACHED_VALUES(0); @@ -12152,7 +12152,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); assert(index < FT_ATOMIC_LOAD_SSIZE_RELAXED(keys->dk_nentries)); PyDictUnicodeEntry *ep = DK_UNICODE_ENTRIES(keys) + index; - PyObject *attr_o = FT_ATOMIC_LOAD_PTR_RELAXED(ep->me_value); + PyObject *attr_o = FT_ATOMIC_LOAD_PTR_CONSUME(ep->me_value); if (attr_o == NULL) { UOP_STAT_INC(uopcode, miss); _tos_cache0 = owner; diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 5adcdcb4521baf5..a6e0f90d8c1ce2a 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -8829,7 +8829,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); assert(index < FT_ATOMIC_LOAD_SSIZE_RELAXED(keys->dk_nentries)); PyDictUnicodeEntry *ep = DK_UNICODE_ENTRIES(keys) + index; - PyObject *attr_o = FT_ATOMIC_LOAD_PTR_RELAXED(ep->me_value); + PyObject *attr_o = FT_ATOMIC_LOAD_PTR_CONSUME(ep->me_value); if (attr_o == NULL) { UPDATE_MISS_STATS(LOAD_ATTR); assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR)); @@ -9705,7 +9705,7 @@ } assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UPDATE_MISS_STATS(LOAD_GLOBAL); assert(_PyOpcode_Deopt[opcode] == (LOAD_GLOBAL)); @@ -9772,7 +9772,7 @@ assert(keys->dk_kind == DICT_KEYS_UNICODE); PyDictUnicodeEntry *entries = DK_UNICODE_ENTRIES(keys); assert(index < DK_SIZE(keys)); - PyObject *res_o = FT_ATOMIC_LOAD_PTR_RELAXED(entries[index].me_value); + PyObject *res_o = FT_ATOMIC_LOAD_PTR_CONSUME(entries[index].me_value); if (res_o == NULL) { UPDATE_MISS_STATS(LOAD_GLOBAL); assert(_PyOpcode_Deopt[opcode] == (LOAD_GLOBAL)); From 5ea1e907d1928c1e08cf4246e8935d62a95ca8a1 Mon Sep 17 00:00:00 2001 From: Langyan Date: Tue, 16 Jun 2026 01:55:57 +0800 Subject: [PATCH 4/4] gh-151128: Improve SyntaxError message for cross language keywords (GH-151129) --- Lib/test/test_traceback.py | 12 +++++++ Lib/traceback.py | 32 +++++++++++++++++-- ...-06-12-00-17-29.gh-issue-151128.-LYO3a.rst | 3 ++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2c6324a14a8e2fc..e38d0942e463e9c 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -1815,6 +1815,18 @@ class TestKeywordTypoSuggestions(unittest.TestCase): ("[x for x\nin range(3)\nof x]", "if"), ("[123 fur x\nin range(3)\nif x]", "for"), ("for x im n:\n pass", "in"), + ("mach x:", "match"), + ("math x:", "match"), + ("match 1:\n cse 1:", "case"), + ("typ x = int", "type"), + ("typed x = int", "type"), + ("lazi import x", "lazy"), + ("lezi import x", "lazy"), + ("switch x:\n case:", "match"), + ("delete x", "del"), + ("function f():", "def"), + ("func f():", "def"), + ("void f():", "def"), ] def test_keyword_suggestions_from_file(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index 614a12f69b32e40..dcdab1f12e9a168 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1485,11 +1485,22 @@ def _find_keyword_typos(self): # Limit the number of possible matches to try max_matches = 3 matches = [] + + hint = _get_cross_language_keyword_hint(wrong_name) + if hint: + matches.append(hint) if _suggestions is not None: - suggestion = _suggestions._generate_suggestions(keyword.kwlist, wrong_name) + suggestion = _suggestions._generate_suggestions(keyword.kwlist + keyword.softkwlist, wrong_name) if suggestion: matches.append(suggestion) - matches.extend(difflib.get_close_matches(wrong_name, keyword.kwlist, n=max_matches, cutoff=0.5)) + matches.extend( + difflib.get_close_matches( + wrong_name, + keyword.kwlist + keyword.softkwlist, + n=max_matches, + cutoff=0.5 + ) + ) matches = matches[:max_matches] for suggestion in matches: if not suggestion or suggestion == wrong_name: @@ -1787,6 +1798,17 @@ def print(self, *, file=None, chain=True, **kwargs): }) +# Cross-language keyword suggestions. +_CROSS_LANGUAGE_KEYWORD_HINTS = frozendict({ + # C/C++ equivalents + 'switch': 'match', + 'delete': 'del', + # function define equivalents + 'function': 'def', + 'func': 'def', + 'void': 'def', +}) + def _substitution_cost(ch_a, ch_b): if ch_a == ch_b: return 0 @@ -1866,6 +1888,12 @@ def _get_cross_language_hint(obj, wrong_name): return None +def _get_cross_language_keyword_hint(wrong_name): + """Check if wrong_name is a common keyword from another language + """ + return _CROSS_LANGUAGE_KEYWORD_HINTS.get(wrong_name) + + def _get_safe___dir__(obj): # Use obj.__dir__() to avoid a TypeError when calling dir(obj). # See gh-131001 and gh-139933. diff --git a/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst new file mode 100644 index 000000000000000..6e760686af2b01f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-12-00-17-29.gh-issue-151128.-LYO3a.rst @@ -0,0 +1,3 @@ +Cross-language keyword suggestions are now shown for :exc:`SyntaxError` messages. +For example, ``switch x:`` suggests ``match``, ``delete x`` suggests ``del``, +``function f():`` suggests ``def``. Contributed by Zang Langyan.