gh-152905: Decode LC_TIME items in nl_langinfo() from glibc wide data#152911
Conversation
…e data On glibc, locale.nl_langinfo() now decodes the LC_TIME text items from the wide (_NL_W*) locale data, independently of the LC_CTYPE encoding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documentation build overview
66 files changed ·
|
The encoding-independence guaranteed by the wide (_NL_W*) decode is glibc-specific, so gate test_nl_langinfo_encoding_independent on glibc (which also covers the previously skipped musl case). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
On my Fedora 44, this change is not only technical, it does actually change nl_langinfo() output on multiple locales. Some examples.
I wrote this script to dump all nl_langinfo() values of all locales on Linux: import locale
import subprocess
def get_all_locales():
cmd = ['locale', '-a']
proc = subprocess.run(cmd, stdout=subprocess.PIPE, text=True)
stdout = proc.stdout
return stdout.splitlines()
langinfo_constants = [
"DAY_1",
"DAY_2",
"DAY_3",
"DAY_4",
"DAY_5",
"DAY_6",
"DAY_7",
"ABDAY_1",
"ABDAY_2",
"ABDAY_3",
"ABDAY_4",
"ABDAY_5",
"ABDAY_6",
"ABDAY_7",
"MON_1",
"MON_2",
"MON_3",
"MON_4",
"MON_5",
"MON_6",
"MON_7",
"MON_8",
"MON_9",
"MON_10",
"MON_11",
"MON_12",
"ABMON_1",
"ABMON_2",
"ABMON_3",
"ABMON_4",
"ABMON_5",
"ABMON_6",
"ABMON_7",
"ABMON_8",
"ABMON_9",
"ABMON_10",
"ABMON_11",
"ABMON_12",
"RADIXCHAR",
"THOUSEP",
"CRNCYSTR",
"D_T_FMT",
"D_FMT",
"T_FMT",
"AM_STR",
"PM_STR",
"CODESET",
"T_FMT_AMPM",
"ERA",
"ERA_D_FMT",
"ERA_D_T_FMT",
"ERA_T_FMT",
"ALT_DIGITS",
"YESEXPR",
"NOEXPR",
"_DATE_FMT",
]
langinfo_constants = [
name
for name in langinfo_constants
if hasattr(locale, name)
]
langinfo_constants.sort()
all_locales = get_all_locales()
all_locales.sort()
for loc in all_locales:
title = f"Locale {loc}"
print(title)
print("=" * len(title))
print()
locale.setlocale(locale.LC_ALL, loc)
for name in langinfo_constants:
key = getattr(locale, name)
value = locale.nl_langinfo(key)
print(f'{name}: {value!r}')
print()
print(f"Total: nl_langinfo() values: {len(langinfo_constants)} per locale") |
| if hasattr(locale, 'T_FMT_AMPM'): | ||
| items.append(locale.T_FMT_AMPM) | ||
| if hasattr(locale, 'ALT_DIGITS'): | ||
| items.append(locale.ALT_DIGITS) |
There was a problem hiding this comment.
You should also test _DATE_FMT, no?
Why not testing ERA_D_FMT, ERA_D_T_FMT and ERA_T_FMT?
| def test_nl_langinfo_encoding_independent(self): | ||
| # gh-152905: The LC_TIME text items are decoded independently of the | ||
| # LC_CTYPE encoding (on glibc via the wide nl_langinfo variants), so | ||
| # the same locale in different encodings yields identical strings. |
There was a problem hiding this comment.
Please mention that ERA has no wide character variant and so is not test.
| #endif | ||
| #ifdef ERA | ||
| if (item == ERA && *result) { | ||
| pyresult = decode_strings(result, SIZE_MAX); |
There was a problem hiding this comment.
You might remove max_count of decode_strings() since it's no longer needed.
|
|
||
| .. versionchanged:: next | ||
| On glibc, the ``LC_TIME`` items are now decoded | ||
| independently of the ``LC_CTYPE`` encoding. |
I'm fine with the change anyway. But since the nl_langinfo() output changes on some locales, I would prefer to not backport this change. |
I know, this is a point. Our On other hand, I found these discrepancies when tried to use |
* Rewrite test_nl_langinfo_encoding_independent to compare each item individually (clearer failures), listing only the legacy locales and deriving the UTF-8 variant; broaden coverage to 20 locales across 17 legacy encodings. Also test ERA_D_FMT, ERA_D_T_FMT, ERA_T_FMT and _DATE_FMT; note that ERA has no wide variant and is not tested. * Drop the now-unused max_count parameter of decode_strings(). * Mention in the docs that ERA is not affected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vstinner
left a comment
There was a problem hiding this comment.
LGTM.
I just have a minor coding style suggestion.
test__locale pass on my Fedora 44.
| for loc in legacy_locales: | ||
| locs = (loc.partition('.')[0] + '.UTF-8', loc) |
There was a problem hiding this comment.
Nitpick: you can replace loc with legacy_locale here:
for legacy_locale in legacy_locales:
locs = (legacy_locale.partition('.')[0] + '.UTF-8', legacy_locale)And replace for l in locs: with for loc in locs: below.
…ng test An 8-bit locale substitutes an equivalent for a space character it cannot encode: e.g. es_ES uses U+202F NARROW NO-BREAK SPACE in AM_STR/PM_STR in its UTF-8 locale but U+00A0 NO-BREAK SPACE in the ISO-8859-1 one. Fold space-separator characters together before comparing so the test ignores such substitutions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Nice change. It's a little bit surprising to use a private API of glibc, but IMO it's worth it. |
On glibc, decode the
LC_TIMEitems from the wide (_NL_W*) locale data, so the result no longer depends on theLC_CTYPEencoding.The wide constant is always
_NL_W+ the narrow name, so it is filled intolanginfo_constants[]by token pasting — one scan yields both the item and its wide form.ERAhas no wide counterpart and keeps the narrow path.🤖 Generated with Claude Code