1"""
2Helpers for configuring locale settings.
3
4Name `localization` is chosen to avoid overlap with builtin `locale` module.
5"""
6
7from __future__ import annotations
8
9from contextlib import contextmanager
10import functools
11import locale
12import platform
13import re
14import subprocess
15from typing import (
16 TYPE_CHECKING,
17 cast,
18)
19
20from pandas._config.config import options
21
22if TYPE_CHECKING:
23 from collections.abc import Generator
24
25
26@contextmanager
27def set_locale(
28 new_locale: str | tuple[str, str], lc_var: int = locale.LC_ALL
29) -> Generator[str | tuple[str, str]]:
30 """
31 Context manager for temporarily setting a locale.
32
33 Parameters
34 ----------
35 new_locale : str or tuple
36 A string of the form <language_country>.<encoding>. For example to set
37 the current locale to US English with a UTF8 encoding, you would pass
38 "en_US.UTF-8".
39 lc_var : int, default `locale.LC_ALL`
40 The category of the locale being set.
41
42 Notes
43 -----
44 This is useful when you want to run a particular block of code under a
45 particular locale, without globally setting the locale. This probably isn't
46 thread-safe.
47 """
48 # getlocale is not always compliant with setlocale, use setlocale. GH#46595
49 current_locale = locale.setlocale(lc_var)
50
51 try:
52 locale.setlocale(lc_var, new_locale)
53 normalized_code, normalized_encoding = locale.getlocale()
54 if normalized_code is not None and normalized_encoding is not None:
55 yield f"{normalized_code}.{normalized_encoding}"
56 else:
57 yield new_locale
58 finally:
59 locale.setlocale(lc_var, current_locale)
60
61
62def can_set_locale(lc: str, lc_var: int = locale.LC_ALL) -> bool:
63 """
64 Check to see if we can set a locale, and subsequently get the locale,
65 without raising an Exception.
66
67 Parameters
68 ----------
69 lc : str
70 The locale to attempt to set.
71 lc_var : int, default `locale.LC_ALL`
72 The category of the locale being set.
73
74 Returns
75 -------
76 bool
77 Whether the passed locale can be set
78 """
79 try:
80 with set_locale(lc, lc_var=lc_var):
81 pass
82 except (ValueError, locale.Error):
83 # horrible name for an Exception subclass
84 return False
85 else:
86 return True
87
88
89def _valid_locales(locales: list[str] | str, normalize: bool) -> list[str]:
90 """
91 Return a list of normalized locales that do not throw an ``Exception``
92 when set.
93
94 Parameters
95 ----------
96 locales : str
97 A string where each locale is separated by a newline.
98 normalize : bool
99 Whether to call ``locale.normalize`` on each locale.
100
101 Returns
102 -------
103 valid_locales : list
104 A list of valid locales.
105 """
106 return [
107 loc
108 for loc in (
109 locale.normalize(loc.strip()) if normalize else loc.strip()
110 for loc in locales
111 )
112 if can_set_locale(loc)
113 ]
114
115
116# Windows has no "locale -a" to enumerate with, so we hardcode a few values.
117# GH#46597
118_WINDOWS_LOCALES = [
119 "de-DE.UTF-8", # Latin with diacritics
120 "el-GR.UTF-8", # Greek, where capitalize() has to handle final sigma
121 "en-US.UTF-8", # ASCII baseline
122 "ja-JP.UTF-8", # caseless script, capitalize() is a no-op
123 "tr-TR.UTF-8", # dotted and dotless i
124]
125
126
127@functools.cache
128def _get_locales(prefix: str | None, normalize: bool) -> tuple[str, ...]:
129 if platform.system() in ("Linux", "Darwin"):
130 raw_locales = subprocess.check_output(["locale", "-a"])
131 # raw_locales is "\n" separated list of locales
132 # it may contain non-decodable parts, so split
133 # extract what we can and then rejoin.
134 out_locales = []
135 for raw_locale in raw_locales.split(b"\n"):
136 try:
137 out_locales.append(
138 str(raw_locale, encoding=cast("str", options.display.encoding))
139 )
140 except UnicodeError:
141 # 'locale -a' is used to populated 'raw_locales' and on
142 # Redhat 7 Linux (and maybe others) prints locale names
143 # using windows-1252 encoding. Bug only triggered by
144 # a few special characters and when there is an
145 # extensive list of installed locales.
146 out_locales.append(str(raw_locale, encoding="windows-1252"))
147 elif platform.system() == "Windows":
148 # Note: is_platform_windows causes circular import here
149 out_locales = _WINDOWS_LOCALES
150 # These are already spelled the way the CRT wants them; running them
151 # through the POSIX alias table would rename them ("ar-SA" -> "ar_AA").
152 normalize = False
153 else:
154 return ()
155
156 if prefix is None:
157 return tuple(_valid_locales(out_locales, normalize))
158
159 pattern = re.compile(f"{prefix}.*")
160 found = pattern.findall("\n".join(out_locales))
161 return tuple(_valid_locales(found, normalize))
162
163
164def get_locales(
165 prefix: str | None = None,
166 normalize: bool = True,
167) -> list[str]:
168 """
169 Get all the locales that are available on the system.
170
171 Parameters
172 ----------
173 prefix : str
174 If not ``None`` then return only those locales with the prefix
175 provided. For example to get all English language locales (those that
176 start with ``"en"``), pass ``prefix="en"``.
177 normalize : bool
178 Call ``locale.normalize`` on the resulting list of available locales.
179 If ``True``, only locales that can be set without throwing an
180 ``Exception`` are returned.
181
182 Returns
183 -------
184 locales : list of strings
185 A list of locale strings that can be set with ``locale.setlocale()``.
186 For example::
187
188 locale.setlocale(locale.LC_ALL, locale_string)
189
190 On error will return an empty list (no locale available)
191
192 """
193 # Every candidate is probed with setlocale, and several test modules call
194 # this at collection time, so the result is cached.
195 return list(_get_locales(prefix, normalize))