Repository: python-babel/babel (python)

Issue:
Missing license for babel/locale-data
## Overview Description

`babel/locale-data` does not provide any license information, although being based on some third-party code.

## Steps to Reproduce

1. Download a *Babel* distribution.
2. Search for the Unicode license inside the package.

## Actual Results

There is no Unicode-related copyright information inside the distributions.

## Expected Results

The obligations of the Unicode/CLDR license terms are met inside the *Babel* distributions.

## Additional Information

Digging through the code of the repository, it turns out that `babel/locale-data` contains *pickle*-based custom representations of files downloaded from https://github.com/python-babel/babel/blob/fe82fbc90d8044d17bfc4ae1c7a0cb24e85153ef/scripts/download_import_cldr.py#L12 The corresponding ZIP file and repository (https://github.com/unicode-org/cldr) state in their `LICENSE.txt` (bold text/highlighting by me):

> Permission is hereby granted, free of charge, to any person obtaining a copy of data files and any associated documentation (the "Data Files") or software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that **either (a) this copyright and permission notice appear with all copies of the Data Files or Software, or (b) this copyright and permission notice appear in associated Documentation.**

At the moment, there corresponding notice seems to be missing from *Babel* distributions and from the whole GitHub repository.

Pull request description:
Include Unicode license in `locale-data` and in documentation
Fixes #1072

Patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 1b79ce1..82cb37a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,6 +2,7 @@ include Makefile CHANGES.rst LICENSE AUTHORS
 include conftest.py tox.ini
 include babel/global.dat
 include babel/locale-data/*.dat
+include babel/locale-data/LICENSE*
 recursive-include docs *
 recursive-exclude docs/_build *
 include scripts/*
diff --git a/babel/locale-data/LICENSE.unicode b/babel/locale-data/LICENSE.unicode
new file mode 100644
index 0000000..a82da82
--- /dev/null
+++ b/babel/locale-data/LICENSE.unicode
@@ -0,0 +1,41 @@
+UNICODE LICENSE V3
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 2004-2024 Unicode, Inc.
+
+NOTICE TO USER: Carefully read the following legal agreement. BY
+DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
+SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
+TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
+DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of data files and any associated documentation (the "Data Files") or
+software and any associated documentation (the "Software") to deal in the
+Data Files or Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, and/or sell
+copies of the Data Files or Software, and to permit persons to whom the
+Data Files or Software are furnished to do so, provided that either (a)
+this copyright and permission notice appear with all copies of the Data
+Files or Software, or (b) this copyright and permission notice appear in
+associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
+THIRD PARTY RIGHTS.
+
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
+BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
+OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
+FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall
+not be used in advertising or otherwise to promote the sale, use or other
+dealings in these Data Files or Software without prior written
+authorization of the copyright holder.
+
+SPDX-License-Identifier: Unicode-3.0
diff --git a/babel/numbers.py b/babel/numbers.py
index 2d46e02..6df1db8 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -998,6 +998,15 @@ class NumberFormatError(ValueError):
         self.suggestions = suggestions
 
 
+SPACE_CHARS = {
+    ' ',  # space
+    '\xa0',  # no-break space
+    '\u202f',  # narrow no-break space
+}
+
+SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS))
+
+
 def parse_number(
     string: str,
     locale: Locale | str | None = LC_NUMERIC,
@@ -1026,8 +1035,18 @@ def parse_number(
     :raise `NumberFormatError`: if the string can not be converted to a number
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
+
+    if (
+        group_symbol in SPACE_CHARS and  # if the grouping symbol is a kind of space,
+        group_symbol not in string and  # and the string to be parsed does not contain it,
+        SPACE_CHARS_RE.search(string)  # but it does contain any other kind of space instead,
+    ):
+        # ... it's reasonable to assume it is taking the place of the grouping symbol.
+        string = SPACE_CHARS_RE.sub(group_symbol, string)
+
     try:
-        return int(string.replace(get_group_symbol(locale, numbering_system=numbering_system), ''))
+        return int(string.replace(group_symbol, ''))
     except ValueError as ve:
         raise NumberFormatError(f"{string!r} is not a valid number") from ve
 
@@ -1085,12 +1104,12 @@ def parse_decimal(
     decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
 
     if not strict and (
-        group_symbol == '\xa0' and  # if the grouping symbol is U+00A0 NO-BREAK SPACE,
+        group_symbol in SPACE_CHARS and  # if the grouping symbol is a kind of space,
         group_symbol not in string and  # and the string to be parsed does not contain it,
-        ' ' in string  # but it does contain a space instead,
+        SPACE_CHARS_RE.search(string)  # but it does contain any other kind of space instead,
     ):
         # ... it's reasonable to assume it is taking the place of the grouping symbol.
-        string = string.replace(' ', group_symbol)
+        string = SPACE_CHARS_RE.sub(group_symbol, string)
 
     try:
         parsed = decimal.Decimal(string.replace(group_symbol, '')
diff --git a/docs/license.rst b/docs/license.rst
index 7c93ab4..1d93751 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -25,13 +25,24 @@ documentation.
 -   "AUTHORS" hereby refers to all the authors listed in the
     :ref:`authors` section.
 
--   The ":ref:`babel-license`" applies to all the sourcecode shipped as
+-   The ":ref:`babel-license`" applies to all the source code shipped as
     part of Babel (Babel itself as well as the examples and the unit tests)
     as well as documentation.
 
+-   The ":ref:`unicode-license`" applies to the transformed Unicode
+    Common Locale Data Repository (CLDR) data files shipped with Babel,
+    in the directory ``babel/locale-data``.
+
 .. _babel-license:
 
 Babel License
 -------------
 
 .. include:: ../LICENSE
+
+.. _unicode-license:
+
+Unicode License
+---------------
+
+.. include:: ../babel/locale-data/LICENSE.unicode


Test patch:
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index e58f073..eeb71a2 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -751,6 +751,15 @@ def test_parse_number():
     with pytest.raises(numbers.UnsupportedNumberingSystemError):
         numbers.parse_number('1.099,98', locale='en', numbering_system="unsupported")
 
+@pytest.mark.parametrize('string', [
+    '1 099',
+    '1\xa0099',
+    '1\u202f099',
+])
+def test_parse_number_group_separator_can_be_any_space(string):
+    assert numbers.parse_number(string, locale='fr') == 1099
+
+
 def test_parse_decimal():
     assert (numbers.parse_decimal('1,099.98', locale='en_US')
             == decimal.Decimal('1099.98'))
@@ -761,6 +770,15 @@ def test_parse_decimal():
     assert excinfo.value.args[0] == "'2,109,998' is not a valid decimal number"
 
 
+@pytest.mark.parametrize('string', [
+    '1 099,98',
+    '1\xa0099,98',
+    '1\u202f099,98',
+])
+def test_parse_decimal_group_separator_can_be_any_space(string):
+    assert decimal.Decimal('1099.98') == numbers.parse_decimal(string, locale='fr')
+
+
 def test_parse_grouping():
     assert numbers.parse_grouping('##') == (1000, 1000)
     assert numbers.parse_grouping('#,###') == (3, 3)