Skip to content

Commit 123855c

Browse files
smarieyehoshuadimarsky
authored andcommitted
BUG: Fixed tm.set_locale context manager, it could error and leak when category LC_ALL was used (pandas-dev#47570)
1 parent 41f66af commit 123855c

File tree

4 files changed

+60
-18
lines changed

4 files changed

+60
-18
lines changed

pandas/_config/localization.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def set_locale(
3939
particular locale, without globally setting the locale. This probably isn't
4040
thread-safe.
4141
"""
42-
current_locale = locale.getlocale()
42+
# getlocale is not always compliant with setlocale, use setlocale. GH#46595
43+
current_locale = locale.setlocale(lc_var)
4344

4445
try:
4546
locale.setlocale(lc_var, new_locale)

pandas/tests/config/test_localization.py

+50-17
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,67 @@
1010
set_locale,
1111
)
1212

13-
from pandas.compat import is_platform_windows
14-
1513
import pandas as pd
1614

1715
_all_locales = get_locales() or []
18-
_current_locale = locale.getlocale()
16+
_current_locale = locale.setlocale(locale.LC_ALL) # getlocale() is wrong, see GH#46595
1917

20-
# Don't run any of these tests if we are on Windows or have no locales.
21-
pytestmark = pytest.mark.skipif(
22-
is_platform_windows() or not _all_locales, reason="Need non-Windows and locales"
23-
)
18+
# Don't run any of these tests if we have no locales.
19+
pytestmark = pytest.mark.skipif(not _all_locales, reason="Need locales")
2420

2521
_skip_if_only_one_locale = pytest.mark.skipif(
2622
len(_all_locales) <= 1, reason="Need multiple locales for meaningful test"
2723
)
2824

2925

30-
def test_can_set_locale_valid_set():
26+
def _get_current_locale(lc_var: int = locale.LC_ALL) -> str:
27+
# getlocale is not always compliant with setlocale, use setlocale. GH#46595
28+
return locale.setlocale(lc_var)
29+
30+
31+
@pytest.mark.parametrize("lc_var", (locale.LC_ALL, locale.LC_CTYPE, locale.LC_TIME))
32+
def test_can_set_current_locale(lc_var):
33+
# Can set the current locale
34+
before_locale = _get_current_locale(lc_var)
35+
assert can_set_locale(before_locale, lc_var=lc_var)
36+
after_locale = _get_current_locale(lc_var)
37+
assert before_locale == after_locale
38+
39+
40+
@pytest.mark.parametrize("lc_var", (locale.LC_ALL, locale.LC_CTYPE, locale.LC_TIME))
41+
def test_can_set_locale_valid_set(lc_var):
3142
# Can set the default locale.
32-
assert can_set_locale("")
43+
before_locale = _get_current_locale(lc_var)
44+
assert can_set_locale("", lc_var=lc_var)
45+
after_locale = _get_current_locale(lc_var)
46+
assert before_locale == after_locale
3347

3448

35-
def test_can_set_locale_invalid_set():
49+
@pytest.mark.parametrize("lc_var", (locale.LC_ALL, locale.LC_CTYPE, locale.LC_TIME))
50+
def test_can_set_locale_invalid_set(lc_var):
3651
# Cannot set an invalid locale.
37-
assert not can_set_locale("non-existent_locale")
52+
before_locale = _get_current_locale(lc_var)
53+
assert not can_set_locale("non-existent_locale", lc_var=lc_var)
54+
after_locale = _get_current_locale(lc_var)
55+
assert before_locale == after_locale
56+
57+
58+
@pytest.mark.parametrize(
59+
"lang,enc",
60+
[
61+
("it_CH", "UTF-8"),
62+
("en_US", "ascii"),
63+
("zh_CN", "GB2312"),
64+
("it_IT", "ISO-8859-1"),
65+
],
66+
)
67+
@pytest.mark.parametrize("lc_var", (locale.LC_ALL, locale.LC_CTYPE, locale.LC_TIME))
68+
def test_can_set_locale_no_leak(lang, enc, lc_var):
69+
# Test that can_set_locale does not leak even when returning False. See GH#46595
70+
before_locale = _get_current_locale(lc_var)
71+
can_set_locale((lang, enc), locale.LC_ALL)
72+
after_locale = _get_current_locale(lc_var)
73+
assert before_locale == after_locale
3874

3975

4076
def test_can_set_locale_invalid_get(monkeypatch):
@@ -72,10 +108,7 @@ def test_get_locales_prefix():
72108
],
73109
)
74110
def test_set_locale(lang, enc):
75-
if all(x is None for x in _current_locale):
76-
# Not sure why, but on some Travis runs with pytest,
77-
# getlocale() returned (None, None).
78-
pytest.skip("Current locale is not set.")
111+
before_locale = _get_current_locale()
79112

80113
enc = codecs.lookup(enc).name
81114
new_locale = lang, enc
@@ -95,8 +128,8 @@ def test_set_locale(lang, enc):
95128
assert normalized_locale == new_locale
96129

97130
# Once we exit the "with" statement, locale should be back to what it was.
98-
current_locale = locale.getlocale()
99-
assert current_locale == _current_locale
131+
after_locale = _get_current_locale()
132+
assert before_locale == after_locale
100133

101134

102135
def test_encoding_detected():

pandas/tests/tools/test_to_datetime.py

+5
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,16 @@ def test_to_datetime_format_microsecond(self, cache):
284284
"%m/%d/%Y %H:%M:%S",
285285
Timestamp("2010-01-10 13:56:01"),
286286
],
287+
# The 3 tests below are locale-dependent.
288+
# They pass, except when the machine locale is zh_CN or it_IT .
287289
pytest.param(
288290
"01/10/2010 08:14 PM",
289291
"%m/%d/%Y %I:%M %p",
290292
Timestamp("2010-01-10 20:14"),
291293
marks=pytest.mark.xfail(
292294
locale.getlocale()[0] in ("zh_CN", "it_IT"),
293295
reason="fail on a CI build with LC_ALL=zh_CN.utf8/it_IT.utf8",
296+
strict=False,
294297
),
295298
),
296299
pytest.param(
@@ -300,6 +303,7 @@ def test_to_datetime_format_microsecond(self, cache):
300303
marks=pytest.mark.xfail(
301304
locale.getlocale()[0] in ("zh_CN", "it_IT"),
302305
reason="fail on a CI build with LC_ALL=zh_CN.utf8/it_IT.utf8",
306+
strict=False,
303307
),
304308
),
305309
pytest.param(
@@ -309,6 +313,7 @@ def test_to_datetime_format_microsecond(self, cache):
309313
marks=pytest.mark.xfail(
310314
locale.getlocale()[0] in ("zh_CN", "it_IT"),
311315
reason="fail on a CI build with LC_ALL=zh_CN.utf8/it_IT.utf8",
316+
strict=False,
312317
),
313318
),
314319
],

pandas/tests/tools/test_to_time.py

+3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
from pandas.core.tools.datetimes import to_time as to_time_alias
1010
from pandas.core.tools.times import to_time
1111

12+
# The tests marked with this are locale-dependent.
13+
# They pass, except when the machine locale is zh_CN or it_IT.
1214
fails_on_non_english = pytest.mark.xfail(
1315
locale.getlocale()[0] in ("zh_CN", "it_IT"),
1416
reason="fail on a CI build with LC_ALL=zh_CN.utf8/it_IT.utf8",
17+
strict=False,
1518
)
1619

1720

0 commit comments

Comments
 (0)