Skip to content

Move locale code out of tm, into _config #25757

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 6 commits into from
Mar 20, 2019
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
4 changes: 4 additions & 0 deletions pandas/_config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
pandas._config is considered explicitly upstream of everything else in pandas,
should have no intra-pandas dependencies.
"""
93 changes: 93 additions & 0 deletions pandas/_config/localization.py
Original file line number Diff line number Diff line change
@@ -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 <language_country>.<encoding>. 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)))
7 changes: 3 additions & 4 deletions pandas/_libs/tslibs/ccalendar.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ----------------------------------------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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)
Empty file added pandas/tests/config/__init__.py
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand All @@ -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


Expand All @@ -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)
Expand All @@ -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

Expand Down
92 changes: 5 additions & 87 deletions pandas/util/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,11 +17,14 @@
import numpy as np
from numpy.random import rand, randn

from pandas._config.localization import ( # noqa:F401
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the noqa for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of the imports are not used in this file, but are retained so that we can still use them as tm.foo

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should try to completely remove these from testing at some point

_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,
Expand All @@ -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
Expand Down Expand Up @@ -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 <language_country>.<encoding>. 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

Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down