diff --git a/pandas/_config/__init__.py b/pandas/_config/__init__.py new file mode 100644 index 0000000000000..52150f526350d --- /dev/null +++ b/pandas/_config/__init__.py @@ -0,0 +1,4 @@ +""" +pandas._config is considered explicitly upstream of everything else in pandas, +should have no intra-pandas dependencies. +""" diff --git a/pandas/_config/localization.py b/pandas/_config/localization.py new file mode 100644 index 0000000000000..a5f348241e4d7 --- /dev/null +++ b/pandas/_config/localization.py @@ -0,0 +1,93 @@ +""" +Helpers for configuring locale settings. + +Name `localization` is chosen to avoid overlap with builtin `locale` module. +""" +from contextlib import contextmanager +import locale + + +@contextmanager +def set_locale(new_locale, lc_var=locale.LC_ALL): + """ + Context manager for temporarily setting a locale. + + Parameters + ---------- + new_locale : str or tuple + A string of the form .. For example to set + the current locale to US English with a UTF8 encoding, you would pass + "en_US.UTF-8". + lc_var : int, default `locale.LC_ALL` + The category of the locale being set. + + Notes + ----- + This is useful when you want to run a particular block of code under a + particular locale, without globally setting the locale. This probably isn't + thread-safe. + """ + current_locale = locale.getlocale() + + try: + locale.setlocale(lc_var, new_locale) + normalized_locale = locale.getlocale() + if all(x is not None for x in normalized_locale): + yield '.'.join(normalized_locale) + else: + yield new_locale + finally: + locale.setlocale(lc_var, current_locale) + + +def can_set_locale(lc, lc_var=locale.LC_ALL): + """ + Check to see if we can set a locale, and subsequently get the locale, + without raising an Exception. + + Parameters + ---------- + lc : str + The locale to attempt to set. + lc_var : int, default `locale.LC_ALL` + The category of the locale being set. + + Returns + ------- + is_valid : bool + Whether the passed locale can be set + """ + + try: + with set_locale(lc, lc_var=lc_var): + pass + except (ValueError, locale.Error): + # horrible name for a Exception subclass + return False + else: + return True + + +def _valid_locales(locales, normalize): + """ + Return a list of normalized locales that do not throw an ``Exception`` + when set. + + Parameters + ---------- + locales : str + A string where each locale is separated by a newline. + normalize : bool + Whether to call ``locale.normalize`` on each locale. + + Returns + ------- + valid_locales : list + A list of valid locales. + """ + if normalize: + normalizer = lambda x: locale.normalize(x.strip()) + else: + normalizer = lambda x: x.strip() + + return list(filter(can_set_locale, map(normalizer, locales))) diff --git a/pandas/_libs/tslibs/ccalendar.pyx b/pandas/_libs/tslibs/ccalendar.pyx index c48812acd3de1..1de3169fd2b63 100644 --- a/pandas/_libs/tslibs/ccalendar.pyx +++ b/pandas/_libs/tslibs/ccalendar.pyx @@ -9,6 +9,8 @@ import cython from numpy cimport int64_t, int32_t from locale import LC_TIME + +from pandas._config.localization import set_locale from pandas._libs.tslibs.strptime import LocaleTime # ---------------------------------------------------------------------- @@ -206,7 +208,7 @@ cpdef int32_t get_day_of_year(int year, int month, int day) nogil: return day_of_year -cpdef get_locale_names(object name_type, object locale=None): +def get_locale_names(name_type: object, locale: object=None): """Returns an array of localized day or month names Parameters @@ -218,9 +220,6 @@ cpdef get_locale_names(object name_type, object locale=None): Returns ------- list of locale names - """ - from pandas.util.testing import set_locale - with set_locale(locale, LC_TIME): return getattr(LocaleTime(), name_type) diff --git a/pandas/tests/config/__init__.py b/pandas/tests/config/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/pandas/tests/util/test_locale.py b/pandas/tests/config/test_localization.py similarity index 76% rename from pandas/tests/util/test_locale.py rename to pandas/tests/config/test_localization.py index b848b22994e7a..fa7b46da4af92 100644 --- a/pandas/tests/util/test_locale.py +++ b/pandas/tests/config/test_localization.py @@ -5,9 +5,12 @@ import pytest +from pandas._config.localization import can_set_locale, set_locale + from pandas.compat import is_platform_windows -import pandas.core.common as com +# TODO: move get_locales into localization, making `tm` import unnecessary. +# This is blocked by the need for core.config to be moved to _config. import pandas.util.testing as tm _all_locales = tm.get_locales() or [] @@ -23,30 +26,29 @@ def test_can_set_locale_valid_set(): # Can set the default locale. - assert tm.can_set_locale("") + assert can_set_locale("") def test_can_set_locale_invalid_set(): # Cannot set an invalid locale. - assert not tm.can_set_locale("non-existent_locale") + assert not can_set_locale("non-existent_locale") def test_can_set_locale_invalid_get(monkeypatch): - # see gh-22129 - # + # see GH#22129 # In some cases, an invalid locale can be set, - # but a subsequent getlocale() raises a ValueError. + # but a subsequent getlocale() raises a ValueError. def mock_get_locale(): raise ValueError() with monkeypatch.context() as m: m.setattr(locale, "getlocale", mock_get_locale) - assert not tm.can_set_locale("") + assert not can_set_locale("") def test_get_locales_at_least_one(): - # see gh-9744 + # see GH#9744 assert len(_all_locales) > 0 @@ -58,9 +60,9 @@ def test_get_locales_prefix(): @_skip_if_only_one_locale def test_set_locale(): - if com._all_none(_current_locale): + if all(x is None for x in _current_locale): # Not sure why, but on some Travis runs with pytest, - # getlocale() returned (None, None). + # getlocale() returned (None, None). pytest.skip("Current locale is not set.") locale_override = os.environ.get("LOCALE_OVERRIDE", None) @@ -75,14 +77,14 @@ def test_set_locale(): enc = codecs.lookup(enc).name new_locale = lang, enc - if not tm.can_set_locale(new_locale): + if not can_set_locale(new_locale): msg = "unsupported locale setting" with pytest.raises(locale.Error, match=msg): - with tm.set_locale(new_locale): + with set_locale(new_locale): pass else: - with tm.set_locale(new_locale) as normalized_locale: + with set_locale(new_locale) as normalized_locale: new_lang, new_enc = normalized_locale.split(".") new_enc = codecs.lookup(enc).name diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 8c5852316068a..dc120856b4a9a 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -4,7 +4,6 @@ from contextlib import contextmanager from datetime import datetime from functools import wraps -import locale import os import re from shutil import rmtree @@ -18,11 +17,14 @@ import numpy as np from numpy.random import rand, randn +from pandas._config.localization import ( # noqa:F401 + _valid_locales, can_set_locale, set_locale) + from pandas._libs import testing as _testing import pandas.compat as compat from pandas.compat import ( - PY2, PY3, filter, httplib, lmap, lrange, lzip, map, raise_with_traceback, - range, string_types, u, unichr, zip) + PY2, PY3, httplib, lmap, lrange, lzip, map, raise_with_traceback, range, + string_types, u, unichr, zip) from pandas.core.dtypes.common import ( is_bool, is_categorical_dtype, is_datetime64_dtype, is_datetime64tz_dtype, @@ -39,7 +41,6 @@ from pandas.core.arrays import ( DatetimeArray, ExtensionArray, IntervalArray, PeriodArray, TimedeltaArray, period_array) -import pandas.core.common as com from pandas.io.common import urlopen from pandas.io.formats.printing import pprint_thing @@ -494,89 +495,6 @@ def get_locales(prefix=None, normalize=True, return _valid_locales(found, normalize) -@contextmanager -def set_locale(new_locale, lc_var=locale.LC_ALL): - """Context manager for temporarily setting a locale. - - Parameters - ---------- - new_locale : str or tuple - A string of the form .. For example to set - the current locale to US English with a UTF8 encoding, you would pass - "en_US.UTF-8". - lc_var : int, default `locale.LC_ALL` - The category of the locale being set. - - Notes - ----- - This is useful when you want to run a particular block of code under a - particular locale, without globally setting the locale. This probably isn't - thread-safe. - """ - current_locale = locale.getlocale() - - try: - locale.setlocale(lc_var, new_locale) - normalized_locale = locale.getlocale() - if com._all_not_none(*normalized_locale): - yield '.'.join(normalized_locale) - else: - yield new_locale - finally: - locale.setlocale(lc_var, current_locale) - - -def can_set_locale(lc, lc_var=locale.LC_ALL): - """ - Check to see if we can set a locale, and subsequently get the locale, - without raising an Exception. - - Parameters - ---------- - lc : str - The locale to attempt to set. - lc_var : int, default `locale.LC_ALL` - The category of the locale being set. - - Returns - ------- - is_valid : bool - Whether the passed locale can be set - """ - - try: - with set_locale(lc, lc_var=lc_var): - pass - except (ValueError, - locale.Error): # horrible name for a Exception subclass - return False - else: - return True - - -def _valid_locales(locales, normalize): - """Return a list of normalized locales that do not throw an ``Exception`` - when set. - - Parameters - ---------- - locales : str - A string where each locale is separated by a newline. - normalize : bool - Whether to call ``locale.normalize`` on each locale. - - Returns - ------- - valid_locales : list - A list of valid locales. - """ - if normalize: - normalizer = lambda x: locale.normalize(x.strip()) - else: - normalizer = lambda x: x.strip() - - return list(filter(can_set_locale, map(normalizer, locales))) - # ----------------------------------------------------------------------------- # Stdout / stderr decorators diff --git a/setup.cfg b/setup.cfg index 84b8f69a83f16..54593009ac1ee 100644 --- a/setup.cfg +++ b/setup.cfg @@ -101,10 +101,11 @@ directory = coverage_html_report # To be kept consistent with "Import Formatting" section in contributing.rst [isort] +known_pre_libs=pandas._config known_pre_core=pandas._libs,pandas.util._*,pandas.compat,pandas.errors known_dtypes=pandas.core.dtypes known_post_core=pandas.tseries,pandas.io,pandas.plotting -sections=FUTURE,STDLIB,THIRDPARTY,PRE_CORE,DTYPES,FIRSTPARTY,POST_CORE,LOCALFOLDER +sections=FUTURE,STDLIB,THIRDPARTY,PRE_LIBS,PRE_CORE,DTYPES,FIRSTPARTY,POST_CORE,LOCALFOLDER known_first_party=pandas known_third_party=Cython,numpy,dateutil,matplotlib,python-dateutil,pytz,pyarrow,pytest