From 859ba8dadcc87ce030670ad2ffb5331c9bc5edff Mon Sep 17 00:00:00 2001 From: Ruff Date: Wed, 19 Mar 2025 13:05:15 +0200 Subject: [PATCH] Run `ruff format babel` --- babel/__init__.py | 22 ++-- babel/core.py | 105 ++++++++++------ babel/dates.py | 130 ++++++++++++------- babel/languages.py | 10 +- babel/lists.py | 29 +++-- babel/localedata.py | 26 ++-- babel/localtime/__init__.py | 12 +- babel/localtime/_fallback.py | 11 +- babel/localtime/_unix.py | 2 +- babel/localtime/_win32.py | 3 +- babel/messages/__init__.py | 10 +- babel/messages/catalog.py | 105 +++++++++++----- babel/messages/checkers.py | 19 +-- babel/messages/extract.py | 116 ++++++++++------- babel/messages/frontend.py | 158 +++++++++++++++-------- babel/messages/jslexer.py | 30 +++-- babel/messages/mofile.py | 22 ++-- babel/messages/plurals.py | 11 +- babel/messages/pofile.py | 70 ++++++++--- babel/numbers.py | 238 ++++++++++++++++++++++++----------- babel/plural.py | 50 +++++--- babel/support.py | 131 ++++++++++++++----- babel/units.py | 32 ++++- babel/util.py | 19 ++- 24 files changed, 908 insertions(+), 453 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index 7b277455..0edfa9a8 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -1,19 +1,19 @@ """ - babel - ~~~~~ +babel +~~~~~ - Integrated collection of utilities that assist in internationalizing and - localizing applications. +Integrated collection of utilities that assist in internationalizing and +localizing applications. - This package is basically composed of two major parts: +This package is basically composed of two major parts: - * tools to build and work with ``gettext`` message catalogs - * a Python interface to the CLDR (Common Locale Data Repository), providing - access to various locale display names, localized number and date - formatting, etc. + * tools to build and work with ``gettext`` message catalogs + * a Python interface to the CLDR (Common Locale Data Repository), providing + access to various locale display names, localized number and date + formatting, etc. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from babel.core import ( diff --git a/babel/core.py b/babel/core.py index eb159c31..a59e7312 100644 --- a/babel/core.py +++ b/babel/core.py @@ -1,11 +1,11 @@ """ - babel.core - ~~~~~~~~~~ +babel.core +~~~~~~~~~~ - Core locale representation and locale data access. +Core locale representation and locale data access. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -56,12 +56,14 @@ _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 ' - 'a source checkout from Babel and you did ' - 'not build the data files. Just make sure ' - 'to run "python setup.py import_cldr" before ' - 'installing the library.') + raise RuntimeError( + 'The babel data files are not available. ' + 'This usually happens because you are using ' + 'a source checkout from Babel and you did ' + 'not build the data files. Just make sure ' + 'to run "python setup.py import_cldr" before ' + 'installing the library.', + ) def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]: @@ -216,7 +218,11 @@ class Locale: raise UnknownLocaleError(identifier) @classmethod - def default(cls, category: str | None = None, aliases: Mapping[str, str] = LOCALE_ALIASES) -> Locale: + 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']: @@ -268,8 +274,7 @@ class Locale: :param aliases: a dictionary of aliases for locale identifiers :param sep: separator for parsing; e.g. Windows tends to use '-' instead of '_'. """ - identifier = negotiate_locale(preferred, available, sep=sep, - aliases=aliases) + identifier = negotiate_locale(preferred, available, sep=sep, aliases=aliases) if identifier: return Locale.parse(identifier, sep=sep) return None @@ -421,7 +426,9 @@ class Locale: else: language2, _, script2, variant2 = parts2 modifier2 = None - locale = _try_load_reducing((language2, territory, script2, variant2, modifier2)) + locale = _try_load_reducing( + (language2, territory, script2, variant2, modifier2), + ) if locale is not None: return locale @@ -432,19 +439,18 @@ class Locale: if not hasattr(other, key): return False return ( - self.language == getattr(other, 'language') and # noqa: B009 - self.territory == getattr(other, 'territory') and # noqa: B009 - self.script == getattr(other, 'script') and # noqa: B009 - self.variant == getattr(other, 'variant') and # noqa: B009 - self.modifier == getattr(other, 'modifier') # noqa: B009 + self.language == getattr(other, 'language') # noqa: B009 + and self.territory == getattr(other, 'territory') # noqa: B009 + and self.script == getattr(other, 'script') # noqa: B009 + and self.variant == getattr(other, 'variant') # noqa: B009 + and self.modifier == getattr(other, 'modifier') # noqa: B009 ) def __ne__(self, other: object) -> bool: return not self.__eq__(other) def __hash__(self) -> int: - return hash((self.language, self.territory, self.script, - self.variant, self.modifier)) + return hash((self.language, self.territory, self.script, self.variant, self.modifier)) def __repr__(self) -> str: parameters = [''] @@ -455,9 +461,9 @@ class Locale: return f"Locale({self.language!r}{', '.join(parameters)})" def __str__(self) -> str: - return get_locale_identifier((self.language, self.territory, - self.script, self.variant, - self.modifier)) + return get_locale_identifier( + (self.language, self.territory, self.script, self.variant, self.modifier), + ) @property def _data(self) -> localedata.LocaleDataDict: @@ -500,7 +506,9 @@ class Locale: retval += f" ({detail_string})" return retval - display_name = property(get_display_name, doc="""\ + display_name = property( + get_display_name, + doc="""\ The localized display name of the locale. >>> Locale('en').display_name @@ -511,7 +519,8 @@ class Locale: u'svenska' :type: `unicode` - """) + """, + ) def get_language_name(self, locale: Locale | str | None = None) -> str | None: """Return the language of this locale in the given locale. @@ -528,12 +537,15 @@ class Locale: locale = Locale.parse(locale) return locale.languages.get(self.language) - language_name = property(get_language_name, doc="""\ + language_name = property( + get_language_name, + doc="""\ The localized language name of the locale. >>> Locale('en', 'US').language_name u'English' - """) + """, + ) def get_territory_name(self, locale: Locale | str | None = None) -> str | None: """Return the territory name in the given locale.""" @@ -542,12 +554,15 @@ class Locale: locale = Locale.parse(locale) return locale.territories.get(self.territory or '') - territory_name = property(get_territory_name, doc="""\ + territory_name = property( + get_territory_name, + doc="""\ The localized territory name of the locale if available. >>> Locale('de', 'DE').territory_name u'Deutschland' - """) + """, + ) def get_script_name(self, locale: Locale | str | None = None) -> str | None: """Return the script name in the given locale.""" @@ -556,12 +571,15 @@ class Locale: locale = Locale.parse(locale) return locale.scripts.get(self.script or '') - script_name = property(get_script_name, doc="""\ + script_name = property( + get_script_name, + doc="""\ The localized script name of the locale if available. >>> Locale('sr', 'ME', script='Latn').script_name u'latinica' - """) + """, + ) @property def english_name(self) -> str | None: @@ -785,8 +803,7 @@ class Locale: @property def day_period_rules(self) -> localedata.LocaleDataDict: - """Day period rules for the locale. Used by `get_period_id`. - """ + """Day period rules for the locale. Used by `get_period_id`.""" return self._data.get('day_period_rules', localedata.LocaleDataDict({})) @property @@ -1150,7 +1167,12 @@ def default_locale( return None -def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: str = '_', aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None: +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']) @@ -1216,7 +1238,10 @@ def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: st def parse_locale( identifier: str, sep: str = '_', -) -> tuple[str, str | None, str | None, str | None] | tuple[str, str | None, str | None, str | None, str | None]: +) -> ( + tuple[str, str | None, str | None, str | None] + | tuple[str, str | None, str | None, str | None, str | None] +): """Parse a locale identifier into a tuple of the form ``(language, territory, script, variant, modifier)``. @@ -1294,8 +1319,10 @@ def parse_locale( territory = parts.pop(0) if parts and ( - len(parts[0]) == 4 and parts[0][0].isdigit() or - len(parts[0]) >= 5 and parts[0][0].isalpha() + len(parts[0]) == 4 + and parts[0][0].isdigit() + or len(parts[0]) >= 5 + and parts[0][0].isalpha() ): variant = parts.pop().upper() diff --git a/babel/dates.py b/babel/dates.py index 8c6e1fd5..70b61532 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1,18 +1,18 @@ """ - babel.dates - ~~~~~~~~~~~ +babel.dates +~~~~~~~~~~~ - Locale dependent formatting and parsing of dates and times. +Locale dependent formatting and parsing of dates and times. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_TIME``, - * ``LC_ALL``, and - * ``LANG`` + * ``LC_TIME``, + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -38,6 +38,7 @@ from babel.localedata import LocaleDataDict if TYPE_CHECKING: from typing_extensions import TypeAlias + _Instant: TypeAlias = datetime.date | datetime.time | float | None _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short'] _Context: TypeAlias = Literal['format', 'stand-alone'] @@ -75,7 +76,9 @@ def _localize(tz: datetime.tzinfo, dt: datetime.datetime) -> datetime.datetime: return dt.astimezone(tz) -def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime.datetime | None, datetime.tzinfo]: +def _get_dt_and_tzinfo( + dt_or_tzinfo: _DtOrTzinfo, +) -> tuple[datetime.datetime | None, datetime.tzinfo]: """ Parse a `dt_or_tzinfo` value into a datetime and a tzinfo. @@ -153,13 +156,16 @@ def _get_datetime(instant: _Instant) -> datetime.datetime: return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None) elif isinstance(instant, datetime.time): return datetime.datetime.combine(datetime.date.today(), instant) - elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): + elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): # fmt: skip return datetime.datetime.combine(instant, datetime.time()) # TODO (3.x): Add an assertion/type check for this fallthrough branch: return instant -def _ensure_datetime_tzinfo(dt: datetime.datetime, tzinfo: datetime.tzinfo | None = None) -> datetime.datetime: +def _ensure_datetime_tzinfo( + dt: datetime.datetime, + tzinfo: datetime.tzinfo | None = None, +) -> datetime.datetime: """ Ensure the datetime passed has an attached tzinfo. @@ -524,7 +530,11 @@ def get_timezone_location( if territory not in locale.territories: territory = 'ZZ' # invalid/unknown territory_name = locale.territories[territory] - if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1: + if ( + not return_city + and territory + and len(get_global('territory_zones').get(territory, [])) == 1 + ): return region_format % territory_name # Otherwise, include the city in the output @@ -543,10 +553,13 @@ def get_timezone_location( if return_city: return city_name - return region_format % (fallback_format % { - '0': city_name, - '1': territory_name, - }) + return region_format % ( + fallback_format + % { + '0': city_name, + '1': territory_name, + } + ) def get_timezone_name( @@ -744,11 +757,12 @@ def format_datetime( locale = Locale.parse(locale or LC_TIME) if format in ('full', 'long', 'medium', 'short'): - return get_datetime_format(format, locale=locale) \ - .replace("'", "") \ - .replace('{0}', format_time(datetime, format, tzinfo=None, - locale=locale)) \ + return ( + get_datetime_format(format, locale=locale) + .replace("'", "") + .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale)) .replace('{1}', format_date(datetime, format, locale=locale)) + ) else: return parse_pattern(format).apply(datetime, locale) @@ -890,8 +904,16 @@ TIMEDELTA_UNITS: tuple[tuple[str, int], ...] = ( def format_timedelta( delta: datetime.timedelta | int, - granularity: Literal['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] = 'second', - threshold: float = .85, + granularity: Literal[ + 'year', + 'month', + 'week', + 'day', + 'hour', + 'minute', + 'second', + ] = 'second', + threshold: float = 0.85, add_direction: bool = False, format: Literal['narrow', 'short', 'medium', 'long'] = 'long', locale: Locale | str | None = None, @@ -955,8 +977,7 @@ def format_timedelta( raise TypeError('Format must be one of "narrow", "short" or "long"') if format == 'medium': warnings.warn( - '"medium" value for format param of format_timedelta' - ' is deprecated. Use "long" instead', + '"medium" value for format param of format_timedelta is deprecated. Use "long" instead', category=DeprecationWarning, stacklevel=2, ) @@ -973,7 +994,7 @@ def format_timedelta( if add_direction: # Try to find the length variant version first ("year-narrow") # before falling back to the default. - unit_rel_patterns = (date_fields.get(f"{a_unit}-{format}") or date_fields[a_unit]) + unit_rel_patterns = date_fields.get(f"{a_unit}-{format}") or date_fields[a_unit] if seconds >= 0: yield unit_rel_patterns['future'] else: @@ -1040,9 +1061,9 @@ def _format_fallback_interval( return format(start) return ( - locale.interval_formats.get(None, "{0}-{1}"). - replace("{0}", formatted_start). - replace("{1}", formatted_end) + locale.interval_formats.get(None, "{0}-{1}") + .replace("{0}", formatted_start) + .replace("{1}", formatted_end) ) @@ -1142,8 +1163,7 @@ def format_interval( # > format the start and end datetime, as above. return "".join( parse_pattern(pattern).apply(instant, locale) - for pattern, instant - in zip(skel_formats[field], (start, end)) + for pattern, instant in zip(skel_formats[field], (start, end)) ) # > Otherwise, format the start and end datetime using the fallback pattern. @@ -1201,8 +1221,10 @@ def get_period_id( return rule_id else: # e.g. from="21:00" before="06:00" - if rule["from"] <= seconds_past_midnight < 86400 or \ - 0 <= seconds_past_midnight < rule["before"]: + if ( + rule["from"] <= seconds_past_midnight < 86400 + or 0 <= seconds_past_midnight < rule["before"] + ): return rule_id start_ok = end_ok = False @@ -1377,7 +1399,6 @@ def parse_time( class DateTimePattern: - def __init__(self, pattern: str, format: DateTimeFormat): self.pattern = pattern self.format = format @@ -1404,7 +1425,6 @@ class DateTimePattern: class DateTimeFormat: - def __init__( self, value: datetime.date | datetime.time, @@ -1485,7 +1505,9 @@ class DateTimeFormat: elif char == 'a': return int(self.value.hour >= 12) # 0 for am, 1 for pm else: - raise NotImplementedError(f"Not implemented: extracting {char!r} from {self.value!r}") + raise NotImplementedError( + f"Not implemented: extracting {char!r} from {self.value!r}", + ) def format_era(self, char: str, num: int) -> str: width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] @@ -1599,8 +1621,12 @@ class DateTimeFormat: :param num: count of format character """ - widths = [{3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], - 'wide', 'narrow', 'abbreviated'] + widths = [ + {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], + 'wide', + 'narrow', + 'abbreviated', + ] if char == 'a': period = 'pm' if self.value.hour >= 12 else 'am' context = 'format' @@ -1623,8 +1649,12 @@ class DateTimeFormat: return self.format(round(value, num) * 10**num, num) def format_milliseconds_in_day(self, num): - msecs = self.value.microsecond // 1000 + self.value.second * 1000 + \ - self.value.minute * 60000 + self.value.hour * 3600000 + msecs = ( + self.value.microsecond // 1000 + + self.value.second * 1000 + + self.value.minute * 60000 + + self.value.hour * 3600000 + ) return self.format(msecs, num) def format_timezone(self, char: str, num: int) -> str: @@ -1681,12 +1711,13 @@ class DateTimeFormat: week = self.get_week_number(day_of_year) if week == 0: date = datetime.date(self.value.year - 1, 12, 31) - week = self.get_week_number(self.get_day_of_year(date), - date.weekday()) + week = self.get_week_number(self.get_day_of_year(date), date.weekday()) elif week > 52: weekday = datetime.date(self.value.year + 1, 1, 1).weekday() - if self.get_week_number(1, weekday) == 1 and \ - 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day: + if ( + self.get_week_number(1, weekday) == 1 + and 32 - (weekday - self.locale.first_week_day) % 7 <= self.value.day + ): week = 1 return week @@ -1715,8 +1746,7 @@ class DateTimeFormat: """ if day_of_week is None: day_of_week = self.value.weekday() - first_day = (day_of_week - self.locale.first_week_day - - day_of_period + 1) % 7 + first_day = (day_of_week - self.locale.first_week_day - day_of_period + 1) % 7 if first_day < 0: first_day += 7 week_number = (day_of_period + first_day - 1) // 7 @@ -1919,7 +1949,11 @@ def split_interval_pattern(pattern: str) -> list[str]: return [untokenize_pattern(tokens) for tokens in parts] -def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields: bool = False) -> str | None: +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 564f555d..5b2396c8 100644 --- a/babel/languages.py +++ b/babel/languages.py @@ -3,7 +3,11 @@ from __future__ import annotations from babel.core import get_global -def get_official_languages(territory: str, regional: bool = False, de_facto: bool = False) -> tuple[str, ...]: +def get_official_languages( + territory: str, + regional: bool = False, + de_facto: bool = False, +) -> tuple[str, ...]: """ Get the official language(s) for the given territory. @@ -43,7 +47,9 @@ def get_official_languages(territory: str, regional: bool = False, de_facto: boo return tuple(lang for _, lang in pairs) -def get_territory_language_info(territory: str) -> dict[str, dict[str, float | str | None]]: +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 353171c7..0c17f75b 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -1,18 +1,19 @@ """ - babel.lists - ~~~~~~~~~~~ +babel.lists +~~~~~~~~~~~ - Locale dependent formatting of lists. +Locale dependent formatting of lists. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_ALL``, and - * ``LANG`` + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2015-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2015-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import warnings @@ -37,7 +38,15 @@ def __getattr__(name): def format_list( lst: Sequence[str], - style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard', + style: Literal[ + 'standard', + 'standard-short', + 'or', + 'or-short', + 'unit', + 'unit-short', + 'unit-narrow', + ] = 'standard', locale: Locale | str | None = None, ) -> str: """ diff --git a/babel/localedata.py b/babel/localedata.py index 59f1db09..7cff4871 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -1,14 +1,14 @@ """ - babel.localedata - ~~~~~~~~~~~~~~~~ +babel.localedata +~~~~~~~~~~~~~~~~ - Low-level locale data access. +Low-level locale data access. - :note: The `Locale` class, which uses this module under the hood, provides a - more convenient interface for accessing the locale data. +:note: The `Locale` class, which uses this module under the hood, provides a + more convenient interface for accessing the locale data. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -89,8 +89,9 @@ def locale_identifiers() -> list[str]: """ return [ stem - for stem, extension in - (os.path.splitext(filename) for filename in os.listdir(_dirname)) + for stem, extension in ( + os.path.splitext(filename) for filename in os.listdir(_dirname) + ) if extension == '.dat' and stem != 'root' ] @@ -151,6 +152,7 @@ def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str data = {} else: from babel.core import get_global + parent = get_global('parent_exceptions').get(name) if not parent: if _is_non_likely_script(name): @@ -242,7 +244,11 @@ class LocaleDataDict(abc.MutableMapping): values. """ - def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = 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 diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index 854c0749..8076516e 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -1,12 +1,12 @@ """ - babel.localtime - ~~~~~~~~~~~~~~~ +babel.localtime +~~~~~~~~~~~~~~~ - Babel specific fork of tzlocal to determine the local timezone - of the system. +Babel specific fork of tzlocal to determine the local timezone +of the system. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ import datetime diff --git a/babel/localtime/_fallback.py b/babel/localtime/_fallback.py index 73def22e..6cd67b5c 100644 --- a/babel/localtime/_fallback.py +++ b/babel/localtime/_fallback.py @@ -1,11 +1,11 @@ """ - babel.localtime._fallback - ~~~~~~~~~~~~~~~~~~~~~~~~~ +babel.localtime._fallback +~~~~~~~~~~~~~~~~~~~~~~~~~ - Emulated fallback local timezone when all else fails. +Emulated fallback local timezone when all else fails. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ import datetime @@ -19,7 +19,6 @@ ZERO = datetime.timedelta(0) class _FallbackLocalTimezone(datetime.tzinfo): - def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta: if self._isdst(dt): return DSTOFFSET diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index 782a7d24..70dd2322 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -51,7 +51,7 @@ def _get_localzone(_root: str = '/') -> datetime.tzinfo: # `None` (as a fix for #1092). # Instead, let's just "fix" the double slash symlink by stripping # leading slashes before passing the assumed zone name forward. - zone_name = link_dst[pos + 10:].lstrip("/") + zone_name = link_dst[pos + 10 :].lstrip("/") tzinfo = _get_tzinfo(zone_name) if tzinfo is not None: return tzinfo diff --git a/babel/localtime/_win32.py b/babel/localtime/_win32.py index 1a52567b..0fb625ba 100644 --- a/babel/localtime/_win32.py +++ b/babel/localtime/_win32.py @@ -92,7 +92,6 @@ def get_localzone_name() -> str: def _get_localzone() -> datetime.tzinfo: if winreg is None: - raise LookupError( - 'Runtime support not available') + raise LookupError('Runtime support not available') return _get_tzinfo_or_raise(get_localzone_name()) diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index ca83faa9..6e445e6b 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -1,11 +1,11 @@ """ - babel.messages - ~~~~~~~~~~~~~~ +babel.messages +~~~~~~~~~~~~~~ - Support for ``gettext`` message catalogs. +Support for ``gettext`` message catalogs. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from babel.messages.catalog import ( diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index b43edf54..4317ec92 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -1,12 +1,13 @@ """ - babel.messages.catalog - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.catalog +~~~~~~~~~~~~~~~~~~~~~~ - Data structures for message catalogs. +Data structures for message catalogs. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import datetime @@ -54,9 +55,11 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): s.set_seq2(word) for x in possibilities: s.set_seq1(x) - if s.real_quick_ratio() >= cutoff and \ - s.quick_ratio() >= cutoff and \ - s.ratio() >= cutoff: + if ( + s.real_quick_ratio() >= cutoff + and s.quick_ratio() >= cutoff + and s.ratio() >= cutoff + ): result.append((s.ratio(), x)) # Move the best scorers to head of list @@ -65,7 +68,8 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): return [x for score, x in result] -PYTHON_FORMAT = re.compile(r''' +PYTHON_FORMAT = re.compile( + r''' \% (?:\(([\w]*)\))? ( @@ -74,7 +78,9 @@ PYTHON_FORMAT = re.compile(r''' [hlL]? ) ([diouxXeEfFgGcrs%]) -''', re.VERBOSE) +''', + re.VERBOSE, +) def _has_python_brace_format(string: str) -> bool: @@ -191,10 +197,12 @@ class Message: 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: return obj.id[0], obj.context or '' return obj.id, obj.context or '' + return _cmp(values_to_compare(self), values_to_compare(other)) def __gt__(self, other: object) -> bool: @@ -246,6 +254,7 @@ class Message: in a catalog. """ from babel.messages.checkers import checkers + errors: list[TranslationError] = [] for checker in checkers: try: @@ -334,6 +343,7 @@ DEFAULT_HEADER = """\ 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() m['content-type'] = value return dict(m.get_params()) @@ -439,7 +449,9 @@ class Catalog: self._locale = None return - raise TypeError(f"`locale` must be a Locale, a locale identifier string, or None; got {locale!r}") + raise TypeError( + f"`locale` must be a Locale, a locale identifier string, or None; got {locale!r}", + ) def _get_locale(self) -> Locale | None: return self._locale @@ -455,11 +467,13 @@ class Catalog: year = datetime.datetime.now(LOCALTZ).strftime('%Y') if hasattr(self.revision_date, 'strftime'): year = self.revision_date.strftime('%Y') - comment = comment.replace('PROJECT', self.project) \ - .replace('VERSION', self.version) \ - .replace('YEAR', year) \ - .replace('ORGANIZATION', self.copyright_holder) - locale_name = (self.locale.english_name if self.locale else self.locale_identifier) + comment = ( + comment.replace('PROJECT', self.project) + .replace('VERSION', self.version) + .replace('YEAR', year) + .replace('ORGANIZATION', self.copyright_holder) + ) + locale_name = self.locale.english_name if self.locale else self.locale_identifier if locale_name: comment = comment.replace("Translations template", f"{locale_name} translations") return comment @@ -467,7 +481,10 @@ class Catalog: def _set_header_comment(self, string: str | None) -> None: self._header_comment = string - header_comment = property(_get_header_comment, _set_header_comment, doc="""\ + header_comment = property( + _get_header_comment, + _set_header_comment, + doc="""\ The header comment for the catalog. >>> catalog = Catalog(project='Foobar', version='1.0', @@ -498,11 +515,16 @@ class Catalog: # :type: `unicode` - """) + """, + ) def _get_mime_headers(self) -> list[tuple[str, str]]: if isinstance(self.revision_date, (datetime.datetime, datetime.time, int, float)): - revision_date = format_datetime(self.revision_date, 'yyyy-MM-dd HH:mmZ', locale='en') + revision_date = format_datetime( + self.revision_date, + 'yyyy-MM-dd HH:mmZ', + locale='en', + ) else: revision_date = self.revision_date @@ -566,7 +588,10 @@ class Catalog: if 'YEAR' not in value: self.revision_date = _parse_datetime_header(value) - mime_headers = property(_get_mime_headers, _set_mime_headers, doc="""\ + mime_headers = property( + _get_mime_headers, + _set_mime_headers, + doc="""\ The MIME headers of the catalog, used for the special ``msgid ""`` entry. The behavior of this property changes slightly depending on whether a locale @@ -616,7 +641,8 @@ class Catalog: Generated-By: Babel ... :type: `list` - """) + """, + ) @property def num_plurals(self) -> int: @@ -749,8 +775,9 @@ class Catalog: self.fuzzy = message.fuzzy else: if isinstance(id, (list, tuple)): - assert isinstance(message.string, (list, tuple)), \ + assert isinstance(message.string, (list, tuple)), ( f"Expected sequence but got {type(message.string)}" + ) self._messages[key] = message def add( @@ -790,9 +817,17 @@ class Catalog: PO file, if any :param context: the message context """ - message = Message(id, string, list(locations), flags, auto_comments, - user_comments, previous_id, lineno=lineno, - context=context) + message = Message( + id, + string, + list(locations), + flags, + auto_comments, + user_comments, + previous_id, + lineno=lineno, + context=context, + ) self[id] = message return message @@ -905,7 +940,11 @@ class Catalog: fuzzy_candidates[self._to_fuzzy_match_key(key)] = (key, ctxt) fuzzy_matches = set() - def _merge(message: Message, oldkey: tuple[str, str] | str, newkey: tuple[str, str] | str) -> None: + def _merge( + message: Message, + oldkey: tuple[str, str] | str, + newkey: tuple[str, str] | str, + ) -> None: message = message.clone() fuzzy = False if oldkey != newkey: @@ -933,7 +972,7 @@ class Catalog: ) elif len(message.string) != self.num_plurals: fuzzy = True - message.string = tuple(message.string[:len(oldmsg.string)]) + message.string = tuple(message.string[: len(oldmsg.string)]) elif isinstance(message.string, (list, tuple)): fuzzy = True message.string = message.string[0] @@ -987,7 +1026,11 @@ class Catalog: matchkey = key return matchkey.lower().strip() - def _key_for(self, id: _MessageID, context: str | None = None) -> tuple[str, str] | str: + 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. @@ -1007,10 +1050,6 @@ class Catalog: for key in self._messages.keys() | other._messages.keys(): message_1 = self.get(key) message_2 = other.get(key) - if ( - message_1 is None - or message_2 is None - or not message_1.is_identical(message_2) - ): + if message_1 is None or message_2 is None or not message_1.is_identical(message_2): return False return dict(self.mime_headers) == dict(other.mime_headers) diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index e56bce9a..28081160 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -1,14 +1,15 @@ """ - babel.messages.checkers - ~~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.checkers +~~~~~~~~~~~~~~~~~~~~~~~ - Various routines that help with validation of translations. +Various routines that help with validation of translations. - :since: version 0.9 +:since: version 0.9 - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations from collections.abc import Callable @@ -115,8 +116,9 @@ def _validate_format(format: str, alternative: str) -> None: positional = name is None else: if (name is None) != positional: - raise TranslationError('format string mixes positional ' - 'and named placeholders') + raise TranslationError( + 'format string mixes positional and named placeholders', + ) return bool(positional) a = _parse(format) @@ -161,6 +163,7 @@ def _validate_format(format: str, alternative: str) -> None: def _find_checkers() -> list[Callable[[Catalog | None, Message], object]]: from babel.messages._compat import find_entrypoints + checkers: list[Callable[[Catalog | None, Message], object]] = [] checkers.extend(load() for (name, load) in find_entrypoints('babel.checkers')) if len(checkers) == 0: diff --git a/babel/messages/extract.py b/babel/messages/extract.py index d3ae9da9..c4ce46e8 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -1,20 +1,21 @@ """ - babel.messages.extract - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.extract +~~~~~~~~~~~~~~~~~~~~~~ - Basic infrastructure for extracting localizable messages from source files. +Basic infrastructure for extracting localizable messages from source files. - This module defines an extensible system for collecting localizable message - strings from a variety of sources. A native extractor for Python source - files is builtin, extractors for other sources can be added using very - simple plugins. +This module defines an extensible system for collecting localizable message +strings from a variety of sources. A native extractor for Python source +files is builtin, extractors for other sources can be added using very +simple plugins. - The main entry points into the extraction functionality are the functions - `extract_from_dir` and `extract_from_file`. +The main entry points into the extraction functionality are the functions +`extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import ast @@ -103,11 +104,13 @@ 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: str): for tag in tags: if line.startswith(tag): - return line[len(tag):].strip() + return line[len(tag) :].strip() return line + comments[:] = [_strip(c) for c in comments] @@ -206,8 +209,7 @@ def extract_from_dir( absname = os.path.abspath(dirname) for root, dirnames, filenames in os.walk(absname): dirnames[:] = [ - subdir for subdir in dirnames - if directory_filter(os.path.join(root, subdir)) + subdir for subdir in dirnames if directory_filter(os.path.join(root, subdir)) ] dirnames.sort() filenames.sort() @@ -280,7 +282,8 @@ def check_and_call_extract_file( if callback: callback(filename, method, options) for message_tuple in extract_from_file( - method, filepath, + method, + filepath, keywords=keywords, comment_tags=comment_tags, options=options, @@ -321,8 +324,9 @@ def extract_from_file( return [] with open(filename, 'rb') as fileobj: - return list(extract(method, fileobj, keywords, comment_tags, - options, strip_comment_tags)) + return list( + extract(method, fileobj, keywords, comment_tags, options, strip_comment_tags), + ) def _match_messages_against_spec( @@ -357,7 +361,7 @@ def _match_messages_against_spec( first_msg_index = spec[0] - 1 # An empty string msgid isn't valid, emit a warning if not messages[first_msg_index]: - filename = (getattr(fileobj, "name", None) or "(unknown)") + filename = getattr(fileobj, "name", None) or "(unknown)" sys.stderr.write( f"{filename}:{lineno}: warning: Empty msgid. It is reserved by GNU gettext: gettext(\"\") " f"returns the header entry with meta information, not the empty string.\n", @@ -431,7 +435,7 @@ def extract( elif ':' in method or '.' in method: if ':' not in method: lastdot = method.rfind('.') - module, attrname = method[:lastdot], method[lastdot + 1:] + module, attrname = method[:lastdot], method[lastdot + 1 :] else: module, attrname = method.split(':', 1) func = getattr(__import__(module, {}, {}, [attrname]), attrname) @@ -445,8 +449,7 @@ def extract( if func is None: raise ValueError(f"Unknown extraction method {method!r}") - results = func(fileobj, keywords.keys(), comment_tags, - options=options or {}) + results = func(fileobj, keywords.keys(), comment_tags, options=options or {}) for lineno, funcname, messages, comments in results: if not isinstance(messages, (list, tuple)): @@ -543,8 +546,7 @@ def extract_python( elif not call_stack and tok == COMMENT: # Strip the comment token from the line value = value[1:].strip() - if in_translator_comments and \ - translator_comments[-1][0] == lineno - 1: + if in_translator_comments and translator_comments[-1][0] == lineno - 1: # We're already inside a translator comment, continue appending translator_comments.append((lineno, value)) continue @@ -556,7 +558,7 @@ def extract_python( translator_comments.append((lineno, value)) break elif funcname and len(call_stack) == 1: - nested = (tok == NAME and value in keywords) + nested = tok == NAME and value in keywords if (tok == OP and value == ')') or nested: if buf: messages.append(''.join(buf)) @@ -574,8 +576,12 @@ def extract_python( # to start this message's translation call is. translator_comments.clear() - yield (message_lineno, funcname, messages, - [comment[1] for comment in translator_comments]) + yield ( + message_lineno, + funcname, + messages, + [comment[1] for comment in translator_comments], + ) funcname = lineno = message_lineno = None call_stack.clear() @@ -682,6 +688,7 @@ def extract_javascript( :param lineno: line number offset (for parsing embedded fragments) """ from babel.messages.jslexer import Token, tokenize, unquote_string + funcname = message_lineno = None messages = [] last_argument = None @@ -711,8 +718,18 @@ def extract_javascript( call_stack = 0 token = Token('operator', ')', token.lineno) - if options.get('parse_template_string') and not funcname and token.type == 'template_string': - yield from parse_template_string(token.value, keywords, comment_tags, options, token.lineno) + if ( + options.get('parse_template_string') + and not funcname + and token.type == 'template_string' + ): + yield from parse_template_string( + token.value, + keywords, + comment_tags, + options, + token.lineno, + ) elif token.type == 'operator' and token.value == '(': if funcname: @@ -721,8 +738,7 @@ def extract_javascript( elif call_stack == -1 and token.type == 'linecomment': value = token.value[2:].strip() - if translator_comments and \ - translator_comments[-1][0] == token.lineno - 1: + if translator_comments and translator_comments[-1][0] == token.lineno - 1: translator_comments.append((token.lineno, value)) continue @@ -742,8 +758,7 @@ def extract_javascript( lines[0] = lines[0].strip() lines[1:] = dedent('\n'.join(lines[1:])).splitlines() for offset, line in enumerate(lines): - translator_comments.append((token.lineno + offset, - line)) + translator_comments.append((token.lineno + offset, line)) break elif funcname and call_stack == 0: @@ -759,13 +774,16 @@ def extract_javascript( # Comments don't apply unless they immediately precede the # message - if translator_comments and \ - translator_comments[-1][0] < message_lineno - 1: + if translator_comments and translator_comments[-1][0] < message_lineno - 1: translator_comments = [] if messages is not None: - yield (message_lineno, funcname, messages, - [comment[1] for comment in translator_comments]) + yield ( + message_lineno, + funcname, + messages, + [comment[1] for comment in translator_comments], + ) funcname = message_lineno = last_argument = None concatenate_next = False @@ -792,17 +810,22 @@ def extract_javascript( elif token.value == '+': concatenate_next = True - elif call_stack > 0 and token.type == 'operator' \ - and token.value == ')': + elif call_stack > 0 and token.type == 'operator' and token.value == ')': call_stack -= 1 elif funcname and call_stack == -1: funcname = None - elif call_stack == -1 and token.type == 'name' and \ - token.value in keywords and \ - (last_token is None or last_token.type != 'name' or - last_token.value != 'function'): + elif ( + call_stack == -1 + and token.type == 'name' + and token.value in keywords + and ( + last_token is None + or last_token.type != 'name' + or last_token.value != 'function' + ) + ): funcname = token.value last_token = token @@ -826,6 +849,7 @@ def parse_template_string( :param lineno: starting line number (optional) """ from babel.messages.jslexer import line_re + prev_character = None level = 0 inside_str = False @@ -845,7 +869,13 @@ def parse_template_string( if level == 0 and expression_contents: expression_contents = expression_contents[0:-1] fake_file_obj = io.BytesIO(expression_contents.encode()) - yield from extract_javascript(fake_file_obj, keywords, comment_tags, options, lineno) + yield from extract_javascript( + fake_file_obj, + keywords, + comment_tags, + options, + lineno, + ) lineno += len(line_re.findall(expression_contents)) expression_contents = '' prev_character = character diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index e5d2a113..6ef62ec4 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -1,11 +1,11 @@ """ - babel.messages.frontend - ~~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.frontend +~~~~~~~~~~~~~~~~~~~~~~~ - Frontends for the message extraction functionality. +Frontends for the message extraction functionality. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -200,7 +200,7 @@ class CompileCatalog(CommandMixin): n_errors += len(errors) if n_errors: self.log.error('%d errors encountered.', n_errors) - return (1 if n_errors else 0) + return 1 if n_errors else 0 def _get_po_mo_triples(self, domain: str): if not self.input_file: @@ -211,7 +211,7 @@ class CompileCatalog(CommandMixin): yield self.locale, po_file, po_file.with_suffix(".mo") else: for locale_path in dir_path.iterdir(): - po_file = locale_path / "LC_MESSAGES"/ f"{domain}.po" + po_file = locale_path / "LC_MESSAGES" / f"{domain}.po" if po_file.exists(): yield locale_path.name, po_file, po_file.with_suffix(".mo") else: @@ -219,7 +219,9 @@ class CompileCatalog(CommandMixin): if self.output_file: mo_file = pathlib.Path(self.output_file) else: - mo_file = pathlib.Path(self.directory) / self.locale / "LC_MESSAGES" / f"{domain}.mo" + mo_file = ( + pathlib.Path(self.directory) / self.locale / "LC_MESSAGES" / f"{domain}.mo" + ) yield self.locale, po_file, mo_file def _run_domain(self, domain): @@ -243,7 +245,10 @@ class CompileCatalog(CommandMixin): percentage = translated * 100 // len(catalog) self.log.info( '%d of %d messages (%d%%) translated in %s', - translated, len(catalog), percentage, po_file, + translated, + len(catalog), + percentage, + po_file, ) if catalog.fuzzy and not self.use_fuzzy: @@ -253,9 +258,7 @@ class CompileCatalog(CommandMixin): catalogs_and_errors[catalog] = catalog_errors = list(catalog.check()) for message, errors in catalog_errors: for error in errors: - self.log.error( - 'error: %s:%d: %s', po_file, message.lineno, error, - ) + self.log.error('error: %s:%d: %s', po_file, message.lineno, error) self.log.info('compiling catalog %s to %s', po_file, mo_file) @@ -273,9 +276,7 @@ def _make_directory_filter(ignore_patterns): def cli_directory_filter(dirname): basename = os.path.basename(dirname) return not any( - fnmatch.fnmatch(basename, ignore_pattern) - for ignore_pattern - in ignore_patterns + fnmatch.fnmatch(basename, ignore_pattern) for ignore_pattern in ignore_patterns ) return cli_directory_filter @@ -340,8 +341,13 @@ class ExtractMessages(CommandMixin): 'set the name and email of the last translator in output'), ] # fmt: skip boolean_options = [ - 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', - 'sort-output', 'sort-by-file', 'strip-comments', + 'no-default-keywords', + 'no-location', + 'omit-header', + 'no-wrap', + 'sort-output', + 'sort-by-file', + 'strip-comments', ] as_args = 'input-paths' multiple_value_options = ( @@ -483,13 +489,15 @@ class ExtractMessages(CommandMixin): def run(self): mappings = self._get_mappings() with open(self.output_file, 'wb') as outfile: - catalog = Catalog(project=self.project, - version=self.version, - msgid_bugs_address=self.msgid_bugs_address, - copyright_holder=self.copyright_holder, - charset=self.charset, - header_comment=(self.header_comment or DEFAULT_HEADER), - last_translator=self.last_translator) + catalog = Catalog( + project=self.project, + version=self.version, + msgid_bugs_address=self.msgid_bugs_address, + copyright_holder=self.copyright_holder, + charset=self.charset, + header_comment=(self.header_comment or DEFAULT_HEADER), + last_translator=self.last_translator, + ) for path, method_map, options_map in mappings: callback = self._build_callback(path) @@ -522,8 +530,13 @@ class ExtractMessages(CommandMixin): else: filepath = os.path.normpath(os.path.join(path, filename)) - catalog.add(message, None, [(filepath, lineno)], - auto_comments=comments, context=context) + catalog.add( + message, + None, + [(filepath, lineno)], + auto_comments=comments, + context=context, + ) self.log.info('writing PO template file to %s', self.output_file) write_po( @@ -555,7 +568,10 @@ class ExtractMessages(CommandMixin): ) else: with open(self.mapping_file) as fileobj: - method_map, options_map = parse_mapping_cfg(fileobj, filename=self.mapping_file) + method_map, options_map = parse_mapping_cfg( + fileobj, + filename=self.mapping_file, + ) for path in self.input_paths: mappings.append((path, method_map, options_map)) @@ -637,7 +653,9 @@ class InitCatalog(CommandMixin): def run(self): self.log.info( - 'creating catalog %s based on %s', self.output_file, self.input_file, + 'creating catalog %s based on %s', + self.output_file, + self.input_file, ) with open(self.input_file, 'rb') as infile: @@ -692,9 +710,15 @@ class UpdateCatalog(CommandMixin): 'ignore changes to POT-Creation-Date when updating or checking'), ] # fmt: skip boolean_options = [ - 'omit-header', 'no-wrap', 'ignore-obsolete', 'init-missing', - 'no-fuzzy-matching', 'previous', 'update-header-comment', - 'check', 'ignore-pot-creation-date', + 'omit-header', + 'no-wrap', + 'ignore-obsolete', + 'init-missing', + 'no-fuzzy-matching', + 'previous', + 'update-header-comment', + 'check', + 'ignore-pot-creation-date', ] def initialize_options(self): @@ -725,8 +749,7 @@ class UpdateCatalog(CommandMixin): if self.init_missing: if not self.locale: raise OptionError( - 'you must specify the locale for ' - 'the init-missing option to work', + 'you must specify the locale for the init-missing option to work', ) try: @@ -779,7 +802,9 @@ class UpdateCatalog(CommandMixin): check_status[filename] = False continue self.log.info( - 'creating catalog %s based on %s', filename, self.input_file, + 'creating catalog %s based on %s', + filename, + self.input_file, ) with open(self.input_file, 'rb') as infile: @@ -805,9 +830,10 @@ class UpdateCatalog(CommandMixin): update_creation_date=not self.ignore_pot_creation_date, ) - tmpname = os.path.join(os.path.dirname(filename), - tempfile.gettempprefix() + - os.path.basename(filename)) + tmpname = os.path.join( + os.path.dirname(filename), + tempfile.gettempprefix() + os.path.basename(filename), + ) try: with open(tmpname, 'wb') as tmpfile: write_po( @@ -891,8 +917,10 @@ class CommandLineInterface: if argv is None: argv = sys.argv - self.parser = optparse.OptionParser(usage=self.usage % ('command', '[args]'), - version=self.version) + self.parser = optparse.OptionParser( + usage=self.usage % ('command', '[args]'), + version=self.version, + ) self.parser.disable_interspersed_args() self.parser.print_help = self._help self.parser.add_option( @@ -1065,40 +1093,58 @@ def _parse_config_object(config: dict, *, filename="(unknown)"): extractors_read = config.get("extractors", {}) if not isinstance(extractors_read, dict): - raise ConfigurationError(f"{filename}: extractors: Expected a dictionary, got {type(extractors_read)!r}") + raise ConfigurationError( + f"{filename}: extractors: Expected a dictionary, got {type(extractors_read)!r}", + ) for method, callable_spec in extractors_read.items(): if not isinstance(method, str): # Impossible via TOML, but could happen with a custom object. - raise ConfigurationError(f"{filename}: extractors: Extraction method must be a string, got {method!r}") + raise ConfigurationError( + f"{filename}: extractors: Extraction method must be a string, got {method!r}", + ) if not isinstance(callable_spec, str): - raise ConfigurationError(f"{filename}: extractors: Callable specification must be a string, got {callable_spec!r}") + raise ConfigurationError( + f"{filename}: extractors: Callable specification must be a string, got {callable_spec!r}", + ) extractors[method] = callable_spec if "mapping" in config: - raise ConfigurationError(f"{filename}: 'mapping' is not a valid key, did you mean 'mappings'?") + raise ConfigurationError( + f"{filename}: 'mapping' is not a valid key, did you mean 'mappings'?", + ) mappings_read = config.get("mappings", []) if not isinstance(mappings_read, list): - raise ConfigurationError(f"{filename}: mappings: Expected a list, got {type(mappings_read)!r}") + raise ConfigurationError( + f"{filename}: mappings: Expected a list, got {type(mappings_read)!r}", + ) for idx, entry in enumerate(mappings_read): if not isinstance(entry, dict): - raise ConfigurationError(f"{filename}: mappings[{idx}]: Expected a dictionary, got {type(entry)!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: Expected a dictionary, got {type(entry)!r}", + ) entry = entry.copy() method = entry.pop("method", None) if not isinstance(method, str): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'method' must be a string, got {method!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'method' must be a string, got {method!r}", + ) method = extractors.get(method, method) # Map the extractor name to the callable now pattern = entry.pop("pattern", None) if not isinstance(pattern, (list, str)): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'pattern' must be a list or a string, got {pattern!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'pattern' must be a list or a string, got {pattern!r}", + ) if not isinstance(pattern, list): pattern = [pattern] for pat in pattern: if not isinstance(pat, str): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'pattern' elements must be strings, got {pat!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'pattern' elements must be strings, got {pat!r}", + ) method_map.append((pat, method)) options_map[pat] = entry @@ -1135,11 +1181,15 @@ def _parse_mapping_toml( try: babel_data = parsed_data["tool"]["babel"] except (TypeError, KeyError) as e: - raise ConfigurationError(f"{filename}: No 'tool.babel' section found in file") from e + raise ConfigurationError( + f"{filename}: No 'tool.babel' section found in file", + ) from e elif style == "standalone": babel_data = parsed_data if "babel" in babel_data: - raise ConfigurationError(f"{filename}: 'babel' should not be present in a stand-alone configuration file") + raise ConfigurationError( + f"{filename}: 'babel' should not be present in a stand-alone configuration file", + ) else: # pragma: no cover raise ValueError(f"Unknown TOML style {style!r}") @@ -1210,7 +1260,13 @@ def parse_keywords(strings: Iterable[str] = ()): def __getattr__(name: str): # Re-exports for backwards compatibility; # `setuptools_frontend` is the canonical import location. - if name in {'check_message_extractors', 'compile_catalog', 'extract_messages', 'init_catalog', 'update_catalog'}: + if name in { + 'check_message_extractors', + 'compile_catalog', + 'extract_messages', + 'init_catalog', + 'update_catalog', + }: from babel.messages import setuptools_frontend return getattr(setuptools_frontend, name) diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index 4cd79d5d..df503e94 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -1,13 +1,14 @@ """ - babel.messages.jslexer - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.jslexer +~~~~~~~~~~~~~~~~~~~~~~ - A simple JavaScript 1.5 lexer which is used for the JavaScript - extractor. +A simple JavaScript 1.5 lexer which is used for the JavaScript +extractor. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import re @@ -62,7 +63,11 @@ _rules: list[tuple[str | None, re.Pattern[str]]] = [ ] # fmt: skip -def get_rules(jsx: bool, dotted: bool, template_string: bool) -> list[tuple[str | None, re.Pattern[str]]]: +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. @@ -95,8 +100,9 @@ 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).) """ - assert string and string[0] == string[-1] and string[0] in '"\'`', \ + assert string and string[0] == string[-1] and string[0] in '"\'`', ( 'string provided is not properly delimited' + ) string = line_join_re.sub('\\1', string[1:-1]) result: list[str] = [] add = result.append @@ -158,7 +164,13 @@ def unquote_string(string: str) -> str: return ''.join(result) -def tokenize(source: str, jsx: bool = True, dotted: bool = True, template_string: bool = True, lineno: int = 1) -> Generator[Token, None, None]: +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 20c67188..9ec822b6 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -1,12 +1,13 @@ """ - babel.messages.mofile - ~~~~~~~~~~~~~~~~~~~~~ +babel.messages.mofile +~~~~~~~~~~~~~~~~~~~~~ - Writing of files in the ``gettext`` MO (machine object) format. +Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import array @@ -18,8 +19,8 @@ from babel.messages.catalog import Catalog, Message if TYPE_CHECKING: from _typeshed import SupportsRead, SupportsWrite -LE_MAGIC: int = 0x950412de -BE_MAGIC: int = 0xde120495 +LE_MAGIC: int = 0x950412DE +BE_MAGIC: int = 0xDE120495 def read_mo(fileobj: SupportsRead[bytes]) -> Catalog: @@ -56,9 +57,9 @@ def read_mo(fileobj: SupportsRead[bytes]) -> Catalog: # Now put all messages from the .mo file buffer into the catalog # dictionary for _i in range(msgcount): - mlen, moff = unpack(ii, buf[origidx:origidx + 8]) + mlen, moff = unpack(ii, buf[origidx : origidx + 8]) mend = moff + mlen - tlen, toff = unpack(ii, buf[transidx:transidx + 8]) + tlen, toff = unpack(ii, buf[transidx : transidx + 8]) tend = toff + tlen if mend < buflen and tend < buflen: msg = buf[moff:mend] @@ -153,8 +154,7 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool = in the output """ messages = list(catalog) - messages[1:] = [m for m in messages[1:] - if m.string and (use_fuzzy or not m.fuzzy)] + messages[1:] = [m for m in messages[1:] if m.string and (use_fuzzy or not m.fuzzy)] messages.sort() ids = strs = b'' diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 5eae86dd..a03fb4ed 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -1,12 +1,13 @@ """ - babel.messages.plurals - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.plurals +~~~~~~~~~~~~~~~~~~~~~~ - Plural form definitions. +Plural form definitions. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations from babel.core import Locale, default_locale diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index e29d8840..bffdc87c 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -1,13 +1,14 @@ """ - babel.messages.pofile - ~~~~~~~~~~~~~~~~~~~~~ +babel.messages.pofile +~~~~~~~~~~~~~~~~~~~~~ - Reading and writing of files in the ``gettext`` PO (portable object) - format. +Reading and writing of files in the ``gettext`` PO (portable object) +format. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import os @@ -38,6 +39,7 @@ def unescape(string: str) -> str: :param string: the string to unescape """ + def replace_escapes(match): m = match.group(1) if m == 'n': @@ -100,14 +102,18 @@ def _extract_locations(line: str) -> list[str]: for c in line: if c == "\u2068": if in_filename: - raise ValueError("location comment contains more First Strong Isolate " - "characters, than Pop Directional Isolate characters") + raise ValueError( + "location comment contains more First Strong Isolate " + "characters, than Pop Directional Isolate characters", + ) in_filename = True continue elif c == "\u2069": if not in_filename: - raise ValueError("location comment contains more Pop Directional Isolate " - "characters, than First Strong Isolate characters") + raise ValueError( + "location comment contains more Pop Directional Isolate " + "characters, than First Strong Isolate characters", + ) in_filename = False continue elif c == " ": @@ -121,8 +127,10 @@ def _extract_locations(line: str) -> list[str]: else: if location: if in_filename: - raise ValueError("location comment contains more First Strong Isolate " - "characters, than Pop Directional Isolate characters") + raise ValueError( + "location comment contains more First Strong Isolate " + "characters, than Pop Directional Isolate characters", + ) locations.append(location) return locations @@ -155,7 +163,12 @@ class PoFileParser: See `read_po` for simple cases. """ - def __init__(self, catalog: Catalog, ignore_obsolete: bool = False, abort_invalid: bool = False) -> None: + 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 @@ -186,7 +199,11 @@ class PoFileParser: string = ['' for _ in range(self.catalog.num_plurals)] for idx, translation in sorted(self.translations): if idx >= self.catalog.num_plurals: - self._invalid_pofile("", self.offset, "msg has more translations than num_plurals of catalog") + self._invalid_pofile( + "", + self.offset, + "msg has more translations than num_plurals of catalog", + ) continue string[idx] = translation.denormalize() string = tuple(string) @@ -194,9 +211,16 @@ class PoFileParser: msgid = self.messages[0].denormalize() string = self.translations[0][1].denormalize() msgctxt = self.context.denormalize() if self.context else None - message = Message(msgid, string, self.locations, self.flags, - self.auto_comments, self.user_comments, lineno=self.offset + 1, - context=msgctxt) + message = Message( + msgid, + string, + self.locations, + self.flags, + self.auto_comments, + self.user_comments, + lineno=self.offset + 1, + context=msgctxt, + ) if self.obsolete: if not self.ignore_obsolete: self.catalog.obsolete[self.catalog._key_for(msgid, msgctxt)] = message @@ -208,7 +232,11 @@ class PoFileParser: def _finish_current_message(self) -> None: if self.messages: if not self.translations: - self._invalid_pofile("", self.offset, f"missing msgstr for msgid '{self.messages[0].denormalize()}'") + self._invalid_pofile( + "", + self.offset, + f"missing msgstr for msgid '{self.messages[0].denormalize()}'", + ) self.translations.append([0, _NormalizedString()]) self._add_message() @@ -263,7 +291,11 @@ class PoFileParser: elif self.in_msgctxt: s = self.context else: - self._invalid_pofile(line, lineno, "Got line starting with \" but not in msgid, msgstr or msgctxt") + self._invalid_pofile( + line, + lineno, + "Got line starting with \" but not in msgid, msgstr or msgctxt", + ) return # For performance reasons, `NormalizedString` doesn't strip internally s.append(line.strip()) diff --git a/babel/numbers.py b/babel/numbers.py index 3ea97bcc..8a6e02dd 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -1,20 +1,21 @@ """ - babel.numbers - ~~~~~~~~~~~~~ +babel.numbers +~~~~~~~~~~~~~ - Locale dependent formatting and parsing of numeric data. +Locale dependent formatting and parsing of numeric data. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_MONETARY`` for currency related functions, - * ``LC_NUMERIC``, and - * ``LC_ALL``, and - * ``LANG`` + * ``LC_MONETARY`` for currency related functions, + * ``LC_NUMERIC``, and + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + # TODO: # Padding and rounding increments in pattern: # - https://www.unicode.org/reports/tr35/ (Appendix G.6) @@ -34,8 +35,7 @@ LC_NUMERIC = default_locale('LC_NUMERIC') class UnknownCurrencyError(Exception): - """Exception thrown when a currency is requested for which no data is available. - """ + """Exception thrown when a currency is requested for which no data is available.""" def __init__(self, identifier: str) -> None: """Create the exception. @@ -48,7 +48,7 @@ class UnknownCurrencyError(Exception): def list_currencies(locale: Locale | str | None = None) -> set[str]: - """ Return a `set` of normalized currency codes. + """Return a `set` of normalized currency codes. .. versionadded:: 2.5.0 @@ -64,7 +64,7 @@ def list_currencies(locale: Locale | str | None = None) -> set[str]: def validate_currency(currency: str, locale: Locale | str | None = None) -> None: - """ Check the currency code is recognized by Babel. + """Check the currency code is recognized by Babel. Accepts a ``locale`` parameter for fined-grained validation, working as the one defined above in ``list_currencies()`` method. @@ -76,7 +76,7 @@ def validate_currency(currency: str, locale: Locale | str | None = None) -> None def is_currency(currency: str, locale: Locale | str | None = None) -> bool: - """ Returns `True` only if a currency is recognized by Babel. + """Returns `True` only if a currency is recognized by Babel. This method always return a Boolean and never raise. """ @@ -208,8 +208,7 @@ def get_territory_currencies( tender: bool = ..., non_tender: bool = ..., include_details: Literal[False] = ..., -) -> list[str]: - ... # pragma: no cover +) -> list[str]: ... # pragma: no cover @overload @@ -220,8 +219,7 @@ def get_territory_currencies( tender: bool = ..., non_tender: bool = ..., include_details: Literal[True] = ..., -) -> list[dict[str, Any]]: - ... # pragma: no cover +) -> list[dict[str, Any]]: ... # pragma: no cover def get_territory_currencies( @@ -295,8 +293,7 @@ def get_territory_currencies( # TODO: validate that the territory exists def _is_active(start, end): - return (start is None or start <= end_date) and \ - (end is None or end >= start_date) + return (start is None or start <= end_date) and (end is None or end >= start_date) result = [] for currency_code, start, end, is_tender in curs: @@ -304,22 +301,29 @@ def get_territory_currencies( start = datetime.date(*start) if end: end = datetime.date(*end) - if ((is_tender and tender) or - (not is_tender and non_tender)) and _is_active(start, end): + if ((is_tender and tender) or (not is_tender and non_tender)) and _is_active( + start, + end, + ): if include_details: - result.append({ - 'currency': currency_code, - 'from': start, - 'to': end, - 'tender': is_tender, - }) + result.append( + { + 'currency': currency_code, + 'from': start, + 'to': end, + 'tender': is_tender, + }, + ) else: result.append(currency_code) return result -def _get_numbering_system(locale: Locale, numbering_system: Literal["default"] | str = "latn") -> str: +def _get_numbering_system( + locale: Locale, + numbering_system: Literal["default"] | str = "latn", +) -> str: if numbering_system == "default": return locale.default_numbering_system else: @@ -335,11 +339,14 @@ def _get_number_symbols( try: return locale.number_symbols[numbering_system] except KeyError as error: - raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {locale}.") from error + raise UnsupportedNumberingSystemError( + f"Unknown numbering system {numbering_system} for Locale {locale}.", + ) from error class UnsupportedNumberingSystemError(Exception): """Exception thrown when an unsupported numbering system is requested for the given Locale.""" + pass @@ -481,7 +488,10 @@ def get_infinity_symbol( return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞') -def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = None) -> str: +def format_number( + number: float | decimal.Decimal | str, + locale: Locale | str | None = None, +) -> str: """Return the given number formatted for a specific locale. >>> format_number(1099, locale='en_US') # doctest: +SKIP @@ -498,7 +508,11 @@ def format_number(number: float | decimal.Decimal | str, locale: Locale | str | """ - warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning, stacklevel=2) + warnings.warn( + 'Use babel.numbers.format_decimal() instead.', + DeprecationWarning, + stacklevel=2, + ) return format_decimal(number, locale=locale) @@ -583,7 +597,12 @@ def format_decimal( format = locale.decimal_formats[format] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) def format_compact_decimal( @@ -626,7 +645,12 @@ def format_compact_decimal( if format is None: format = locale.decimal_formats[None] pattern = parse_pattern(format) - return pattern.apply(number, locale, decimal_quantization=False, numbering_system=numbering_system) + return pattern.apply( + number, + locale, + decimal_quantization=False, + numbering_system=numbering_system, + ) def _get_compact_format( @@ -654,7 +678,10 @@ def _get_compact_format( break # otherwise, we need to divide the number by the magnitude but remove zeros # equal to the number of 0's in the pattern minus 1 - number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1)))) + number = cast( + decimal.Decimal, + number / (magnitude // (10 ** (pattern.count("0") - 1))), + ) # round to the number of fraction digits requested rounded = round(number, fraction_digits) # if the remaining number is singular, use the singular format @@ -797,11 +824,19 @@ def format_currency( try: pattern = locale.currency_formats[format_type] except KeyError: - raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None + raise UnknownCurrencyFormatError( + f"{format_type!r} is not a known currency format type", + ) from None return pattern.apply( - number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + currency=currency, + currency_digits=currency_digits, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) def _format_currency_long_name( @@ -839,8 +874,14 @@ def _format_currency_long_name( pattern = parse_pattern(format) number_part = pattern.apply( - number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + currency=currency, + currency_digits=currency_digits, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) return unit_pattern.format(number_part, display_name) @@ -877,7 +918,9 @@ def format_compact_currency( try: compact_format = locale.compact_currency_formats[format_type] except KeyError as error: - raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error + raise UnknownCurrencyFormatError( + f"{format_type!r} is not a known compact currency format type", + ) from error number, format = _get_compact_format(number, compact_format, locale, fraction_digits) # Did not find a format, fall back. if format is None or "¤" not in str(format): @@ -894,8 +937,14 @@ def format_compact_currency( if format is None: raise ValueError('No compact currency format found for the given number and locale.') pattern = parse_pattern(format) - return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False, - numbering_system=numbering_system) + return pattern.apply( + number, + locale, + currency=currency, + currency_digits=False, + decimal_quantization=False, + numbering_system=numbering_system, + ) def format_percent( @@ -954,7 +1003,10 @@ def format_percent( format = locale.percent_formats[None] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, + number, + locale, + decimal_quantization=decimal_quantization, + group_separator=group_separator, numbering_system=numbering_system, ) @@ -1002,7 +1054,11 @@ def format_scientific( format = locale.scientific_formats[None] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system) + number, + locale, + decimal_quantization=decimal_quantization, + numbering_system=numbering_system, + ) class NumberFormatError(ValueError): @@ -1132,16 +1188,21 @@ def parse_decimal( string = SPACE_CHARS_RE.sub(group_symbol, string) try: - parsed = decimal.Decimal(string.replace(group_symbol, '') - .replace(decimal_symbol, '.')) + parsed = decimal.Decimal(string.replace(group_symbol, '').replace(decimal_symbol, '.')) except decimal.InvalidOperation as exc: raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc if strict and group_symbol in string: - proper = format_decimal(parsed, locale=locale, decimal_quantization=False, numbering_system=numbering_system) - if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): + proper = format_decimal( + parsed, + locale=locale, + decimal_quantization=False, + numbering_system=numbering_system, + ) + if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): # fmt: skip try: - parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') - .replace(group_symbol, '.')) + parsed_alt = decimal.Decimal( + string.replace(decimal_symbol, '').replace(group_symbol, '.'), + ) except decimal.InvalidOperation as exc: raise NumberFormatError( f"{string!r} is not a properly formatted decimal number. " @@ -1230,7 +1291,7 @@ def parse_grouping(p: str) -> tuple[int, int]: if g1 == -1: return 1000, 1000 g1 = width - g1 - 1 - g2 = p[:-g1 - 1].rfind(',') + g2 = p[: -g1 - 1].rfind(',') if g2 == -1: return g1, g1 g2 = width - g1 - g2 - 2 @@ -1296,14 +1357,20 @@ def parse_pattern(pattern: NumberPattern | str) -> NumberPattern: exp_plus = None exp_prec = None grouping = parse_grouping(integer) - return NumberPattern(pattern, (pos_prefix, neg_prefix), - (pos_suffix, neg_suffix), grouping, - int_prec, frac_prec, - exp_prec, exp_plus, number) + return NumberPattern( + pattern, + (pos_prefix, neg_prefix), + (pos_suffix, neg_suffix), + grouping, + int_prec, + frac_prec, + exp_prec, + exp_plus, + number, + ) class NumberPattern: - def __init__( self, pattern: str, @@ -1352,8 +1419,7 @@ class NumberPattern: *, numbering_system: Literal["default"] | str = "latn", ) -> tuple[decimal.Decimal, int, str]: - """ Returns normalized scientific notation components of a value. - """ + """Returns normalized scientific notation components of a value.""" # Normalize value to only have one lead digit. exp = value.adjusted() value = value * get_decimal_quantum(exp) @@ -1430,7 +1496,11 @@ class NumberPattern: # Prepare scientific notation metadata. if self.exp_prec: - value, exp, exp_sign = self.scientific_notation_elements(value, locale, numbering_system=numbering_system) + value, exp, exp_sign = self.scientific_notation_elements( + value, + locale, + numbering_system=numbering_system, + ) # Adjust the precision of the fractional part and force it to the # currency's if necessary. @@ -1443,7 +1513,7 @@ class NumberPattern: ) frac_prec = force_frac elif currency and currency_digits: - frac_prec = (get_currency_precision(currency), ) * 2 + frac_prec = (get_currency_precision(currency),) * 2 else: frac_prec = self.frac_prec @@ -1463,13 +1533,11 @@ class NumberPattern: get_exponential_symbol(locale, numbering_system=numbering_system), exp_sign, # type: ignore # exp_sign is always defined here self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale, numbering_system=numbering_system), # type: ignore # exp is always defined here - ]) + ]) # fmt: skip # Is it a significant digits pattern? elif '@' in self.pattern: - text = self._format_significant(value, - self.int_prec[0], - self.int_prec[1]) + text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) a, sep, b = text.partition(".") number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system) if sep: @@ -1477,12 +1545,21 @@ class NumberPattern: # A normal number pattern. else: - number = self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system) + number = self._quantize_value( + value, + locale, + frac_prec, + group_separator, + numbering_system=numbering_system, + ) - retval = ''.join([ - self.prefix[is_negative], - number if self.number_pattern != '' else '', - self.suffix[is_negative]]) + retval = ''.join( + ( + self.prefix[is_negative], + number if self.number_pattern != '' else '', + self.suffix[is_negative], + ), + ) if '¤' in retval and currency is not None: retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale)) @@ -1572,8 +1649,19 @@ class NumberPattern: a, sep, b = f"{rounded:f}".partition(".") integer_part = a if group_separator: - integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale, numbering_system=numbering_system) - number = integer_part + self._format_frac(b or '0', locale=locale, force_frac=frac_prec, numbering_system=numbering_system) + integer_part = self._format_int( + a, + self.int_prec[0], + self.int_prec[1], + locale, + numbering_system=numbering_system, + ) + number = integer_part + self._format_frac( + b or '0', + locale=locale, + force_frac=frac_prec, + numbering_system=numbering_system, + ) return number def _format_frac( @@ -1586,7 +1674,7 @@ class NumberPattern: ) -> str: min, max = force_frac or self.frac_prec if len(value) < min: - value += ('0' * (min - len(value))) + value += '0' * (min - len(value)) if max == 0 or (min == 0 and int(value) == 0): return '' while len(value) > min and value[-1] == '0': diff --git a/babel/plural.py b/babel/plural.py index 638dfefe..e368b1ac 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -1,12 +1,13 @@ """ - babel.numbers - ~~~~~~~~~~~~~ +babel.numbers +~~~~~~~~~~~~~ - CLDR Plural support. See UTS #35. +CLDR Plural support. See UTS #35. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import decimal @@ -18,7 +19,9 @@ _plural_tags = ('zero', 'one', 'two', 'few', 'many', 'other') _fallback_tag = 'other' -def extract_operands(source: float | decimal.Decimal) -> tuple[decimal.Decimal | int, int, int, int, int, int, Literal[0], Literal[0]]: +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 an 8-tuple (n, i, v, w, f, t, c, e), where those symbols are as follows: @@ -124,11 +127,14 @@ class PluralRule: def __repr__(self) -> str: rules = self.rules - args = ", ".join([f"{tag}: {rules[tag]}" for tag in _plural_tags if tag in 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: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> PluralRule: + 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. @@ -193,7 +199,9 @@ def to_javascript(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRu return ''.join(result) -def to_python(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) -> Callable[[float | decimal.Decimal], str]: +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: @@ -256,7 +264,10 @@ def to_gettext(rule: Mapping[str, str] | Iterable[tuple[str, str]] | PluralRule) return ''.join(result) -def in_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool: +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: @@ -276,7 +287,10 @@ def in_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[fl return num == int(num) and within_range_list(num, range_list) -def within_range_list(num: float | decimal.Decimal, range_list: Iterable[Iterable[float | decimal.Decimal]]) -> bool: +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: @@ -336,7 +350,7 @@ _VARS = { _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')), + ('word', re.compile(rf'\b(and|or|is|(?:with)?in|not|mod|[{"".join(_VARS)}])\b')), ('value', re.compile(r'\d+')), ('symbol', re.compile(r'%|,|!=|=')), ('ellipsis', re.compile(r'\.{2,3}|\u2026', re.UNICODE)), # U+2026: ELLIPSIS @@ -366,8 +380,7 @@ def test_next_token( 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) + return tokens and tokens[-1][0] == type_ and (value is None or tokens[-1][1] == value) def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = None): @@ -376,7 +389,7 @@ def skip_token(tokens: list[tuple[str, str]], type_: str, value: str | None = No def value_node(value: int) -> tuple[Literal['value'], tuple[int]]: - return 'value', (value, ) + return 'value', (value,) def ident_node(name: str) -> tuple[str, tuple[()]]: @@ -566,7 +579,9 @@ class _PythonCompiler(_Compiler): compile_mod = _binary_compiler('MOD(%s, %s)') def compile_relation(self, method, expr, range_list): - ranges = ",".join([f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1]]) + ranges = ",".join( + f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1] + ) return f"{method.upper()}({self.compile(expr)}, [{ranges}])" @@ -604,8 +619,7 @@ class _JavaScriptCompiler(_GettextCompiler): compile_t = compile_zero def compile_relation(self, method, expr, range_list): - code = _GettextCompiler.compile_relation( - self, method, expr, range_list) + code = _GettextCompiler.compile_relation(self, method, expr, range_list) if method == 'in': expr = self.compile(expr) code = f"(parseInt({expr}, 10) == {expr} && {code})" diff --git a/babel/support.py b/babel/support.py index d181fbd1..dfc7fb6e 100644 --- a/babel/support.py +++ b/babel/support.py @@ -1,15 +1,16 @@ """ - babel.support - ~~~~~~~~~~~~~ +babel.support +~~~~~~~~~~~~~ - Several classes and functions that help with integrating and using Babel - in applications. +Several classes and functions that help with integrating and using Babel +in applications. - .. note: the code in this module is not used by Babel itself +.. note: the code in this module is not used by Babel itself - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import gettext @@ -114,7 +115,15 @@ class Format: def timedelta( self, delta: _datetime.timedelta | int, - granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second", + 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, @@ -126,10 +135,14 @@ class Format: >>> fmt.timedelta(timedelta(weeks=11)) u'3 months' """ - return format_timedelta(delta, granularity=granularity, - threshold=threshold, - format=format, add_direction=add_direction, - locale=self.locale) + return format_timedelta( + delta, + granularity=granularity, + threshold=threshold, + format=format, + add_direction=add_direction, + locale=self.locale, + ) def number(self, number: float | Decimal | str) -> str: """Return an integer number formatted for the locale. @@ -138,7 +151,11 @@ class Format: >>> fmt.number(1099) u'1,099' """ - return format_decimal(number, locale=self.locale, numbering_system=self.numbering_system) + return format_decimal( + number, + locale=self.locale, + numbering_system=self.numbering_system, + ) def decimal(self, number: float | Decimal | str, format: str | None = None) -> str: """Return a decimal number formatted for the locale. @@ -147,7 +164,12 @@ class Format: >>> fmt.decimal(1.2345) u'1.234' """ - return format_decimal(number, format, locale=self.locale, numbering_system=self.numbering_system) + return format_decimal( + number, + format, + locale=self.locale, + numbering_system=self.numbering_system, + ) def compact_decimal( self, @@ -172,9 +194,13 @@ class Format: ) def currency(self, number: float | Decimal | str, currency: str) -> str: - """Return a number in the given currency formatted for the locale. - """ - return format_currency(number, currency, locale=self.locale, numbering_system=self.numbering_system) + """Return a number in the given currency formatted for the locale.""" + return format_currency( + number, + currency, + locale=self.locale, + numbering_system=self.numbering_system, + ) def compact_currency( self, @@ -189,8 +215,14 @@ class Format: >>> Format('en_US').compact_currency(1234567, "USD", format_type='short', fraction_digits=2) '$1.23M' """ - return format_compact_currency(number, currency, format_type=format_type, fraction_digits=fraction_digits, - locale=self.locale, numbering_system=self.numbering_system) + return format_compact_currency( + number, + currency, + format_type=format_type, + fraction_digits=fraction_digits, + locale=self.locale, + numbering_system=self.numbering_system, + ) def percent(self, number: float | Decimal | str, format: str | None = None) -> str: """Return a number formatted as percentage for the locale. @@ -199,12 +231,20 @@ class Format: >>> fmt.percent(0.34) u'34%' """ - return format_percent(number, format, locale=self.locale, numbering_system=self.numbering_system) + return format_percent( + number, + format, + locale=self.locale, + numbering_system=self.numbering_system, + ) def scientific(self, number: float | Decimal | str) -> str: - """Return a number formatted using scientific notation for the locale. - """ - return format_scientific(number, locale=self.locale, numbering_system=self.numbering_system) + """Return a number formatted using scientific notation for the locale.""" + return format_scientific( + number, + locale=self.locale, + numbering_system=self.numbering_system, + ) class LazyProxy: @@ -242,7 +282,15 @@ class LazyProxy: Hello, universe! Hello, world! """ - __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error'] + + __slots__ = [ + '_func', + '_args', + '_kwargs', + '_value', + '_is_cache_enabled', + '_attribute_error', + ] if TYPE_CHECKING: _func: Callable[..., Any] @@ -252,7 +300,13 @@ class LazyProxy: _value: Any _attribute_error: AttributeError | None - def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> 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) @@ -362,6 +416,7 @@ class LazyProxy: def __deepcopy__(self, memo: Any) -> LazyProxy: from copy import deepcopy + return LazyProxy( deepcopy(self._func, memo), enable_cache=deepcopy(self._is_cache_enabled, memo), @@ -371,7 +426,6 @@ class LazyProxy: class NullTranslations(gettext.NullTranslations): - if TYPE_CHECKING: _info: dict[str, str] _fallback: NullTranslations | None @@ -406,6 +460,7 @@ class NullTranslations(gettext.NullTranslations): domain. """ import warnings + warnings.warn( 'ldgettext() is deprecated, use dgettext() instead', DeprecationWarning, @@ -418,6 +473,7 @@ class NullTranslations(gettext.NullTranslations): domain. """ return self._domains.get(domain, self).ugettext(message) + # backward compatibility with 0.9 dugettext = udgettext @@ -432,6 +488,7 @@ class NullTranslations(gettext.NullTranslations): domain. """ import warnings + warnings.warn( 'ldngettext() is deprecated, use dngettext() instead', DeprecationWarning, @@ -444,6 +501,7 @@ class NullTranslations(gettext.NullTranslations): domain. """ return self._domains.get(domain, self).ungettext(singular, plural, num) + # backward compatibility with 0.9 dungettext = udngettext @@ -479,6 +537,7 @@ class NullTranslations(gettext.NullTranslations): ``bind_textdomain_codeset()``. """ import warnings + warnings.warn( 'lpgettext() is deprecated, use pgettext() instead', DeprecationWarning, @@ -517,6 +576,7 @@ class NullTranslations(gettext.NullTranslations): ``bind_textdomain_codeset()``. """ import warnings + warnings.warn( 'lnpgettext() is deprecated, use npgettext() instead', DeprecationWarning, @@ -583,6 +643,7 @@ class NullTranslations(gettext.NullTranslations): `domain`. """ return self._domains.get(domain, self).upgettext(context, message) + # backward compatibility with 0.9 dupgettext = udpgettext @@ -608,13 +669,19 @@ class NullTranslations(gettext.NullTranslations): # backward compatibility with 0.9 dunpgettext = udnpgettext - def ldnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str | bytes: + 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()``. """ - return self._domains.get(domain, self).lnpgettext(context, singular, - plural, num) + return self._domains.get(domain, self).lnpgettext(context, singular, plural, num) ugettext = gettext.NullTranslations.gettext ungettext = gettext.NullTranslations.ngettext @@ -625,7 +692,11 @@ class Translations(NullTranslations, gettext.GNUTranslations): DEFAULT_DOMAIN = 'messages' - def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = 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 diff --git a/babel/units.py b/babel/units.py index 86ac2abc..e66e6b47 100644 --- a/babel/units.py +++ b/babel/units.py @@ -143,7 +143,12 @@ def format_unit( formatted_value = value plural_form = "one" else: - formatted_value = format_decimal(value, format, locale, numbering_system=numbering_system) + formatted_value = format_decimal( + value, + format, + locale, + numbering_system=numbering_system, + ) plural_form = locale.plural_form(value) if plural_form in unit_patterns: @@ -151,7 +156,11 @@ def format_unit( # Fall back to a somewhat bad representation. # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen. - fallback_name = get_unit_name(measurement_unit, length=length, locale=locale) # pragma: no cover + fallback_name = get_unit_name( # pragma: no cover + measurement_unit, + length=length, + locale=locale, + ) return f"{formatted_value} {fallback_name or measurement_unit}" # pragma: no cover @@ -204,7 +213,10 @@ def _find_compound_unit( # Now we can try and rebuild a compound unit specifier, then qualify it: - return _find_unit_pattern(f"{bare_numerator_unit}-per-{bare_denominator_unit}", locale=locale) + return _find_unit_pattern( + f"{bare_numerator_unit}-per-{bare_denominator_unit}", + locale=locale, + ) def format_compound_unit( @@ -310,7 +322,12 @@ def format_compound_unit( elif denominator_unit: # Denominator has unit if denominator_value == 1: # support perUnitPatterns when the denominator is 1 denominator_unit = _find_unit_pattern(denominator_unit, locale=locale) - per_pattern = locale._data["unit_patterns"].get(denominator_unit, {}).get(length, {}).get("per") + per_pattern = ( + locale._data["unit_patterns"] + .get(denominator_unit, {}) + .get(length, {}) + .get("per") + ) if per_pattern: return per_pattern.format(formatted_numerator) # See TR-35's per-unit pattern algorithm, point 3.2. @@ -335,6 +352,11 @@ def format_compound_unit( ) # TODO: this doesn't support "compound_variations" (or "prefix"), and will fall back to the "x/y" representation - per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, {}).get("compound", "{0}/{1}") + per_pattern = ( + locale._data["compound_unit_patterns"] + .get("per", {}) + .get(length, {}) + .get("compound", "{0}/{1}") + ) return per_pattern.format(formatted_numerator, formatted_denominator) diff --git a/babel/util.py b/babel/util.py index c995434f..001fd646 100644 --- a/babel/util.py +++ b/babel/util.py @@ -70,12 +70,13 @@ def parse_encoding(fp: IO[bytes]) -> str | None: line1 = fp.readline() has_bom = line1.startswith(codecs.BOM_UTF8) if has_bom: - line1 = line1[len(codecs.BOM_UTF8):] + line1 = line1[len(codecs.BOM_UTF8) :] m = PYTHON_MAGIC_COMMENT_re.match(line1) if not m: try: import ast + ast.parse(line1.decode('latin-1')) except (ImportError, SyntaxError, UnicodeEncodeError): # Either it's a real syntax error, in which case the source is @@ -101,8 +102,7 @@ def parse_encoding(fp: IO[bytes]) -> str | None: fp.seek(pos) -PYTHON_FUTURE_IMPORT_re = re.compile( - r'from\s+__future__\s+import\s+\(*(.+)\)*') +PYTHON_FUTURE_IMPORT_re = re.compile(r'from\s+__future__\s+import\s+\(*(.+)\)*') def parse_future_flags(fp: IO[bytes], encoding: str = 'latin-1') -> int: @@ -110,6 +110,7 @@ def parse_future_flags(fp: IO[bytes], encoding: str = 'latin-1') -> int: code. """ import __future__ + pos = fp.tell() fp.seek(0) flags = 0 @@ -204,8 +205,8 @@ def pathmatch(pattern: str, filename: str) -> bool: class TextWrapper(textwrap.TextWrapper): wordsep_re = re.compile( - r'(\s+|' # any whitespace - r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))', # em-dash + r'(\s+|' # any whitespace + r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))', # em-dash ) # e.g. '\u2068foo bar.py\u2069:42' @@ -229,7 +230,12 @@ class TextWrapper(textwrap.TextWrapper): return [c for c in chunks if c] -def wraptext(text: str, width: int = 70, initial_indent: str = '', subsequent_indent: str = '') -> list[str]: +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. It also does not wrap PO file locations containing spaces. @@ -265,6 +271,7 @@ class FixedOffsetTimezone(datetime.tzinfo): DEPRECATED: Use the standard library `datetime.timezone` instead. """ + # TODO (Babel 3.x): Remove this class def __init__(self, offset: float, name: str | None = None) -> None: -- 2.47.2