Repository: fonttools/fonttools

Issue:
Importing a TTFont from XML fails when LC_TIME is set
Importing a font from XML while LC_TIME locale is set to non-English, causes an error.

### How to reproduce?

This might be easy when a non-English locale is available in the system. I came across this, while using a package on top. The corresponding issue in their package is amueller/word_cloud#530. There is a script to reproduce, which only throws an error, when a non-English locale like 'de_DE' is set with e. g. `locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8')` or just by opening Spyder-IDE.

**A simplified test is:**
```python
import locale
locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8') # works if de_DE is available

from fontTools.misc.timeTools import timestampFromString,timestampToString,timestampNow
ts_now = timestampNow()
str_now = timestampToString(ts_now)
timestampFromString(str_now) # ValueError
```

Let's go into the cause of the error.

### Basics

The locale for LC_TIME can be checked with
```python
import locale
print(locale.getlocale(locale.LC_TIME))
```
This outputs `('de_DE', 'UTF-8')` in my case.

With this locale the following fails:
```python
import time
time.strptime('Mon', '%a')
# ValueError: unconverted data remains: n
```
`'Mo'` is the localized abbreviation in de_DE for Monday.

### TTFont

The method [`importXML`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/ttLib/ttFont.py#L318) in `TTFont` receives the font object as XML. This can contain created and modified dates. The XML is parsed by the `XMLReader`, which somehow uses the [`fromXML`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/ttLib/tables/_h_e_a_d.py#L107) method in `table__h_e_a_d`. There the created and modified dates are parsed using [`timestampFromString`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/misc/timeTools.py#L46) from timeTools. This helper function uses `time.strptime(value)`.

In my test case `value` is initialized from the 'created' attribute of a font as `'Mon Jan 8 12:28:04 2007'`, which throws the following error:
```
ValueError: time data 'Mon Jan  8 12:28:04 2007' does not match format '%a %b %d %H:%M:%S %Y'
```


### How to resolve?

I think the parsing should be done without locale, since the XML attribute is likely to be non-local. In the opposite function [`timestampToString`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/misc/timeTools.py#L43) `asctime` is used, which uses a fixed list of abbreviated week days and months. So that is not localized. Hence [`timestampFromString`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/misc/timeTools.py#L46) shouldn't be localized as well.

A simple solution could be
```python
def timestampFromString(value):
	import locale
	l = locale.getlocale(locale.LC_TIME)
	locale.setlocale(locale.LC_TIME, 'C')
	try:
		t = time.strptime(value)
	finally:
		locale.setlocale(locale.LC_TIME, l)
	return calendar.timegm(t) - epoch_diff
```

However, changing the locale is not recommended. It's better to use a function that can parse a date with specified locale without changing it. You could use [dateparser](https://dateparser.readthedocs.io/en/latest/) for example, but I don't know about your dependencies and how you handle it.


Discussion:
Maybe the stdlib datetime module has a solution? 
As far as I can see, it doesn't. However, since the format is fixed and you have [`DAYNAMES`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/misc/timeTools.py#L12) and [`MONTHNAMES`](https://github.com/fonttools/fonttools/blob/master/Lib/fontTools/misc/timeTools.py#L13), it might be easy to parse it without a date library. Or maybe parse only weekday (or skip) and month and parse the remaining part with `time.strptime`.
Right. I think your suggestion to parse it without a date library is a good one.

Resolving patch:
diff --git a/Lib/fontTools/misc/timeTools.py b/Lib/fontTools/misc/timeTools.py
--- a/Lib/fontTools/misc/timeTools.py
+++ b/Lib/fontTools/misc/timeTools.py
@@ -4,6 +4,7 @@
 from fontTools.misc.py23 import *
 import os
 import time
+from datetime import datetime, timezone
 import calendar
 
 
@@ -44,7 +45,12 @@ def timestampToString(value):
 	return asctime(time.gmtime(max(0, value + epoch_diff)))
 
 def timestampFromString(value):
-	return calendar.timegm(time.strptime(value)) - epoch_diff
+	wkday, mnth = value[:7].split()
+	t = datetime.strptime(value[7:], ' %d %H:%M:%S %Y')
+	t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc)
+	wkday_idx = DAYNAMES.index(wkday)
+	assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday'
+	return int(t.timestamp()) - epoch_diff
 
 def timestampNow():
 	# https://reproducible-builds.org/specs/source-date-epoch/


Test patch:
diff --git a/Tests/misc/timeTools_test.py b/Tests/misc/timeTools_test.py
--- a/Tests/misc/timeTools_test.py
+++ b/Tests/misc/timeTools_test.py
@@ -1,7 +1,8 @@
 from fontTools.misc.py23 import *
-from fontTools.misc.timeTools import asctime, timestampNow, epoch_diff
+from fontTools.misc.timeTools import asctime, timestampNow, timestampToString, timestampFromString, epoch_diff
 import os
 import time
+import locale
 import pytest
 
 
@@ -21,3 +22,17 @@ def test_source_date_epoch():
 
     del os.environ["SOURCE_DATE_EPOCH"]
     assert timestampNow() + epoch_diff != 150687315
+
+
+# test for issue #1838
+def test_date_parsing_with_locale():
+    l = locale.getlocale(locale.LC_TIME)
+    try:
+        locale.setlocale(locale.LC_TIME, 'de_DE.utf8')
+    except locale.Error:
+        pytest.skip("Locale de_DE not available")
+
+    try:
+        assert timestampFromString(timestampToString(timestampNow()))
+    finally:
+        locale.setlocale(locale.LC_TIME, l)