Repository: python-babel/babel
Pull request #491 (2017-04-07 14:10:07)

Add currency utilities and helpers.
Add a couple of new utilities for currencies:
* `list_currency`: to list the whole set of currency codes supported by Babel.
* `validate_currency `.
* `is_currency `: to quickly validate if a currency is supported by Babel or not, not unlike what is currently available for locale (see: `babel.localedata.exists()`).
* `normalize_currency`: to normalize a currency string to the format expected by Babel.
* `get_currency_precision`: to get the natural precision of a currency.

codecov-io: # [Codecov](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=h1) Report
> Merging [#491](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=desc) into [master](https://codecov.io/gh/python-babel/babel/commit/37069a196f9bd28426ef87458b9d9af7ee6d7275?src=pr&el=desc) will **decrease** coverage by `0.28%`.
> The diff coverage is `56.66%`.

[![Impacted file tree graph](https://codecov.io/gh/python-babel/babel/pull/491/graphs/tree.svg?height=150&width=650&token=lEYaWCjCnu&src=pr)](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #491      +/-   ##
==========================================
- Coverage   90.17%   89.88%   -0.29%     
==========================================
  Files          24       24              
  Lines        3979     3996      +17     
==========================================
+ Hits         3588     3592       +4     
- Misses        391      404      +13
```


| [Impacted Files](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [babel/numbers.py](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=tree#diff-YmFiZWwvbnVtYmVycy5weQ==) | `92.98% <56.66%> (-4.65%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=footer). Last update [37069a1...8f73844](https://codecov.io/gh/python-babel/babel/pull/491?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).
kdeldycke: Thanks @akx for the thorough and quick review! I'm going to answer and fix each of your comment monday.
akx: @kdeldycke Sure thing! Enjoy your weekend!
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
kdeldycke: Thanks @akx for that second round of reviews. Going to fix all remaining issues and add tests. I'll notify you once everything is clean and tidy.
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
gitmate-bot: Comment on 6fd2539e7f1e069b6b8aeadd965881315c24abc4, file babel/numbers.py, line 25.

This file contains unused source code.

*PyUnusedCodeBear, severity NORMAL, section `default`.*

The issue can be fixed by applying the following patch:

```diff
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -22,7 +22,10 @@
 from datetime import date as date_, datetime as datetime_
 from itertools import chain
 
-from babel.core import default_locale, Locale, get_global, UnknownLocaleError
+from babel.core import Locale
+from babel.core import UnknownLocaleError
+from babel.core import default_locale
+from babel.core import get_global
 from babel._compat import decimal
 from babel.localedata import locale_identifiers
 
```
kdeldycke: Just added unit-tests on all methods and squashed commits.

The only thing left to do is to get rid of the brute force approach in `list_currencies`. I'll work on that now.
kdeldycke: I'm finished working on this PR. All issues have been addressed.

@akx : unless you have further comments to add, this PR is ready to be merged.
kdeldycke: The tests seems to fail for an unrelated reason... 😢 
kdeldycke: Everything's fine now. Just waiting for @akx final review before merging. 😃 
benselme: So, should we merge this ?
kdeldycke: Oh yes please! Let's merge this! 😃 

AFAIK this PR is just waiting for @akx to lift the reviewing embargo.
akx: Sorry for the delay! Let me just re-read the final version :)

Patch:
diff --git a/babel/core.py b/babel/core.py
index bb59b74..5140f49 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -45,6 +45,7 @@ def get_global(key):
 
     The keys available are:
 
+    - ``all_currencies``
     - ``currency_fractions``
     - ``language_aliases``
     - ``likely_subtags``
diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py
index fd40058..3145fa3 100644
--- a/babel/messages/catalog.py
+++ b/babel/messages/catalog.py
@@ -395,7 +395,6 @@ class Catalog(object):
             elif name == 'last-translator':
                 self.last_translator = value
             elif name == 'language':
-                value = value.replace('-', '_')
                 self.locale = Locale.parse(value)
             elif name == 'language-team':
                 self.language_team = value
diff --git a/babel/messages/extract.py b/babel/messages/extract.py
index 351a290..efddbf5 100644
--- a/babel/messages/extract.py
+++ b/babel/messages/extract.py
@@ -137,10 +137,9 @@ def extract_from_dir(dirname=None, method_map=DEFAULT_MAPPING,
 
     absname = os.path.abspath(dirname)
     for root, dirnames, filenames in os.walk(absname):
-        dirnames[:] = [
-            subdir for subdir in dirnames
-            if not (subdir.startswith('.') or subdir.startswith('_'))
-        ]
+        for subdir in dirnames:
+            if subdir.startswith('.') or subdir.startswith('_'):
+                dirnames.remove(subdir)
         dirnames.sort()
         filenames.sort()
         for filename in filenames:
@@ -446,8 +445,7 @@ def extract_python(fileobj, keywords, comment_tags, options):
                     translator_comments.append((lineno, value))
                     break
         elif funcname and call_stack == 0:
-            nested = (tok == NAME and value in keywords)
-            if (tok == OP and value == ')') or nested:
+            if tok == OP and value == ')':
                 if buf:
                     messages.append(''.join(buf))
                     del buf[:]
@@ -472,8 +470,6 @@ def extract_python(fileobj, keywords, comment_tags, options):
                 messages = []
                 translator_comments = []
                 in_translator_comments = False
-                if nested:
-                    funcname = value
             elif tok == STRING:
                 # Unwrap quotes in a safe manner, maintaining the string's
                 # encoding
diff --git a/babel/numbers.py b/babel/numbers.py
index a5d30ef..9a60d72 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -20,14 +20,89 @@
 #  - http://www.unicode.org/reports/tr35/ (Appendix G.6)
 import re
 from datetime import date as date_, datetime as datetime_
+from itertools import chain
 
 from babel.core import default_locale, Locale, get_global
-from babel._compat import decimal
+from babel._compat import decimal, string_types
+from babel.localedata import locale_identifiers
 
 
 LC_NUMERIC = default_locale('LC_NUMERIC')
 
 
+class UnknownCurrencyError(Exception):
+    """Exception thrown when a currency is requested for which no data is available.
+    """
+
+    def __init__(self, identifier):
+        """Create the exception.
+        :param identifier: the identifier string of the unsupported currency
+        """
+        Exception.__init__(self, 'Unknown currency %r.' % identifier)
+
+        #: The identifier of the locale that could not be found.
+        self.identifier = identifier
+
+
+def list_currencies(locale=None):
+    """ Return a `set` of normalized currency codes.
+
+    .. versionadded:: 2.5.0
+
+    :param locale: filters returned currency codes by the provided locale.
+                   Expected to be a locale instance or code. If no locale is
+                   provided, returns the list of all currencies from all
+                   locales.
+    """
+    # Get locale-scoped currencies.
+    if locale:
+        currencies = Locale.parse(locale).currencies.keys()
+    else:
+        currencies = get_global('all_currencies')
+    return set(currencies)
+
+
+def validate_currency(currency, locale=None):
+    """ Check the currency code is recognized by Babel.
+
+    Accepts a ``locale`` parameter for fined-grained validation, working as
+    the one defined above in ``list_currencies()`` method.
+
+    Raises a `ValueError` exception if the currency is unknown to Babel.
+    """
+    if currency not in list_currencies(locale):
+        raise UnknownCurrencyError(currency)
+
+
+def is_currency(currency, locale=None):
+    """ Returns `True` only if a currency is recognized by Babel.
+
+    This method always return a Boolean and never raise.
+    """
+    if not currency or not isinstance(currency, string_types):
+        return False
+    try:
+        validate_currency(currency, locale)
+    except UnknownCurrencyError:
+        return False
+    return True
+
+
+def normalize_currency(currency, locale=None):
+    """Returns the normalized sting of any currency code.
+
+    Accepts a ``locale`` parameter for fined-grained validation, working as
+    the one defined above in ``list_currencies()`` method.
+
+    Returns None if the currency is unknown to Babel.
+    """
+    if isinstance(currency, string_types):
+        currency = currency.upper()
+    if not is_currency(currency, locale):
+        return
+    return currency
+
+
 def get_currency_name(currency, count=None, locale=LC_NUMERIC):
     """Return the name used by the locale for the specified currency.
 
@@ -36,10 +111,10 @@ def get_currency_name(currency, count=None, locale=LC_NUMERIC):
 
     .. versionadded:: 0.9.4
 
-    :param currency: the currency code
+    :param currency: the currency code.
     :param count: the optional count.  If provided the currency name
                   will be pluralized to that number if possible.
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier.
     """
     loc = Locale.parse(locale)
     if count is not None:
@@ -56,12 +131,26 @@ def get_currency_symbol(currency, locale=LC_NUMERIC):
     >>> get_currency_symbol('USD', locale='en_US')
     u'$'
 
-    :param currency: the currency code
-    :param locale: the `Locale` object or locale identifier
+    :param currency: the currency code.
+    :param locale: the `Locale` object or locale identifier.
     """
     return Locale.parse(locale).currency_symbols.get(currency, currency)
 
 
+def get_currency_precision(currency):
+    """Return currency's precision.
+
+    Precision is the number of decimals found after the decimal point in the
+    currency's format pattern.
+
+    .. versionadded:: 2.5.0
+
+    :param currency: the currency code.
+    """
+    precisions = get_global('currency_fractions')
+    return precisions.get(currency, precisions['DEFAULT'])[0]
+
+
 def get_territory_currencies(territory, start_date=None, end_date=None,
                              tender=True, non_tender=False,
                              include_details=False):
@@ -102,7 +191,7 @@ def get_territory_currencies(territory, start_date=None, end_date=None,
 
     .. versionadded:: 2.0
 
-    :param territory: the name of the territory to find the currency fo
+    :param territory: the name of the territory to find the currency for.
     :param start_date: the start date.  If not given today is assumed.
     :param end_date: the end date.  If not given the start date is assumed.
     :param tender: controls whether tender currencies should be included.
@@ -326,12 +415,8 @@ def format_currency(number, currency, format=None, locale=LC_NUMERIC,
             raise UnknownCurrencyFormatError("%r is not a known currency format"
                                              " type" % format_type)
     if currency_digits:
-        fractions = get_global('currency_fractions')
-        try:
-            digits = fractions[currency][0]
-        except KeyError:
-            digits = fractions['DEFAULT'][0]
-        frac = (digits, digits)
+        precision = get_currency_precision(currency)
+        frac = (precision, precision)
     else:
         frac = None
     return pattern.apply(number, locale, currency=currency, force_frac=frac)
diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py
index b804119..7b9e773 100755
--- a/scripts/import_cldr.py
+++ b/scripts/import_cldr.py
@@ -12,6 +12,7 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://babel.edgewall.org/log/.
 
+import collections
 from optparse import OptionParser
 import os
 import re
@@ -218,6 +219,7 @@ def parse_global(srcdir, sup):
     likely_subtags = global_data.setdefault('likely_subtags', {})
     territory_currencies = global_data.setdefault('territory_currencies', {})
     parent_exceptions = global_data.setdefault('parent_exceptions', {})
+    all_currencies = collections.defaultdict(set)
     currency_fractions = global_data.setdefault('currency_fractions', {})
     territory_languages = global_data.setdefault('territory_languages', {})
     bcp47_timezone = parse(os.path.join(srcdir, 'bcp47', 'timezone.xml'))
@@ -286,14 +288,18 @@ def parse_global(srcdir, sup):
         region_code = region.attrib['iso3166']
         region_currencies = []
         for currency in region.findall('./currency'):
+            cur_code = currency.attrib['iso4217']
             cur_start = _parse_currency_date(currency.attrib.get('from'))
             cur_end = _parse_currency_date(currency.attrib.get('to'))
-            region_currencies.append((currency.attrib['iso4217'],
-                                      cur_start, cur_end,
-                                      currency.attrib.get(
-                                          'tender', 'true') == 'true'))
+            cur_tender = currency.attrib.get('tender', 'true') == 'true'
+            # Tie region to currency.
+            region_currencies.append((cur_code, cur_start, cur_end, cur_tender))
+            # Keep a reverse index of currencies to territorie.
+            all_currencies[cur_code].add(region_code)
         region_currencies.sort(key=_currency_sort_key)
         territory_currencies[region_code] = region_currencies
+    global_data['all_currencies'] = dict([
+        (currency, tuple(sorted(regions))) for currency, regions in all_currencies.items()])
 
     # Explicit parent locales
     for paternity in sup.findall('.//parentLocales/parentLocale'):


Test patch:
diff --git a/tests/messages/test_extract.py b/tests/messages/test_extract.py
index 873439d..22ea1cd 100644
--- a/tests/messages/test_extract.py
+++ b/tests/messages/test_extract.py
@@ -424,37 +424,6 @@ _('Babatschi')""")
         self.assertEqual([u'This is a multiline comment with',
                           u'a prefix too'], messages[1][2])
 
-    def test_nested_messages(self):
-        buf = BytesIO(b"""
-# NOTE: First
-_(u'Hello, {name}!', name=_(u'Foo Bar'))
-
-# NOTE: Second
-_(u'Hello, {name1} and {name2}!', name1=_(u'Heungsub'),
-  name2=_(u'Armin'))
-
-# NOTE: Third
-_(u'Hello, {0} and {1}!', _(u'Heungsub'),
-  _(u'Armin'))
-""")
-        messages = list(extract.extract_python(buf, ('_',), ['NOTE:'], {}))
-        self.assertEqual((u'Hello, {name}!', None), messages[0][2])
-        self.assertEqual([u'NOTE: First'], messages[0][3])
-        self.assertEqual(u'Foo Bar', messages[1][2])
-        self.assertEqual([], messages[1][3])
-        self.assertEqual((u'Hello, {name1} and {name2}!', None), messages[2][2])
-        self.assertEqual([u'NOTE: Second'], messages[2][3])
-        self.assertEqual(u'Heungsub', messages[3][2])
-        self.assertEqual([], messages[3][3])
-        self.assertEqual(u'Armin', messages[4][2])
-        self.assertEqual([], messages[4][3])
-        self.assertEqual((u'Hello, {0} and {1}!', None), messages[5][2])
-        self.assertEqual([u'NOTE: Third'], messages[5][3])
-        self.assertEqual(u'Heungsub', messages[6][2])
-        self.assertEqual([], messages[6][3])
-        self.assertEqual(u'Armin', messages[7][2])
-        self.assertEqual([], messages[7][3])
-
 
 class ExtractTestCase(unittest.TestCase):
 
diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py
index f6cd66d..ebf9702 100644
--- a/tests/messages/test_pofile.py
+++ b/tests/messages/test_pofile.py
@@ -36,12 +36,6 @@ msgstr ""
 "Language: en_US\n"''')
         catalog = pofile.read_po(buf, locale='de')
         self.assertEqual(Locale('en', 'US'), catalog.locale)
-        buf = StringIO(r'''
-msgid ""
-msgstr ""
-"Language: ko-KR\n"''')
-        catalog = pofile.read_po(buf, locale='de')
-        self.assertEqual(Locale('ko', 'KR'), catalog.locale)
 
     def test_preserve_domain(self):
         buf = StringIO(r'''msgid "foo"
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index 2593cc0..5bcd171 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -17,6 +17,10 @@ import pytest
 from datetime import date
 
 from babel import numbers
+from babel.numbers import (
+    list_currencies, validate_currency, UnknownCurrencyError, is_currency, normalize_currency, get_currency_precision)
+from babel.core import Locale
+from babel.localedata import locale_identifiers
 from babel._compat import decimal
 
 
@@ -162,6 +166,55 @@ class NumberParsingTestCase(unittest.TestCase):
                           lambda: numbers.parse_decimal('2,109,998', locale='de'))
 
 
+def test_list_currencies():
+    assert isinstance(list_currencies(), set)
+    assert list_currencies().issuperset(['BAD', 'BAM', 'KRO'])
+
+    assert isinstance(list_currencies(locale='fr'), set)
+    assert list_currencies('fr').issuperset(['BAD', 'BAM', 'KRO'])
+
+    with pytest.raises(ValueError) as excinfo:
+        list_currencies('yo!')
+    assert excinfo.value.args[0] == "expected only letters, got 'yo!'"
+
+    assert list_currencies(locale='pa_Arab') == set(['PKR', 'INR', 'EUR'])
+    assert list_currencies(locale='kok') == set([])
+
+    assert len(list_currencies()) == 296
+
+
+def test_validate_currency():
+    validate_currency('EUR')
+
+    with pytest.raises(UnknownCurrencyError) as excinfo:
+        validate_currency('FUU')
+    assert excinfo.value.args[0] == "Unknown currency 'FUU'."
+
+
+def test_is_currency():
+    assert is_currency('EUR') == True
+    assert is_currency('eUr') == False
+    assert is_currency('FUU') == False
+    assert is_currency('') == False
+    assert is_currency(None) == False
+    assert is_currency('   EUR    ') == False
+    assert is_currency('   ') == False
+    assert is_currency([]) == False
+    assert is_currency(set()) == False
+
+
+def test_normalize_currency():
+    assert normalize_currency('EUR') == 'EUR'
+    assert normalize_currency('eUr') == 'EUR'
+    assert normalize_currency('FUU') == None
+    assert normalize_currency('') == None
+    assert normalize_currency(None) == None
+    assert normalize_currency('   EUR    ') == None
+    assert normalize_currency('   ') == None
+    assert normalize_currency([]) == None
+    assert normalize_currency(set()) == None
+
+
 def test_get_currency_name():
     assert numbers.get_currency_name('USD', locale='en_US') == u'US Dollar'
     assert numbers.get_currency_name('USD', count=2, locale='en_US') == u'US dollars'
@@ -171,6 +224,11 @@ def test_get_currency_symbol():
     assert numbers.get_currency_symbol('USD', 'en_US') == u'$'
 
 
+def test_get_currency_precision():
+    assert get_currency_precision('EUR') == 2
+    assert get_currency_precision('JPY') == 0
+
+
 def test_get_territory_currencies():
     assert numbers.get_territory_currencies('AT', date(1995, 1, 1)) == ['ATS']
     assert numbers.get_territory_currencies('AT', date(2011, 1, 1)) == ['EUR']