Repository: python-babel/babel (python)

Issue:
`Locale.get_display_name` raises a cryptic `TypeError` with valid args
# Overview Description

`Locale.get_display_name` raises a cryptic `TypeError` with valid args.

# Steps to Reproduce

1. Create environment with python3.6 and babel-2.6.0
2. Run

```
from babel.core import Locale
Locale.parse('fi_FI').get_display_name('kw_GB')
```

# Actual Results
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/will/tmp/babel-bug/env/lib/python3.6/site-packages/babel/core.py", line 392, in get_display_name
    retval += ' (%s)' % u', '.join(details)
TypeError: unsupported operand type(s) for +=: 'NoneType' and 'str'
```

# Expected Results

Some interpretation of "Finish (Finland)"

# Reproducibility

Yes

Pull request:
get_display_name(): Don't attempt to concatenate variant information to None
Fixes #601

Patch:
diff --git a/babel/core.py b/babel/core.py
index d028c07..211849b 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -379,7 +379,7 @@ class Locale(object):
             locale = self
         locale = Locale.parse(locale)
         retval = locale.languages.get(self.language)
-        if self.territory or self.script or self.variant:
+        if retval and (self.territory or self.script or self.variant):
             details = []
             if self.script:
                 details.append(locale.scripts.get(self.script))
diff --git a/babel/localedata.py b/babel/localedata.py
index 2a8c442..03381e2 100644
--- a/babel/localedata.py
+++ b/babel/localedata.py
@@ -57,13 +57,24 @@ def locale_identifiers():
     """Return a list of all locale identifiers for which locale data is
     available.
 
+    This data is cached after the first invocation in `locale_identifiers.cache`.
+
+    Removing the `locale_identifiers.cache` attribute or setting it to `None`
+    will cause this function to re-read the list from disk.
+
     .. versionadded:: 0.8.1
 
     :return: a list of locale identifiers (strings)
     """
-    return [stem for stem, extension in [
-        os.path.splitext(filename) for filename in os.listdir(_dirname)
-    ] if extension == '.dat' and stem != 'root']
+    data = getattr(locale_identifiers, 'cache', None)
+    if data is None:
+        locale_identifiers.cache = data = [
+            stem
+            for stem, extension in
+            (os.path.splitext(filename) for filename in os.listdir(_dirname))
+            if extension == '.dat' and stem != 'root'
+        ]
+    return data
 
 
 def load(name, merge_inherited=True):
diff --git a/scripts/download_import_cldr.py b/scripts/download_import_cldr.py
index f0d9b10..f118c69 100755
--- a/scripts/download_import_cldr.py
+++ b/scripts/download_import_cldr.py
@@ -13,9 +13,9 @@ except ImportError:
     from urllib import urlretrieve
 
 
-URL = 'https://unicode.org/Public/cldr/34/core.zip'
-FILENAME = 'cldr-core-34.0.zip'
-FILESUM = '1d79f6ce294ff350a3ee59d7dd69451fd24776f7105fd6a55c255ffb813c03ba'
+URL = 'https://unicode.org/Public/cldr/35.1/core.zip'
+FILENAME = 'cldr-core-35.1.zip'
+FILESUM = 'e2ede8cb8f9c29157e281ee9e696ce540a72c598841bed595a406b710eea87b0'
 BLKSIZE = 131072
 
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index f22ab30..fc637d2 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -317,3 +317,13 @@ def test_compatible_classes_in_global_and_localedata(filename):
 
     with open(filename, 'rb') as f:
         return Unpickler(f).load()
+
+
+def test_issue_601_no_language_name_but_has_variant():
+    # kw_GB has a variant for Finnish but no actual language name for Finnish,
+    # so `get_display_name()` previously crashed with a TypeError as it attempted
+    # to concatenate " (Finnish)" to None.
+    # Instead, it's better to return None altogether, as we can't reliably format
+    # part of a language name.
+
+    assert Locale.parse('fi_FI').get_display_name('kw_GB') == None
diff --git a/tests/test_localedata.py b/tests/test_localedata.py
index dceb984..bffe763 100644
--- a/tests/test_localedata.py
+++ b/tests/test_localedata.py
@@ -17,6 +17,7 @@ from operator import methodcaller
 
 from babel import localedata
 
+
 class MergeResolveTestCase(unittest.TestCase):
 
     def test_merge_items(self):
@@ -78,6 +79,7 @@ def test_locale_identification():
     for l in localedata.locale_identifiers():
         assert localedata.exists(l)
 
+
 def test_unique_ids():
     # Check all locale IDs are uniques.
     all_ids = localedata.locale_identifiers()
@@ -93,6 +95,7 @@ def test_mixedcased_locale():
             methodcaller(random.choice(['lower', 'upper']))(c) for c in l])
         assert localedata.exists(locale_id)
 
+
 def test_locale_argument_acceptance():
     # Testing None input.
     normalized_locale = localedata.normalize_locale(None)
@@ -105,3 +108,26 @@ def test_locale_argument_acceptance():
     assert normalized_locale is None
     locale_exist = localedata.exists(['en_us', None])
     assert locale_exist == False
+
+
+def test_locale_identifiers_cache(monkeypatch):
+    original_listdir = localedata.os.listdir
+    listdir_calls = []
+    def listdir_spy(*args):
+        rv = original_listdir(*args)
+        listdir_calls.append((args, rv))
+        return rv
+    monkeypatch.setattr(localedata.os, 'listdir', listdir_spy)
+
+    # In case we've already run some tests...
+    if hasattr(localedata.locale_identifiers, 'cache'):
+        del localedata.locale_identifiers.cache
+
+    assert not listdir_calls
+    assert localedata.locale_identifiers()
+    assert len(listdir_calls) == 1
+    assert localedata.locale_identifiers() is localedata.locale_identifiers.cache
+    assert len(listdir_calls) == 1
+    localedata.locale_identifiers.cache = None
+    assert localedata.locale_identifiers()
+    assert len(listdir_calls) == 2