From b959df926853f6361a8c68f54711142f902c9eff Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Tue, 11 Aug 2026 16:23:49 +0530 Subject: [PATCH] gh-151316: Prefer UTF-8 for the en_IN locale alias X11 locale.alias mapped en_IN to ISO8859-1, so getlocale() invented a codeset that does not exist on modern UTF-8-only systems and broke setlocale(getlocale()) round-trips. Prefer en_IN.UTF-8 (mirroring other modernized aliases) and cover the round-trip in tests. Signed-off-by: Sankalp Thakur --- Lib/locale.py | 7 +++- Lib/test/test_locale.py | 39 +++++++++++++++++++ ...-08-11-09-47-40.gh-issue-151316.M7ANwQ.rst | 4 ++ Tools/i18n/makelocalealias.py | 3 ++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-11-09-47-40.gh-issue-151316.M7ANwQ.rst diff --git a/Lib/locale.py b/Lib/locale.py index 25efff5b8568543..e37fe73ae039d93 100644 --- a/Lib/locale.py +++ b/Lib/locale.py @@ -921,6 +921,11 @@ def getpreferredencoding(do_setlocale=True): # # removed 'el_gr@euro' # removed 'uz_uz@cyrillic' +# +# gh-151316: +# Prefer UTF-8 for en_IN. X11 locale.alias maps en_IN to ISO8859-1 and that +# entry overrides glibc's en_IN/UTF-8 during makelocalealias regeneration, +# so hardcode the modern codeset (mirroring the c.utf8 carve-out). locale_alias = { 'a3': 'az_AZ.KOI8-C', @@ -1066,7 +1071,7 @@ def getpreferredencoding(do_setlocale=True): 'en_hk': 'en_HK.ISO8859-1', 'en_ie': 'en_IE.ISO8859-1', 'en_il': 'en_IL.ISO8859-1', - 'en_in': 'en_IN.ISO8859-1', + 'en_in': 'en_IN.UTF-8', 'en_ng': 'en_NG.UTF-8', 'en_nz': 'en_NZ.ISO8859-1', 'en_ph': 'en_PH.ISO8859-1', diff --git a/Lib/test/test_locale.py b/Lib/test/test_locale.py index 8057c35f540841b..3570f96fdfe7786 100644 --- a/Lib/test/test_locale.py +++ b/Lib/test/test_locale.py @@ -417,6 +417,13 @@ def test_english(self): self.check('english', 'en_EN.ISO8859-1') self.check('english_uk.ascii', 'en_GB.ISO8859-1') + def test_en_in_utf8(self): + # gh-151316: en_IN is UTF-8 on modern glibc; do not invent ISO8859-1. + self.check('en_IN', 'en_IN.UTF-8') + self.check('en_in', 'en_IN.UTF-8') + self.assertEqual(locale._parse_localename('en_IN'), ('en_IN', 'UTF-8')) + self.assertEqual(locale._parse_localename('en_in'), ('en_IN', 'UTF-8')) + def test_hyphenated_encoding(self): self.check('az_AZ.iso88599e', 'az_AZ.ISO8859-9E') self.check('az_AZ.ISO8859-9E', 'az_AZ.ISO8859-9E') @@ -642,6 +649,38 @@ def test_getlocale_with_modifier(self, localename, localetuple): self.assertEqual(locale.getlocale(locale.LC_CTYPE), localetuple) +class TestEnINLocale(unittest.TestCase): + """gh-151316: en_IN must round-trip without inventing ISO8859-1.""" + + def setUp(self): + self.oldlocale = locale.setlocale(locale.LC_CTYPE) + self.addCleanup(locale.setlocale, locale.LC_CTYPE, self.oldlocale) + + def test_getlocale_setlocale_roundtrip(self): + try: + locale.setlocale(locale.LC_CTYPE, 'en_IN') + except locale.Error as exc: + self.skipTest(str(exc)) + loc = locale.getlocale(locale.LC_CTYPE) + self.assertEqual(loc[0], 'en_IN') + self.assertNotEqual(loc[1], 'ISO8859-1') + locale.setlocale(locale.LC_CTYPE, loc) + self.assertEqual(locale.getlocale(locale.LC_CTYPE), loc) + + def test_setlocale_from_getlocale_tuple(self): + # Reproduces the issue report: setlocale(LC_*, getlocale()). + try: + locale.setlocale(locale.LC_CTYPE, 'en_IN.UTF-8') + except locale.Error: + try: + locale.setlocale(locale.LC_CTYPE, 'en_IN') + except locale.Error as exc: + self.skipTest(str(exc)) + loc = locale.getlocale(locale.LC_CTYPE) + locale.setlocale(locale.LC_CTYPE, loc) + self.assertEqual(locale.getlocale(locale.LC_CTYPE)[0], 'en_IN') + + class TestMiscellaneous(unittest.TestCase): def test_defaults_UTF8(self): # Issue #18378: on (at least) macOS setting LC_CTYPE to "UTF-8" is diff --git a/Misc/NEWS.d/next/Library/2026-08-11-09-47-40.gh-issue-151316.M7ANwQ.rst b/Misc/NEWS.d/next/Library/2026-08-11-09-47-40.gh-issue-151316.M7ANwQ.rst new file mode 100644 index 000000000000000..ed342d0252d9d08 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-11-09-47-40.gh-issue-151316.M7ANwQ.rst @@ -0,0 +1,4 @@ +:mod:`locale` now maps the ``en_IN`` locale alias to ``en_IN.UTF-8`` +instead of the obsolete X11 ``ISO8859-1`` codeset. This restores +``setlocale(getlocale())`` round-trips on modern glibc systems where +``en_IN`` is UTF-8-only. diff --git a/Tools/i18n/makelocalealias.py b/Tools/i18n/makelocalealias.py index f825862ffdf350a..6c1b2ffa8a40a7a 100755 --- a/Tools/i18n/makelocalealias.py +++ b/Tools/i18n/makelocalealias.py @@ -154,6 +154,9 @@ def check(data): # Hardcode 'c.utf8' -> 'C.UTF-8' because 'en_US.UTF-8' does not exist # on all platforms. data['c.utf8'] = 'C.UTF-8' + # Hardcode 'en_in' -> 'en_IN.UTF-8'. X11 locale.alias still maps en_IN to + # ISO8859-1 and would otherwise override glibc's en_IN/UTF-8 (gh-151316). + data['en_in'] = 'en_IN.UTF-8' while True: # Repeat optimization while the size is decreased. n = len(data)