From dbc019cb19bd830e169a8a8989acd7491d23ab58 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:03:41 -0400 Subject: [PATCH] core: Add get_official_territories() --- babel/core.py | 27 +++++++++++++++++++++++++++ docs/api/core.rst | 2 ++ scripts/import_cldr.py | 25 +++++++++++++++++++++++++ tests/test_core.py | 15 +++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/babel/core.py b/babel/core.py index 3ba8b85ea..51f60a9ba 100644 --- a/babel/core.py +++ b/babel/core.py @@ -25,6 +25,7 @@ 'get_cldr_version', 'get_global', 'get_locale_identifier', + 'get_official_territories', 'negotiate_locale', 'parse_locale', ] @@ -42,6 +43,7 @@ "parent_exceptions", "script_aliases", "territory_aliases", + "territory_codes", "territory_currencies", "territory_languages", "territory_zones", @@ -89,6 +91,7 @@ def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]: - ``parent_exceptions`` - ``script_aliases`` - ``territory_aliases`` + - ``territory_codes`` - ``territory_currencies`` - ``territory_languages`` - ``territory_zones`` @@ -1379,3 +1382,27 @@ def get_cldr_version() -> str: :rtype: str """ return str(get_global("cldr")["version"]) + + +def get_official_territories() -> frozenset[str]: + """Return the ISO 3166-1 alpha-2 territory codes known to Babel. + + This is the set of two-letter territory (country) codes that the CLDR + provides, with deprecated codes and the ISO user-assigned ranges removed. + + >>> codes = get_official_territories() + >>> 'US' in codes + True + >>> 'ZZ' in codes # reserved for private use + False + + Since the data comes from the CLDR, the returned set does not match the + official ISO 3166-1 list exactly: a few territories that the CLDR omits + are missing, and a handful of exceptionally reserved codes (such as + ``EU``) are included. + + .. versionadded:: 2.19 + + :rtype: frozenset[str] + """ + return frozenset(get_global('territory_codes')) diff --git a/docs/api/core.rst b/docs/api/core.rst index 9f297b2a1..438b938f4 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -38,3 +38,5 @@ Utility Functions .. autofunction:: get_locale_identifier .. autofunction:: get_cldr_version + +.. autofunction:: get_official_territories diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 02e01f199..e701e901b 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -98,6 +98,12 @@ def _parse_currency_date(s): return tuple(map(int, parts + [1] * (3 - len(parts)))) +def _is_user_assigned_territory(code): + # ISO 3166-1 reserves these code ranges for private use, so they never + # denote an actual territory. + return code in ('AA', 'ZZ') or 'QM' <= code <= 'QZ' or 'XA' <= code <= 'XZ' + + def _currency_sort_key(tup): code, start, end, tender = tup return int(not tender), start or (1, 1, 1) @@ -359,6 +365,25 @@ def parse_global(srcdir, sup): 'official_status': language.attrib.get('officialStatus'), } territory_languages[territory.attrib['type']] = languages + + # ISO 3166-1 alpha-2 territory codes (see GH #904). + # CLDR's supplemental code mappings list every territory code alongside its + # ISO numeric and alpha-3 equivalents. We keep the two-letter codes that + # have a numeric ISO code assigned (this drops exceptionally reserved codes + # such as EA or IC), and skip the ISO user-assigned ranges as well as any + # code CLDR records as a deprecated alias. + territory_codes = set() + for mapping in sup.findall('.//codeMappings/territoryCodes'): + code = mapping.attrib['type'] + if len(code) != 2 or not code.isalpha(): + continue + if 'numeric' not in mapping.attrib: + continue + if code in territory_aliases or _is_user_assigned_territory(code): + continue + territory_codes.add(code) + global_data['territory_codes'] = tuple(sorted(territory_codes)) + return global_data diff --git a/tests/test_core.py b/tests/test_core.py index 2c1e6d92e..8915983f4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -183,3 +183,18 @@ def test_locale_parse_empty(): def test_get_cldr_version(): assert core.get_cldr_version() == "48" + + +def test_get_official_territories(): + codes = core.get_official_territories() + assert isinstance(codes, frozenset) + # A good spread of everyday country codes should be present. + assert {'US', 'DE', 'FR', 'JP', 'ZA', 'BR'} <= codes + # All codes are two uppercase letters. + assert all(len(code) == 2 and code.isalpha() and code.isupper() for code in codes) + # The ISO user-assigned ranges are excluded. + assert 'ZZ' not in codes + assert not any('XA' <= code <= 'XZ' for code in codes) + # Deprecated codes such as Yugoslavia or the USSR are excluded. + assert 'YU' not in codes + assert 'SU' not in codes