Skip to content

Commit fc24c2c

Browse files
jbrockmendeljreback
authored andcommitted
Move locale code out of tm, into _config (#25757)
1 parent 05780f8 commit fc24c2c

File tree

7 files changed

+122
-105
lines changed

7 files changed

+122
-105
lines changed

pandas/_config/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""
2+
pandas._config is considered explicitly upstream of everything else in pandas,
3+
should have no intra-pandas dependencies.
4+
"""

pandas/_config/localization.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Helpers for configuring locale settings.
3+
4+
Name `localization` is chosen to avoid overlap with builtin `locale` module.
5+
"""
6+
from contextlib import contextmanager
7+
import locale
8+
9+
10+
@contextmanager
11+
def set_locale(new_locale, lc_var=locale.LC_ALL):
12+
"""
13+
Context manager for temporarily setting a locale.
14+
15+
Parameters
16+
----------
17+
new_locale : str or tuple
18+
A string of the form <language_country>.<encoding>. For example to set
19+
the current locale to US English with a UTF8 encoding, you would pass
20+
"en_US.UTF-8".
21+
lc_var : int, default `locale.LC_ALL`
22+
The category of the locale being set.
23+
24+
Notes
25+
-----
26+
This is useful when you want to run a particular block of code under a
27+
particular locale, without globally setting the locale. This probably isn't
28+
thread-safe.
29+
"""
30+
current_locale = locale.getlocale()
31+
32+
try:
33+
locale.setlocale(lc_var, new_locale)
34+
normalized_locale = locale.getlocale()
35+
if all(x is not None for x in normalized_locale):
36+
yield '.'.join(normalized_locale)
37+
else:
38+
yield new_locale
39+
finally:
40+
locale.setlocale(lc_var, current_locale)
41+
42+
43+
def can_set_locale(lc, lc_var=locale.LC_ALL):
44+
"""
45+
Check to see if we can set a locale, and subsequently get the locale,
46+
without raising an Exception.
47+
48+
Parameters
49+
----------
50+
lc : str
51+
The locale to attempt to set.
52+
lc_var : int, default `locale.LC_ALL`
53+
The category of the locale being set.
54+
55+
Returns
56+
-------
57+
is_valid : bool
58+
Whether the passed locale can be set
59+
"""
60+
61+
try:
62+
with set_locale(lc, lc_var=lc_var):
63+
pass
64+
except (ValueError, locale.Error):
65+
# horrible name for a Exception subclass
66+
return False
67+
else:
68+
return True
69+
70+
71+
def _valid_locales(locales, normalize):
72+
"""
73+
Return a list of normalized locales that do not throw an ``Exception``
74+
when set.
75+
76+
Parameters
77+
----------
78+
locales : str
79+
A string where each locale is separated by a newline.
80+
normalize : bool
81+
Whether to call ``locale.normalize`` on each locale.
82+
83+
Returns
84+
-------
85+
valid_locales : list
86+
A list of valid locales.
87+
"""
88+
if normalize:
89+
normalizer = lambda x: locale.normalize(x.strip())
90+
else:
91+
normalizer = lambda x: x.strip()
92+
93+
return list(filter(can_set_locale, map(normalizer, locales)))

pandas/_libs/tslibs/ccalendar.pyx

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import cython
99
from numpy cimport int64_t, int32_t
1010

1111
from locale import LC_TIME
12+
13+
from pandas._config.localization import set_locale
1214
from pandas._libs.tslibs.strptime import LocaleTime
1315

1416
# ----------------------------------------------------------------------
@@ -206,7 +208,7 @@ cpdef int32_t get_day_of_year(int year, int month, int day) nogil:
206208
return day_of_year
207209

208210

209-
cpdef get_locale_names(object name_type, object locale=None):
211+
def get_locale_names(name_type: object, locale: object=None):
210212
"""Returns an array of localized day or month names
211213
212214
Parameters
@@ -218,9 +220,6 @@ cpdef get_locale_names(object name_type, object locale=None):
218220
Returns
219221
-------
220222
list of locale names
221-
222223
"""
223-
from pandas.util.testing import set_locale
224-
225224
with set_locale(locale, LC_TIME):
226225
return getattr(LocaleTime(), name_type)

pandas/tests/config/__init__.py

Whitespace-only changes.

pandas/tests/util/test_locale.py renamed to pandas/tests/config/test_localization.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
import pytest
77

8+
from pandas._config.localization import can_set_locale, set_locale
9+
810
from pandas.compat import is_platform_windows
911

10-
import pandas.core.common as com
12+
# TODO: move get_locales into localization, making `tm` import unnecessary.
13+
# This is blocked by the need for core.config to be moved to _config.
1114
import pandas.util.testing as tm
1215

1316
_all_locales = tm.get_locales() or []
@@ -23,30 +26,29 @@
2326

2427
def test_can_set_locale_valid_set():
2528
# Can set the default locale.
26-
assert tm.can_set_locale("")
29+
assert can_set_locale("")
2730

2831

2932
def test_can_set_locale_invalid_set():
3033
# Cannot set an invalid locale.
31-
assert not tm.can_set_locale("non-existent_locale")
34+
assert not can_set_locale("non-existent_locale")
3235

3336

3437
def test_can_set_locale_invalid_get(monkeypatch):
35-
# see gh-22129
36-
#
38+
# see GH#22129
3739
# In some cases, an invalid locale can be set,
38-
# but a subsequent getlocale() raises a ValueError.
40+
# but a subsequent getlocale() raises a ValueError.
3941

4042
def mock_get_locale():
4143
raise ValueError()
4244

4345
with monkeypatch.context() as m:
4446
m.setattr(locale, "getlocale", mock_get_locale)
45-
assert not tm.can_set_locale("")
47+
assert not can_set_locale("")
4648

4749

4850
def test_get_locales_at_least_one():
49-
# see gh-9744
51+
# see GH#9744
5052
assert len(_all_locales) > 0
5153

5254

@@ -58,9 +60,9 @@ def test_get_locales_prefix():
5860

5961
@_skip_if_only_one_locale
6062
def test_set_locale():
61-
if com._all_none(_current_locale):
63+
if all(x is None for x in _current_locale):
6264
# Not sure why, but on some Travis runs with pytest,
63-
# getlocale() returned (None, None).
65+
# getlocale() returned (None, None).
6466
pytest.skip("Current locale is not set.")
6567

6668
locale_override = os.environ.get("LOCALE_OVERRIDE", None)
@@ -75,14 +77,14 @@ def test_set_locale():
7577
enc = codecs.lookup(enc).name
7678
new_locale = lang, enc
7779

78-
if not tm.can_set_locale(new_locale):
80+
if not can_set_locale(new_locale):
7981
msg = "unsupported locale setting"
8082

8183
with pytest.raises(locale.Error, match=msg):
82-
with tm.set_locale(new_locale):
84+
with set_locale(new_locale):
8385
pass
8486
else:
85-
with tm.set_locale(new_locale) as normalized_locale:
87+
with set_locale(new_locale) as normalized_locale:
8688
new_lang, new_enc = normalized_locale.split(".")
8789
new_enc = codecs.lookup(enc).name
8890

pandas/util/testing.py

+5-87
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from contextlib import contextmanager
55
from datetime import datetime
66
from functools import wraps
7-
import locale
87
import os
98
import re
109
from shutil import rmtree
@@ -18,11 +17,14 @@
1817
import numpy as np
1918
from numpy.random import rand, randn
2019

20+
from pandas._config.localization import ( # noqa:F401
21+
_valid_locales, can_set_locale, set_locale)
22+
2123
from pandas._libs import testing as _testing
2224
import pandas.compat as compat
2325
from pandas.compat import (
24-
PY2, PY3, filter, httplib, lmap, lrange, lzip, map, raise_with_traceback,
25-
range, string_types, u, unichr, zip)
26+
PY2, PY3, httplib, lmap, lrange, lzip, map, raise_with_traceback, range,
27+
string_types, u, unichr, zip)
2628

2729
from pandas.core.dtypes.common import (
2830
is_bool, is_categorical_dtype, is_datetime64_dtype, is_datetime64tz_dtype,
@@ -39,7 +41,6 @@
3941
from pandas.core.arrays import (
4042
DatetimeArray, ExtensionArray, IntervalArray, PeriodArray, TimedeltaArray,
4143
period_array)
42-
import pandas.core.common as com
4344

4445
from pandas.io.common import urlopen
4546
from pandas.io.formats.printing import pprint_thing
@@ -494,89 +495,6 @@ def get_locales(prefix=None, normalize=True,
494495
return _valid_locales(found, normalize)
495496

496497

497-
@contextmanager
498-
def set_locale(new_locale, lc_var=locale.LC_ALL):
499-
"""Context manager for temporarily setting a locale.
500-
501-
Parameters
502-
----------
503-
new_locale : str or tuple
504-
A string of the form <language_country>.<encoding>. For example to set
505-
the current locale to US English with a UTF8 encoding, you would pass
506-
"en_US.UTF-8".
507-
lc_var : int, default `locale.LC_ALL`
508-
The category of the locale being set.
509-
510-
Notes
511-
-----
512-
This is useful when you want to run a particular block of code under a
513-
particular locale, without globally setting the locale. This probably isn't
514-
thread-safe.
515-
"""
516-
current_locale = locale.getlocale()
517-
518-
try:
519-
locale.setlocale(lc_var, new_locale)
520-
normalized_locale = locale.getlocale()
521-
if com._all_not_none(*normalized_locale):
522-
yield '.'.join(normalized_locale)
523-
else:
524-
yield new_locale
525-
finally:
526-
locale.setlocale(lc_var, current_locale)
527-
528-
529-
def can_set_locale(lc, lc_var=locale.LC_ALL):
530-
"""
531-
Check to see if we can set a locale, and subsequently get the locale,
532-
without raising an Exception.
533-
534-
Parameters
535-
----------
536-
lc : str
537-
The locale to attempt to set.
538-
lc_var : int, default `locale.LC_ALL`
539-
The category of the locale being set.
540-
541-
Returns
542-
-------
543-
is_valid : bool
544-
Whether the passed locale can be set
545-
"""
546-
547-
try:
548-
with set_locale(lc, lc_var=lc_var):
549-
pass
550-
except (ValueError,
551-
locale.Error): # horrible name for a Exception subclass
552-
return False
553-
else:
554-
return True
555-
556-
557-
def _valid_locales(locales, normalize):
558-
"""Return a list of normalized locales that do not throw an ``Exception``
559-
when set.
560-
561-
Parameters
562-
----------
563-
locales : str
564-
A string where each locale is separated by a newline.
565-
normalize : bool
566-
Whether to call ``locale.normalize`` on each locale.
567-
568-
Returns
569-
-------
570-
valid_locales : list
571-
A list of valid locales.
572-
"""
573-
if normalize:
574-
normalizer = lambda x: locale.normalize(x.strip())
575-
else:
576-
normalizer = lambda x: x.strip()
577-
578-
return list(filter(can_set_locale, map(normalizer, locales)))
579-
580498
# -----------------------------------------------------------------------------
581499
# Stdout / stderr decorators
582500

setup.cfg

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ directory = coverage_html_report
101101

102102
# To be kept consistent with "Import Formatting" section in contributing.rst
103103
[isort]
104+
known_pre_libs=pandas._config
104105
known_pre_core=pandas._libs,pandas.util._*,pandas.compat,pandas.errors
105106
known_dtypes=pandas.core.dtypes
106107
known_post_core=pandas.tseries,pandas.io,pandas.plotting
107-
sections=FUTURE,STDLIB,THIRDPARTY,PRE_CORE,DTYPES,FIRSTPARTY,POST_CORE,LOCALFOLDER
108+
sections=FUTURE,STDLIB,THIRDPARTY,PRE_LIBS,PRE_CORE,DTYPES,FIRSTPARTY,POST_CORE,LOCALFOLDER
108109

109110
known_first_party=pandas
110111
known_third_party=Cython,numpy,dateutil,matplotlib,python-dateutil,pytz,pyarrow,pytest

0 commit comments

Comments
 (0)