From 53637ddbacaef2474429b22176091a362ce6567f Mon Sep 17 00:00:00 2001 From: Jonah Lawrence Date: Wed, 11 Jan 2023 01:54:11 -0700 Subject: Add type annotations (#934) Refs e.g. https://github.com/python/typeshed/pull/9455 Co-authored-by: Spencer Brown Co-authored-by: Aarni Koskela --- .coveragerc | 1 + babel/core.py | 186 +++++++++++++++++++++++++--------------- babel/dates.py | 169 +++++++++++++++++++++--------------- babel/languages.py | 6 +- babel/lists.py | 11 ++- babel/localedata.py | 43 +++++----- babel/localtime/__init__.py | 16 ++-- babel/localtime/_unix.py | 4 +- babel/localtime/_win32.py | 14 +-- babel/messages/catalog.py | 157 +++++++++++++++++++++------------ babel/messages/checkers.py | 25 +++--- babel/messages/extract.py | 133 ++++++++++++++++++++++------ babel/messages/jslexer.py | 23 +++-- babel/messages/mofile.py | 12 ++- babel/messages/plurals.py | 11 +-- babel/messages/pofile.py | 92 +++++++++++++------- babel/numbers.py | 205 ++++++++++++++++++++++++++++++++------------ babel/plural.py | 74 +++++++++------- babel/py.typed | 1 + babel/support.py | 201 ++++++++++++++++++++++++++----------------- babel/units.py | 45 +++++++--- babel/util.py | 33 ++++--- 22 files changed, 958 insertions(+), 504 deletions(-) create mode 100644 babel/py.typed diff --git a/.coveragerc b/.coveragerc index f98d802..32128ff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,4 @@ exclude_lines = NotImplemented pragma: no cover warnings.warn + if TYPE_CHECKING: 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" {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.*?)(?P[+-]\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 , 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'