summaryrefslogtreecommitdiff
path: root/babel
diff options
context:
space:
mode:
authorJonah Lawrence <jonah@freshidea.com>2023-01-11 01:54:11 -0700
committerGitHub <noreply@github.com>2023-01-11 08:54:11 +0000
commit53637ddbacaef2474429b22176091a362ce6567f (patch)
tree45318cc88faad0c81935f0ae4a99c3437e7aca4e /babel
parent61097845764f5a6afc2251172168e1b1732b290f (diff)
downloadbabel-53637ddbacaef2474429b22176091a362ce6567f.tar.gz
Add type annotations (#934)
Refs e.g. https://github.com/python/typeshed/pull/9455 Co-authored-by: Spencer Brown <spencerb21@live.com> Co-authored-by: Aarni Koskela <akx@iki.fi>
Diffstat (limited to 'babel')
-rw-r--r--babel/core.py186
-rw-r--r--babel/dates.py169
-rw-r--r--babel/languages.py6
-rw-r--r--babel/lists.py11
-rw-r--r--babel/localedata.py43
-rw-r--r--babel/localtime/__init__.py16
-rw-r--r--babel/localtime/_unix.py4
-rw-r--r--babel/localtime/_win32.py14
-rw-r--r--babel/messages/catalog.py157
-rw-r--r--babel/messages/checkers.py25
-rw-r--r--babel/messages/extract.py133
-rw-r--r--babel/messages/jslexer.py23
-rw-r--r--babel/messages/mofile.py12
-rw-r--r--babel/messages/plurals.py11
-rw-r--r--babel/messages/pofile.py92
-rw-r--r--babel/numbers.py205
-rw-r--r--babel/plural.py74
-rw-r--r--babel/py.typed1
-rw-r--r--babel/support.py201
-rw-r--r--babel/units.py45
-rw-r--r--babel/util.py33
21 files changed, 957 insertions, 504 deletions
diff --git a/babel/core.py b/babel/core.py
index 825af81..704957b 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -8,8 +8,12 @@
:license: BSD, see LICENSE for more details.
"""
-import pickle
+from __future__ import annotations
+
import os
+import pickle
+from collections.abc import Iterable, Mapping
+from typing import TYPE_CHECKING, Any, overload
from babel import localedata
from babel.plural import PluralRule
@@ -17,11 +21,31 @@ from babel.plural import PluralRule
__all__ = ['UnknownLocaleError', 'Locale', 'default_locale', 'negotiate_locale',
'parse_locale']
+if TYPE_CHECKING:
+ from typing_extensions import Literal, TypeAlias
+
+ _GLOBAL_KEY: TypeAlias = Literal[
+ "all_currencies",
+ "currency_fractions",
+ "language_aliases",
+ "likely_subtags",
+ "parent_exceptions",
+ "script_aliases",
+ "territory_aliases",
+ "territory_currencies",
+ "territory_languages",
+ "territory_zones",
+ "variant_aliases",
+ "windows_zone_mapping",
+ "zone_aliases",
+ "zone_territories",
+ ]
+
+ _global_data: Mapping[_GLOBAL_KEY, Mapping[str, Any]] | None
_global_data = None
_default_plural_rule = PluralRule({})
-
def _raise_no_data_error():
raise RuntimeError('The babel data files are not available. '
'This usually happens because you are using '
@@ -31,7 +55,7 @@ def _raise_no_data_error():
'installing the library.')
-def get_global(key):
+def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]:
"""Return the dictionary for the given key in the global data.
The global data is stored in the ``babel/global.dat`` file and contains
@@ -73,6 +97,7 @@ def get_global(key):
_raise_no_data_error()
with open(filename, 'rb') as fileobj:
_global_data = pickle.load(fileobj)
+ assert _global_data is not None
return _global_data.get(key, {})
@@ -93,7 +118,7 @@ class UnknownLocaleError(Exception):
is available.
"""
- def __init__(self, identifier):
+ def __init__(self, identifier: str) -> None:
"""Create the exception.
:param identifier: the identifier string of the unsupported locale
@@ -136,7 +161,13 @@ class Locale:
For more information see :rfc:`3066`.
"""
- def __init__(self, language, territory=None, script=None, variant=None):
+ def __init__(
+ self,
+ language: str,
+ territory: str | None = None,
+ script: str | None = None,
+ variant: str | None = None,
+ ) -> None:
"""Initialize the locale object from the given identifier components.
>>> locale = Locale('en', 'US')
@@ -167,7 +198,7 @@ class Locale:
raise UnknownLocaleError(identifier)
@classmethod
- def default(cls, category=None, aliases=LOCALE_ALIASES):
+ def default(cls, category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> Locale:
"""Return the system default locale for the specified category.
>>> for name in ['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES']:
@@ -192,7 +223,13 @@ class Locale:
return cls.parse(locale_string)
@classmethod
- def negotiate(cls, preferred, available, sep='_', aliases=LOCALE_ALIASES):
+ def negotiate(
+ cls,
+ preferred: Iterable[str],
+ available: Iterable[str],
+ sep: str = '_',
+ aliases: Mapping[str, str] = LOCALE_ALIASES,
+ ) -> Locale | None:
"""Find the best match between available and requested locale strings.
>>> Locale.negotiate(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
@@ -217,8 +254,21 @@ class Locale:
if identifier:
return Locale.parse(identifier, sep=sep)
+ @overload
+ @classmethod
+ def parse(cls, identifier: None, sep: str = ..., resolve_likely_subtags: bool = ...) -> None: ...
+
+ @overload
+ @classmethod
+ def parse(cls, identifier: str | Locale, sep: str = ..., resolve_likely_subtags: bool = ...) -> Locale: ...
+
@classmethod
- def parse(cls, identifier, sep='_', resolve_likely_subtags=True):
+ def parse(
+ cls,
+ identifier: str | Locale | None,
+ sep: str = '_',
+ resolve_likely_subtags: bool = True,
+ ) -> Locale | None:
"""Create a `Locale` instance for the given locale identifier.
>>> l = Locale.parse('de-DE', sep='-')
@@ -329,22 +379,22 @@ class Locale:
raise UnknownLocaleError(input_id)
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
for key in ('language', 'territory', 'script', 'variant'):
if not hasattr(other, key):
return False
- return (self.language == other.language) and \
- (self.territory == other.territory) and \
- (self.script == other.script) and \
- (self.variant == other.variant)
+ return (self.language == getattr(other, 'language')) and \
+ (self.territory == getattr(other, 'territory')) and \
+ (self.script == getattr(other, 'script')) and \
+ (self.variant == getattr(other, 'variant'))
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash((self.language, self.territory, self.script, self.variant))
- def __repr__(self):
+ def __repr__(self) -> str:
parameters = ['']
for key in ('territory', 'script', 'variant'):
value = getattr(self, key)
@@ -352,17 +402,17 @@ class Locale:
parameters.append(f"{key}={value!r}")
return f"Locale({self.language!r}{', '.join(parameters)})"
- def __str__(self):
+ def __str__(self) -> str:
return get_locale_identifier((self.language, self.territory,
self.script, self.variant))
@property
- def _data(self):
+ def _data(self) -> localedata.LocaleDataDict:
if self.__data is None:
self.__data = localedata.LocaleDataDict(localedata.load(str(self)))
return self.__data
- def get_display_name(self, locale=None):
+ def get_display_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the display name of the locale using the given locale.
The display name will include the language, territory, script, and
@@ -403,7 +453,7 @@ class Locale:
:type: `unicode`
""")
- def get_language_name(self, locale=None):
+ def get_language_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the language of this locale in the given locale.
>>> Locale('zh', 'CN', script='Hans').get_language_name('de')
@@ -425,7 +475,7 @@ class Locale:
u'English'
""")
- def get_territory_name(self, locale=None):
+ def get_territory_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the territory name in the given locale."""
if locale is None:
locale = self
@@ -439,7 +489,7 @@ class Locale:
u'Deutschland'
""")
- def get_script_name(self, locale=None):
+ def get_script_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the script name in the given locale."""
if locale is None:
locale = self
@@ -454,7 +504,7 @@ class Locale:
""")
@property
- def english_name(self):
+ def english_name(self) -> str | None:
"""The english display name of the locale.
>>> Locale('de').english_name
@@ -468,7 +518,7 @@ class Locale:
# { General Locale Display Names
@property
- def languages(self):
+ def languages(self) -> localedata.LocaleDataDict:
"""Mapping of language codes to translated language names.
>>> Locale('de', 'DE').languages['ja']
@@ -480,7 +530,7 @@ class Locale:
return self._data['languages']
@property
- def scripts(self):
+ def scripts(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('en', 'US').scripts['Hira']
@@ -492,7 +542,7 @@ class Locale:
return self._data['scripts']
@property
- def territories(self):
+ def territories(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('es', 'CO').territories['DE']
@@ -504,7 +554,7 @@ class Locale:
return self._data['territories']
@property
- def variants(self):
+ def variants(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('de', 'DE').variants['1901']
@@ -515,7 +565,7 @@ class Locale:
# { Number Formatting
@property
- def currencies(self):
+ def currencies(self) -> localedata.LocaleDataDict:
"""Mapping of currency codes to translated currency names. This
only returns the generic form of the currency name, not the count
specific one. If an actual number is requested use the
@@ -529,7 +579,7 @@ class Locale:
return self._data['currency_names']
@property
- def currency_symbols(self):
+ def currency_symbols(self) -> localedata.LocaleDataDict:
"""Mapping of currency codes to symbols.
>>> Locale('en', 'US').currency_symbols['USD']
@@ -540,7 +590,7 @@ class Locale:
return self._data['currency_symbols']
@property
- def number_symbols(self):
+ def number_symbols(self) -> localedata.LocaleDataDict:
"""Symbols used in number formatting.
.. note:: The format of the value returned may change between
@@ -552,7 +602,7 @@ class Locale:
return self._data['number_symbols']
@property
- def decimal_formats(self):
+ def decimal_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for decimal number formatting.
.. note:: The format of the value returned may change between
@@ -564,7 +614,7 @@ class Locale:
return self._data['decimal_formats']
@property
- def compact_decimal_formats(self):
+ def compact_decimal_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for compact decimal number formatting.
.. note:: The format of the value returned may change between
@@ -576,7 +626,7 @@ class Locale:
return self._data['compact_decimal_formats']
@property
- def currency_formats(self):
+ def currency_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for currency number formatting.
.. note:: The format of the value returned may change between
@@ -590,7 +640,7 @@ class Locale:
return self._data['currency_formats']
@property
- def compact_currency_formats(self):
+ def compact_currency_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for compact currency number formatting.
.. note:: The format of the value returned may change between
@@ -602,7 +652,7 @@ class Locale:
return self._data['compact_currency_formats']
@property
- def percent_formats(self):
+ def percent_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for percent number formatting.
.. note:: The format of the value returned may change between
@@ -614,7 +664,7 @@ class Locale:
return self._data['percent_formats']
@property
- def scientific_formats(self):
+ def scientific_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for scientific number formatting.
.. note:: The format of the value returned may change between
@@ -628,7 +678,7 @@ class Locale:
# { Calendar Information and Date Formatting
@property
- def periods(self):
+ def periods(self) -> localedata.LocaleDataDict:
"""Locale display names for day periods (AM/PM).
>>> Locale('en', 'US').periods['am']
@@ -637,10 +687,10 @@ class Locale:
try:
return self._data['day_periods']['stand-alone']['wide']
except KeyError:
- return {}
+ return localedata.LocaleDataDict({}) # pragma: no cover
@property
- def day_periods(self):
+ def day_periods(self) -> localedata.LocaleDataDict:
"""Locale display names for various day periods (not necessarily only AM/PM).
These are not meant to be used without the relevant `day_period_rules`.
@@ -648,13 +698,13 @@ class Locale:
return self._data['day_periods']
@property
- def day_period_rules(self):
+ def day_period_rules(self) -> localedata.LocaleDataDict:
"""Day period rules for the locale. Used by `get_period_id`.
"""
- return self._data.get('day_period_rules', {})
+ return self._data.get('day_period_rules', localedata.LocaleDataDict({}))
@property
- def days(self):
+ def days(self) -> localedata.LocaleDataDict:
"""Locale display names for weekdays.
>>> Locale('de', 'DE').days['format']['wide'][3]
@@ -663,7 +713,7 @@ class Locale:
return self._data['days']
@property
- def months(self):
+ def months(self) -> localedata.LocaleDataDict:
"""Locale display names for months.
>>> Locale('de', 'DE').months['format']['wide'][10]
@@ -672,7 +722,7 @@ class Locale:
return self._data['months']
@property
- def quarters(self):
+ def quarters(self) -> localedata.LocaleDataDict:
"""Locale display names for quarters.
>>> Locale('de', 'DE').quarters['format']['wide'][1]
@@ -681,7 +731,7 @@ class Locale:
return self._data['quarters']
@property
- def eras(self):
+ def eras(self) -> localedata.LocaleDataDict:
"""Locale display names for eras.
.. note:: The format of the value returned may change between
@@ -695,7 +745,7 @@ class Locale:
return self._data['eras']
@property
- def time_zones(self):
+ def time_zones(self) -> localedata.LocaleDataDict:
"""Locale display names for time zones.
.. note:: The format of the value returned may change between
@@ -709,7 +759,7 @@ class Locale:
return self._data['time_zones']
@property
- def meta_zones(self):
+ def meta_zones(self) -> localedata.LocaleDataDict:
"""Locale display names for meta time zones.
Meta time zones are basically groups of different Olson time zones that
@@ -726,7 +776,7 @@ class Locale:
return self._data['meta_zones']
@property
- def zone_formats(self):
+ def zone_formats(self) -> localedata.LocaleDataDict:
"""Patterns related to the formatting of time zones.
.. note:: The format of the value returned may change between
@@ -742,7 +792,7 @@ class Locale:
return self._data['zone_formats']
@property
- def first_week_day(self):
+ def first_week_day(self) -> int:
"""The first day of a week, with 0 being Monday.
>>> Locale('de', 'DE').first_week_day
@@ -753,7 +803,7 @@ class Locale:
return self._data['week_data']['first_day']
@property
- def weekend_start(self):
+ def weekend_start(self) -> int:
"""The day the weekend starts, with 0 being Monday.
>>> Locale('de', 'DE').weekend_start
@@ -762,7 +812,7 @@ class Locale:
return self._data['week_data']['weekend_start']
@property
- def weekend_end(self):
+ def weekend_end(self) -> int:
"""The day the weekend ends, with 0 being Monday.
>>> Locale('de', 'DE').weekend_end
@@ -771,7 +821,7 @@ class Locale:
return self._data['week_data']['weekend_end']
@property
- def min_week_days(self):
+ def min_week_days(self) -> int:
"""The minimum number of days in a week so that the week is counted as
the first week of a year or month.
@@ -781,7 +831,7 @@ class Locale:
return self._data['week_data']['min_days']
@property
- def date_formats(self):
+ def date_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for date formatting.
.. note:: The format of the value returned may change between
@@ -795,7 +845,7 @@ class Locale:
return self._data['date_formats']
@property
- def time_formats(self):
+ def time_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for time formatting.
.. note:: The format of the value returned may change between
@@ -809,7 +859,7 @@ class Locale:
return self._data['time_formats']
@property
- def datetime_formats(self):
+ def datetime_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for datetime formatting.
.. note:: The format of the value returned may change between
@@ -823,7 +873,7 @@ class Locale:
return self._data['datetime_formats']
@property
- def datetime_skeletons(self):
+ def datetime_skeletons(self) -> localedata.LocaleDataDict:
"""Locale patterns for formatting parts of a datetime.
>>> Locale('en').datetime_skeletons['MEd']
@@ -836,7 +886,7 @@ class Locale:
return self._data['datetime_skeletons']
@property
- def interval_formats(self):
+ def interval_formats(self) -> localedata.LocaleDataDict:
"""Locale patterns for interval formatting.
.. note:: The format of the value returned may change between
@@ -858,7 +908,7 @@ class Locale:
return self._data['interval_formats']
@property
- def plural_form(self):
+ def plural_form(self) -> PluralRule:
"""Plural rules for the locale.
>>> Locale('en').plural_form(1)
@@ -873,7 +923,7 @@ class Locale:
return self._data.get('plural_form', _default_plural_rule)
@property
- def list_patterns(self):
+ def list_patterns(self) -> localedata.LocaleDataDict:
"""Patterns for generating lists
.. note:: The format of the value returned may change between
@@ -889,7 +939,7 @@ class Locale:
return self._data['list_patterns']
@property
- def ordinal_form(self):
+ def ordinal_form(self) -> PluralRule:
"""Plural rules for the locale.
>>> Locale('en').ordinal_form(1)
@@ -906,7 +956,7 @@ class Locale:
return self._data.get('ordinal_form', _default_plural_rule)
@property
- def measurement_systems(self):
+ def measurement_systems(self) -> localedata.LocaleDataDict:
"""Localized names for various measurement systems.
>>> Locale('fr', 'FR').measurement_systems['US']
@@ -918,7 +968,7 @@ class Locale:
return self._data['measurement_systems']
@property
- def character_order(self):
+ def character_order(self) -> str:
"""The text direction for the language.
>>> Locale('de', 'DE').character_order
@@ -929,7 +979,7 @@ class Locale:
return self._data['character_order']
@property
- def text_direction(self):
+ def text_direction(self) -> str:
"""The text direction for the language in CSS short-hand form.
>>> Locale('de', 'DE').text_direction
@@ -940,7 +990,7 @@ class Locale:
return ''.join(word[0] for word in self.character_order.split('-'))
@property
- def unit_display_names(self):
+ def unit_display_names(self) -> localedata.LocaleDataDict:
"""Display names for units of measurement.
.. seealso::
@@ -954,7 +1004,7 @@ class Locale:
return self._data['unit_display_names']
-def default_locale(category=None, aliases=LOCALE_ALIASES):
+def default_locale(category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None:
"""Returns the system default locale for a given category, based on
environment variables.
@@ -999,7 +1049,7 @@ def default_locale(category=None, aliases=LOCALE_ALIASES):
pass
-def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES):
+def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: str = '_', aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None:
"""Find the best match between available and requested locale strings.
>>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT'])
@@ -1062,7 +1112,7 @@ def negotiate_locale(preferred, available, sep='_', aliases=LOCALE_ALIASES):
return None
-def parse_locale(identifier, sep='_'):
+def parse_locale(identifier: str, sep: str = '_') -> tuple[str, str | None, str | None, str | None]:
"""Parse a locale identifier into a tuple of the form ``(language,
territory, script, variant)``.
@@ -1143,7 +1193,7 @@ def parse_locale(identifier, sep='_'):
return lang, territory, script, variant
-def get_locale_identifier(tup, sep='_'):
+def get_locale_identifier(tup: tuple[str, str | None, str | None, str | None], sep: str = '_') -> str:
"""The reverse of :func:`parse_locale`. It creates a locale identifier out
of a ``(language, territory, script, variant)`` tuple. Items can be set to
``None`` and trailing ``None``\\s can also be left out of the tuple.
diff --git a/babel/dates.py b/babel/dates.py
index e9f6f6d..27f3fe6 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -15,16 +15,28 @@
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
import re
import warnings
+from bisect import bisect_right
+from collections.abc import Iterable
+from datetime import date, datetime, time, timedelta, tzinfo
+from typing import TYPE_CHECKING, SupportsInt
+
import pytz as _pytz
-from datetime import date, datetime, time, timedelta
-from bisect import bisect_right
+from babel.core import Locale, default_locale, get_global
+from babel.localedata import LocaleDataDict
+from babel.util import LOCALTZ, UTC
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal, TypeAlias
-from babel.core import default_locale, get_global, Locale
-from babel.util import UTC, LOCALTZ
+ _Instant: TypeAlias = date | time | float | None
+ _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short']
+ _Context: TypeAlias = Literal['format', 'stand-alone']
+ _DtOrTzinfo: TypeAlias = datetime | tzinfo | str | int | time | None
# "If a given short metazone form is known NOT to be understood in a given
# locale and the parent locale has this value such that it would normally
@@ -44,7 +56,7 @@ datetime_ = datetime
time_ = time
-def _get_dt_and_tzinfo(dt_or_tzinfo):
+def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime_ | None, tzinfo]:
"""
Parse a `dt_or_tzinfo` value into a datetime and a tzinfo.
@@ -73,7 +85,7 @@ def _get_dt_and_tzinfo(dt_or_tzinfo):
return dt, tzinfo
-def _get_tz_name(dt_or_tzinfo):
+def _get_tz_name(dt_or_tzinfo: _DtOrTzinfo) -> str:
"""
Get the timezone name out of a time, datetime, or tzinfo object.
@@ -88,7 +100,7 @@ def _get_tz_name(dt_or_tzinfo):
return tzinfo.tzname(dt or datetime.utcnow())
-def _get_datetime(instant):
+def _get_datetime(instant: _Instant) -> datetime_:
"""
Get a datetime out of an "instant" (date, time, datetime, number).
@@ -130,7 +142,7 @@ def _get_datetime(instant):
return instant
-def _ensure_datetime_tzinfo(datetime, tzinfo=None):
+def _ensure_datetime_tzinfo(datetime: datetime_, tzinfo: tzinfo | None = None) -> datetime_:
"""
Ensure the datetime passed has an attached tzinfo.
@@ -159,7 +171,7 @@ def _ensure_datetime_tzinfo(datetime, tzinfo=None):
return datetime
-def _get_time(time, tzinfo=None):
+def _get_time(time: time | datetime | None, tzinfo: tzinfo | None = None) -> time:
"""
Get a timezoned time from a given instant.
@@ -185,7 +197,7 @@ def _get_time(time, tzinfo=None):
return time
-def get_timezone(zone=None):
+def get_timezone(zone: str | _pytz.BaseTzInfo | None = None) -> _pytz.BaseTzInfo:
"""Looks up a timezone by name and returns it. The timezone object
returned comes from ``pytz`` and corresponds to the `tzinfo` interface and
can be used with all of the functions of Babel that operate with dates.
@@ -206,7 +218,7 @@ def get_timezone(zone=None):
raise LookupError(f"Unknown timezone {zone}")
-def get_next_timezone_transition(zone=None, dt=None):
+def get_next_timezone_transition(zone: _pytz.BaseTzInfo | None = None, dt: _Instant = None) -> TimezoneTransition:
"""Given a timezone it will return a :class:`TimezoneTransition` object
that holds the information about the next timezone transition that's going
to happen. For instance this can be used to detect when the next DST
@@ -278,7 +290,7 @@ class TimezoneTransition:
to the :func:`get_next_timezone_transition`.
"""
- def __init__(self, activates, from_tzinfo, to_tzinfo, reference_date=None):
+ def __init__(self, activates: datetime_, from_tzinfo: tzinfo, to_tzinfo: tzinfo, reference_date: datetime_ | None = None):
warnings.warn(
"TimezoneTransition is deprecated and will be "
"removed in the next version of Babel. "
@@ -292,30 +304,31 @@ class TimezoneTransition:
self.reference_date = reference_date
@property
- def from_tz(self):
+ def from_tz(self) -> str:
"""The name of the timezone before the transition."""
return self.from_tzinfo._tzname
@property
- def to_tz(self):
+ def to_tz(self) -> str:
"""The name of the timezone after the transition."""
return self.to_tzinfo._tzname
@property
- def from_offset(self):
+ def from_offset(self) -> int:
"""The UTC offset in seconds before the transition."""
return int(self.from_tzinfo._utcoffset.total_seconds())
@property
- def to_offset(self):
+ def to_offset(self) -> int:
"""The UTC offset in seconds after the transition."""
return int(self.to_tzinfo._utcoffset.total_seconds())
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<TimezoneTransition {self.from_tz} -> {self.to_tz} ({self.activates})>"
-def get_period_names(width='wide', context='stand-alone', locale=LC_TIME):
+def get_period_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+ context: _Context = 'stand-alone', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
"""Return the names for day periods (AM/PM) used by the locale.
>>> get_period_names(locale='en_US')['am']
@@ -328,7 +341,8 @@ def get_period_names(width='wide', context='stand-alone', locale=LC_TIME):
return Locale.parse(locale).day_periods[context][width]
-def get_day_names(width='wide', context='format', locale=LC_TIME):
+def get_day_names(width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide',
+ context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
"""Return the day names used by the locale for the specified format.
>>> get_day_names('wide', locale='en_US')[1]
@@ -347,7 +361,8 @@ def get_day_names(width='wide', context='format', locale=LC_TIME):
return Locale.parse(locale).days[context][width]
-def get_month_names(width='wide', context='format', locale=LC_TIME):
+def get_month_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+ context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
"""Return the month names used by the locale for the specified format.
>>> get_month_names('wide', locale='en_US')[1]
@@ -364,7 +379,8 @@ def get_month_names(width='wide', context='format', locale=LC_TIME):
return Locale.parse(locale).months[context][width]
-def get_quarter_names(width='wide', context='format', locale=LC_TIME):
+def get_quarter_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+ context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
"""Return the quarter names used by the locale for the specified format.
>>> get_quarter_names('wide', locale='en_US')[1]
@@ -381,7 +397,8 @@ def get_quarter_names(width='wide', context='format', locale=LC_TIME):
return Locale.parse(locale).quarters[context][width]
-def get_era_names(width='wide', locale=LC_TIME):
+def get_era_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+ locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
"""Return the era names used by the locale for the specified format.
>>> get_era_names('wide', locale='en_US')[1]
@@ -395,7 +412,7 @@ def get_era_names(width='wide', locale=LC_TIME):
return Locale.parse(locale).eras[width]
-def get_date_format(format='medium', locale=LC_TIME):
+def get_date_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
"""Return the date formatting patterns used by the locale for the specified
format.
@@ -411,7 +428,7 @@ def get_date_format(format='medium', locale=LC_TIME):
return Locale.parse(locale).date_formats[format]
-def get_datetime_format(format='medium', locale=LC_TIME):
+def get_datetime_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
"""Return the datetime formatting patterns used by the locale for the
specified format.
@@ -428,7 +445,7 @@ def get_datetime_format(format='medium', locale=LC_TIME):
return patterns[format]
-def get_time_format(format='medium', locale=LC_TIME):
+def get_time_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
"""Return the time formatting patterns used by the locale for the specified
format.
@@ -444,7 +461,8 @@ def get_time_format(format='medium', locale=LC_TIME):
return Locale.parse(locale).time_formats[format]
-def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME, return_z=False):
+def get_timezone_gmt(datetime: _Instant = None, width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long',
+ locale: Locale | str | None = LC_TIME, return_z: bool = False) -> str:
"""Return the timezone associated with the given `datetime` object formatted
as string indicating the offset from GMT.
@@ -498,7 +516,8 @@ def get_timezone_gmt(datetime=None, width='long', locale=LC_TIME, return_z=False
return pattern % (hours, seconds // 60)
-def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False):
+def get_timezone_location(dt_or_tzinfo: _DtOrTzinfo = None, locale: Locale | str | None = LC_TIME,
+ return_city: bool = False) -> str:
u"""Return a representation of the given timezone using "location format".
The result depends on both the local display name of the country and the
@@ -574,8 +593,9 @@ def get_timezone_location(dt_or_tzinfo=None, locale=LC_TIME, return_city=False):
})
-def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
- locale=LC_TIME, zone_variant=None, return_zone=False):
+def get_timezone_name(dt_or_tzinfo: _DtOrTzinfo = None, width: Literal['long', 'short'] = 'long', uncommon: bool = False,
+ locale: Locale | str | None = LC_TIME, zone_variant: Literal['generic', 'daylight', 'standard'] | None = None,
+ return_zone: bool = False) -> str:
r"""Return the localized display name for the given timezone. The timezone
may be specified using a ``datetime`` or `tzinfo` object.
@@ -693,7 +713,8 @@ def get_timezone_name(dt_or_tzinfo=None, width='long', uncommon=False,
return get_timezone_location(dt_or_tzinfo, locale=locale)
-def format_date(date=None, format='medium', locale=LC_TIME):
+def format_date(date: date | None = None, format: _PredefinedTimeFormat | str = 'medium',
+ locale: Locale | str | None = LC_TIME) -> str:
"""Return a date formatted according to the given pattern.
>>> d = date(2007, 4, 1)
@@ -726,8 +747,8 @@ def format_date(date=None, format='medium', locale=LC_TIME):
return pattern.apply(date, locale)
-def format_datetime(datetime=None, format='medium', tzinfo=None,
- locale=LC_TIME):
+def format_datetime(datetime: _Instant = None, format: _PredefinedTimeFormat | str = 'medium', tzinfo: tzinfo | None = None,
+ locale: Locale | str | None = LC_TIME) -> str:
r"""Return a date formatted according to the given pattern.
>>> dt = datetime(2007, 4, 1, 15, 30)
@@ -764,7 +785,8 @@ def format_datetime(datetime=None, format='medium', tzinfo=None,
return parse_pattern(format).apply(datetime, locale)
-def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME):
+def format_time(time: time | datetime | float | None = None, format: _PredefinedTimeFormat | str = 'medium',
+ tzinfo: tzinfo | None = None, locale: Locale | str | None = LC_TIME) -> str:
r"""Return a time formatted according to the given pattern.
>>> t = time(15, 30)
@@ -827,7 +849,8 @@ def format_time(time=None, format='medium', tzinfo=None, locale=LC_TIME):
return parse_pattern(format).apply(time, locale)
-def format_skeleton(skeleton, datetime=None, tzinfo=None, fuzzy=True, locale=LC_TIME):
+def format_skeleton(skeleton: str, datetime: _Instant = None, tzinfo: tzinfo | None = None,
+ fuzzy: bool = True, locale: Locale | str | None = LC_TIME) -> str:
r"""Return a time and/or date formatted according to the given pattern.
The skeletons are defined in the CLDR data and provide more flexibility
@@ -865,7 +888,7 @@ def format_skeleton(skeleton, datetime=None, tzinfo=None, fuzzy=True, locale=LC_
return format_datetime(datetime, format, tzinfo, locale)
-TIMEDELTA_UNITS = (
+TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = (
('year', 3600 * 24 * 365),
('month', 3600 * 24 * 30),
('week', 3600 * 24 * 7),
@@ -876,9 +899,10 @@ TIMEDELTA_UNITS = (
)
-def format_timedelta(delta, granularity='second', threshold=.85,
- add_direction=False, format='long',
- locale=LC_TIME):
+def format_timedelta(delta: timedelta | int,
+ granularity: Literal['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] = 'second',
+ threshold: float = .85, add_direction: bool = False, format: Literal['narrow', 'short', 'medium', 'long'] = 'long',
+ locale: Locale | str | None = LC_TIME) -> str:
"""Return a time delta according to the rules of the given locale.
>>> format_timedelta(timedelta(weeks=12), locale='en_US')
@@ -977,7 +1001,8 @@ def format_timedelta(delta, granularity='second', threshold=.85,
return u''
-def _format_fallback_interval(start, end, skeleton, tzinfo, locale):
+def _format_fallback_interval(start: _Instant, end: _Instant, skeleton: str | None, tzinfo: tzinfo | None,
+ locale: Locale | str | None = LC_TIME) -> str:
if skeleton in locale.datetime_skeletons: # Use the given skeleton
format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
elif all((isinstance(d, date) and not isinstance(d, datetime)) for d in (start, end)): # Both are just dates
@@ -1000,7 +1025,8 @@ def _format_fallback_interval(start, end, skeleton, tzinfo, locale):
)
-def format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=LC_TIME):
+def format_interval(start: _Instant, end: _Instant, skeleton: str | None = None, tzinfo: tzinfo | None = None,
+ fuzzy: bool = True, locale: Locale | str | None = LC_TIME) -> str:
"""
Format an interval between two instants according to the locale's rules.
@@ -1098,7 +1124,8 @@ def format_interval(start, end, skeleton=None, tzinfo=None, fuzzy=True, locale=L
return _format_fallback_interval(start, end, skeleton, tzinfo, locale)
-def get_period_id(time, tzinfo=None, type=None, locale=LC_TIME):
+def get_period_id(time: _Instant, tzinfo: _pytz.BaseTzInfo | None = None, type: Literal['selection'] | None = None,
+ locale: Locale | str | None = LC_TIME) -> str:
"""
Get the day period ID for a given time.
@@ -1172,7 +1199,7 @@ class ParseError(ValueError):
pass
-def parse_date(string, locale=LC_TIME, format='medium'):
+def parse_date(string: str, locale: Locale | str | None = LC_TIME, format: _PredefinedTimeFormat = 'medium') -> date:
"""Parse a date from a string.
This function first tries to interpret the string as ISO-8601
@@ -1232,7 +1259,7 @@ def parse_date(string, locale=LC_TIME, format='medium'):
return date(year, month, day)
-def parse_time(string, locale=LC_TIME, format='medium'):
+def parse_time(string: str, locale: Locale | str | None = LC_TIME, format: _PredefinedTimeFormat = 'medium') -> time:
"""Parse a time from a string.
This function uses the time format for the locale as a hint to determine
@@ -1284,36 +1311,36 @@ def parse_time(string, locale=LC_TIME, format='medium'):
class DateTimePattern:
- def __init__(self, pattern, format):
+ def __init__(self, pattern: str, format: DateTimeFormat):
self.pattern = pattern
self.format = format
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{type(self).__name__} {self.pattern!r}>"
- def __str__(self):
+ def __str__(self) -> str:
pat = self.pattern
return pat
- def __mod__(self, other):
+ def __mod__(self, other: DateTimeFormat) -> str:
if type(other) is not DateTimeFormat:
return NotImplemented
return self.format % other
- def apply(self, datetime, locale):
+ def apply(self, datetime: date | time, locale: Locale | str | None) -> str:
return self % DateTimeFormat(datetime, locale)
class DateTimeFormat:
- def __init__(self, value, locale):
+ def __init__(self, value: date | time, locale: Locale | str):
assert isinstance(value, (date, datetime, time))
if isinstance(value, (datetime, time)) and value.tzinfo is None:
value = value.replace(tzinfo=UTC)
self.value = value
self.locale = Locale.parse(locale)
- def __getitem__(self, name):
+ def __getitem__(self, name: str) -> str:
char = name[0]
num = len(name)
if char == 'G':
@@ -1363,7 +1390,7 @@ class DateTimeFormat:
else:
raise KeyError(f"Unsupported date/time field {char!r}")
- def extract(self, char):
+ def extract(self, char: str) -> int:
char = str(char)[0]
if char == 'y':
return self.value.year
@@ -1382,12 +1409,12 @@ class DateTimeFormat:
else:
raise NotImplementedError(f"Not implemented: extracting {char!r} from {self.value!r}")
- def format_era(self, char, num):
+ def format_era(self, char: str, num: int) -> str:
width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)]
era = int(self.value.year >= 0)
return get_era_names(width, self.locale)[era]
- def format_year(self, char, num):
+ def format_year(self, char: str, num: int) -> str:
value = self.value.year
if char.isupper():
value = self.value.isocalendar()[0]
@@ -1396,7 +1423,7 @@ class DateTimeFormat:
year = year[-2:]
return year
- def format_quarter(self, char, num):
+ def format_quarter(self, char: str, num: int) -> str:
quarter = (self.value.month - 1) // 3 + 1
if num <= 2:
return '%0*d' % (num, quarter)
@@ -1404,14 +1431,14 @@ class DateTimeFormat:
context = {'Q': 'format', 'q': 'stand-alone'}[char]
return get_quarter_names(width, context, self.locale)[quarter]
- def format_month(self, char, num):
+ def format_month(self, char: str, num: int) -> str:
if num <= 2:
return '%0*d' % (num, self.value.month)
width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[num]
context = {'M': 'format', 'L': 'stand-alone'}[char]
return get_month_names(width, context, self.locale)[self.value.month]
- def format_week(self, char, num):
+ 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)
@@ -1427,7 +1454,7 @@ class DateTimeFormat:
week = self.get_week_number(date.day, date.weekday())
return str(week)
- def format_weekday(self, char='E', num=4):
+ def format_weekday(self, char: str = 'E', num: int = 4) -> str:
"""
Return weekday from parsed datetime according to format pattern.
@@ -1467,13 +1494,13 @@ class DateTimeFormat:
context = 'format'
return get_day_names(width, context, self.locale)[weekday]
- def format_day_of_year(self, num):
+ def format_day_of_year(self, num: int) -> str:
return self.format(self.get_day_of_year(), num)
- def format_day_of_week_in_month(self):
+ def format_day_of_week_in_month(self) -> str:
return str((self.value.day - 1) // 7 + 1)
- def format_period(self, char, num):
+ def format_period(self, char: str, num: int) -> str:
"""
Return period from parsed datetime according to format pattern.
@@ -1515,7 +1542,7 @@ class DateTimeFormat:
return period_names[period]
raise ValueError(f"Could not format period {period} in {self.locale}")
- def format_frac_seconds(self, num):
+ def format_frac_seconds(self, num: int) -> str:
""" Return fractional seconds.
Rounds the time's microseconds to the precision given by the number \
@@ -1529,7 +1556,7 @@ class DateTimeFormat:
self.value.minute * 60000 + self.value.hour * 3600000
return self.format(msecs, num)
- def format_timezone(self, char, num):
+ def format_timezone(self, char: str, num: int) -> str:
width = {3: 'short', 4: 'long', 5: 'iso8601'}[max(3, num)]
if char == 'z':
return get_timezone_name(self.value, width, locale=self.locale)
@@ -1572,15 +1599,15 @@ class DateTimeFormat:
elif num in (3, 5):
return get_timezone_gmt(self.value, width='iso8601', locale=self.locale)
- def format(self, value, length):
+ def format(self, value: SupportsInt, length: int) -> str:
return '%0*d' % (length, value)
- def get_day_of_year(self, date=None):
+ def get_day_of_year(self, date: date | None = None) -> int:
if date is None:
date = self.value
return (date - date.replace(month=1, day=1)).days + 1
- def get_week_number(self, day_of_period, day_of_week=None):
+ 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.
@@ -1625,7 +1652,7 @@ class DateTimeFormat:
return week_number
-PATTERN_CHARS = {
+PATTERN_CHARS: dict[str, list[int] | None] = {
'G': [1, 2, 3, 4, 5], # era
'y': None, 'Y': None, 'u': None, # year
'Q': [1, 2, 3, 4, 5], 'q': [1, 2, 3, 4, 5], # quarter
@@ -1649,7 +1676,7 @@ PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx"
_pattern_cache = {}
-def parse_pattern(pattern):
+def parse_pattern(pattern: str) -> DateTimePattern:
"""Parse date, time, and datetime format patterns.
>>> parse_pattern("MMMMd").format
@@ -1694,7 +1721,7 @@ def parse_pattern(pattern):
return pat
-def tokenize_pattern(pattern):
+def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]:
"""
Tokenize date format patterns.
@@ -1763,7 +1790,7 @@ def tokenize_pattern(pattern):
return result
-def untokenize_pattern(tokens):
+def untokenize_pattern(tokens: Iterable[tuple[str, str | tuple[str, int]]]) -> str:
"""
Turn a date format pattern token stream back into a string.
@@ -1784,7 +1811,7 @@ def untokenize_pattern(tokens):
return "".join(output)
-def split_interval_pattern(pattern):
+def split_interval_pattern(pattern: str) -> list[str]:
"""
Split an interval-describing datetime pattern into multiple pieces.
@@ -1822,7 +1849,7 @@ def split_interval_pattern(pattern):
return [untokenize_pattern(tokens) for tokens in parts]
-def match_skeleton(skeleton, options, allow_different_fields=False):
+def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields: bool = False) -> str | None:
"""
Find the closest match for the given datetime skeleton among the options given.
diff --git a/babel/languages.py b/babel/languages.py
index cac59c1..564f555 100644
--- a/babel/languages.py
+++ b/babel/languages.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
from babel.core import get_global
-def get_official_languages(territory, regional=False, de_facto=False):
+def get_official_languages(territory: str, regional: bool = False, de_facto: bool = False) -> tuple[str, ...]:
"""
Get the official language(s) for the given territory.
@@ -41,7 +43,7 @@ def get_official_languages(territory, regional=False, de_facto=False):
return tuple(lang for _, lang in pairs)
-def get_territory_language_info(territory):
+def get_territory_language_info(territory: str) -> dict[str, dict[str, float | str | None]]:
"""
Get a dictionary of language information for a territory.
diff --git a/babel/lists.py b/babel/lists.py
index ea983ef..97fc49a 100644
--- a/babel/lists.py
+++ b/babel/lists.py
@@ -13,13 +13,22 @@
:copyright: (c) 2015-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
from babel.core import Locale, default_locale
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+
DEFAULT_LOCALE = default_locale()
-def format_list(lst, style='standard', locale=DEFAULT_LOCALE):
+def format_list(lst: Sequence[str],
+ style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard',
+ locale: Locale | str | None = DEFAULT_LOCALE) -> str:
"""
Format the items in `lst` as a list.
diff --git a/babel/localedata.py b/babel/localedata.py
index 8ec8f4a..0d3508d 100644
--- a/babel/localedata.py
+++ b/babel/localedata.py
@@ -11,22 +11,25 @@
:license: BSD, see LICENSE for more details.
"""
-import pickle
+from __future__ import annotations
+
import os
+import pickle
import re
import sys
import threading
from collections import abc
+from collections.abc import Iterator, Mapping, MutableMapping
from itertools import chain
+from typing import Any
-
-_cache = {}
+_cache: dict[str, Any] = {}
_cache_lock = threading.RLock()
_dirname = os.path.join(os.path.dirname(__file__), 'locale-data')
_windows_reserved_name_re = re.compile("^(con|prn|aux|nul|com[0-9]|lpt[0-9])$", re.I)
-def normalize_locale(name):
+def normalize_locale(name: str) -> str | None:
"""Normalize a locale ID by stripping spaces and apply proper casing.
Returns the normalized locale ID string or `None` if the ID is not
@@ -40,7 +43,7 @@ def normalize_locale(name):
return locale_id
-def resolve_locale_filename(name):
+def resolve_locale_filename(name: os.PathLike[str] | str) -> str:
"""
Resolve a locale identifier to a `.dat` path on disk.
"""
@@ -56,7 +59,7 @@ def resolve_locale_filename(name):
return os.path.join(_dirname, f"{name}.dat")
-def exists(name):
+def exists(name: str) -> bool:
"""Check whether locale data is available for the given locale.
Returns `True` if it exists, `False` otherwise.
@@ -71,7 +74,7 @@ def exists(name):
return True if file_found else bool(normalize_locale(name))
-def locale_identifiers():
+def locale_identifiers() -> list[str]:
"""Return a list of all locale identifiers for which locale data is
available.
@@ -95,7 +98,7 @@ def locale_identifiers():
return data
-def load(name, merge_inherited=True):
+def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]:
"""Load the locale data for the given locale.
The locale data is a dictionary that contains much of the data defined by
@@ -150,7 +153,7 @@ def load(name, merge_inherited=True):
_cache_lock.release()
-def merge(dict1, dict2):
+def merge(dict1: MutableMapping[Any, Any], dict2: Mapping[Any, Any]) -> None:
"""Merge the data from `dict2` into the `dict1` dictionary, making copies
of nested dictionaries.
@@ -190,13 +193,13 @@ class Alias:
as specified by the `keys`.
"""
- def __init__(self, keys):
+ def __init__(self, keys: tuple[str, ...]) -> None:
self.keys = tuple(keys)
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{type(self).__name__} {self.keys!r}>"
- def resolve(self, data):
+ def resolve(self, data: Mapping[str | int | None, Any]) -> Mapping[str | int | None, Any]:
"""Resolve the alias based on the given data.
This is done recursively, so if one alias resolves to a second alias,
@@ -221,19 +224,19 @@ class LocaleDataDict(abc.MutableMapping):
values.
"""
- def __init__(self, data, base=None):
+ def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None):
self._data = data
if base is None:
base = data
self.base = base
- def __len__(self):
+ def __len__(self) -> int:
return len(self._data)
- def __iter__(self):
+ def __iter__(self) -> Iterator[str | int | None]:
return iter(self._data)
- def __getitem__(self, key):
+ def __getitem__(self, key: str | int | None) -> Any:
orig = val = self._data[key]
if isinstance(val, Alias): # resolve an alias
val = val.resolve(self.base)
@@ -241,17 +244,17 @@ class LocaleDataDict(abc.MutableMapping):
alias, others = val
val = alias.resolve(self.base).copy()
merge(val, others)
- if type(val) is dict: # Return a nested alias-resolving dict
+ if isinstance(val, dict): # Return a nested alias-resolving dict
val = LocaleDataDict(val, base=self.base)
if val is not orig:
self._data[key] = val
return val
- def __setitem__(self, key, value):
+ def __setitem__(self, key: str | int | None, value: Any) -> None:
self._data[key] = value
- def __delitem__(self, key):
+ def __delitem__(self, key: str | int | None) -> None:
del self._data[key]
- def copy(self):
+ def copy(self) -> LocaleDataDict:
return LocaleDataDict(self._data.copy(), base=self.base)
diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py
index 7e626a0..ffe2d49 100644
--- a/babel/localtime/__init__.py
+++ b/babel/localtime/__init__.py
@@ -10,12 +10,12 @@
"""
import sys
-import pytz
import time
-from datetime import timedelta
-from datetime import tzinfo
+from datetime import datetime, timedelta, tzinfo
from threading import RLock
+import pytz
+
if sys.platform == 'win32':
from babel.localtime._win32 import _get_localzone
else:
@@ -37,22 +37,22 @@ ZERO = timedelta(0)
class _FallbackLocalTimezone(tzinfo):
- def utcoffset(self, dt):
+ def utcoffset(self, dt: datetime) -> timedelta:
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
- def dst(self, dt):
+ def dst(self, dt: datetime) -> timedelta:
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
- def tzname(self, dt):
+ def tzname(self, dt: datetime) -> str:
return time.tzname[self._isdst(dt)]
- def _isdst(self, dt):
+ def _isdst(self, dt: datetime) -> bool:
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, -1)
@@ -61,7 +61,7 @@ class _FallbackLocalTimezone(tzinfo):
return tt.tm_isdst > 0
-def get_localzone():
+def get_localzone() -> pytz.BaseTzInfo:
"""Returns the current underlying local timezone object.
Generally this function does not need to be used, it's a
better idea to use the :data:`LOCALTZ` singleton instead.
diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py
index 3d1480e..beb7f60 100644
--- a/babel/localtime/_unix.py
+++ b/babel/localtime/_unix.py
@@ -3,7 +3,7 @@ import re
import pytz
-def _tz_from_env(tzenv):
+def _tz_from_env(tzenv: str) -> pytz.BaseTzInfo:
if tzenv[0] == ':':
tzenv = tzenv[1:]
@@ -23,7 +23,7 @@ def _tz_from_env(tzenv):
"Please use a timezone in the form of Continent/City")
-def _get_localzone(_root='/'):
+def _get_localzone(_root: str = '/') -> pytz.BaseTzInfo:
"""Tries to find the local timezone configuration.
This method prefers finding the timezone name and passing that to pytz,
over passing in the localtime file, as in the later case the zoneinfo
diff --git a/babel/localtime/_win32.py b/babel/localtime/_win32.py
index a4f6d55..98d5170 100644
--- a/babel/localtime/_win32.py
+++ b/babel/localtime/_win32.py
@@ -1,23 +1,27 @@
+from __future__ import annotations
+
try:
import winreg
except ImportError:
winreg = None
-from babel.core import get_global
+from typing import Any, Dict, cast
+
import pytz
+from babel.core import get_global
# When building the cldr data on windows this module gets imported.
# Because at that point there is no global.dat yet this call will
# fail. We want to catch it down in that case then and just assume
# the mapping was empty.
try:
- tz_names = get_global('windows_zone_mapping')
+ tz_names: dict[str, str] = cast(Dict[str, str], get_global('windows_zone_mapping'))
except RuntimeError:
tz_names = {}
-def valuestodict(key):
+def valuestodict(key) -> dict[str, Any]:
"""Convert a registry key's values to a dictionary."""
dict = {}
size = winreg.QueryInfoKey(key)[1]
@@ -27,7 +31,7 @@ def valuestodict(key):
return dict
-def get_localzone_name():
+def get_localzone_name() -> str:
# Windows is special. It has unique time zone names (in several
# meanings of the word) available, but unfortunately, they can be
# translated to the language of the operating system, so we need to
@@ -86,7 +90,7 @@ def get_localzone_name():
return timezone
-def _get_localzone():
+def _get_localzone() -> pytz.BaseTzInfo:
if winreg is None:
raise pytz.UnknownTimeZoneError(
'Runtime support not available')
diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py
index 22ce660..0801de3 100644
--- a/babel/messages/catalog.py
+++ b/babel/messages/catalog.py
@@ -7,14 +7,17 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
import re
from collections import OrderedDict
+from collections.abc import Generator, Iterable, Iterator
from datetime import datetime, time as time_
from difflib import get_close_matches
from email import message_from_string
from copy import copy
+from typing import TYPE_CHECKING
from babel import __version__ as VERSION
from babel.core import Locale, UnknownLocaleError
@@ -22,6 +25,11 @@ from babel.dates import format_datetime
from babel.messages.plurals import get_plural
from babel.util import distinct, LOCALTZ, FixedOffsetTimezone, _cmp
+if TYPE_CHECKING:
+ from typing_extensions import TypeAlias
+
+ _MessageID: TypeAlias = str | tuple[str, ...] | list[str]
+
__all__ = ['Message', 'Catalog', 'TranslationError']
@@ -37,7 +45,7 @@ PYTHON_FORMAT = re.compile(r'''
''', re.VERBOSE)
-def _parse_datetime_header(value):
+def _parse_datetime_header(value: str) -> datetime:
match = re.match(r'^(?P<datetime>.*?)(?P<tzoffset>[+-]\d{4})?$', value)
dt = datetime.strptime(match.group('datetime'), '%Y-%m-%d %H:%M')
@@ -70,8 +78,18 @@ def _parse_datetime_header(value):
class Message:
"""Representation of a single message in a catalog."""
- def __init__(self, id, string=u'', locations=(), flags=(), auto_comments=(),
- user_comments=(), previous_id=(), lineno=None, context=None):
+ def __init__(
+ self,
+ id: _MessageID,
+ string: _MessageID | None = u'',
+ locations: Iterable[tuple[str, int]] = (),
+ flags: Iterable[str] = (),
+ auto_comments: Iterable[str] = (),
+ user_comments: Iterable[str] = (),
+ previous_id: _MessageID = (),
+ lineno: int | None = None,
+ context: str | None = None,
+ ) -> None:
"""Create the message object.
:param id: the message ID, or a ``(singular, plural)`` tuple for
@@ -107,10 +125,10 @@ class Message:
self.lineno = lineno
self.context = context
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{type(self).__name__} {self.id!r} (flags: {list(self.flags)!r})>"
- def __cmp__(self, other):
+ def __cmp__(self, other: object) -> int:
"""Compare Messages, taking into account plural ids"""
def values_to_compare(obj):
if isinstance(obj, Message) and obj.pluralizable:
@@ -118,38 +136,38 @@ class Message:
return obj.id, obj.context or ''
return _cmp(values_to_compare(self), values_to_compare(other))
- def __gt__(self, other):
+ def __gt__(self, other: object) -> bool:
return self.__cmp__(other) > 0
- def __lt__(self, other):
+ def __lt__(self, other: object) -> bool:
return self.__cmp__(other) < 0
- def __ge__(self, other):
+ def __ge__(self, other: object) -> bool:
return self.__cmp__(other) >= 0
- def __le__(self, other):
+ def __le__(self, other: object) -> bool:
return self.__cmp__(other) <= 0
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return self.__cmp__(other) == 0
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return self.__cmp__(other) != 0
- def is_identical(self, other):
+ def is_identical(self, other: Message) -> bool:
"""Checks whether messages are identical, taking into account all
properties.
"""
assert isinstance(other, Message)
return self.__dict__ == other.__dict__
- def clone(self):
+ def clone(self) -> Message:
return Message(*map(copy, (self.id, self.string, self.locations,
self.flags, self.auto_comments,
self.user_comments, self.previous_id,
self.lineno, self.context)))
- def check(self, catalog=None):
+ def check(self, catalog: Catalog | None = None) -> list[TranslationError]:
"""Run various validation checks on the message. Some validations
are only performed if the catalog is provided. This method returns
a sequence of `TranslationError` objects.
@@ -160,7 +178,7 @@ class Message:
in a catalog.
"""
from babel.messages.checkers import checkers
- errors = []
+ errors: list[TranslationError] = []
for checker in checkers:
try:
checker(catalog, self)
@@ -169,7 +187,7 @@ class Message:
return errors
@property
- def fuzzy(self):
+ def fuzzy(self) -> bool:
"""Whether the translation is fuzzy.
>>> Message('foo').fuzzy
@@ -184,7 +202,7 @@ class Message:
return 'fuzzy' in self.flags
@property
- def pluralizable(self):
+ def pluralizable(self) -> bool:
"""Whether the message is plurizable.
>>> Message('foo').pluralizable
@@ -196,7 +214,7 @@ class Message:
return isinstance(self.id, (list, tuple))
@property
- def python_format(self):
+ def python_format(self) -> bool:
"""Whether the message contains Python-style parameters.
>>> Message('foo %(name)s bar').python_format
@@ -223,7 +241,7 @@ DEFAULT_HEADER = u"""\
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#"""
-def parse_separated_header(value: str):
+def parse_separated_header(value: str) -> dict[str, str]:
# Adapted from https://peps.python.org/pep-0594/#cgi
from email.message import Message
m = Message()
@@ -234,11 +252,22 @@ def parse_separated_header(value: str):
class Catalog:
"""Representation of a message catalog."""
- def __init__(self, locale=None, domain=None, header_comment=DEFAULT_HEADER,
- project=None, version=None, copyright_holder=None,
- msgid_bugs_address=None, creation_date=None,
- revision_date=None, last_translator=None, language_team=None,
- charset=None, fuzzy=True):
+ def __init__(
+ self,
+ locale: str | Locale | None = None,
+ domain: str | None = None,
+ header_comment: str | None = DEFAULT_HEADER,
+ project: str | None = None,
+ version: str | None = None,
+ copyright_holder: str | None = None,
+ msgid_bugs_address: str | None = None,
+ creation_date: datetime | str | None = None,
+ revision_date: datetime | time_ | float | str | None = None,
+ last_translator: str | None = None,
+ language_team: str | None = None,
+ charset: str | None = None,
+ fuzzy: bool = True,
+ ) -> None:
"""Initialize the catalog object.
:param locale: the locale identifier or `Locale` object, or `None`
@@ -262,7 +291,7 @@ class Catalog:
self.domain = domain
self.locale = locale
self._header_comment = header_comment
- self._messages = OrderedDict()
+ self._messages: OrderedDict[str | tuple[str, str], Message] = OrderedDict()
self.project = project or 'PROJECT'
self.version = version or 'VERSION'
@@ -288,11 +317,12 @@ class Catalog:
self.revision_date = revision_date
self.fuzzy = fuzzy
- self.obsolete = OrderedDict() # Dictionary of obsolete messages
+ # Dictionary of obsolete messages
+ self.obsolete: OrderedDict[str | tuple[str, str], Message] = OrderedDict()
self._num_plurals = None
self._plural_expr = None
- def _set_locale(self, locale):
+ def _set_locale(self, locale: Locale | str | None) -> None:
if locale is None:
self._locale_identifier = None
self._locale = None
@@ -313,16 +343,16 @@ class Catalog:
raise TypeError(f"`locale` must be a Locale, a locale identifier string, or None; got {locale!r}")
- def _get_locale(self):
+ def _get_locale(self) -> Locale | None:
return self._locale
- def _get_locale_identifier(self):
+ def _get_locale_identifier(self) -> str | None:
return self._locale_identifier
locale = property(_get_locale, _set_locale)
locale_identifier = property(_get_locale_identifier)
- def _get_header_comment(self):
+ def _get_header_comment(self) -> str:
comment = self._header_comment
year = datetime.now(LOCALTZ).strftime('%Y')
if hasattr(self.revision_date, 'strftime'):
@@ -336,7 +366,7 @@ class Catalog:
comment = comment.replace("Translations template", f"{locale_name} translations")
return comment
- def _set_header_comment(self, string):
+ def _set_header_comment(self, string: str | None) -> None:
self._header_comment = string
header_comment = property(_get_header_comment, _set_header_comment, doc="""\
@@ -372,8 +402,8 @@ class Catalog:
:type: `unicode`
""")
- def _get_mime_headers(self):
- headers = []
+ def _get_mime_headers(self) -> list[tuple[str, str]]:
+ headers: list[tuple[str, str]] = []
headers.append(("Project-Id-Version", f"{self.project} {self.version}"))
headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address))
headers.append(('POT-Creation-Date',
@@ -402,14 +432,14 @@ class Catalog:
headers.append(("Generated-By", f"Babel {VERSION}\n"))
return headers
- def _force_text(self, s, encoding='utf-8', errors='strict'):
+ def _force_text(self, s: str | bytes, encoding: str = 'utf-8', errors: str = 'strict') -> str:
if isinstance(s, str):
return s
if isinstance(s, bytes):
return s.decode(encoding, errors)
return str(s)
- def _set_mime_headers(self, headers):
+ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None:
for name, value in headers:
name = self._force_text(name.lower(), encoding=self.charset)
value = self._force_text(value, encoding=self.charset)
@@ -493,7 +523,7 @@ class Catalog:
""")
@property
- def num_plurals(self):
+ def num_plurals(self) -> int:
"""The number of plurals used by the catalog or locale.
>>> Catalog(locale='en').num_plurals
@@ -510,7 +540,7 @@ class Catalog:
return self._num_plurals
@property
- def plural_expr(self):
+ def plural_expr(self) -> str:
"""The plural expression used by the catalog or locale.
>>> Catalog(locale='en').plural_expr
@@ -529,7 +559,7 @@ class Catalog:
return self._plural_expr
@property
- def plural_forms(self):
+ def plural_forms(self) -> str:
"""Return the plural forms declaration for the locale.
>>> Catalog(locale='en').plural_forms
@@ -540,17 +570,17 @@ class Catalog:
:type: `str`"""
return f"nplurals={self.num_plurals}; plural={self.plural_expr};"
- def __contains__(self, id):
+ def __contains__(self, id: _MessageID) -> bool:
"""Return whether the catalog has a message with the specified ID."""
return self._key_for(id) in self._messages
- def __len__(self):
+ def __len__(self) -> int:
"""The number of messages in the catalog.
This does not include the special ``msgid ""`` entry."""
return len(self._messages)
- def __iter__(self):
+ def __iter__(self) -> Iterator[Message]:
"""Iterates through all the entries in the catalog, in the order they
were added, yielding a `Message` object for every entry.
@@ -565,24 +595,24 @@ class Catalog:
for key in self._messages:
yield self._messages[key]
- def __repr__(self):
+ def __repr__(self) -> str:
locale = ''
if self.locale:
locale = f" {self.locale}"
return f"<{type(self).__name__} {self.domain!r}{locale}>"
- def __delitem__(self, id):
+ def __delitem__(self, id: _MessageID) -> None:
"""Delete the message with the specified ID."""
self.delete(id)
- def __getitem__(self, id):
+ def __getitem__(self, id: _MessageID) -> Message:
"""Return the message with the specified ID.
:param id: the message ID
"""
return self.get(id)
- def __setitem__(self, id, message):
+ def __setitem__(self, id: _MessageID, message: Message) -> None:
"""Add or update the message with the specified ID.
>>> catalog = Catalog()
@@ -631,8 +661,18 @@ class Catalog:
f"Expected sequence but got {type(message.string)}"
self._messages[key] = message
- def add(self, id, string=None, locations=(), flags=(), auto_comments=(),
- user_comments=(), previous_id=(), lineno=None, context=None):
+ def add(
+ self,
+ id: _MessageID,
+ string: _MessageID | None = None,
+ locations: Iterable[tuple[str, int]] = (),
+ flags: Iterable[str] = (),
+ auto_comments: Iterable[str] = (),
+ user_comments: Iterable[str] = (),
+ previous_id: _MessageID = (),
+ lineno: int | None = None,
+ context: str | None = None,
+ ) -> Message:
"""Add or update the message with the specified ID.
>>> catalog = Catalog()
@@ -664,21 +704,21 @@ class Catalog:
self[id] = message
return message
- def check(self):
+ def check(self) -> Iterable[tuple[Message, list[TranslationError]]]:
"""Run various validation checks on the translations in the catalog.
For every message which fails validation, this method yield a
``(message, errors)`` tuple, where ``message`` is the `Message` object
and ``errors`` is a sequence of `TranslationError` objects.
- :rtype: ``iterator``
+ :rtype: ``generator`` of ``(message, errors)``
"""
for message in self._messages.values():
errors = message.check(catalog=self)
if errors:
yield message, errors
- def get(self, id, context=None):
+ def get(self, id: _MessageID, context: str | None = None) -> Message | None:
"""Return the message with the specified ID and context.
:param id: the message ID
@@ -686,7 +726,7 @@ class Catalog:
"""
return self._messages.get(self._key_for(id, context))
- def delete(self, id, context=None):
+ def delete(self, id: _MessageID, context: str | None = None) -> None:
"""Delete the message with the specified ID and context.
:param id: the message ID
@@ -696,7 +736,12 @@ class Catalog:
if key in self._messages:
del self._messages[key]
- def update(self, template, no_fuzzy_matching=False, update_header_comment=False, keep_user_comments=True):
+ def update(self,
+ template: Catalog,
+ no_fuzzy_matching: bool = False,
+ update_header_comment: bool = False,
+ keep_user_comments: bool = True,
+ ) -> None:
"""Update the catalog based on the given template catalog.
>>> from babel.messages import Catalog
@@ -762,19 +807,21 @@ class Catalog:
}
fuzzy_matches = set()
- def _merge(message, oldkey, newkey):
+ def _merge(message: Message, oldkey: tuple[str, str] | str, newkey: tuple[str, str] | str) -> None:
message = message.clone()
fuzzy = False
if oldkey != newkey:
fuzzy = True
fuzzy_matches.add(oldkey)
oldmsg = messages.get(oldkey)
+ assert oldmsg is not None
if isinstance(oldmsg.id, str):
message.previous_id = [oldmsg.id]
else:
message.previous_id = list(oldmsg.id)
else:
oldmsg = remaining.pop(oldkey, None)
+ assert oldmsg is not None
message.string = oldmsg.string
if keep_user_comments:
@@ -834,7 +881,7 @@ class Catalog:
# used to update the catalog
self.creation_date = template.creation_date
- def _key_for(self, id, context=None):
+ def _key_for(self, id: _MessageID, context: str | None = None) -> tuple[str, str] | str:
"""The key for a message is just the singular ID even for pluralizable
messages, but is a ``(msgid, msgctxt)`` tuple for context-specific
messages.
@@ -846,7 +893,7 @@ class Catalog:
key = (key, context)
return key
- def is_identical(self, other):
+ def is_identical(self, other: Catalog) -> bool:
"""Checks if catalogs are identical, taking into account messages and
headers.
"""
diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py
index 2706c5b..9231c67 100644
--- a/babel/messages/checkers.py
+++ b/babel/messages/checkers.py
@@ -9,8 +9,11 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
-from babel.messages.catalog import TranslationError, PYTHON_FORMAT
+from collections.abc import Callable
+
+from babel.messages.catalog import Catalog, Message, TranslationError, PYTHON_FORMAT
#: list of format chars that are compatible to each other
@@ -21,7 +24,7 @@ _string_format_compatibilities = [
]
-def num_plurals(catalog, message):
+def num_plurals(catalog: Catalog | None, message: Message) -> None:
"""Verify the number of plurals in the translation."""
if not message.pluralizable:
if not isinstance(message.string, str):
@@ -41,7 +44,7 @@ def num_plurals(catalog, message):
catalog.num_plurals)
-def python_format(catalog, message):
+def python_format(catalog: Catalog | None, message: Message) -> None:
"""Verify the format string placeholders in the translation."""
if 'python-format' not in message.flags:
return
@@ -57,7 +60,7 @@ def python_format(catalog, message):
_validate_format(msgid, msgstr)
-def _validate_format(format, alternative):
+def _validate_format(format: str, alternative: str) -> None:
"""Test format string `alternative` against `format`. `format` can be the
msgid of a message and `alternative` one of the `msgstr`\\s. The two
arguments are not interchangeable as `alternative` may contain less
@@ -89,8 +92,8 @@ def _validate_format(format, alternative):
:raises TranslationError: on formatting errors
"""
- def _parse(string):
- result = []
+ def _parse(string: str) -> list[tuple[str, str]]:
+ result: list[tuple[str, str]] = []
for match in PYTHON_FORMAT.finditer(string):
name, format, typechar = match.groups()
if typechar == '%' and name is None:
@@ -98,7 +101,7 @@ def _validate_format(format, alternative):
result.append((name, str(typechar)))
return result
- def _compatible(a, b):
+ def _compatible(a: str, b: str) -> bool:
if a == b:
return True
for set in _string_format_compatibilities:
@@ -106,7 +109,7 @@ def _validate_format(format, alternative):
return True
return False
- def _check_positional(results):
+ def _check_positional(results: list[tuple[str, str]]) -> bool:
positional = None
for name, char in results:
if positional is None:
@@ -152,8 +155,8 @@ def _validate_format(format, alternative):
(name, typechar, type_map[name]))
-def _find_checkers():
- checkers = []
+def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]:
+ checkers: list[Callable[[Catalog | None, Message], object]] = []
try:
from pkg_resources import working_set
except ImportError:
@@ -168,4 +171,4 @@ def _find_checkers():
return checkers
-checkers = _find_checkers()
+checkers: list[Callable[[Catalog | None, Message], object]] = _find_checkers()
diff --git a/babel/messages/extract.py b/babel/messages/extract.py
index c19dd5a..5c331c0 100644
--- a/babel/messages/extract.py
+++ b/babel/messages/extract.py
@@ -15,20 +15,58 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
+
import ast
+from collections.abc import Callable, Collection, Generator, Iterable, Mapping, MutableSequence
import io
import os
import sys
from os.path import relpath
from tokenize import generate_tokens, COMMENT, NAME, OP, STRING
+from typing import Any, TYPE_CHECKING
from babel.util import parse_encoding, parse_future_flags, pathmatch
from textwrap import dedent
+if TYPE_CHECKING:
+ from typing import IO, Protocol
+ from typing_extensions import Final, TypeAlias, TypedDict
+ from _typeshed import SupportsItems, SupportsRead, SupportsReadline
+
+ class _PyOptions(TypedDict, total=False):
+ encoding: str
+
+ class _JSOptions(TypedDict, total=False):
+ encoding: str
+ jsx: bool
+ template_string: bool
+ parse_template_string: bool
+
+ class _FileObj(SupportsRead[bytes], SupportsReadline[bytes], Protocol):
+ def seek(self, __offset: int, __whence: int = ...) -> int: ...
+ def tell(self) -> int: ...
+
+ _Keyword: TypeAlias = tuple[int | tuple[int, int] | tuple[int, str], ...] | None
+
+ # 5-tuple of (filename, lineno, messages, comments, context)
+ _FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None]
+
+ # 4-tuple of (lineno, message, comments, context)
+ _ExtractionResult: TypeAlias = tuple[int, str | tuple[str, ...], list[str], str | None]
+
+ # Required arguments: fileobj, keywords, comment_tags, options
+ # Return value: Iterable of (lineno, message, comments, context)
+ _CallableExtractionMethod: TypeAlias = Callable[
+ [_FileObj | IO[bytes], Mapping[str, _Keyword], Collection[str], Mapping[str, Any]],
+ Iterable[_ExtractionResult],
+ ]
+
+ _ExtractionMethod: TypeAlias = _CallableExtractionMethod | str
-GROUP_NAME = 'babel.extractors'
+GROUP_NAME: Final[str] = 'babel.extractors'
-DEFAULT_KEYWORDS = {
+DEFAULT_KEYWORDS: dict[str, _Keyword] = {
'_': None,
'gettext': None,
'ngettext': (1, 2),
@@ -41,15 +79,15 @@ DEFAULT_KEYWORDS = {
'npgettext': ((1, 'c'), 2, 3)
}
-DEFAULT_MAPPING = [('**.py', 'python')]
+DEFAULT_MAPPING: list[tuple[str, str]] = [('**.py', 'python')]
-def _strip_comment_tags(comments, tags):
+def _strip_comment_tags(comments: MutableSequence[str], tags: Iterable[str]):
"""Helper function for `extract` that strips comment tags from strings
in a list of comment lines. This functions operates in-place.
"""
- def _strip(line):
+ def _strip(line: str):
for tag in tags:
if line.startswith(tag):
return line[len(tag):].strip()
@@ -57,22 +95,22 @@ def _strip_comment_tags(comments, tags):
comments[:] = map(_strip, comments)
-def default_directory_filter(dirpath):
+def default_directory_filter(dirpath: str | os.PathLike[str]) -> bool:
subdir = os.path.basename(dirpath)
# Legacy default behavior: ignore dot and underscore directories
return not (subdir.startswith('.') or subdir.startswith('_'))
def extract_from_dir(
- dirname=None,
- method_map=DEFAULT_MAPPING,
- options_map=None,
- keywords=DEFAULT_KEYWORDS,
- comment_tags=(),
- callback=None,
- strip_comment_tags=False,
- directory_filter=None,
-):
+ dirname: str | os.PathLike[str] | None = None,
+ method_map: Iterable[tuple[str, str]] = DEFAULT_MAPPING,
+ options_map: SupportsItems[str, dict[str, Any]] | None = None,
+ keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,
+ comment_tags: Collection[str] = (),
+ callback: Callable[[str, str, dict[str, Any]], object] | None = None,
+ strip_comment_tags: bool = False,
+ directory_filter: Callable[[str], bool] | None = None,
+) -> Generator[_FileExtractionResult, None, None]:
"""Extract messages from any source files found in the given directory.
This function generates tuples of the form ``(filename, lineno, message,
@@ -172,9 +210,16 @@ def extract_from_dir(
)
-def check_and_call_extract_file(filepath, method_map, options_map,
- callback, keywords, comment_tags,
- strip_comment_tags, dirpath=None):
+def check_and_call_extract_file(
+ filepath: str | os.PathLike[str],
+ method_map: Iterable[tuple[str, str]],
+ options_map: SupportsItems[str, dict[str, Any]],
+ callback: Callable[[str, str, dict[str, Any]], object] | None,
+ keywords: Mapping[str, _Keyword],
+ comment_tags: Collection[str],
+ strip_comment_tags: bool,
+ dirpath: str | os.PathLike[str] | None = None,
+) -> Generator[_FileExtractionResult, None, None]:
"""Checks if the given file matches an extraction method mapping, and if so, calls extract_from_file.
Note that the extraction method mappings are based relative to dirpath.
@@ -229,8 +274,14 @@ def check_and_call_extract_file(filepath, method_map, options_map,
break
-def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS,
- comment_tags=(), options=None, strip_comment_tags=False):
+def extract_from_file(
+ method: _ExtractionMethod,
+ filename: str | os.PathLike[str],
+ keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,
+ comment_tags: Collection[str] = (),
+ options: Mapping[str, Any] | None = None,
+ strip_comment_tags: bool = False,
+) -> list[_ExtractionResult]:
"""Extract messages from a specific file.
This function returns a list of tuples of the form ``(lineno, message, comments, context)``.
@@ -257,8 +308,14 @@ def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS,
options, strip_comment_tags))
-def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(),
- options=None, strip_comment_tags=False):
+def extract(
+ method: _ExtractionMethod,
+ fileobj: _FileObj,
+ keywords: Mapping[str, _Keyword] = DEFAULT_KEYWORDS,
+ comment_tags: Collection[str] = (),
+ options: Mapping[str, Any] | None = None,
+ strip_comment_tags: bool = False,
+) -> Generator[_ExtractionResult, None, None]:
"""Extract messages from the given file-like object using the specified
extraction method.
@@ -391,14 +448,24 @@ def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(),
yield lineno, messages, comments, context
-def extract_nothing(fileobj, keywords, comment_tags, options):
+def extract_nothing(
+ fileobj: _FileObj,
+ keywords: Mapping[str, _Keyword],
+ comment_tags: Collection[str],
+ options: Mapping[str, Any],
+) -> list[_ExtractionResult]:
"""Pseudo extractor that does not actually extract anything, but simply
returns an empty list.
"""
return []
-def extract_python(fileobj, keywords, comment_tags, options):
+def extract_python(
+ fileobj: IO[bytes],
+ keywords: Mapping[str, _Keyword],
+ comment_tags: Collection[str],
+ options: _PyOptions,
+) -> Generator[_ExtractionResult, None, None]:
"""Extract messages from Python source code.
It returns an iterator yielding tuples in the following form ``(lineno,
@@ -511,7 +578,7 @@ def extract_python(fileobj, keywords, comment_tags, options):
funcname = value
-def _parse_python_string(value, encoding, future_flags):
+def _parse_python_string(value: str, encoding: str, future_flags: int) -> str | None:
# Unwrap quotes in a safe manner, maintaining the string's encoding
# https://sourceforge.net/tracker/?func=detail&atid=355470&aid=617979&group_id=5470
code = compile(
@@ -533,7 +600,13 @@ def _parse_python_string(value, encoding, future_flags):
return None
-def extract_javascript(fileobj, keywords, comment_tags, options, lineno=1):
+def extract_javascript(
+ fileobj: _FileObj,
+ keywords: Mapping[str, _Keyword],
+ comment_tags: Collection[str],
+ options: _JSOptions,
+ lineno: int = 1,
+) -> Generator[_ExtractionResult, None, None]:
"""Extract messages from JavaScript source code.
:param fileobj: the seekable, file-like object the messages should be
@@ -676,7 +749,13 @@ def extract_javascript(fileobj, keywords, comment_tags, options, lineno=1):
last_token = token
-def parse_template_string(template_string, keywords, comment_tags, options, lineno=1):
+def parse_template_string(
+ template_string: str,
+ keywords: Mapping[str, _Keyword],
+ comment_tags: Collection[str],
+ options: _JSOptions,
+ lineno: int = 1,
+) -> Generator[_ExtractionResult, None, None]:
"""Parse JavaScript template string.
:param template_string: the template string to be parsed
diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py
index 886f69d..07fffde 100644
--- a/babel/messages/jslexer.py
+++ b/babel/messages/jslexer.py
@@ -9,17 +9,21 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
+
from collections import namedtuple
+from collections.abc import Generator, Iterator, Sequence
import re
+from typing import NamedTuple
-operators = sorted([
+operators: list[str] = sorted([
'+', '-', '*', '%', '!=', '==', '<', '>', '<=', '>=', '=',
'+=', '-=', '*=', '%=', '<<', '>>', '>>>', '<<=', '>>=',
'>>>=', '&', '&=', '|', '|=', '&&', '||', '^', '^=', '(', ')',
'[', ']', '{', '}', '!', '--', '++', '~', ',', ';', '.', ':'
], key=len, reverse=True)
-escapes = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}
+escapes: dict[str, str] = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}
name_re = re.compile(r'[\w$_][\w\d$_]*', re.UNICODE)
dotted_name_re = re.compile(r'[\w$_][\w\d$_.]*[\w\d$_.]', re.UNICODE)
@@ -30,9 +34,12 @@ line_join_re = re.compile(r'\\' + line_re.pattern)
uni_escape_re = re.compile(r'[a-fA-F0-9]{1,4}')
hex_escape_re = re.compile(r'[a-fA-F0-9]{1,2}')
-Token = namedtuple('Token', 'type value lineno')
+class Token(NamedTuple):
+ type: str
+ value: str
+ lineno: int
-_rules = [
+_rules: list[tuple[str | None, re.Pattern[str]]] = [
(None, re.compile(r'\s+', re.UNICODE)),
(None, re.compile(r'<!--.*')),
('linecomment', re.compile(r'//.*')),
@@ -55,7 +62,7 @@ _rules = [
]
-def get_rules(jsx, dotted, template_string):
+def get_rules(jsx: bool, dotted: bool, template_string: bool) -> list[tuple[str | None, re.Pattern[str]]]:
"""
Get a tokenization rule list given the passed syntax options.
@@ -75,7 +82,7 @@ def get_rules(jsx, dotted, template_string):
return rules
-def indicates_division(token):
+def indicates_division(token: Token) -> bool:
"""A helper function that helps the tokenizer to decide if the current
token may be followed by a division operator.
"""
@@ -84,7 +91,7 @@ def indicates_division(token):
return token.type in ('name', 'number', 'string', 'regexp')
-def unquote_string(string):
+def unquote_string(string: str) -> str:
"""Unquote a string with JavaScript rules. The string has to start with
string delimiters (``'``, ``"`` or the back-tick/grave accent (for template strings).)
"""
@@ -151,7 +158,7 @@ def unquote_string(string):
return u''.join(result)
-def tokenize(source, jsx=True, dotted=True, template_string=True, lineno=1):
+def tokenize(source: str, jsx: bool = True, dotted: bool = True, template_string: bool = True, lineno: int = 1) -> Generator[Token, None, None]:
"""
Tokenize JavaScript/JSX source. Returns a generator of tokens.
diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py
index 8284574..a96f059 100644
--- a/babel/messages/mofile.py
+++ b/babel/messages/mofile.py
@@ -7,18 +7,22 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
import array
import struct
+from typing import TYPE_CHECKING
from babel.messages.catalog import Catalog, Message
+if TYPE_CHECKING:
+ from _typeshed import SupportsRead, SupportsWrite
-LE_MAGIC = 0x950412de
-BE_MAGIC = 0xde120495
+LE_MAGIC: int = 0x950412de
+BE_MAGIC: int = 0xde120495
-def read_mo(fileobj):
+def read_mo(fileobj: SupportsRead[bytes]) -> Catalog:
"""Read a binary MO file from the given file-like object and return a
corresponding `Catalog` object.
@@ -102,7 +106,7 @@ def read_mo(fileobj):
return catalog
-def write_mo(fileobj, catalog, use_fuzzy=False):
+def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool = False) -> None:
"""Write a catalog to the specified file-like object using the GNU MO file
format.
diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py
index 8722566..0fdf53b 100644
--- a/babel/messages/plurals.py
+++ b/babel/messages/plurals.py
@@ -7,6 +7,7 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
from babel.core import default_locale, Locale
from operator import itemgetter
@@ -15,10 +16,10 @@ from operator import itemgetter
# XXX: remove this file, duplication with babel.plural
-LC_CTYPE = default_locale('LC_CTYPE')
+LC_CTYPE: str | None = default_locale('LC_CTYPE')
-PLURALS = {
+PLURALS: dict[str, tuple[int, str]] = {
# Afar
# 'aa': (),
# Abkhazian
@@ -201,7 +202,7 @@ PLURALS = {
}
-DEFAULT_PLURAL = (2, '(n != 1)')
+DEFAULT_PLURAL: tuple[int, str] = (2, '(n != 1)')
class _PluralTuple(tuple):
@@ -215,11 +216,11 @@ class _PluralTuple(tuple):
plural_forms = property(lambda x: 'nplurals=%s; plural=%s;' % x, doc="""
The plural expression used by the catalog or locale.""")
- def __str__(self):
+ def __str__(self) -> str:
return self.plural_forms
-def get_plural(locale=LC_CTYPE):
+def get_plural(locale: str | None = LC_CTYPE) -> _PluralTuple:
"""A tuple with the information catalogs need to perform proper
pluralization. The first item of the tuple is the number of plural
forms, the second the plural expression.
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index b366ccb..b6d0d6e 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -8,15 +8,24 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
import os
import re
+from collections.abc import Iterable
+from typing import TYPE_CHECKING
+from babel.core import Locale
from babel.messages.catalog import Catalog, Message
from babel.util import wraptext, _cmp
+if TYPE_CHECKING:
+ from _typeshed import SupportsWrite
+ from typing import IO, AnyStr
+ from typing_extensions import Literal
-def unescape(string):
+
+def unescape(string: str) -> str:
r"""Reverse `escape` the given string.
>>> print(unescape('"Say:\\n \\"hello, world!\\"\\n"'))
@@ -39,7 +48,7 @@ def unescape(string):
return re.compile(r'\\([\\trn"])').sub(replace_escapes, string[1:-1])
-def denormalize(string):
+def denormalize(string: str) -> str:
r"""Reverse the normalization done by the `normalize` function.
>>> print(denormalize(r'''""
@@ -72,7 +81,8 @@ def denormalize(string):
class PoFileError(Exception):
"""Exception thrown by PoParser when an invalid po file is encountered."""
- def __init__(self, message, catalog, line, lineno):
+
+ def __init__(self, message: str, catalog: Catalog, line: str, lineno: int) -> None:
super().__init__(f'{message} on {lineno}')
self.catalog = catalog
self.line = line
@@ -81,45 +91,45 @@ class PoFileError(Exception):
class _NormalizedString:
- def __init__(self, *args):
- self._strs = []
+ def __init__(self, *args: str) -> None:
+ self._strs: list[str] = []
for arg in args:
self.append(arg)
- def append(self, s):
+ def append(self, s: str) -> None:
self._strs.append(s.strip())
- def denormalize(self):
+ def denormalize(self) -> str:
return ''.join(map(unescape, self._strs))
- def __bool__(self):
+ def __bool__(self) -> bool:
return bool(self._strs)
- def __repr__(self):
+ def __repr__(self) -> str:
return os.linesep.join(self._strs)
- def __cmp__(self, other):
+ def __cmp__(self, other: object) -> int:
if not other:
return 1
return _cmp(str(self), str(other))
- def __gt__(self, other):
+ def __gt__(self, other: object) -> bool:
return self.__cmp__(other) > 0
- def __lt__(self, other):
+ def __lt__(self, other: object) -> bool:
return self.__cmp__(other) < 0
- def __ge__(self, other):
+ def __ge__(self, other: object) -> bool:
return self.__cmp__(other) >= 0
- def __le__(self, other):
+ def __le__(self, other: object) -> bool:
return self.__cmp__(other) <= 0
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return self.__cmp__(other) == 0
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return self.__cmp__(other) != 0
@@ -138,7 +148,7 @@ class PoFileParser:
'msgid_plural',
]
- def __init__(self, catalog, ignore_obsolete=False, abort_invalid=False):
+ def __init__(self, catalog: Catalog, ignore_obsolete: bool = False, abort_invalid: bool = False) -> None:
self.catalog = catalog
self.ignore_obsolete = ignore_obsolete
self.counter = 0
@@ -146,7 +156,7 @@ class PoFileParser:
self.abort_invalid = abort_invalid
self._reset_message_state()
- def _reset_message_state(self):
+ def _reset_message_state(self) -> None:
self.messages = []
self.translations = []
self.locations = []
@@ -159,7 +169,7 @@ class PoFileParser:
self.in_msgstr = False
self.in_msgctxt = False
- def _add_message(self):
+ def _add_message(self) -> None:
"""
Add a message to the catalog based on the current parser state and
clear the state ready to process the next message.
@@ -194,17 +204,17 @@ class PoFileParser:
self.counter += 1
self._reset_message_state()
- def _finish_current_message(self):
+ def _finish_current_message(self) -> None:
if self.messages:
self._add_message()
- def _process_message_line(self, lineno, line, obsolete=False):
+ def _process_message_line(self, lineno, line, obsolete=False) -> None:
if line.startswith('"'):
self._process_string_continuation_line(line, lineno)
else:
self._process_keyword_line(lineno, line, obsolete)
- def _process_keyword_line(self, lineno, line, obsolete=False):
+ def _process_keyword_line(self, lineno, line, obsolete=False) -> None:
for keyword in self._keywords:
try:
@@ -245,7 +255,7 @@ class PoFileParser:
self.in_msgctxt = True
self.context = _NormalizedString(arg)
- def _process_string_continuation_line(self, line, lineno):
+ def _process_string_continuation_line(self, line, lineno) -> None:
if self.in_msgid:
s = self.messages[-1]
elif self.in_msgstr:
@@ -257,7 +267,7 @@ class PoFileParser:
return
s.append(line)
- def _process_comment(self, line):
+ def _process_comment(self, line) -> None:
self._finish_current_message()
@@ -284,7 +294,7 @@ class PoFileParser:
# These are called user comments
self.user_comments.append(line[1:].strip())
- def parse(self, fileobj):
+ def parse(self, fileobj: IO[AnyStr]) -> None:
"""
Reads from the file-like object `fileobj` and adds any po file
units found in it to the `Catalog` supplied to the constructor.
@@ -313,7 +323,7 @@ class PoFileParser:
self.translations.append([0, _NormalizedString(u'""')])
self._add_message()
- def _invalid_pofile(self, line, lineno, msg):
+ def _invalid_pofile(self, line, lineno, msg) -> None:
assert isinstance(line, str)
if self.abort_invalid:
raise PoFileError(msg, self.catalog, line, lineno)
@@ -321,7 +331,14 @@ class PoFileParser:
print(f"WARNING: Problem on line {lineno + 1}: {line!r}")
-def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None, abort_invalid=False):
+def read_po(
+ fileobj: IO[AnyStr],
+ locale: str | Locale | None = None,
+ domain: str | None = None,
+ ignore_obsolete: bool = False,
+ charset: str | None = None,
+ abort_invalid: bool = False,
+) -> Catalog:
"""Read messages from a ``gettext`` PO (portable object) file from the given
file-like object and return a `Catalog`.
@@ -381,7 +398,7 @@ WORD_SEP = re.compile('('
')')
-def escape(string):
+def escape(string: str) -> str:
r"""Escape the given string so that it can be included in double-quoted
strings in ``PO`` files.
@@ -399,7 +416,7 @@ def escape(string):
.replace('\"', '\\"')
-def normalize(string, prefix='', width=76):
+def normalize(string: str, prefix: str = '', width: int = 76) -> str:
r"""Convert a string into a format that is appropriate for .po files.
>>> print(normalize('''Say:
@@ -460,9 +477,18 @@ def normalize(string, prefix='', width=76):
return u'""\n' + u'\n'.join([(prefix + escape(line)) for line in lines])
-def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
- sort_output=False, sort_by_file=False, ignore_obsolete=False,
- include_previous=False, include_lineno=True):
+def write_po(
+ fileobj: SupportsWrite[bytes],
+ catalog: Catalog,
+ width: int = 76,
+ no_location: bool = False,
+ omit_header: bool = False,
+ sort_output: bool = False,
+ sort_by_file: bool = False,
+ ignore_obsolete: bool = False,
+ include_previous: bool = False,
+ include_lineno: bool = True,
+) -> None:
r"""Write a ``gettext`` PO (portable object) template file for a given
message catalog to the provided file-like object.
@@ -612,7 +638,7 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False,
_write('\n')
-def _sort_messages(messages, sort_by):
+def _sort_messages(messages: Iterable[Message], sort_by: Literal["message", "location"]) -> list[Message]:
"""
Sort the given message iterable by the given criteria.
diff --git a/babel/numbers.py b/babel/numbers.py
index 94259f8..73155c4 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -18,13 +18,18 @@
# Padding and rounding increments in pattern:
# - https://www.unicode.org/reports/tr35/ (Appendix G.6)
from __future__ import annotations
+
import decimal
import re
-from datetime import date as date_, datetime as datetime_
+from typing import TYPE_CHECKING, Any, overload
import warnings
+from datetime import date as date_, datetime as datetime_
-from babel.core import default_locale, Locale, get_global
+from babel.core import Locale, default_locale, get_global
+from babel.localedata import LocaleDataDict
+if TYPE_CHECKING:
+ from typing_extensions import Literal
LC_NUMERIC = default_locale('LC_NUMERIC')
@@ -33,7 +38,7 @@ class UnknownCurrencyError(Exception):
"""Exception thrown when a currency is requested for which no data is available.
"""
- def __init__(self, identifier):
+ def __init__(self, identifier: str) -> None:
"""Create the exception.
:param identifier: the identifier string of the unsupported currency
"""
@@ -43,7 +48,7 @@ class UnknownCurrencyError(Exception):
self.identifier = identifier
-def list_currencies(locale=None):
+def list_currencies(locale: Locale | str | None = None) -> set[str]:
""" Return a `set` of normalized currency codes.
.. versionadded:: 2.5.0
@@ -61,7 +66,7 @@ def list_currencies(locale=None):
return set(currencies)
-def validate_currency(currency, locale=None):
+def validate_currency(currency: str, locale: Locale | str | None = None) -> None:
""" Check the currency code is recognized by Babel.
Accepts a ``locale`` parameter for fined-grained validation, working as
@@ -73,7 +78,7 @@ def validate_currency(currency, locale=None):
raise UnknownCurrencyError(currency)
-def is_currency(currency, locale=None):
+def is_currency(currency: str, locale: Locale | str | None = None) -> bool:
""" Returns `True` only if a currency is recognized by Babel.
This method always return a Boolean and never raise.
@@ -87,7 +92,7 @@ def is_currency(currency, locale=None):
return True
-def normalize_currency(currency, locale=None):
+def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None:
"""Returns the normalized sting of any currency code.
Accepts a ``locale`` parameter for fined-grained validation, working as
@@ -102,7 +107,11 @@ def normalize_currency(currency, locale=None):
return currency
-def get_currency_name(currency, count=None, locale=LC_NUMERIC):
+def get_currency_name(
+ currency: str,
+ count: float | decimal.Decimal | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str:
"""Return the name used by the locale for the specified currency.
>>> get_currency_name('USD', locale='en_US')
@@ -128,7 +137,7 @@ def get_currency_name(currency, count=None, locale=LC_NUMERIC):
return loc.currencies.get(currency, currency)
-def get_currency_symbol(currency, locale=LC_NUMERIC):
+def get_currency_symbol(currency: str, locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale for the specified currency.
>>> get_currency_symbol('USD', locale='en_US')
@@ -140,7 +149,7 @@ def get_currency_symbol(currency, locale=LC_NUMERIC):
return Locale.parse(locale).currency_symbols.get(currency, currency)
-def get_currency_precision(currency):
+def get_currency_precision(currency: str) -> int:
"""Return currency's precision.
Precision is the number of decimals found after the decimal point in the
@@ -154,7 +163,11 @@ def get_currency_precision(currency):
return precisions.get(currency, precisions['DEFAULT'])[0]
-def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC):
+def get_currency_unit_pattern(
+ currency: str,
+ count: float | decimal.Decimal | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str:
"""
Return the unit pattern used for long display of a currency value
for a given locale.
@@ -184,9 +197,38 @@ def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC):
return loc._data['currency_unit_patterns']['other']
-def get_territory_currencies(territory, start_date=None, end_date=None,
- tender=True, non_tender=False,
- include_details=False):
+@overload
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = ...,
+ end_date: date_ | None = ...,
+ tender: bool = ...,
+ non_tender: bool = ...,
+ include_details: Literal[False] = ...,
+) -> list[str]:
+ ... # pragma: no cover
+
+
+@overload
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = ...,
+ end_date: date_ | None = ...,
+ tender: bool = ...,
+ non_tender: bool = ...,
+ include_details: Literal[True] = ...,
+) -> list[dict[str, Any]]:
+ ... # pragma: no cover
+
+
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = None,
+ end_date: date_ | None = None,
+ tender: bool = True,
+ non_tender: bool = False,
+ include_details: bool = False,
+) -> list[str] | list[dict[str, Any]]:
"""Returns the list of currencies for the given territory that are valid for
the given date range. In addition to that the currency database
distinguishes between tender and non-tender currencies. By default only
@@ -274,7 +316,7 @@ def get_territory_currencies(territory, start_date=None, end_date=None,
return result
-def get_decimal_symbol(locale=LC_NUMERIC):
+def get_decimal_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate decimal fractions.
>>> get_decimal_symbol('en_US')
@@ -285,7 +327,7 @@ def get_decimal_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('decimal', u'.')
-def get_plus_sign_symbol(locale=LC_NUMERIC):
+def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the plus sign symbol used by the current locale.
>>> get_plus_sign_symbol('en_US')
@@ -296,7 +338,7 @@ def get_plus_sign_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('plusSign', u'+')
-def get_minus_sign_symbol(locale=LC_NUMERIC):
+def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the plus sign symbol used by the current locale.
>>> get_minus_sign_symbol('en_US')
@@ -307,7 +349,7 @@ def get_minus_sign_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('minusSign', u'-')
-def get_exponential_symbol(locale=LC_NUMERIC):
+def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate mantissa and exponent.
>>> get_exponential_symbol('en_US')
@@ -318,7 +360,7 @@ def get_exponential_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('exponential', u'E')
-def get_group_symbol(locale=LC_NUMERIC):
+def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate groups of thousands.
>>> get_group_symbol('en_US')
@@ -329,7 +371,7 @@ def get_group_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('group', u',')
-def format_number(number, locale=LC_NUMERIC):
+def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
u"""Return the given number formatted for a specific locale.
>>> format_number(1099, locale='en_US') # doctest: +SKIP
@@ -350,7 +392,7 @@ def format_number(number, locale=LC_NUMERIC):
return format_decimal(number, locale=locale)
-def get_decimal_precision(number):
+def get_decimal_precision(number: decimal.Decimal) -> int:
"""Return maximum precision of a decimal instance's fractional part.
Precision is extracted from the fractional part only.
@@ -363,14 +405,19 @@ def get_decimal_precision(number):
return abs(decimal_tuple.exponent)
-def get_decimal_quantum(precision):
+def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
"""Return minimal quantum of a number, as defined by precision."""
assert isinstance(precision, (int, decimal.Decimal))
return decimal.Decimal(10) ** (-precision)
def format_decimal(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
u"""Return the given decimal number formatted for a specific locale.
>>> format_decimal(1.2345, locale='en_US')
@@ -419,7 +466,13 @@ def format_decimal(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
-def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
+def format_compact_decimal(
+ number: float | decimal.Decimal | str,
+ *,
+ format_type: Literal["short", "long"] = "short",
+ locale: Locale | str | None = LC_NUMERIC,
+ fraction_digits: int = 0,
+) -> str:
u"""Return the given decimal number formatted for a specific locale in compact form.
>>> format_compact_decimal(12345, format_type="short", locale='en_US')
@@ -450,7 +503,12 @@ def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fr
return pattern.apply(number, locale, decimal_quantization=False)
-def _get_compact_format(number, compact_format, locale, fraction_digits=0):
+def _get_compact_format(
+ number: float | decimal.Decimal | str,
+ compact_format: LocaleDataDict,
+ locale: Locale | str | None,
+ fraction_digits: int,
+) -> tuple[decimal.Decimal, NumberPattern | None]:
"""Returns the number after dividing by the unit and the format pattern to use.
The algorithm is described here:
https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
@@ -488,8 +546,15 @@ class UnknownCurrencyFormatError(KeyError):
def format_currency(
- number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
- format_type='standard', decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ currency: str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ currency_digits: bool = True,
+ format_type: Literal["name", "standard", "accounting"] = "standard",
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
u"""Return formatted currency value.
>>> format_currency(1099.98, 'USD', locale='en_US')
@@ -596,8 +661,15 @@ def format_currency(
def _format_currency_long_name(
- number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
- format_type='standard', decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ currency: str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ currency_digits: bool = True,
+ format_type: Literal["name", "standard", "accounting"] = "standard",
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
# Algorithm described here:
# https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
locale = Locale.parse(locale)
@@ -631,7 +703,14 @@ def _format_currency_long_name(
return unit_pattern.format(number_part, display_name)
-def format_compact_currency(number, currency, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
+def format_compact_currency(
+ number: float | decimal.Decimal | str,
+ currency: str,
+ *,
+ format_type: Literal["short"] = "short",
+ locale: Locale | str | None = LC_NUMERIC,
+ fraction_digits: int = 0
+) -> str:
u"""Format a number as a currency value in compact form.
>>> format_compact_currency(12345, 'USD', locale='en_US')
@@ -670,7 +749,12 @@ def format_compact_currency(number, currency, *, format_type="short", locale=LC_
def format_percent(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
"""Return formatted percent value for a specific locale.
>>> format_percent(0.34, locale='en_US')
@@ -717,7 +801,11 @@ def format_percent(
def format_scientific(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+) -> str:
"""Return value formatted in scientific notation for a specific locale.
>>> format_scientific(10000, locale='en_US')
@@ -754,13 +842,13 @@ def format_scientific(
class NumberFormatError(ValueError):
"""Exception raised when a string cannot be parsed into a number."""
- def __init__(self, message, suggestions=None):
+ def __init__(self, message: str, suggestions: str | None = None) -> None:
super().__init__(message)
#: a list of properly formatted numbers derived from the invalid input
self.suggestions = suggestions
-def parse_number(string, locale=LC_NUMERIC):
+def parse_number(string: str, locale: Locale | str | None = LC_NUMERIC) -> int:
"""Parse localized number string into an integer.
>>> parse_number('1,099', locale='en_US')
@@ -786,7 +874,7 @@ def parse_number(string, locale=LC_NUMERIC):
raise NumberFormatError(f"{string!r} is not a valid number")
-def parse_decimal(string, locale=LC_NUMERIC, strict=False):
+def parse_decimal(string: str, locale: Locale | str | None = LC_NUMERIC, strict: bool = False) -> decimal.Decimal:
"""Parse localized decimal string into a decimal.
>>> parse_decimal('1,099.98', locale='en_US')
@@ -879,7 +967,7 @@ SUFFIX_PATTERN = r"(?P<suffix>.*)"
number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}")
-def parse_grouping(p):
+def parse_grouping(p: str) -> tuple[int, int]:
"""Parse primary and secondary digit grouping
>>> parse_grouping('##')
@@ -901,7 +989,7 @@ def parse_grouping(p):
return g1, g2
-def parse_pattern(pattern):
+def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
"""Parse number format patterns"""
if isinstance(pattern, NumberPattern):
return pattern
@@ -970,9 +1058,18 @@ def parse_pattern(pattern):
class NumberPattern:
- def __init__(self, pattern, prefix, suffix, grouping,
- int_prec, frac_prec, exp_prec, exp_plus,
- number_pattern: str | None = None):
+ def __init__(
+ self,
+ pattern: str,
+ prefix: tuple[str, str],
+ suffix: tuple[str, str],
+ grouping: tuple[int, int],
+ int_prec: tuple[int, int],
+ frac_prec: tuple[int, int],
+ exp_prec: tuple[int, int] | None,
+ exp_plus: bool | None,
+ number_pattern: str | None = None,
+ ) -> None:
# Metadata of the decomposed parsed pattern.
self.pattern = pattern
self.prefix = prefix
@@ -985,10 +1082,10 @@ class NumberPattern:
self.exp_plus = exp_plus
self.scale = self.compute_scale()
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{type(self).__name__} {self.pattern!r}>"
- def compute_scale(self):
+ def compute_scale(self) -> Literal[0, 2, 3]:
"""Return the scaling factor to apply to the number before rendering.
Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``ā€°`` sign is
@@ -1002,7 +1099,7 @@ class NumberPattern:
scale = 3
return scale
- def scientific_notation_elements(self, value, locale):
+ def scientific_notation_elements(self, value: decimal.Decimal, locale: Locale | str | None) -> tuple[decimal.Decimal, int, str]:
""" Returns normalized scientific notation components of a value.
"""
# Normalize value to only have one lead digit.
@@ -1031,13 +1128,13 @@ class NumberPattern:
def apply(
self,
- value,
- locale,
- currency=None,
- currency_digits=True,
- decimal_quantization=True,
- force_frac=None,
- group_separator=True,
+ value: float | decimal.Decimal,
+ locale: Locale | str | None,
+ currency: str | None = None,
+ currency_digits: bool = True,
+ decimal_quantization: bool = True,
+ force_frac: tuple[int, int] | None = None,
+ group_separator: bool = True,
):
"""Renders into a string a number following the defined pattern.
@@ -1157,7 +1254,7 @@ class NumberPattern:
# - Restore the original position of the decimal point, potentially
# padding with zeroes on either side
#
- def _format_significant(self, value, minimum, maximum):
+ def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
exp = value.adjusted()
scale = maximum - 1 - exp
digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
@@ -1176,7 +1273,7 @@ class NumberPattern:
).rstrip('.')
return result
- def _format_int(self, value, min, max, locale):
+ def _format_int(self, value: str, min: int, max: int, locale: Locale | str | None) -> str:
width = len(value)
if width < min:
value = '0' * (min - width) + value
@@ -1189,7 +1286,7 @@ class NumberPattern:
gsize = self.grouping[1]
return value + ret
- def _quantize_value(self, value, locale, frac_prec, group_separator):
+ def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str:
quantum = get_decimal_quantum(frac_prec[1])
rounded = value.quantize(quantum)
a, sep, b = f"{rounded:f}".partition(".")
@@ -1199,7 +1296,7 @@ class NumberPattern:
number = integer_part + self._format_frac(b or '0', locale, frac_prec)
return number
- def _format_frac(self, value, locale, force_frac=None):
+ def _format_frac(self, value: str, locale: Locale | str | None, force_frac: tuple[int, int] | None = None) -> str:
min, max = force_frac or self.frac_prec
if len(value) < min:
value += ('0' * (min - len(value)))
diff --git a/babel/plural.py b/babel/plural.py
index 712acec..efd0f1f 100644
--- a/babel/plural.py
+++ b/babel/plural.py
@@ -7,15 +7,21 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+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
_plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other')
_fallback_tag = 'other'
-def extract_operands(source):
+def extract_operands(source: float | decimal.Decimal) -> tuple[decimal.Decimal | int, int, int, int, int, int, Literal[0], Literal[0]]:
"""Extract operands from a decimal, a float or an int, according to `CLDR rules`_.
The result is a 8-tuple (n, i, v, w, f, t, c, e), where those symbols are as follows:
@@ -97,7 +103,7 @@ class PluralRule:
__slots__ = ('abstract', '_func')
- def __init__(self, rules):
+ def __init__(self, rules: Mapping[str, str] | Iterable[tuple[str, str]]) -> None:
"""Initialize the rule instance.
:param rules: a list of ``(tag, expr)``) tuples with the rules
@@ -105,10 +111,10 @@ class PluralRule:
and expressions as values.
:raise RuleError: if the expression is malformed
"""
- if isinstance(rules, dict):
+ if isinstance(rules, Mapping):
rules = rules.items()
found = set()
- self.abstract = []
+ self.abstract: list[tuple[str, Any]] = []
for key, expr in sorted(list(rules)):
if key not in _plural_tags:
raise ValueError(f"unknown tag {key!r}")
@@ -119,25 +125,25 @@ class PluralRule:
if ast:
self.abstract.append((key, ast))
- def __repr__(self):
+ def __repr__(self) -> str:
rules = self.rules
args = ", ".join([f"{tag}: {rules[tag]}" for tag in _plural_tags if tag in rules])
return f"<{type(self).__name__} {args!r}>"
@classmethod
- def parse(cls, rules):
+ def parse(cls, rules: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> PluralRule:
"""Create a `PluralRule` instance for the given rules. If the rules
are a `PluralRule` object, that object is returned.
:param rules: the rules as list or dict, or a `PluralRule` object
:raise RuleError: if the expression is malformed
"""
- if isinstance(rules, cls):
+ if isinstance(rules, PluralRule):
return rules
return cls(rules)
@property
- def rules(self):
+ def rules(self) -> Mapping[str, str]:
"""The `PluralRule` as a dict of unicode plural rules.
>>> rule = PluralRule({'one': 'n is 1'})
@@ -147,24 +153,27 @@ class PluralRule:
_compile = _UnicodeCompiler().compile
return {tag: _compile(ast) for tag, ast in self.abstract}
- tags = property(lambda x: frozenset(i[0] for i in x.abstract), doc="""
- A set of explicitly defined tags in this rule. The implicit default
+ @property
+ def tags(self) -> frozenset[str]:
+ """A set of explicitly defined tags in this rule. The implicit default
``'other'`` rules is not part of this set unless there is an explicit
- rule for it.""")
+ rule for it.
+ """
+ return frozenset(i[0] for i in self.abstract)
- def __getstate__(self):
+ def __getstate__(self) -> list[tuple[str, Any]]:
return self.abstract
- def __setstate__(self, abstract):
+ def __setstate__(self, abstract: list[tuple[str, Any]]) -> None:
self.abstract = abstract
- def __call__(self, n):
+ def __call__(self, n: float | decimal.Decimal) -> str:
if not hasattr(self, '_func'):
self._func = to_python(self)
return self._func(n)
-def to_javascript(rule):
+def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str:
"""Convert a list/dict of rules or a `PluralRule` object into a JavaScript
function. This function depends on no external library:
@@ -187,7 +196,7 @@ def to_javascript(rule):
return ''.join(result)
-def to_python(rule):
+def to_python(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> Callable[[float | decimal.Decimal], str]:
"""Convert a list/dict of rules or a `PluralRule` object into a regular
Python function. This is useful in situations where you need a real
function and don't are about the actual rule object:
@@ -227,7 +236,7 @@ def to_python(rule):
return namespace['evaluate']
-def to_gettext(rule):
+def to_gettext(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> str:
"""The plural rule as gettext expression. The gettext expression is
technically limited to integers and returns indices rather than tags.
@@ -250,7 +259,7 @@ def to_gettext(rule):
return ''.join(result)
-def in_range_list(num, range_list):
+def in_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool:
"""Integer range list test. This is the callback for the "in" operator
of the UTS #35 pluralization rule language:
@@ -270,7 +279,7 @@ def in_range_list(num, range_list):
return num == int(num) and within_range_list(num, range_list)
-def within_range_list(num, range_list):
+def within_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool:
"""Float range test. This is the callback for the "within" operator
of the UTS #35 pluralization rule language:
@@ -290,7 +299,7 @@ def within_range_list(num, range_list):
return any(num >= min_ and num <= max_ for min_, max_ in range_list)
-def cldr_modulo(a, b):
+def cldr_modulo(a: float, b: float) -> float:
"""Javaish modulo. This modulo operator returns the value with the sign
of the dividend rather than the divisor like Python does:
@@ -327,7 +336,7 @@ _VARS = {
'e', # currently, synonym for ā€˜cā€™. however, may be redefined in the future.
}
-_RULES = [
+_RULES: list[tuple[str | None, re.Pattern[str]]] = [
(None, re.compile(r'\s+', re.UNICODE)),
('word', re.compile(fr'\b(and|or|is|(?:with)?in|not|mod|[{"".join(_VARS)}])\b')),
('value', re.compile(r'\d+')),
@@ -336,9 +345,9 @@ _RULES = [
]
-def tokenize_rule(s):
+def tokenize_rule(s: str) -> list[tuple[str, str]]:
s = s.split('@')[0]
- result = []
+ result: list[tuple[str, str]] = []
pos = 0
end = len(s)
while pos < end:
@@ -354,30 +363,35 @@ def tokenize_rule(s):
'Got unexpected %r' % s[pos])
return result[::-1]
-
-def test_next_token(tokens, type_, value=None):
+def test_next_token(
+ tokens: list[tuple[str, str]],
+ type_: str,
+ value: str | None = None,
+) -> list[tuple[str, str]] | bool:
return tokens and tokens[-1][0] == type_ and \
(value is None or tokens[-1][1] == value)
-def skip_token(tokens, type_, value=None):
+def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = None):
if test_next_token(tokens, type_, value):
return tokens.pop()
-def value_node(value):
+def value_node(value: int) -> tuple[Literal['value'], tuple[int]]:
return 'value', (value, )
-def ident_node(name):
+def ident_node(name: str) -> tuple[str, tuple[()]]:
return name, ()
-def range_list_node(range_list):
+def range_list_node(
+ range_list: Iterable[Iterable[float | decimal.Decimal]],
+) -> tuple[Literal['range_list'], Iterable[Iterable[float | decimal.Decimal]]]:
return 'range_list', range_list
-def negate(rv):
+def negate(rv: tuple[Any, ...]) -> tuple[Literal['not'], tuple[tuple[Any, ...]]]:
return 'not', (rv,)
diff --git a/babel/py.typed b/babel/py.typed
new file mode 100644
index 0000000..d3245e7
--- /dev/null
+++ b/babel/py.typed
@@ -0,0 +1 @@
+# Marker file for PEP 561. This package uses inline types.
diff --git a/babel/support.py b/babel/support.py
index 8cebd7d..7a6eaa2 100644
--- a/babel/support.py
+++ b/babel/support.py
@@ -10,16 +10,29 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
+import decimal
import gettext
import locale
+import os
+from collections.abc import Iterator
+from datetime import date as _date, datetime as _datetime, time as _time, timedelta as _timedelta
+from typing import TYPE_CHECKING, Any, Callable
+
+from pytz import BaseTzInfo
from babel.core import Locale
-from babel.dates import format_date, format_datetime, format_time, \
- format_timedelta
-from babel.numbers import format_decimal, format_currency, format_compact_currency, \
- format_percent, format_scientific, format_compact_decimal
+from babel.dates import (format_date, format_datetime, format_time,
+ format_timedelta)
+from babel.numbers import (format_compact_currency, format_compact_decimal,
+ format_currency, format_decimal, format_percent,
+ format_scientific)
+
+if TYPE_CHECKING:
+ from typing_extensions import Literal
+ from babel.dates import _PredefinedTimeFormat
class Format:
"""Wrapper class providing the various date and number formatting functions
@@ -34,7 +47,7 @@ class Format:
u'1.234'
"""
- def __init__(self, locale, tzinfo=None):
+ def __init__(self, locale: Locale | str, tzinfo: BaseTzInfo | None = None) -> None:
"""Initialize the formatter.
:param locale: the locale identifier or `Locale` instance
@@ -43,7 +56,7 @@ class Format:
self.locale = Locale.parse(locale)
self.tzinfo = tzinfo
- def date(self, date=None, format='medium'):
+ def date(self, date: _date | None = None, format: _PredefinedTimeFormat | str = 'medium') -> str:
"""Return a date formatted according to the given pattern.
>>> from datetime import date
@@ -53,7 +66,7 @@ class Format:
"""
return format_date(date, format, locale=self.locale)
- def datetime(self, datetime=None, format='medium'):
+ def datetime(self, datetime: _date | None = None, format: _PredefinedTimeFormat | str = 'medium') -> str:
"""Return a date and time formatted according to the given pattern.
>>> from datetime import datetime
@@ -65,7 +78,7 @@ class Format:
return format_datetime(datetime, format, tzinfo=self.tzinfo,
locale=self.locale)
- def time(self, time=None, format='medium'):
+ def time(self, time: _time | _datetime | None = None, format: _PredefinedTimeFormat | str = 'medium') -> str:
"""Return a time formatted according to the given pattern.
>>> from datetime import datetime
@@ -76,8 +89,14 @@ class Format:
"""
return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
- def timedelta(self, delta, granularity='second', threshold=.85,
- format='long', add_direction=False):
+ def timedelta(
+ self,
+ delta: _timedelta | int,
+ granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second",
+ threshold: float = 0.85,
+ format: Literal["narrow", "short", "medium", "long"] = "long",
+ add_direction: bool = False,
+ ) -> str:
"""Return a time delta according to the rules of the given locale.
>>> from datetime import timedelta
@@ -90,7 +109,7 @@ class Format:
format=format, add_direction=add_direction,
locale=self.locale)
- def number(self, number):
+ def number(self, number: float | decimal.Decimal | str) -> str:
"""Return an integer number formatted for the locale.
>>> fmt = Format('en_US')
@@ -99,7 +118,7 @@ class Format:
"""
return format_decimal(number, locale=self.locale)
- def decimal(self, number, format=None):
+ def decimal(self, number: float | decimal.Decimal | str, format: str | None = None) -> str:
"""Return a decimal number formatted for the locale.
>>> fmt = Format('en_US')
@@ -108,7 +127,12 @@ class Format:
"""
return format_decimal(number, format, locale=self.locale)
- def compact_decimal(self, number, format_type='short', fraction_digits=0):
+ def compact_decimal(
+ self,
+ number: float | decimal.Decimal | str,
+ format_type: Literal['short', 'long'] = 'short',
+ fraction_digits: int = 0,
+ ) -> str:
"""Return a number formatted in compact form for the locale.
>>> fmt = Format('en_US')
@@ -119,19 +143,25 @@ class Format:
fraction_digits=fraction_digits,
locale=self.locale)
- def currency(self, number, currency):
+ def currency(self, number: float | decimal.Decimal | str, currency: str) -> str:
"""Return a number in the given currency formatted for the locale.
"""
return format_currency(number, currency, locale=self.locale)
- def compact_currency(self, number, currency, format_type='short', fraction_digits=0):
+ def compact_currency(
+ self,
+ number: float | decimal.Decimal | str,
+ currency: str,
+ format_type: Literal['short'] = 'short',
+ fraction_digits: int = 0,
+ ) -> str:
"""Return a number in the given currency formatted for the locale
using the compact number format.
"""
return format_compact_currency(number, currency, format_type=format_type,
fraction_digits=fraction_digits, locale=self.locale)
- def percent(self, number, format=None):
+ def percent(self, number: float | decimal.Decimal | str, format: str | None = None) -> str:
"""Return a number formatted as percentage for the locale.
>>> fmt = Format('en_US')
@@ -140,7 +170,7 @@ class Format:
"""
return format_percent(number, format, locale=self.locale)
- def scientific(self, number):
+ def scientific(self, number: float | decimal.Decimal | str) -> str:
"""Return a number formatted using scientific notation for the locale.
"""
return format_scientific(number, locale=self.locale)
@@ -183,18 +213,25 @@ class LazyProxy:
"""
__slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error']
- def __init__(self, func, *args, **kwargs):
- is_cache_enabled = kwargs.pop('enable_cache', True)
+ if TYPE_CHECKING:
+ _func: Callable[..., Any]
+ _args: tuple[Any, ...]
+ _kwargs: dict[str, Any]
+ _is_cache_enabled: bool
+ _value: Any
+ _attribute_error: AttributeError | None
+
+ def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> None:
# Avoid triggering our own __setattr__ implementation
object.__setattr__(self, '_func', func)
object.__setattr__(self, '_args', args)
object.__setattr__(self, '_kwargs', kwargs)
- object.__setattr__(self, '_is_cache_enabled', is_cache_enabled)
+ object.__setattr__(self, '_is_cache_enabled', enable_cache)
object.__setattr__(self, '_value', None)
object.__setattr__(self, '_attribute_error', None)
@property
- def value(self):
+ def value(self) -> Any:
if self._value is None:
try:
value = self._func(*self._args, **self._kwargs)
@@ -207,84 +244,84 @@ class LazyProxy:
object.__setattr__(self, '_value', value)
return self._value
- def __contains__(self, key):
+ def __contains__(self, key: object) -> bool:
return key in self.value
- def __bool__(self):
+ def __bool__(self) -> bool:
return bool(self.value)
- def __dir__(self):
+ def __dir__(self) -> list[str]:
return dir(self.value)
- def __iter__(self):
+ def __iter__(self) -> Iterator[Any]:
return iter(self.value)
- def __len__(self):
+ def __len__(self) -> int:
return len(self.value)
- def __str__(self):
+ def __str__(self) -> str:
return str(self.value)
- def __add__(self, other):
+ def __add__(self, other: object) -> Any:
return self.value + other
- def __radd__(self, other):
+ def __radd__(self, other: object) -> Any:
return other + self.value
- def __mod__(self, other):
+ def __mod__(self, other: object) -> Any:
return self.value % other
- def __rmod__(self, other):
+ def __rmod__(self, other: object) -> Any:
return other % self.value
- def __mul__(self, other):
+ def __mul__(self, other: object) -> Any:
return self.value * other
- def __rmul__(self, other):
+ def __rmul__(self, other: object) -> Any:
return other * self.value
- def __call__(self, *args, **kwargs):
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.value(*args, **kwargs)
- def __lt__(self, other):
+ def __lt__(self, other: object) -> bool:
return self.value < other
- def __le__(self, other):
+ def __le__(self, other: object) -> bool:
return self.value <= other
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
return self.value == other
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return self.value != other
- def __gt__(self, other):
+ def __gt__(self, other: object) -> bool:
return self.value > other
- def __ge__(self, other):
+ def __ge__(self, other: object) -> bool:
return self.value >= other
- def __delattr__(self, name):
+ def __delattr__(self, name: str) -> None:
delattr(self.value, name)
- def __getattr__(self, name):
+ def __getattr__(self, name: str) -> Any:
if self._attribute_error is not None:
raise self._attribute_error
return getattr(self.value, name)
- def __setattr__(self, name, value):
+ def __setattr__(self, name: str, value: Any) -> None:
setattr(self.value, name, value)
- def __delitem__(self, key):
+ def __delitem__(self, key: Any) -> None:
del self.value[key]
- def __getitem__(self, key):
+ def __getitem__(self, key: Any) -> Any:
return self.value[key]
- def __setitem__(self, key, value):
+ def __setitem__(self, key: Any, value: Any) -> None:
self.value[key] = value
- def __copy__(self):
+ def __copy__(self) -> LazyProxy:
return LazyProxy(
self._func,
enable_cache=self._is_cache_enabled,
@@ -292,7 +329,7 @@ class LazyProxy:
**self._kwargs
)
- def __deepcopy__(self, memo):
+ def __deepcopy__(self, memo: Any) -> LazyProxy:
from copy import deepcopy
return LazyProxy(
deepcopy(self._func, memo),
@@ -304,9 +341,13 @@ class LazyProxy:
class NullTranslations(gettext.NullTranslations):
+ if TYPE_CHECKING:
+ _info: dict[str, str]
+ _fallback: NullTranslations | None
+
DEFAULT_DOMAIN = None
- def __init__(self, fp=None):
+ def __init__(self, fp: gettext._TranslationsReader | None = None) -> None:
"""Initialize a simple translations class which is not backed by a
real catalog. Behaves similar to gettext.NullTranslations but also
offers Babel's on *gettext methods (e.g. 'dgettext()').
@@ -316,20 +357,20 @@ class NullTranslations(gettext.NullTranslations):
# These attributes are set by gettext.NullTranslations when a catalog
# is parsed (fp != None). Ensure that they are always present because
# some *gettext methods (including '.gettext()') rely on the attributes.
- self._catalog = {}
- self.plural = lambda n: int(n != 1)
+ self._catalog: dict[tuple[str, Any] | str, str] = {}
+ self.plural: Callable[[float | decimal.Decimal], int] = lambda n: int(n != 1)
super().__init__(fp=fp)
self.files = list(filter(None, [getattr(fp, 'name', None)]))
self.domain = self.DEFAULT_DOMAIN
- self._domains = {}
+ self._domains: dict[str, NullTranslations] = {}
- def dgettext(self, domain, message):
+ def dgettext(self, domain: str, message: str) -> str:
"""Like ``gettext()``, but look the message up in the specified
domain.
"""
return self._domains.get(domain, self).gettext(message)
- def ldgettext(self, domain, message):
+ def ldgettext(self, domain: str, message: str) -> str:
"""Like ``lgettext()``, but look the message up in the specified
domain.
"""
@@ -338,7 +379,7 @@ class NullTranslations(gettext.NullTranslations):
DeprecationWarning, 2)
return self._domains.get(domain, self).lgettext(message)
- def udgettext(self, domain, message):
+ def udgettext(self, domain: str, message: str) -> str:
"""Like ``ugettext()``, but look the message up in the specified
domain.
"""
@@ -346,13 +387,13 @@ class NullTranslations(gettext.NullTranslations):
# backward compatibility with 0.9
dugettext = udgettext
- def dngettext(self, domain, singular, plural, num):
+ def dngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
"""Like ``ngettext()``, but look the message up in the specified
domain.
"""
return self._domains.get(domain, self).ngettext(singular, plural, num)
- def ldngettext(self, domain, singular, plural, num):
+ def ldngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
"""Like ``lngettext()``, but look the message up in the specified
domain.
"""
@@ -361,7 +402,7 @@ class NullTranslations(gettext.NullTranslations):
DeprecationWarning, 2)
return self._domains.get(domain, self).lngettext(singular, plural, num)
- def udngettext(self, domain, singular, plural, num):
+ def udngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
"""Like ``ungettext()`` but look the message up in the specified
domain.
"""
@@ -376,7 +417,7 @@ class NullTranslations(gettext.NullTranslations):
# msgctxt + "\x04" + msgid (gettext version >= 0.15)
CONTEXT_ENCODING = '%s\x04%s'
- def pgettext(self, context, message):
+ def pgettext(self, context: str, message: str) -> str | object:
"""Look up the `context` and `message` id in the catalog and return the
corresponding message string, as an 8-bit string encoded with the
catalog's charset encoding, if known. If there is no entry in the
@@ -393,7 +434,7 @@ class NullTranslations(gettext.NullTranslations):
return message
return tmsg
- def lpgettext(self, context, message):
+ def lpgettext(self, context: str, message: str) -> str | bytes | object:
"""Equivalent to ``pgettext()``, but the translation is returned in the
preferred system encoding, if no other encoding was explicitly set with
``bind_textdomain_codeset()``.
@@ -403,9 +444,9 @@ class NullTranslations(gettext.NullTranslations):
DeprecationWarning, 2)
tmsg = self.pgettext(context, message)
encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding()
- return tmsg.encode(encoding)
+ return tmsg.encode(encoding) if isinstance(tmsg, str) else tmsg
- def npgettext(self, context, singular, plural, num):
+ def npgettext(self, context: str, singular: str, plural: str, num: int) -> str:
"""Do a plural-forms lookup of a message id. `singular` is used as the
message id for purposes of lookup in the catalog, while `num` is used to
determine which plural form to use. The returned message string is an
@@ -428,7 +469,7 @@ class NullTranslations(gettext.NullTranslations):
else:
return plural
- def lnpgettext(self, context, singular, plural, num):
+ def lnpgettext(self, context: str, singular: str, plural: str, num: int) -> str | bytes:
"""Equivalent to ``npgettext()``, but the translation is returned in the
preferred system encoding, if no other encoding was explicitly set with
``bind_textdomain_codeset()``.
@@ -449,7 +490,7 @@ class NullTranslations(gettext.NullTranslations):
else:
return plural
- def upgettext(self, context, message):
+ def upgettext(self, context: str, message: str) -> str:
"""Look up the `context` and `message` id in the catalog and return the
corresponding message string, as a Unicode string. If there is no entry
in the catalog for the `message` id and `context`, and a fallback has
@@ -463,9 +504,10 @@ class NullTranslations(gettext.NullTranslations):
if self._fallback:
return self._fallback.upgettext(context, message)
return str(message)
+ assert isinstance(tmsg, str)
return tmsg
- def unpgettext(self, context, singular, plural, num):
+ def unpgettext(self, context: str, singular: str, plural: str, num: int) -> str:
"""Do a plural-forms lookup of a message id. `singular` is used as the
message id for purposes of lookup in the catalog, while `num` is used to
determine which plural form to use. The returned message string is a
@@ -488,13 +530,13 @@ class NullTranslations(gettext.NullTranslations):
tmsg = str(plural)
return tmsg
- def dpgettext(self, domain, context, message):
+ def dpgettext(self, domain: str, context: str, message: str) -> str | object:
"""Like `pgettext()`, but look the message up in the specified
`domain`.
"""
return self._domains.get(domain, self).pgettext(context, message)
- def udpgettext(self, domain, context, message):
+ def udpgettext(self, domain: str, context: str, message: str) -> str:
"""Like `upgettext()`, but look the message up in the specified
`domain`.
"""
@@ -502,21 +544,21 @@ class NullTranslations(gettext.NullTranslations):
# backward compatibility with 0.9
dupgettext = udpgettext
- def ldpgettext(self, domain, context, message):
+ def ldpgettext(self, domain: str, context: str, message: str) -> str | bytes | object:
"""Equivalent to ``dpgettext()``, but the translation is returned in the
preferred system encoding, if no other encoding was explicitly set with
``bind_textdomain_codeset()``.
"""
return self._domains.get(domain, self).lpgettext(context, message)
- def dnpgettext(self, domain, context, singular, plural, num):
+ def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
"""Like ``npgettext``, but look the message up in the specified
`domain`.
"""
return self._domains.get(domain, self).npgettext(context, singular,
plural, num)
- def udnpgettext(self, domain, context, singular, plural, num):
+ def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
"""Like ``unpgettext``, but look the message up in the specified
`domain`.
"""
@@ -525,7 +567,7 @@ class NullTranslations(gettext.NullTranslations):
# backward compatibility with 0.9
dunpgettext = udnpgettext
- def ldnpgettext(self, domain, context, singular, plural, num):
+ def ldnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str | bytes:
"""Equivalent to ``dnpgettext()``, but the translation is returned in
the preferred system encoding, if no other encoding was explicitly set
with ``bind_textdomain_codeset()``.
@@ -542,7 +584,7 @@ class Translations(NullTranslations, gettext.GNUTranslations):
DEFAULT_DOMAIN = 'messages'
- def __init__(self, fp=None, domain=None):
+ def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None):
"""Initialize the translations catalog.
:param fp: the file-like object the translation should be read from
@@ -555,7 +597,12 @@ class Translations(NullTranslations, gettext.GNUTranslations):
ungettext = gettext.GNUTranslations.ngettext
@classmethod
- def load(cls, dirname=None, locales=None, domain=None):
+ def load(
+ cls,
+ dirname: str | os.PathLike[str] | None = None,
+ locales: list[str] | tuple[str, ...] | str | None = None,
+ domain: str | None = None,
+ ) -> NullTranslations:
"""Load translations from the given directory.
:param dirname: the directory containing the ``MO`` files
@@ -576,11 +623,11 @@ class Translations(NullTranslations, gettext.GNUTranslations):
with open(filename, 'rb') as fp:
return cls(fp=fp, domain=domain)
- def __repr__(self):
+ def __repr__(self) -> str:
version = self._info.get('project-id-version')
return f'<{type(self).__name__}: "{version}">'
- def add(self, translations, merge=True):
+ def add(self, translations: Translations, merge: bool = True):
"""Add the given translations to the catalog.
If the domain of the translations is different than that of the
@@ -598,7 +645,7 @@ class Translations(NullTranslations, gettext.GNUTranslations):
return self.merge(translations)
existing = self._domains.get(domain)
- if merge and existing is not None:
+ if merge and isinstance(existing, Translations):
existing.merge(translations)
else:
translations.add_fallback(self)
@@ -606,7 +653,7 @@ class Translations(NullTranslations, gettext.GNUTranslations):
return self
- def merge(self, translations):
+ def merge(self, translations: Translations):
"""Merge the given translations into the catalog.
Message translations in the specified catalog override any messages
diff --git a/babel/units.py b/babel/units.py
index f8f2675..1180bd1 100644
--- a/babel/units.py
+++ b/babel/units.py
@@ -1,13 +1,24 @@
+from __future__ import annotations
+
+import decimal
+from typing import TYPE_CHECKING
+
from babel.core import Locale
-from babel.numbers import format_decimal, LC_NUMERIC
+from babel.numbers import LC_NUMERIC, format_decimal
+if TYPE_CHECKING:
+ from typing_extensions import Literal
class UnknownUnitError(ValueError):
- def __init__(self, unit, locale):
+ def __init__(self, unit: str, locale: Locale) -> None:
ValueError.__init__(self, f"{unit} is not a known unit in {locale}")
-def get_unit_name(measurement_unit, length='long', locale=LC_NUMERIC):
+def get_unit_name(
+ measurement_unit: str,
+ length: Literal['short', 'long', 'narrow'] = 'long',
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str | None:
"""
Get the display name for a measurement unit in the given locale.
@@ -36,7 +47,7 @@ def get_unit_name(measurement_unit, length='long', locale=LC_NUMERIC):
return locale.unit_display_names.get(unit, {}).get(length)
-def _find_unit_pattern(unit_id, locale=LC_NUMERIC):
+def _find_unit_pattern(unit_id: str, locale: Locale | str | None = LC_NUMERIC) -> str | None:
"""
Expand an unit into a qualified form.
@@ -62,7 +73,13 @@ def _find_unit_pattern(unit_id, locale=LC_NUMERIC):
return unit_pattern
-def format_unit(value, measurement_unit, length='long', format=None, locale=LC_NUMERIC):
+def format_unit(
+ value: float | decimal.Decimal,
+ measurement_unit: str,
+ length: Literal['short', 'long', 'narrow'] = 'long',
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str:
"""Format a value of a given unit.
Values are formatted according to the locale's usual pluralization rules
@@ -132,7 +149,11 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N
return f"{formatted_value} {fallback_name or measurement_unit}" # pragma: no cover
-def _find_compound_unit(numerator_unit, denominator_unit, locale=LC_NUMERIC):
+def _find_compound_unit(
+ numerator_unit: str,
+ denominator_unit: str,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str | None:
"""
Find a predefined compound unit pattern.
@@ -181,10 +202,14 @@ def _find_compound_unit(numerator_unit, denominator_unit, locale=LC_NUMERIC):
def format_compound_unit(
- numerator_value, numerator_unit=None,
- denominator_value=1, denominator_unit=None,
- length='long', format=None, locale=LC_NUMERIC
-):
+ numerator_value: float | decimal.Decimal,
+ numerator_unit: str | None = None,
+ denominator_value: float | decimal.Decimal = 1,
+ denominator_unit: str | None = None,
+ length: Literal["short", "long", "narrow"] = "long",
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str | None:
"""
Format a compound number value, i.e. "kilometers per hour" or similar.
diff --git a/babel/util.py b/babel/util.py
index 0436b9e..f159c33 100644
--- a/babel/util.py
+++ b/babel/util.py
@@ -7,20 +7,26 @@
:copyright: (c) 2013-2022 by the Babel Team.
:license: BSD, see LICENSE for more details.
"""
+from __future__ import annotations
import codecs
import collections
-from datetime import timedelta, tzinfo
import os
import re
import textwrap
+from collections.abc import Generator, Iterable
+from datetime import datetime as datetime_, timedelta, tzinfo
+from typing import IO, Any, TypeVar
+
import pytz as _pytz
+
from babel import localtime
missing = object()
+_T = TypeVar("_T")
-def distinct(iterable):
+def distinct(iterable: Iterable[_T]) -> Generator[_T, None, None]:
"""Yield all items in an iterable collection that are distinct.
Unlike when using sets for a similar effect, the original ordering of the
@@ -44,7 +50,7 @@ PYTHON_MAGIC_COMMENT_re = re.compile(
br'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
-def parse_encoding(fp):
+def parse_encoding(fp: IO[bytes]) -> str | None:
"""Deduce the encoding of a source file from magic comment.
It does this in the same way as the `Python interpreter`__
@@ -96,7 +102,7 @@ PYTHON_FUTURE_IMPORT_re = re.compile(
r'from\s+__future__\s+import\s+\(*(.+)\)*')
-def parse_future_flags(fp, encoding='latin-1'):
+def parse_future_flags(fp: IO[bytes], encoding: str = 'latin-1') -> int:
"""Parse the compiler flags by :mod:`__future__` from the given Python
code.
"""
@@ -128,7 +134,7 @@ def parse_future_flags(fp, encoding='latin-1'):
return flags
-def pathmatch(pattern, filename):
+def pathmatch(pattern: str, filename: str) -> bool:
"""Extended pathname pattern matching.
This function is similar to what is provided by the ``fnmatch`` module in
@@ -200,7 +206,7 @@ class TextWrapper(textwrap.TextWrapper):
)
-def wraptext(text, width=70, initial_indent='', subsequent_indent=''):
+def wraptext(text: str, width: int = 70, initial_indent: str = '', subsequent_indent: str = '') -> list[str]:
"""Simple wrapper around the ``textwrap.wrap`` function in the standard
library. This version does not wrap lines on hyphens in words.
@@ -224,25 +230,26 @@ odict = collections.OrderedDict
class FixedOffsetTimezone(tzinfo):
"""Fixed offset in minutes east from UTC."""
- def __init__(self, offset, name=None):
+ def __init__(self, offset: float, name: str | None = None) -> None:
+
self._offset = timedelta(minutes=offset)
if name is None:
name = 'Etc/GMT%+d' % offset
self.zone = name
- def __str__(self):
+ def __str__(self) -> str:
return self.zone
- def __repr__(self):
+ def __repr__(self) -> str:
return f'<FixedOffset "{self.zone}" {self._offset}>'
- def utcoffset(self, dt):
+ def utcoffset(self, dt: datetime_) -> timedelta:
return self._offset
- def tzname(self, dt):
+ def tzname(self, dt: datetime_) -> str:
return self.zone
- def dst(self, dt):
+ def dst(self, dt: datetime_) -> timedelta:
return ZERO
@@ -258,5 +265,5 @@ DSTDIFF = localtime.DSTDIFF
ZERO = localtime.ZERO
-def _cmp(a, b):
+def _cmp(a: Any, b: Any):
return (a > b) - (a < b)