Skip to content

gh-124969: Make locale.nl_langinfo(locale.ALT_DIGITS) returning a string again #125774

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Doc/library/locale.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,7 @@ The :mod:`locale` module defines the following exception and functions:

.. function:: nl_langinfo(option)

Return some locale-specific information as a string (or a tuple for
``ALT_DIGITS``). This function is not
Return some locale-specific information as a string. This function is not
available on all systems, and the set of possible options might also vary
across platforms. The possible argument values are numbers, for which
symbolic constants are available in the locale module.
Expand Down Expand Up @@ -312,7 +311,9 @@ The :mod:`locale` module defines the following exception and functions:

.. data:: ALT_DIGITS

Get a tuple of up to 100 strings used to represent the values 0 to 99.
Get a string consisting of up to 100 semicolon-separated symbols used
to represent the values 0 to 99 in a locale-specific way.
In most locales this is an empty string.

The function temporarily sets the ``LC_CTYPE`` locale to the locale
of the category that determines the requested value (``LC_TIME``,
Expand Down
30 changes: 21 additions & 9 deletions Lib/test/test__locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
'bs_BA', 'fr_LU', 'kl_GL', 'fa_IR', 'de_BE', 'sv_SE', 'it_CH', 'uk_UA',
'eu_ES', 'vi_VN', 'af_ZA', 'nb_NO', 'en_DK', 'tg_TJ', 'ps_AF', 'en_US',
'fr_FR.ISO8859-1', 'fr_FR.UTF-8', 'fr_FR.ISO8859-15@euro',
'ru_RU.KOI8-R', 'ko_KR.eucKR']
'ru_RU.KOI8-R', 'ko_KR.eucKR',
'ja_JP.UTF-8', 'lzh_TW.UTF-8', 'my_MM.UTF-8', 'or_IN.UTF-8', 'shn_MM.UTF-8',
'ar_AE.UTF-8', 'bn_IN.UTF-8', 'mr_IN.UTF-8', 'th_TH.TIS620',
]

def setUpModule():
global candidate_locales
Expand Down Expand Up @@ -78,11 +81,13 @@ def accept(loc):
'C': (0, {}),
'en_US': (0, {}),
'fa_IR': (100, {0: '\u06f0\u06f0', 10: '\u06f1\u06f0', 99: '\u06f9\u06f9'}),
'ja_JP': (100, {0: '\u3007', 10: '\u5341', 99: '\u4e5d\u5341\u4e5d'}),
'ja_JP': (100, {1: '\u4e00', 10: '\u5341', 99: '\u4e5d\u5341\u4e5d'}),
'lzh_TW': (32, {0: '\u3007', 10: '\u5341', 31: '\u5345\u4e00'}),
'my_MM': (100, {0: '\u1040\u1040', 10: '\u1041\u1040', 99: '\u1049\u1049'}),
'or_IN': (100, {0: '\u0b66', 10: '\u0b67\u0b66', 99: '\u0b6f\u0b6f'}),
'shn_MM': (100, {0: '\u1090\u1090', 10: '\u1091\u1090', 99: '\u1099\u1099'}),
'ar_AE': (100, {0: '\u0660', 10: '\u0661\u0660', 99: '\u0669\u0669'}),
'bn_IN': (100, {0: '\u09e6', 10: '\u09e7\u09e6', 99: '\u09ef\u09ef'}),
}

if sys.platform == 'win32':
Expand Down Expand Up @@ -199,21 +204,28 @@ def test_lc_numeric_basic(self):
def test_alt_digits_nl_langinfo(self):
# Test nl_langinfo(ALT_DIGITS)
tested = False
for loc, (count, samples) in known_alt_digits.items():
for loc in candidate_locales:
with self.subTest(locale=loc):
try:
setlocale(LC_TIME, loc)
except Error:
self.skipTest(f'no locale {loc!r}')
continue

with self.subTest(locale=loc):
alt_digits = nl_langinfo(locale.ALT_DIGITS)
self.assertIsInstance(alt_digits, tuple)
if count and not alt_digits and support.is_apple:
self.skipTest(f'ALT_DIGITS is not set for locale {loc!r} on Apple platforms')
self.assertEqual(len(alt_digits), count)
for i in samples:
self.assertEqual(alt_digits[i], samples[i])
self.assertIsInstance(alt_digits, str)
alt_digits = alt_digits.split(';') if alt_digits else []
if alt_digits:
self.assertGreaterEqual(len(alt_digits), 10, alt_digits)
loc1 = loc.split('.', 1)[0]
if loc1 in known_alt_digits:
count, samples = known_alt_digits[loc1]
if count and not alt_digits:
self.skipTest(f'ALT_DIGITS is not set for locale {loc!r} on this platform')
self.assertEqual(len(alt_digits), count, alt_digits)
for i in samples:
self.assertEqual(alt_digits[i], samples[i])
tested = True
if not tested:
self.skipTest('no suitable locales')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``locale.nl_langinfo(locale.ALT_DIGITS)`` now returns a string again. The
returned value consists of up to 100 semicolon-separated symbols.
38 changes: 23 additions & 15 deletions Modules/_localemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -667,28 +667,36 @@ _locale_nl_langinfo_impl(PyObject *module, int item)
return NULL;
}
PyObject *pyresult;
#ifdef __GLIBC__
#ifdef ALT_DIGITS
if (item == ALT_DIGITS) {
/* The result is a sequence of up to 100 NUL-separated strings. */
const char *s = result;
if (item == ALT_DIGITS && *result) {
/* According to the POSIX specification the result must be
* a sequence of up to 100 semicolon-separated strings.
* But in Glibc they are NUL-separated. */
Py_ssize_t i = 0;
int count = 0;
for (; count < 100 && *s; count++) {
s += strlen(s) + 1;
for (; count < 100 && result[i]; count++) {
i += strlen(result + i) + 1;
}
pyresult = PyTuple_New(count);
if (pyresult != NULL) {
for (int i = 0; i < count; i++) {
PyObject *unicode = PyUnicode_DecodeLocale(result, NULL);
if (unicode == NULL) {
Py_CLEAR(pyresult);
break;
}
PyTuple_SET_ITEM(pyresult, i, unicode);
result += strlen(result) + 1;
char *buf = PyMem_Malloc(i);
if (buf == NULL) {
PyErr_NoMemory();
pyresult = NULL;
}
else {
memcpy(buf, result, i);
/* Replace all NULs with semicolons. */
i = 0;
while (--count) {
i += strlen(buf + i);
buf[i++] = ';';
}
pyresult = PyUnicode_DecodeLocale(buf, NULL);
PyMem_Free(buf);
}
}
else
#endif
#endif
{
pyresult = PyUnicode_DecodeLocale(result, NULL);
Expand Down
Loading