Skip to content

Commit a72886c

Browse files
[3.13] pythongh-126727: Fix locale.nl_langinfo(locale.ERA) (pythonGH-126730)
It now returns multiple era description segments separated by semicolons. Previously it only returned the first segment on platforms with Glibc. (cherry picked from commit 4803cd0) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 746a0c5 commit a72886c

File tree

4 files changed

+96
-28
lines changed

4 files changed

+96
-28
lines changed

Doc/library/locale.rst

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ The :mod:`locale` module defines the following exception and functions:
281281

282282
.. data:: ERA
283283

284-
Get a string that represents the era used in the current locale.
284+
Get a string which describes how years are counted and displayed for
285+
each era in a locale.
285286

286287
Most locales do not define this value. An example of a locale which does
287288
define this value is the Japanese one. In Japan, the traditional
@@ -290,9 +291,10 @@ The :mod:`locale` module defines the following exception and functions:
290291

291292
Normally it should not be necessary to use this value directly. Specifying
292293
the ``E`` modifier in their format strings causes the :func:`time.strftime`
293-
function to use this information. The format of the returned string is not
294-
specified, and therefore you should not assume knowledge of it on different
295-
systems.
294+
function to use this information.
295+
The format of the returned string is specified in *The Open Group Base
296+
Specifications Issue 8*, paragraph `7.3.5.2 LC_TIME C-Language Access
297+
<https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap07.html#tag_07_03_05_02>`_.
296298

297299
.. data:: ERA_D_T_FMT
298300

Lib/test/test__locale.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ def accept(loc):
9090
'bn_IN': (100, {0: '\u09e6', 10: '\u09e7\u09e6', 99: '\u09ef\u09ef'}),
9191
}
9292

93+
known_era = {
94+
'C': (0, ''),
95+
'en_US': (0, ''),
96+
'ja_JP': (11, '+:1:2019/05/01:2019/12/31:令和:%EC元年'),
97+
'zh_TW': (3, '+:1:1912/01/01:1912/12/31:民國:%EC元年'),
98+
'th_TW': (1, '+:1:-543/01/01:+*:พ.ศ.:%EC %Ey'),
99+
}
100+
93101
if sys.platform == 'win32':
94102
# ps_AF doesn't work on Windows: see bpo-38324 (msg361830)
95103
del known_numerics['ps_AF']
@@ -228,6 +236,44 @@ def test_alt_digits_nl_langinfo(self):
228236
if not tested:
229237
self.skipTest('no suitable locales')
230238

239+
@unittest.skipUnless(nl_langinfo, "nl_langinfo is not available")
240+
@unittest.skipUnless(hasattr(locale, 'ERA'), "requires locale.ERA")
241+
@unittest.skipIf(
242+
support.is_emscripten or support.is_wasi,
243+
"musl libc issue on Emscripten, bpo-46390"
244+
)
245+
def test_era_nl_langinfo(self):
246+
# Test nl_langinfo(ERA)
247+
tested = False
248+
for loc in candidate_locales:
249+
with self.subTest(locale=loc):
250+
try:
251+
setlocale(LC_TIME, loc)
252+
setlocale(LC_CTYPE, loc)
253+
except Error:
254+
self.skipTest(f'no locale {loc!r}')
255+
continue
256+
257+
with self.subTest(locale=loc):
258+
era = nl_langinfo(locale.ERA)
259+
self.assertIsInstance(era, str)
260+
if era:
261+
self.assertEqual(era.count(':'), (era.count(';') + 1) * 5, era)
262+
263+
loc1 = loc.split('.', 1)[0]
264+
if loc1 in known_era:
265+
count, sample = known_era[loc1]
266+
if count:
267+
if not era:
268+
self.skipTest(f'ERA is not set for locale {loc!r} on this platform')
269+
self.assertGreaterEqual(era.count(';') + 1, count)
270+
self.assertIn(sample, era)
271+
else:
272+
self.assertEqual(era, '')
273+
tested = True
274+
if not tested:
275+
self.skipTest('no suitable locales')
276+
231277
def test_float_parsing(self):
232278
# Bug #1391872: Test whether float parsing is okay on European
233279
# locales.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
``locale.nl_langinfo(locale.ERA)`` now returns multiple era description
2+
segments separated by semicolons. Previously it only returned the first
3+
segment on platforms with Glibc.

Modules/_localemodule.c

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,37 @@ static struct langinfo_constant{
585585
{0, 0}
586586
};
587587

588+
#ifdef __GLIBC__
589+
#if defined(ALT_DIGITS) || defined(ERA)
590+
static PyObject *
591+
decode_strings(const char *result, size_t max_count)
592+
{
593+
/* Convert a sequence of NUL-separated C strings to a Python string
594+
* containing semicolon separated items. */
595+
size_t i = 0;
596+
size_t count = 0;
597+
for (; count < max_count && result[i]; count++) {
598+
i += strlen(result + i) + 1;
599+
}
600+
char *buf = PyMem_Malloc(i);
601+
if (buf == NULL) {
602+
PyErr_NoMemory();
603+
return NULL;
604+
}
605+
memcpy(buf, result, i);
606+
/* Replace all NULs with semicolons. */
607+
i = 0;
608+
while (--count) {
609+
i += strlen(buf + i);
610+
buf[i++] = ';';
611+
}
612+
PyObject *pyresult = PyUnicode_DecodeLocale(buf, NULL);
613+
PyMem_Free(buf);
614+
return pyresult;
615+
}
616+
#endif
617+
#endif
618+
588619
/*[clinic input]
589620
_locale.nl_langinfo
590621
@@ -610,32 +641,18 @@ _locale_nl_langinfo_impl(PyObject *module, int item)
610641
result = result != NULL ? result : "";
611642
PyObject *pyresult;
612643
#ifdef __GLIBC__
644+
/* According to the POSIX specification the result must be
645+
* a sequence of semicolon-separated strings.
646+
* But in Glibc they are NUL-separated. */
613647
#ifdef ALT_DIGITS
614648
if (item == ALT_DIGITS && *result) {
615-
/* According to the POSIX specification the result must be
616-
* a sequence of up to 100 semicolon-separated strings.
617-
* But in Glibc they are NUL-separated. */
618-
Py_ssize_t i = 0;
619-
int count = 0;
620-
for (; count < 100 && result[i]; count++) {
621-
i += strlen(result + i) + 1;
622-
}
623-
char *buf = PyMem_Malloc(i);
624-
if (buf == NULL) {
625-
PyErr_NoMemory();
626-
pyresult = NULL;
627-
}
628-
else {
629-
memcpy(buf, result, i);
630-
/* Replace all NULs with semicolons. */
631-
i = 0;
632-
while (--count) {
633-
i += strlen(buf + i);
634-
buf[i++] = ';';
635-
}
636-
pyresult = PyUnicode_DecodeLocale(buf, NULL);
637-
PyMem_Free(buf);
638-
}
649+
pyresult = decode_strings(result, 100);
650+
}
651+
else
652+
#endif
653+
#ifdef ERA
654+
if (item == ERA && *result) {
655+
pyresult = decode_strings(result, SIZE_MAX);
639656
}
640657
else
641658
#endif

0 commit comments

Comments
 (0)