Repository: urwid/urwid (language: python)

Issue:
Importing urwid breaks strptime() because of locale
##### Description:
Consider following snippet:
```python
import datetime
import locale
import os

print("Lang: "+os.getenv("LANG"))
print("LC_ALL: "+os.getenv("LC_ALL", "None"))

try:
    print(locale.getlocale(locale.LC_ALL))
except BaseException as exc:
    print("First exception: %s" % exc)

from urwid import util

try:
    print(locale.getlocale(locale.LC_ALL))
except BaseException as exc:
    print("Second exception: %s" % exc)

val = 'Fri, 07 Aug 2020 18:15:28 GMT'
expires = datetime.datetime.strptime(val, "%a, %d %b %Y %H:%M:%S GMT")
print("worked")
```

I have following output:
```
# /usr/bin/python3.8 demo.py
Lang: ru_RU.UTF-8
LC_ALL: None
First exception: category LC_ALL is not supported
('ru_RU', 'UTF-8')
Traceback (most recent call last):
  File "demo.py", line 21, in <module>
    expires = datetime.datetime.strptime(val, "%a, %d %b %Y %H:%M:%S GMT")
  File "/usr/lib/python3.8/_strptime.py", line 568, in _strptime_datetime
    tt, fraction, gmtoff_fraction = _strptime(data_string, format)
  File "/usr/lib/python3.8/_strptime.py", line 349, in _strptime
    raise ValueError("time data %r does not match format %r" %
ValueError: time data 'Fri, 07 Aug 2020 18:15:28 GMT' does not match format '%a, %d %b %Y %H:%M:%S GMT'
```

##### Affected versions (if applicable)
- [ ] `master` branch (specify commit)
- [x] Latest stable version from `pypi`
- [ ] Other (specify source)

##### Steps to reproduce (if applicable)
Just run the provided snippet with corresponding locale setup

##### Expected/actual outcome
If I comment out import of urwid util, it starts to work as expected.

From my investigation, this happens because urwid sets locale back for LC_ALL, while it was not set beforehand. It reads from `initial = locale.getlocale()` with no LC_ALL, but then sets to LC_ALL with `locale.setlocale(locale.LC_ALL, initial)`, which creates global side effect.




Discussion / hints:
Output of my `locale`:
```
LANG=ru_RU.UTF-8
LANGUAGE=en_US:en
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC=C
LC_TIME="ru_RU.UTF-8"
LC_COLLATE="ru_RU.UTF-8"
LC_MONETARY="ru_RU.UTF-8"
LC_MESSAGES="ru_RU.UTF-8"
LC_PAPER="ru_RU.UTF-8"
LC_NAME="ru_RU.UTF-8"
LC_ADDRESS="ru_RU.UTF-8"
LC_TELEPHONE="ru_RU.UTF-8"
LC_MEASUREMENT="ru_RU.UTF-8"
LC_IDENTIFICATION="ru_RU.UTF-8"
LC_ALL=
```
I'm encountering this as well, apparently it's caused by urwid's encoding detection messing with locales. Is there any reason urwid does not just call locale.getpreferredencoding()?


Fix patch:
diff --git a/urwid/util.py b/urwid/util.py
index 3ac465797..9def76222 100644
--- a/urwid/util.py
+++ b/urwid/util.py
@@ -37,28 +37,33 @@
 within_double_byte = str_util.within_double_byte
 
 
-def detect_encoding():
+def detect_encoding() -> str:
     # Try to determine if using a supported double-byte encoding
     import locale
-    initial = locale.getlocale()
+
+    no_set_locale = locale.getpreferredencoding(False)
+
+    if no_set_locale != "ascii":
+        # ascii is fallback locale in case of detect failed
+
+        return no_set_locale
+
+    # Use actual `getpreferredencoding` with public API only
+    old_loc = locale.setlocale(locale.LC_CTYPE)  # == getlocale, but not mangle data
     try:
         try:
-            locale.setlocale(locale.LC_ALL, "")
+            locale.setlocale(locale.LC_CTYPE, "")
         except locale.Error:
             pass
-        return locale.getlocale()[1] or ""
-    except ValueError as e:
-        # with invalid LANG value python will throw ValueError
-        if e.args and e.args[0].startswith("unknown locale"):
-            return ""
-        else:
-            raise
+        # internally call private `_get_locale_encoding`
+        return locale.getpreferredencoding(False)
     finally:
         try:
-            locale.setlocale(locale.LC_ALL, initial)
+            locale.setlocale(locale.LC_CTYPE, old_loc)
         except locale.Error:
             pass
 
+
 if 'detected_encoding' not in locals():
     detected_encoding = detect_encoding()
 else:


Test patch:
diff --git a/urwid/tests/test_util.py b/urwid/tests/test_util.py
index abe7aa5e7..eeae2f902 100644
--- a/urwid/tests/test_util.py
+++ b/urwid/tests/test_util.py
@@ -193,6 +193,7 @@ def test_rle_append(self):
         self.assertListEqual(rle3, [('A', 10), ('B', 20)])
         self.assertListEqual(rle4, [('A', 10), ('B', 15), ('K', 1)])
 
+
 class PortabilityTest(unittest.TestCase):
     def test_locale(self):
         initial = locale.getlocale()
@@ -201,8 +202,28 @@ def test_locale(self):
         util.detect_encoding()
         self.assertEqual(locale.getlocale(), (None, None))
 
-        locale.setlocale(locale.LC_ALL, ('en_US', 'UTF-8'))
+        try:
+            locale.setlocale(locale.LC_ALL, ('en_US', 'UTF-8'))
+        except locale.Error as exc:
+            if "unsupported locale setting" not in str(exc):
+                raise
+
+            print(
+                f"Locale change impossible, probably locale not supported by system (libc ignores this error).\n"
+                f"{exc}"
+            )
+            return
+
         util.detect_encoding()
         self.assertEqual(locale.getlocale(), ('en_US', 'UTF-8'))
 
-        locale.setlocale(locale.LC_ALL, initial)
+        try:
+            locale.setlocale(locale.LC_ALL, initial)
+        except locale.Error as exc:
+            if "unsupported locale setting" not in str(exc):
+                raise
+
+            print(
+                f"Locale restore impossible, probably locale not supported by system (libc ignores this error).\n"
+                f"{exc}"
+            )