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 locale
11import platform
12import re
13import subprocess
14from typing import (
15 TYPE_CHECKING,
16 cast,
17)
18
19from pandas._config.config import options
20
21if TYPE_CHECKING:
22 from collections.abc import Generator
23
24
25@contextmanager
26def set_locale(
27 new_locale: str | tuple[str, str], lc_var: int = locale.LC_ALL
28) -> Generator[str | tuple[str, str]]:
29 """
30 Context manager for temporarily setting a locale.
31
32 Parameters
33 ----------
34 new_locale : str or tuple
35 A string of the form <language_country>.<encoding>. For example to set
36 the current locale to US English with a UTF8 encoding, you would pass
37 "en_US.UTF-8".
38 lc_var : int, default `locale.LC_ALL`
39 The category of the locale being set.
40
41 Notes
42 -----
43 This is useful when you want to run a particular block of code under a
44 particular locale, without globally setting the locale. This probably isn't
45 thread-safe.
46 """
47 # getlocale is not always compliant with setlocale, use setlocale. GH#46595
48 current_locale = locale.setlocale(lc_var)
49
50 try:
51 locale.setlocale(lc_var, new_locale)
52 normalized_code, normalized_encoding = locale.getlocale()
53 if normalized_code is not None and normalized_encoding is not None:
54 yield f"{normalized_code}.{normalized_encoding}"
55 else:
56 yield new_locale
57 finally:
58 locale.setlocale(lc_var, current_locale)
59
60
61def can_set_locale(lc: str, lc_var: int = locale.LC_ALL) -> bool:
62 """
63 Check to see if we can set a locale, and subsequently get the locale,
64 without raising an Exception.
65
66 Parameters
67 ----------
68 lc : str
69 The locale to attempt to set.
70 lc_var : int, default `locale.LC_ALL`
71 The category of the locale being set.
72
73 Returns
74 -------
75 bool
76 Whether the passed locale can be set
77 """
78 try:
79 with set_locale(lc, lc_var=lc_var):
80 pass
81 except (ValueError, locale.Error):
82 # horrible name for an Exception subclass
83 return False
84 else:
85 return True
86
87
88def _valid_locales(locales: list[str] | str, normalize: bool) -> list[str]:
89 """
90 Return a list of normalized locales that do not throw an ``Exception``
91 when set.
92
93 Parameters
94 ----------
95 locales : str
96 A string where each locale is separated by a newline.
97 normalize : bool
98 Whether to call ``locale.normalize`` on each locale.
99
100 Returns
101 -------
102 valid_locales : list
103 A list of valid locales.
104 """
105 return [
106 loc
107 for loc in (
108 locale.normalize(loc.strip()) if normalize else loc.strip()
109 for loc in locales
110 )
111 if can_set_locale(loc)
112 ]
113
114
115def get_locales(
116 prefix: str | None = None,
117 normalize: bool = True,
118) -> list[str]:
119 """
120 Get all the locales that are available on the system.
121
122 Parameters
123 ----------
124 prefix : str
125 If not ``None`` then return only those locales with the prefix
126 provided. For example to get all English language locales (those that
127 start with ``"en"``), pass ``prefix="en"``.
128 normalize : bool
129 Call ``locale.normalize`` on the resulting list of available locales.
130 If ``True``, only locales that can be set without throwing an
131 ``Exception`` are returned.
132
133 Returns
134 -------
135 locales : list of strings
136 A list of locale strings that can be set with ``locale.setlocale()``.
137 For example::
138
139 locale.setlocale(locale.LC_ALL, locale_string)
140
141 On error will return an empty list (no locale available, e.g. Windows)
142
143 """
144 if platform.system() in ("Linux", "Darwin"):
145 raw_locales = subprocess.check_output(["locale", "-a"])
146 else:
147 # Other platforms e.g. windows platforms don't define "locale -a"
148 # Note: is_platform_windows causes circular import here
149 return []
150
151 try:
152 # raw_locales is "\n" separated list of locales
153 # it may contain non-decodable parts, so split
154 # extract what we can and then rejoin.
155 split_raw_locales = raw_locales.split(b"\n")
156 out_locales = []
157 for x in split_raw_locales:
158 try:
159 out_locales.append(str(x, encoding=cast(str, options.display.encoding)))
160 except UnicodeError:
161 # 'locale -a' is used to populated 'raw_locales' and on
162 # Redhat 7 Linux (and maybe others) prints locale names
163 # using windows-1252 encoding. Bug only triggered by
164 # a few special characters and when there is an
165 # extensive list of installed locales.
166 out_locales.append(str(x, encoding="windows-1252"))
167
168 except TypeError:
169 pass
170
171 if prefix is None:
172 return _valid_locales(out_locales, normalize)
173
174 pattern = re.compile(f"{prefix}.*")
175 found = pattern.findall("\n".join(out_locales))
176 return _valid_locales(found, normalize)