Repository: python-babel/babel
Pull request #1176 (2025-01-17 22:57:48)

Increase test coverage of the `python_format` checker
There a few bugs that I found in the `python_format` checker while working on the linter that I'd like to fix (e.g. one is that it does not validate `msgstr[N]` for `N >= 2`).

Though before I start fixing anything, I want to make sure we have the checker fully covered with tests as it would be pretty easy to accidentally introduce other bugs.

cc @akx 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/python-babel/babel/pull/1176?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.54%. Comparing base [(`98b9562`)](https://app.codecov.io/gh/python-babel/babel/commit/98b9562c05e5276038c27ec12c12f3e92dc027b6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) to head [(`7135ae9`)](https://app.codecov.io/gh/python-babel/babel/commit/7135ae94b22471e78404e5ed91828ce78e30ccac?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1176      +/-   ##
==========================================
+ Coverage   91.37%   91.54%   +0.17%     
==========================================
  Files          27       27              
  Lines        4672     4674       +2     
==========================================
+ Hits         4269     4279      +10     
+ Misses        403      395       -8     
```

| [Flag](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | Coverage Δ | |
|---|---|---|
| [macos-14-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.56% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.50% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.71% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.71% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.43% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.49% <100.00%> (+0.17%)` | :arrow_up: |
| [macos-14-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [ubuntu-24.04-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.58% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.52% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.73% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.73% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.45% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.51% <100.00%> (+0.17%)` | :arrow_up: |
| [ubuntu-24.04-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.58% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.59% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-3.11](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.53% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-3.12](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.74% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-3.13](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |
| [windows-2022-3.8](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.57% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-3.9](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `90.52% <100.00%> (+0.17%)` | :arrow_up: |
| [windows-2022-pypy3.10](https://app.codecov.io/gh/python-babel/babel/pull/1176/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel) | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/python-babel/babel/pull/1176?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-babel).

tomasr8: `+0.17%` in overall coverage :sweat_smile: 

Patch:
diff --git a/babel/core.py b/babel/core.py
index 39a2cf6..bffef94 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -13,7 +13,7 @@ from __future__ import annotations
 import os
 import pickle
 from collections.abc import Iterable, Mapping
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Literal
 
 from babel import localedata
 from babel.plural import PluralRule
@@ -29,7 +29,7 @@ __all__ = [
 ]
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal, TypeAlias
+    from typing_extensions import TypeAlias
 
     _GLOBAL_KEY: TypeAlias = Literal[
         "all_currencies",
@@ -1089,7 +1089,10 @@ class Locale:
         return self._data['unit_display_names']
 
 
-def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None:
+def default_locale(
+    category: str | tuple[str, ...] | list[str] | None = None,
+    aliases: Mapping[str, str] = LOCALE_ALIASES,
+) -> str | None:
     """Returns the system default locale for a given category, based on
     environment variables.
 
@@ -1113,11 +1116,22 @@ def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOC
     - ``LC_CTYPE``
     - ``LANG``
 
-    :param category: one of the ``LC_XXX`` environment variable names
+    :param category: one or more of the ``LC_XXX`` environment variable names
     :param aliases: a dictionary of aliases for locale identifiers
     """
-    varnames = (category, 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG')
-    for name in filter(None, varnames):
+
+    varnames = ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG')
+    if category:
+        if isinstance(category, str):
+            varnames = (category, *varnames)
+        elif isinstance(category, (list, tuple)):
+            varnames = (*category, *varnames)
+        else:
+            raise TypeError(f"Invalid type for category: {category!r}")
+
+    for name in varnames:
+        if not name:
+            continue
         locale = os.getenv(name)
         if locale:
             if name == 'LANGUAGE' and ':' in locale:
diff --git a/babel/dates.py b/babel/dates.py
index 98dd3f0..d060fde 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -21,7 +21,7 @@ import math
 import re
 import warnings
 from functools import lru_cache
-from typing import TYPE_CHECKING, SupportsInt
+from typing import TYPE_CHECKING, Literal, SupportsInt
 
 try:
     import pytz
@@ -37,7 +37,7 @@ from babel.core import Locale, default_locale, get_global
 from babel.localedata import LocaleDataDict
 
 if TYPE_CHECKING:
-    from typing_extensions import Literal, TypeAlias
+    from typing_extensions import TypeAlias
     _Instant: TypeAlias = datetime.date | datetime.time | float | None
     _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short']
     _Context: TypeAlias = Literal['format', 'stand-alone']
@@ -1482,7 +1482,11 @@ class DateTimeFormat:
     def format_year(self, char: str, num: int) -> str:
         value = self.value.year
         if char.isupper():
-            value = self.value.isocalendar()[0]
+            month = self.value.month
+            if month == 1 and self.value.day < 7 and self.get_week_of_year() >= 52:
+                value -= 1
+            elif month == 12 and self.value.day > 25 and self.get_week_of_year() <= 2:
+                value += 1
         year = self.format(value, num)
         if num == 2:
             year = year[-2:]
@@ -1505,18 +1509,10 @@ class DateTimeFormat:
 
     def format_week(self, char: str, num: int) -> str:
         if char.islower():  # week of year
-            day_of_year = self.get_day_of_year()
-            week = self.get_week_number(day_of_year)
-            if week == 0:
-                date = self.value - datetime.timedelta(days=day_of_year)
-                week = self.get_week_number(self.get_day_of_year(date),
-                                            date.weekday())
+            week = self.get_week_of_year()
             return self.format(week, num)
         else:  # week of month
-            week = self.get_week_number(self.value.day)
-            if week == 0:
-                date = self.value - datetime.timedelta(days=self.value.day)
-                week = self.get_week_number(date.day, date.weekday())
+            week = self.get_week_of_month()
             return str(week)
 
     def format_weekday(self, char: str = 'E', num: int = 4) -> str:
@@ -1677,6 +1673,25 @@ class DateTimeFormat:
             date = self.value
         return (date - date.replace(month=1, day=1)).days + 1
 
+    def get_week_of_year(self) -> int:
+        """Return the week of the year."""
+        day_of_year = self.get_day_of_year(self.value)
+        week = self.get_week_number(day_of_year)
+        if week == 0:
+            date = datetime.date(self.value.year - 1, 12, 31)
+            week = self.get_week_number(self.get_day_of_year(date),
+                                        date.weekday())
+        elif week > 52:
+            weekday = datetime.date(self.value.year + 1, 1, 1).weekday()
+            if self.get_week_number(1, weekday) == 1 and \
+                    32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day:
+                week = 1
+        return week
+
+    def get_week_of_month(self) -> int:
+        """Return the week of the month."""
+        return self.get_week_number(self.value.day)
+
     def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> int:
         """Return the number of the week of a day within a period. This may be
         the week number in a year or the week number in a month.
@@ -1703,20 +1718,8 @@ class DateTimeFormat:
         if first_day < 0:
             first_day += 7
         week_number = (day_of_period + first_day - 1) // 7
-
         if 7 - first_day >= self.locale.min_week_days:
             week_number += 1
-
-        if self.locale.first_week_day == 0:
-            # Correct the weeknumber in case of iso-calendar usage (first_week_day=0).
-            # If the weeknumber exceeds the maximum number of weeks for the given year
-            # we must count from zero.For example the above calculation gives week 53
-            # for 2018-12-31. By iso-calender definition 2018 has a max of 52
-            # weeks, thus the weeknumber must be 53-52=1.
-            max_weeks = datetime.date(year=self.value.year, day=28, month=12).isocalendar()[1]
-            if week_number > max_weeks:
-                week_number -= max_weeks
-
         return week_number
 
 
diff --git a/babel/lists.py b/babel/lists.py
index f1d3790..103a177 100644
--- a/babel/lists.py
+++ b/babel/lists.py
@@ -17,15 +17,10 @@ from __future__ import annotations
 
 import warnings
 from collections.abc import Sequence
-from typing import TYPE_CHECKING
+from typing import Literal
 
 from babel.core import Locale, default_locale
 
-if TYPE_CHECKING:
-    from typing_extensions import Literal
-
-
-
 _DEFAULT_LOCALE = default_locale()  # TODO(3.0): Remove this.
 
 
diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py
index 2889b4e..79b3a6f 100644
--- a/babel/messages/checkers.py
+++ b/babel/messages/checkers.py
@@ -65,9 +65,6 @@ def _validate_format(format: str, alternative: str) -> None:
     arguments are not interchangeable as `alternative` may contain less
     placeholders if `format` uses named placeholders.
 
-    The behavior of this function is undefined if the string does not use
-    string formatting.
-
     If the string formatting of `alternative` is compatible to `format` the
     function returns `None`, otherwise a `TranslationError` is raised.
 
@@ -121,6 +118,9 @@ def _validate_format(format: str, alternative: str) -> None:
 
     a, b = map(_parse, (format, alternative))
 
+    if not a:
+        return
+
     # now check if both strings are positional or named
     a_positional, b_positional = map(_check_positional, (a, b))
     if a_positional and not b_positional and not b:
diff --git a/babel/messages/extract.py b/babel/messages/extract.py
index 1c86e4a..6dd0313 100644
--- a/babel/messages/extract.py
+++ b/babel/messages/extract.py
@@ -34,7 +34,7 @@ from functools import lru_cache
 from os.path import relpath
 from textwrap import dedent
 from tokenize import COMMENT, NAME, NL, OP, STRING, generate_tokens
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, TypedDict
 
 from babel.messages._compat import find_entrypoints
 from babel.util import parse_encoding, parse_future_flags, pathmatch
@@ -43,7 +43,7 @@ if TYPE_CHECKING:
     from typing import IO, Final, Protocol
 
     from _typeshed import SupportsItems, SupportsRead, SupportsReadline
-    from typing_extensions import TypeAlias, TypedDict
+    from typing_extensions import TypeAlias
 
     class _PyOptions(TypedDict, total=False):
         encoding: str
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index a292c8a..ad9b07a 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -13,7 +13,7 @@ from __future__ import annotations
 import os
 import re
 from collections.abc import Iterable
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
 
 from babel.core import Locale
 from babel.messages.catalog import Catalog, Message
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
     from typing import IO, AnyStr
 
     from _typeshed import SupportsWrite
-    from typing_extensions import Literal
 
 
 def unescape(string: str) -> str:
diff --git a/babel/numbers.py b/babel/numbers.py
index 6d33491..8dd4d9e 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -7,7 +7,8 @@
     The default locale for the functions in this module is determined by the
     following environment variables, in that order:
 
-     * ``LC_NUMERIC``,
+     * ``LC_MONETARY`` for currency related functions,
+     * ``LC_NUMERIC``, and
      * ``LC_ALL``, and
      * ``LANG``
 
@@ -23,14 +24,12 @@ import datetime
 import decimal
 import re
 import warnings
-from typing import TYPE_CHECKING, Any, cast, overload
+from typing import Any, Literal, cast, overload
 
 from babel.core import Locale, default_locale, get_global
 from babel.localedata import LocaleDataDict
 
-if TYPE_CHECKING:
-    from typing_extensions import Literal
-
+LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC'))
 LC_NUMERIC = default_locale('LC_NUMERIC')
 
 
@@ -120,9 +119,10 @@ def get_currency_name(
     :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. Defaults to the system numeric locale.
+    :param locale: the `Locale` object or locale identifier.
+                   Defaults to the system currency locale or numeric locale.
     """
-    loc = Locale.parse(locale or LC_NUMERIC)
+    loc = Locale.parse(locale or LC_MONETARY)
     if count is not None:
         try:
             plural_form = loc.plural_form(count)
@@ -145,9 +145,10 @@ def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> st
     u'$'
 
     :param currency: the currency code.
-    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
+    :param locale: the `Locale` object or locale identifier.
+                   Defaults to the system currency locale or numeric locale.
     """
-    return Locale.parse(locale or LC_NUMERIC).currency_symbols.get(currency, currency)
+    return Locale.parse(locale or LC_MONETARY).currency_symbols.get(currency, currency)
 
 
 def get_currency_precision(currency: str) -> int:
@@ -184,9 +185,10 @@ def get_currency_unit_pattern(
     :param currency: the currency code.
     :param count: the optional count.  If provided the unit
                   pattern for that number will be returned.
-    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
+    :param locale: the `Locale` object or locale identifier.
+                   Defaults to the system currency locale or numeric locale.
     """
-    loc = Locale.parse(locale or LC_NUMERIC)
+    loc = Locale.parse(locale or LC_MONETARY)
     if count is not None:
         plural_form = loc.plural_form(count)
         try:
@@ -763,7 +765,8 @@ def format_currency(
     :param number: the number to format
     :param currency: the currency code
     :param format: the format string to use
-    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
+    :param locale: the `Locale` object or locale identifier.
+                   Defaults to the system currency locale or numeric locale.
     :param currency_digits: use the currency's natural number of decimal digits
     :param format_type: the currency format type to use
     :param decimal_quantization: Truncate and round high-precision numbers to
@@ -774,7 +777,7 @@ def format_currency(
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale or LC_NUMERIC)
+    locale = Locale.parse(locale or LC_MONETARY)
 
     if format_type == 'name':
         return _format_currency_long_name(
@@ -863,13 +866,14 @@ def format_compact_currency(
     :param number: the number to format
     :param currency: the currency code
     :param format_type: the compact format type to use. Defaults to "short".
-    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
+    :param locale: the `Locale` object or locale identifier.
+                   Defaults to the system currency locale or numeric locale.
     :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale or LC_NUMERIC)
+    locale = Locale.parse(locale or LC_MONETARY)
     try:
         compact_format = locale.compact_currency_formats[format_type]
     except KeyError as error:
diff --git a/babel/plural.py b/babel/plural.py
index 528d417..a2e30f4 100644
--- a/babel/plural.py
+++ b/babel/plural.py
@@ -12,10 +12,7 @@ from __future__ import annotations
 import decimal
 import re
 from collections.abc import Iterable, Mapping
-from typing import TYPE_CHECKING, Any, Callable
-
-if TYPE_CHECKING:
-    from typing_extensions import Literal
+from typing import Any, Callable, Literal
 
 _plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other')
 _fallback_tag = 'other'
diff --git a/babel/support.py b/babel/support.py
index ad95828..4374a0c 100644
--- a/babel/support.py
+++ b/babel/support.py
@@ -16,7 +16,7 @@ import gettext
 import locale
 import os
 from collections.abc import Iterator
-from typing import TYPE_CHECKING, Any, Callable, Iterable
+from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
 
 from babel.core import Locale
 from babel.dates import format_date, format_datetime, format_time, format_timedelta
@@ -33,8 +33,6 @@ if TYPE_CHECKING:
     import datetime as _datetime
     from decimal import Decimal
 
-    from typing_extensions import Literal
-
     from babel.dates import _PredefinedTimeFormat
 
 
diff --git a/babel/units.py b/babel/units.py
index d7d5e84..86ac2ab 100644
--- a/babel/units.py
+++ b/babel/units.py
@@ -1,14 +1,11 @@
 from __future__ import annotations
 
 import decimal
-from typing import TYPE_CHECKING
+from typing import Literal
 
 from babel.core import Locale
 from babel.numbers import LC_NUMERIC, format_decimal
 
-if TYPE_CHECKING:
-    from typing_extensions import Literal
-
 
 class UnknownUnitError(ValueError):
     def __init__(self, unit: str, locale: Locale) -> None:


Test patch:
diff --git a/tests/messages/test_checkers.py b/tests/messages/test_checkers.py
index 6d87b74..586347c 100644
--- a/tests/messages/test_checkers.py
+++ b/tests/messages/test_checkers.py
@@ -14,10 +14,14 @@ import unittest
 from datetime import datetime
 from io import BytesIO
 
+import pytest
+
 from babel import __version__ as VERSION
 from babel.core import Locale, UnknownLocaleError
 from babel.dates import format_datetime
-from babel.messages import checkers
+from babel.messages import Message, checkers
+from babel.messages.catalog import TranslationError
+from babel.messages.checkers import _validate_format, python_format
 from babel.messages.plurals import PLURALS
 from babel.messages.pofile import read_po
 from babel.util import LOCALTZ
@@ -325,3 +329,59 @@ msgstr[4] ""
             catalog = read_po(BytesIO(po_file), _locale)
             message = catalog['foobar']
             checkers.num_plurals(catalog, message)
+
+
+class TestPythonFormat:
+    @pytest.mark.parametrize(('msgid', 'msgstr'), [
+        ('foo %s', 'foo'),
+        (('foo %s', 'bar'), ('foo', 'bar')),
+        (('foo', 'bar %s'), ('foo', 'bar')),
+        (('foo %s', 'bar'), ('foo')),
+    ])
+    def test_python_format_invalid(self, msgid, msgstr):
+        msg = Message(msgid, msgstr)
+        with pytest.raises(TranslationError):
+            python_format(None, msg)
+
+    @pytest.mark.parametrize(('msgid', 'msgstr'), [
+        ('foo', 'foo'),
+        ('foo', 'foo %s'),
+        (('foo %s', 'bar %d'), ('foo %s', 'bar %d')),
+        (('foo %s', 'bar %d'), ('foo %s', 'bar %d', 'baz')),
+        (('foo', 'bar %s'), ('foo')),
+    ])
+    def test_python_format_valid(self, msgid, msgstr):
+        msg = Message(msgid, msgstr)
+        python_format(None, msg)
+
+    @pytest.mark.parametrize(('msgid', 'msgstr', 'error'), [
+        ('%s %(foo)s', '%s %(foo)s', 'format string mixes positional and named placeholders'),
+        ('foo %s', 'foo', 'placeholders are incompatible'),
+        ('%s', '%(foo)s', 'the format strings are of different kinds'),
+        ('%s', '%s %d', 'positional format placeholders are unbalanced'),
+        ('%s', '%d', "incompatible format for placeholder 1: 's' and 'd' are not compatible"),
+        ('%s %s %d', '%s %s %s', "incompatible format for placeholder 3: 'd' and 's' are not compatible"),
+        ('%(foo)s', '%(bar)s', "unknown named placeholder 'bar'"),
+        ('%(foo)s', '%(bar)d', "unknown named placeholder 'bar'"),
+        ('%(foo)s', '%(foo)d', "incompatible format for placeholder 'foo': 'd' and 's' are not compatible"),
+    ])
+    def test__validate_format_invalid(self, msgid, msgstr, error):
+        with pytest.raises(TranslationError, match=error):
+            _validate_format(msgid, msgstr)
+
+    @pytest.mark.parametrize(('msgid', 'msgstr'), [
+        ('foo', 'foo'),
+        ('foo', 'foo %s'),
+        ('%s foo', 'foo %s'),
+        ('%i', '%d'),
+        ('%d', '%u'),
+        ('%x', '%X'),
+        ('%f', '%F'),
+        ('%F', '%g'),
+        ('%g', '%G'),
+        ('%(foo)s', 'foo'),
+        ('%(foo)s', '%(foo)s %(foo)s'),
+        ('%(bar)s foo %(n)d', '%(n)d foo %(bar)s'),
+    ])
+    def test__validate_format_valid(self, msgid, msgstr):
+        _validate_format(msgid, msgstr)
diff --git a/tests/test_core.py b/tests/test_core.py
index b6c5562..8770cf8 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -265,7 +265,7 @@ class TestLocaleClass:
 
 
 def test_default_locale(monkeypatch):
-    for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
+    for name in ['LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
         monkeypatch.setenv(name, '')
     monkeypatch.setenv('LANG', 'fr_FR.UTF-8')
     assert default_locale('LC_MESSAGES') == 'fr_FR'
@@ -277,6 +277,23 @@ def test_default_locale(monkeypatch):
         assert default_locale() == 'en_US_POSIX'
 
 
+def test_default_locale_multiple_args(monkeypatch):
+    for name in ['LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_MONETARY', 'LC_NUMERIC']:
+        monkeypatch.setenv(name, '')
+    assert default_locale(["", 0, None]) is None
+    monkeypatch.setenv('LANG', 'en_US')
+    assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'en_US'  # No LC_MONETARY or LC_NUMERIC set
+    monkeypatch.setenv('LC_NUMERIC', 'fr_FR.UTF-8')
+    assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'fr_FR'  # LC_NUMERIC set
+    monkeypatch.setenv('LC_MONETARY', 'fi_FI.UTF-8')
+    assert default_locale(('LC_MONETARY', 'LC_NUMERIC')) == 'fi_FI'  # LC_MONETARY set, it takes precedence
+
+
+def test_default_locale_bad_arg():
+    with pytest.raises(TypeError):
+        default_locale(42)
+
+
 def test_negotiate_locale():
     assert (core.negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) ==
             'de_DE')
diff --git a/tests/test_dates.py b/tests/test_dates.py
index bc6107f..18bf434 100644
--- a/tests/test_dates.py
+++ b/tests/test_dates.py
@@ -785,6 +785,365 @@ def test_russian_week_numbering():
     assert dates.format_date(v, format='YYYY-ww', locale='de_DE') == '2016-52'
 
 
+def test_week_numbering_isocalendar():
+    locale = Locale.parse('de_DE')
+    assert locale.first_week_day == 0
+    assert locale.min_week_days == 4
+
+    def week_number(value):
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    for year in range(1991, 2010):
+        for delta in range(-9, 9):
+            value = date(year, 1, 1) + timedelta(days=delta)
+            expected = '%04d-W%02d-%d' % value.isocalendar()
+            assert week_number(value) == expected
+
+def test_week_numbering_monday_mindays_4():
+    locale = Locale.parse('de_DE')
+    assert locale.first_week_day == 0
+    assert locale.min_week_days == 4
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-3'
+    assert week_number('2003-01-06') == '2003-W02-1'
+    assert week_number('2003-12-28') == '2003-W52-7'
+    assert week_number('2003-12-29') == '2004-W01-1'
+    assert week_number('2003-12-31') == '2004-W01-3'
+    assert week_number('2004-01-01') == '2004-W01-4'
+    assert week_number('2004-01-05') == '2004-W02-1'
+    assert week_number('2004-12-26') == '2004-W52-7'
+    assert week_number('2004-12-27') == '2004-W53-1'
+    assert week_number('2004-12-31') == '2004-W53-5'
+    assert week_number('2005-01-01') == '2004-W53-6'
+    assert week_number('2005-01-03') == '2005-W01-1'
+    assert week_number('2005-01-10') == '2005-W02-1'
+    assert week_number('2005-12-31') == '2005-W52-6'
+    assert week_number('2006-01-01') == '2005-W52-7'
+    assert week_number('2006-01-02') == '2006-W01-1'
+    assert week_number('2006-01-09') == '2006-W02-1'
+    assert week_number('2006-12-31') == '2006-W52-7'
+    assert week_number('2007-01-01') == '2007-W01-1'
+    assert week_number('2007-12-30') == '2007-W52-7'
+    assert week_number('2007-12-31') == '2008-W01-1'
+    assert week_number('2008-01-01') == '2008-W01-2'
+    assert week_number('2008-01-07') == '2008-W02-1'
+    assert week_number('2008-12-28') == '2008-W52-7'
+    assert week_number('2008-12-29') == '2009-W01-1'
+    assert week_number('2008-12-31') == '2009-W01-3'
+    assert week_number('2009-01-01') == '2009-W01-4'
+    assert week_number('2009-01-05') == '2009-W02-1'
+    assert week_number('2009-12-27') == '2009-W52-7'
+    assert week_number('2009-12-28') == '2009-W53-1'
+    assert week_number('2009-12-31') == '2009-W53-4'
+    assert week_number('2010-01-01') == '2009-W53-5'
+    assert week_number('2010-01-03') == '2009-W53-7'
+    assert week_number('2010-01-04') == '2010-W01-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-28') == '4'
+    assert week_of_month('2003-05-01') == '1'  # Thu
+    assert week_of_month('2003-08-01') == '0'  # Fri
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '1'  # Thu
+    assert week_of_month('2004-02-29') == '4'
+
+
+def test_week_numbering_monday_mindays_1():
+    locale = Locale.parse('tr_TR')
+    assert locale.first_week_day == 0
+    assert locale.min_week_days == 1
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-3'
+    assert week_number('2003-01-06') == '2003-W02-1'
+    assert week_number('2003-12-28') == '2003-W52-7'
+    assert week_number('2003-12-29') == '2004-W01-1'
+    assert week_number('2003-12-31') == '2004-W01-3'
+    assert week_number('2004-01-01') == '2004-W01-4'
+    assert week_number('2004-01-05') == '2004-W02-1'
+    assert week_number('2004-12-26') == '2004-W52-7'
+    assert week_number('2004-12-27') == '2005-W01-1'
+    assert week_number('2004-12-31') == '2005-W01-5'
+    assert week_number('2005-01-01') == '2005-W01-6'
+    assert week_number('2005-01-03') == '2005-W02-1'
+    assert week_number('2005-12-25') == '2005-W52-7'
+    assert week_number('2005-12-26') == '2006-W01-1'
+    assert week_number('2005-12-31') == '2006-W01-6'
+    assert week_number('2006-01-01') == '2006-W01-7'
+    assert week_number('2006-01-02') == '2006-W02-1'
+    assert week_number('2006-12-31') == '2006-W53-7'
+    assert week_number('2007-01-01') == '2007-W01-1'
+    assert week_number('2007-01-08') == '2007-W02-1'
+    assert week_number('2007-12-30') == '2007-W52-7'
+    assert week_number('2007-12-31') == '2008-W01-1'
+    assert week_number('2008-01-01') == '2008-W01-2'
+    assert week_number('2008-01-07') == '2008-W02-1'
+    assert week_number('2008-12-28') == '2008-W52-7'
+    assert week_number('2008-12-29') == '2009-W01-1'
+    assert week_number('2008-12-31') == '2009-W01-3'
+    assert week_number('2009-01-01') == '2009-W01-4'
+    assert week_number('2009-01-05') == '2009-W02-1'
+    assert week_number('2009-12-27') == '2009-W52-7'
+    assert week_number('2009-12-28') == '2010-W01-1'
+    assert week_number('2009-12-31') == '2010-W01-4'
+    assert week_number('2010-01-01') == '2010-W01-5'
+    assert week_number('2010-01-03') == '2010-W01-7'
+    assert week_number('2010-01-04') == '2010-W02-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-28') == '5'
+    assert week_of_month('2003-06-01') == '1'  # Sun
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '1'  # Thu
+    assert week_of_month('2004-02-29') == '5'
+
+
+def test_week_numbering_sunday_mindays_1():
+    locale = Locale.parse('en_US')
+    assert locale.first_week_day == 6
+    assert locale.min_week_days == 1
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-4'
+    assert week_number('2003-01-05') == '2003-W02-1'
+    assert week_number('2003-12-27') == '2003-W52-7'
+    assert week_number('2003-12-28') == '2004-W01-1'
+    assert week_number('2003-12-31') == '2004-W01-4'
+    assert week_number('2004-01-01') == '2004-W01-5'
+    assert week_number('2004-01-04') == '2004-W02-1'
+    assert week_number('2004-12-25') == '2004-W52-7'
+    assert week_number('2004-12-26') == '2005-W01-1'
+    assert week_number('2004-12-31') == '2005-W01-6'
+    assert week_number('2005-01-01') == '2005-W01-7'
+    assert week_number('2005-01-02') == '2005-W02-1'
+    assert week_number('2005-12-24') == '2005-W52-7'
+    assert week_number('2005-12-25') == '2005-W53-1'
+    assert week_number('2005-12-31') == '2005-W53-7'
+    assert week_number('2006-01-01') == '2006-W01-1'
+    assert week_number('2006-01-08') == '2006-W02-1'
+    assert week_number('2006-12-30') == '2006-W52-7'
+    assert week_number('2006-12-31') == '2007-W01-1'
+    assert week_number('2007-01-01') == '2007-W01-2'
+    assert week_number('2007-01-07') == '2007-W02-1'
+    assert week_number('2007-12-29') == '2007-W52-7'
+    assert week_number('2007-12-30') == '2008-W01-1'
+    assert week_number('2007-12-31') == '2008-W01-2'
+    assert week_number('2008-01-01') == '2008-W01-3'
+    assert week_number('2008-01-06') == '2008-W02-1'
+    assert week_number('2008-12-27') == '2008-W52-7'
+    assert week_number('2008-12-28') == '2009-W01-1'
+    assert week_number('2008-12-31') == '2009-W01-4'
+    assert week_number('2009-01-01') == '2009-W01-5'
+    assert week_number('2009-01-04') == '2009-W02-1'
+    assert week_number('2009-12-26') == '2009-W52-7'
+    assert week_number('2009-12-27') == '2010-W01-1'
+    assert week_number('2009-12-31') == '2010-W01-5'
+    assert week_number('2010-01-01') == '2010-W01-6'
+    assert week_number('2010-01-03') == '2010-W02-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-01') == '1'  # Sat
+    assert week_of_month('2003-02-28') == '5'
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '1'  # Thu
+    assert week_of_month('2004-02-29') == '5'
+
+
+def test_week_numbering_sunday_mindays_4():
+    locale = Locale.parse('pt_PT')
+    assert locale.first_week_day == 6
+    assert locale.min_week_days == 4
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-4'
+    assert week_number('2003-01-05') == '2003-W02-1'
+    assert week_number('2003-12-27') == '2003-W52-7'
+    assert week_number('2003-12-28') == '2003-W53-1'
+    assert week_number('2003-12-31') == '2003-W53-4'
+    assert week_number('2004-01-01') == '2003-W53-5'
+    assert week_number('2004-01-04') == '2004-W01-1'
+    assert week_number('2004-12-25') == '2004-W51-7'
+    assert week_number('2004-12-26') == '2004-W52-1'
+    assert week_number('2004-12-31') == '2004-W52-6'
+    assert week_number('2005-01-01') == '2004-W52-7'
+    assert week_number('2005-01-02') == '2005-W01-1'
+    assert week_number('2005-12-24') == '2005-W51-7'
+    assert week_number('2005-12-25') == '2005-W52-1'
+    assert week_number('2005-12-31') == '2005-W52-7'
+    assert week_number('2006-01-01') == '2006-W01-1'
+    assert week_number('2006-01-08') == '2006-W02-1'
+    assert week_number('2006-12-30') == '2006-W52-7'
+    assert week_number('2006-12-31') == '2007-W01-1'
+    assert week_number('2007-01-01') == '2007-W01-2'
+    assert week_number('2007-01-07') == '2007-W02-1'
+    assert week_number('2007-12-29') == '2007-W52-7'
+    assert week_number('2007-12-30') == '2008-W01-1'
+    assert week_number('2007-12-31') == '2008-W01-2'
+    assert week_number('2008-01-01') == '2008-W01-3'
+    assert week_number('2008-01-06') == '2008-W02-1'
+    assert week_number('2008-12-27') == '2008-W52-7'
+    assert week_number('2008-12-28') == '2008-W53-1'
+    assert week_number('2008-12-31') == '2008-W53-4'
+    assert week_number('2009-01-01') == '2008-W53-5'
+    assert week_number('2009-01-04') == '2009-W01-1'
+    assert week_number('2009-12-26') == '2009-W51-7'
+    assert week_number('2009-12-27') == '2009-W52-1'
+    assert week_number('2009-12-31') == '2009-W52-5'
+    assert week_number('2010-01-01') == '2009-W52-6'
+    assert week_number('2010-01-03') == '2010-W01-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-28') == '4'
+    assert week_of_month('2003-05-01') == '0'  # Thu
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '0'  # Thu
+    assert week_of_month('2004-02-29') == '5'
+
+
+def test_week_numbering_friday_mindays_1():
+    locale = Locale.parse('dv_MV')
+    assert locale.first_week_day == 4
+    assert locale.min_week_days == 1
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-6'
+    assert week_number('2003-01-03') == '2003-W02-1'
+    assert week_number('2003-12-25') == '2003-W52-7'
+    assert week_number('2003-12-26') == '2004-W01-1'
+    assert week_number('2003-12-31') == '2004-W01-6'
+    assert week_number('2004-01-01') == '2004-W01-7'
+    assert week_number('2004-01-02') == '2004-W02-1'
+    assert week_number('2004-12-23') == '2004-W52-7'
+    assert week_number('2004-12-24') == '2004-W53-1'
+    assert week_number('2004-12-30') == '2004-W53-7'
+    assert week_number('2004-12-31') == '2005-W01-1'
+    assert week_number('2005-01-01') == '2005-W01-2'
+    assert week_number('2005-01-07') == '2005-W02-1'
+    assert week_number('2005-12-29') == '2005-W52-7'
+    assert week_number('2005-12-30') == '2006-W01-1'
+    assert week_number('2005-12-31') == '2006-W01-2'
+    assert week_number('2006-01-01') == '2006-W01-3'
+    assert week_number('2006-01-06') == '2006-W02-1'
+    assert week_number('2006-12-28') == '2006-W52-7'
+    assert week_number('2006-12-29') == '2007-W01-1'
+    assert week_number('2006-12-31') == '2007-W01-3'
+    assert week_number('2007-01-01') == '2007-W01-4'
+    assert week_number('2007-01-05') == '2007-W02-1'
+    assert week_number('2007-12-27') == '2007-W52-7'
+    assert week_number('2007-12-28') == '2008-W01-1'
+    assert week_number('2007-12-31') == '2008-W01-4'
+    assert week_number('2008-01-01') == '2008-W01-5'
+    assert week_number('2008-01-04') == '2008-W02-1'
+    assert week_number('2008-12-25') == '2008-W52-7'
+    assert week_number('2008-12-26') == '2009-W01-1'
+    assert week_number('2008-12-31') == '2009-W01-6'
+    assert week_number('2009-01-01') == '2009-W01-7'
+    assert week_number('2009-01-02') == '2009-W02-1'
+    assert week_number('2009-12-24') == '2009-W52-7'
+    assert week_number('2009-12-25') == '2009-W53-1'
+    assert week_number('2009-12-31') == '2009-W53-7'
+    assert week_number('2010-01-01') == '2010-W01-1'
+    assert week_number('2010-01-08') == '2010-W02-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-28') == '5'
+    assert week_of_month('2003-03-01') == '1'  # Sun
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '1'  # Thu
+    assert week_of_month('2004-02-29') == '5'
+
+
+def test_week_numbering_saturday_mindays_1():
+    locale = Locale.parse('fr_DZ')
+    assert locale.first_week_day == 5
+    assert locale.min_week_days == 1
+
+    def week_number(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format="YYYY-'W'ww-e", locale=locale)
+
+    def week_of_month(value):
+        value = date.fromisoformat(value)
+        return dates.format_date(value, format='W', locale=locale)
+
+    assert week_number('2003-01-01') == '2003-W01-5'
+    assert week_number('2003-01-04') == '2003-W02-1'
+    assert week_number('2003-12-26') == '2003-W52-7'
+    assert week_number('2003-12-27') == '2004-W01-1'
+    assert week_number('2003-12-31') == '2004-W01-5'
+    assert week_number('2004-01-01') == '2004-W01-6'
+    assert week_number('2004-01-03') == '2004-W02-1'
+    assert week_number('2004-12-24') == '2004-W52-7'
+    assert week_number('2004-12-31') == '2004-W53-7'
+    assert week_number('2005-01-01') == '2005-W01-1'
+    assert week_number('2005-01-08') == '2005-W02-1'
+    assert week_number('2005-12-30') == '2005-W52-7'
+    assert week_number('2005-12-31') == '2006-W01-1'
+    assert week_number('2006-01-01') == '2006-W01-2'
+    assert week_number('2006-01-07') == '2006-W02-1'
+    assert week_number('2006-12-29') == '2006-W52-7'
+    assert week_number('2006-12-31') == '2007-W01-2'
+    assert week_number('2007-01-01') == '2007-W01-3'
+    assert week_number('2007-01-06') == '2007-W02-1'
+    assert week_number('2007-12-28') == '2007-W52-7'
+    assert week_number('2007-12-31') == '2008-W01-3'
+    assert week_number('2008-01-01') == '2008-W01-4'
+    assert week_number('2008-01-05') == '2008-W02-1'
+    assert week_number('2008-12-26') == '2008-W52-7'
+    assert week_number('2008-12-27') == '2009-W01-1'
+    assert week_number('2008-12-31') == '2009-W01-5'
+    assert week_number('2009-01-01') == '2009-W01-6'
+    assert week_number('2009-01-03') == '2009-W02-1'
+    assert week_number('2009-12-25') == '2009-W52-7'
+    assert week_number('2009-12-26') == '2010-W01-1'
+    assert week_number('2009-12-31') == '2010-W01-6'
+    assert week_number('2010-01-01') == '2010-W01-7'
+    assert week_number('2010-01-02') == '2010-W02-1'
+
+    assert week_of_month('2003-01-01') == '1'  # Wed
+    assert week_of_month('2003-02-28') == '4'
+    assert week_of_month('2003-08-01') == '1'  # Fri
+    assert week_of_month('2003-12-31') == '5'
+    assert week_of_month('2004-01-01') == '1'  # Thu
+    assert week_of_month('2004-02-29') == '5'
+
+
 def test_en_gb_first_weekday():
     assert Locale.parse('en').first_week_day == 0  # Monday in general
     assert Locale.parse('en_US').first_week_day == 6  # Sunday in the US
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index cf0e8d1..45892fe 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -485,19 +485,6 @@ def test_format_currency():
             == 'US$0,00')          # other
 
 
-def test_format_currency_with_none_locale_with_default(monkeypatch):
-    """Test that the default locale is used when locale is None."""
-    monkeypatch.setattr(numbers, "LC_NUMERIC", "fi_FI")
-    assert numbers.format_currency(0, "USD", locale=None) == "0,00\xa0$"
-
-
-def test_format_currency_with_none_locale(monkeypatch):
-    """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too."""
-    monkeypatch.setattr(numbers, "LC_NUMERIC", None)  # Pretend we couldn't find any locale when importing the module
-    with pytest.raises(TypeError, match="Empty"):
-        numbers.format_currency(0, "USD", locale=None)
-
-
 def test_format_currency_format_type():
     assert (numbers.format_currency(1099.98, 'USD', locale='en_US',
                                     format_type="standard")
@@ -867,3 +854,31 @@ def test_single_quotes_in_pattern():
     assert numbers.format_decimal(123, "'$'''0", locale='en') == "$'123"
 
     assert numbers.format_decimal(12, "'#'0 o''clock", locale='en') == "#12 o'clock"
+
+
+def test_format_currency_with_none_locale_with_default(monkeypatch):
+    """Test that the default locale is used when locale is None."""
+    monkeypatch.setattr(numbers, "LC_MONETARY", "fi_FI")
+    monkeypatch.setattr(numbers, "LC_NUMERIC", None)
+    assert numbers.format_currency(0, "USD", locale=None) == "0,00\xa0$"
+
+
+def test_format_currency_with_none_locale(monkeypatch):
+    """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too."""
+    monkeypatch.setattr(numbers, "LC_MONETARY", None)  # Pretend we couldn't find any locale when importing the module
+    with pytest.raises(TypeError, match="Empty"):
+        numbers.format_currency(0, "USD", locale=None)
+
+
+def test_format_decimal_with_none_locale_with_default(monkeypatch):
+    """Test that the default locale is used when locale is None."""
+    monkeypatch.setattr(numbers, "LC_NUMERIC", "fi_FI")
+    monkeypatch.setattr(numbers, "LC_MONETARY", None)
+    assert numbers.format_decimal("1.23", locale=None) == "1,23"
+
+
+def test_format_decimal_with_none_locale(monkeypatch):
+    """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too."""
+    monkeypatch.setattr(numbers, "LC_NUMERIC", None)  # Pretend we couldn't find any locale when importing the module
+    with pytest.raises(TypeError, match="Empty"):
+        numbers.format_decimal(0, locale=None)