codekingpro/portable-devtools
114k
1"""2 babel.support3 ~~~~~~~~~~~~~4 5 Several classes and functions that help with integrating and using Babel6 in applications.7 8 .. note: the code in this module is not used by Babel itself9 10 :copyright: (c) 2013-2024 by the Babel Team.11 :license: BSD, see LICENSE for more details.12"""13from __future__ import annotations14 15import decimal16import gettext17import locale18import os19from collections.abc import Iterator20from typing import TYPE_CHECKING, Any, Callable, Iterable21 22from babel.core import Locale23from babel.dates import format_date, format_datetime, format_time, format_timedelta24from babel.numbers import (25 format_compact_currency,26 format_compact_decimal,27 format_currency,28 format_decimal,29 format_percent,30 format_scientific,31)32 33if TYPE_CHECKING:34 from typing_extensions import Literal35 36 from babel.dates import _PredefinedTimeFormat37 38 39class Format:40 """Wrapper class providing the various date and number formatting functions41 bound to a specific locale and time-zone.42 43 >>> from babel.util import UTC44 >>> from datetime import date45 >>> fmt = Format('en_US', UTC)46 >>> fmt.date(date(2007, 4, 1))47 u'Apr 1, 2007'48 >>> fmt.decimal(1.2345)49 u'1.234'50 """51 52 def __init__(53 self,54 locale: Locale | str,55 tzinfo: datetime.tzinfo | None = None,56 *,57 numbering_system: Literal["default"] | str = "latn",58 ) -> None:59 """Initialize the formatter.60 61 :param locale: the locale identifier or `Locale` instance62 :param tzinfo: the time-zone info (a `tzinfo` instance or `None`)63 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".64 The special value "default" will use the default numbering system of the locale.65 """66 self.locale = Locale.parse(locale)67 self.tzinfo = tzinfo68 self.numbering_system = numbering_system69 70 def date(71 self,72 date: datetime.date | None = None,73 format: _PredefinedTimeFormat | str = 'medium',74 ) -> str:75 """Return a date formatted according to the given pattern.76 77 >>> from datetime import date78 >>> fmt = Format('en_US')79 >>> fmt.date(date(2007, 4, 1))80 u'Apr 1, 2007'81 """82 return format_date(date, format, locale=self.locale)83 84 def datetime(85 self,86 datetime: datetime.date | None = None,87 format: _PredefinedTimeFormat | str = 'medium',88 ) -> str:89 """Return a date and time formatted according to the given pattern.90 91 >>> from datetime import datetime92 >>> from babel.dates import get_timezone93 >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))94 >>> fmt.datetime(datetime(2007, 4, 1, 15, 30))95 u'Apr 1, 2007, 11:30:00\u202fAM'96 """97 return format_datetime(datetime, format, tzinfo=self.tzinfo, locale=self.locale)98 99 def time(100 self,101 time: datetime.time | datetime.datetime | None = None,102 format: _PredefinedTimeFormat | str = 'medium',103 ) -> str:104 """Return a time formatted according to the given pattern.105 106 >>> from datetime import datetime107 >>> from babel.dates import get_timezone108 >>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))109 >>> fmt.time(datetime(2007, 4, 1, 15, 30))110 u'11:30:00\u202fAM'111 """112 return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)113 114 def timedelta(115 self,116 delta: datetime.timedelta | int,117 granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second",118 threshold: float = 0.85,119 format: Literal["narrow", "short", "medium", "long"] = "long",120 add_direction: bool = False,121 ) -> str:122 """Return a time delta according to the rules of the given locale.123 124 >>> from datetime import timedelta125 >>> fmt = Format('en_US')126 >>> fmt.timedelta(timedelta(weeks=11))127 u'3 months'128 """129 return format_timedelta(delta, granularity=granularity,130 threshold=threshold,131 format=format, add_direction=add_direction,132 locale=self.locale)133 134 def number(self, number: float | decimal.Decimal | str) -> str:135 """Return an integer number formatted for the locale.136 137 >>> fmt = Format('en_US')138 >>> fmt.number(1099)139 u'1,099'140 """141 return format_decimal(number, locale=self.locale, numbering_system=self.numbering_system)142 143 def decimal(self, number: float | decimal.Decimal | str, format: str | None = None) -> str:144 """Return a decimal number formatted for the locale.145 146 >>> fmt = Format('en_US')147 >>> fmt.decimal(1.2345)148 u'1.234'149 """150 return format_decimal(number, format, locale=self.locale, numbering_system=self.numbering_system)151 152 def compact_decimal(153 self,154 number: float | decimal.Decimal | str,155 format_type: Literal['short', 'long'] = 'short',156 fraction_digits: int = 0,157 ) -> str:158 """Return a number formatted in compact form for the locale.159 160 >>> fmt = Format('en_US')161 >>> fmt.compact_decimal(123456789)162 u'123M'163 >>> fmt.compact_decimal(1234567, format_type='long', fraction_digits=2)164 '1.23 million'165 """166 return format_compact_decimal(167 number,168 format_type=format_type,169 fraction_digits=fraction_digits,170 locale=self.locale,171 numbering_system=self.numbering_system,172 )173 174 def currency(self, number: float | decimal.Decimal | str, currency: str) -> str:175 """Return a number in the given currency formatted for the locale.176 """177 return format_currency(number, currency, locale=self.locale, numbering_system=self.numbering_system)178 179 def compact_currency(180 self,181 number: float | decimal.Decimal | str,182 currency: str,183 format_type: Literal['short'] = 'short',184 fraction_digits: int = 0,185 ) -> str:186 """Return a number in the given currency formatted for the locale187 using the compact number format.188 189 >>> Format('en_US').compact_currency(1234567, "USD", format_type='short', fraction_digits=2)190 '$1.23M'191 """192 return format_compact_currency(number, currency, format_type=format_type, fraction_digits=fraction_digits,193 locale=self.locale, numbering_system=self.numbering_system)194 195 def percent(self, number: float | decimal.Decimal | str, format: str | None = None) -> str:196 """Return a number formatted as percentage for the locale.197 198 >>> fmt = Format('en_US')199 >>> fmt.percent(0.34)200 u'34%'201 """202 return format_percent(number, format, locale=self.locale, numbering_system=self.numbering_system)203 204 def scientific(self, number: float | decimal.Decimal | str) -> str:205 """Return a number formatted using scientific notation for the locale.206 """207 return format_scientific(number, locale=self.locale, numbering_system=self.numbering_system)208 209 210class LazyProxy:211 """Class for proxy objects that delegate to a specified function to evaluate212 the actual object.213 214 >>> def greeting(name='world'):215 ... return 'Hello, %s!' % name216 >>> lazy_greeting = LazyProxy(greeting, name='Joe')217 >>> print(lazy_greeting)218 Hello, Joe!219 >>> u' ' + lazy_greeting220 u' Hello, Joe!'221 >>> u'(%s)' % lazy_greeting222 u'(Hello, Joe!)'223 224 This can be used, for example, to implement lazy translation functions that225 delay the actual translation until the string is actually used. The226 rationale for such behavior is that the locale of the user may not always227 be available. In web applications, you only know the locale when processing228 a request.229 230 The proxy implementation attempts to be as complete as possible, so that231 the lazy objects should mostly work as expected, for example for sorting:232 233 >>> greetings = [234 ... LazyProxy(greeting, 'world'),235 ... LazyProxy(greeting, 'Joe'),236 ... LazyProxy(greeting, 'universe'),237 ... ]238 >>> greetings.sort()239 >>> for greeting in greetings:240 ... print(greeting)241 Hello, Joe!242 Hello, universe!243 Hello, world!244 """245 __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error']246 247 if TYPE_CHECKING:248 _func: Callable[..., Any]249 _args: tuple[Any, ...]250 _kwargs: dict[str, Any]251 _is_cache_enabled: bool252 _value: Any253 _attribute_error: AttributeError | None254 255 def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> None:256 # Avoid triggering our own __setattr__ implementation257 object.__setattr__(self, '_func', func)258 object.__setattr__(self, '_args', args)259 object.__setattr__(self, '_kwargs', kwargs)260 object.__setattr__(self, '_is_cache_enabled', enable_cache)261 object.__setattr__(self, '_value', None)262 object.__setattr__(self, '_attribute_error', None)263 264 @property265 def value(self) -> Any:266 if self._value is None:267 try:268 value = self._func(*self._args, **self._kwargs)269 except AttributeError as error:270 object.__setattr__(self, '_attribute_error', error)271 raise272 273 if not self._is_cache_enabled:274 return value275 object.__setattr__(self, '_value', value)276 return self._value277 278 def __contains__(self, key: object) -> bool:279 return key in self.value280 281 def __bool__(self) -> bool:282 return bool(self.value)283 284 def __dir__(self) -> list[str]:285 return dir(self.value)286 287 def __iter__(self) -> Iterator[Any]:288 return iter(self.value)289 290 def __len__(self) -> int:291 return len(self.value)292 293 def __str__(self) -> str:294 return str(self.value)295 296 def __add__(self, other: object) -> Any:297 return self.value + other298 299 def __radd__(self, other: object) -> Any:300 return other + self.value301 302 def __mod__(self, other: object) -> Any:303 return self.value % other304 305 def __rmod__(self, other: object) -> Any:306 return other % self.value307 308 def __mul__(self, other: object) -> Any:309 return self.value * other310 311 def __rmul__(self, other: object) -> Any:312 return other * self.value313 314 def __call__(self, *args: Any, **kwargs: Any) -> Any:315 return self.value(*args, **kwargs)316 317 def __lt__(self, other: object) -> bool:318 return self.value < other319 320 def __le__(self, other: object) -> bool:321 return self.value <= other322 323 def __eq__(self, other: object) -> bool:324 return self.value == other325 326 def __ne__(self, other: object) -> bool:327 return self.value != other328 329 def __gt__(self, other: object) -> bool:330 return self.value > other331 332 def __ge__(self, other: object) -> bool:333 return self.value >= other334 335 def __delattr__(self, name: str) -> None:336 delattr(self.value, name)337 338 def __getattr__(self, name: str) -> Any:339 if self._attribute_error is not None:340 raise self._attribute_error341 return getattr(self.value, name)342 343 def __setattr__(self, name: str, value: Any) -> None:344 setattr(self.value, name, value)345 346 def __delitem__(self, key: Any) -> None:347 del self.value[key]348 349 def __getitem__(self, key: Any) -> Any:350 return self.value[key]351 352 def __setitem__(self, key: Any, value: Any) -> None:353 self.value[key] = value354 355 def __copy__(self) -> LazyProxy:356 return LazyProxy(357 self._func,358 enable_cache=self._is_cache_enabled,359 *self._args, # noqa: B026360 **self._kwargs,361 )362 363 def __deepcopy__(self, memo: Any) -> LazyProxy:364 from copy import deepcopy365 return LazyProxy(366 deepcopy(self._func, memo),367 enable_cache=deepcopy(self._is_cache_enabled, memo),368 *deepcopy(self._args, memo), # noqa: B026369 **deepcopy(self._kwargs, memo),370 )371 372 373class NullTranslations(gettext.NullTranslations):374 375 if TYPE_CHECKING:376 _info: dict[str, str]377 _fallback: NullTranslations | None378 379 DEFAULT_DOMAIN = None380 381 def __init__(self, fp: gettext._TranslationsReader | None = None) -> None:382 """Initialize a simple translations class which is not backed by a383 real catalog. Behaves similar to gettext.NullTranslations but also384 offers Babel's on *gettext methods (e.g. 'dgettext()').385 386 :param fp: a file-like object (ignored in this class)387 """388 # These attributes are set by gettext.NullTranslations when a catalog389 # is parsed (fp != None). Ensure that they are always present because390 # some *gettext methods (including '.gettext()') rely on the attributes.391 self._catalog: dict[tuple[str, Any] | str, str] = {}392 self.plural: Callable[[float | decimal.Decimal], int] = lambda n: int(n != 1)393 super().__init__(fp=fp)394 self.files = list(filter(None, [getattr(fp, 'name', None)]))395 self.domain = self.DEFAULT_DOMAIN396 self._domains: dict[str, NullTranslations] = {}397 398 def dgettext(self, domain: str, message: str) -> str:399 """Like ``gettext()``, but look the message up in the specified400 domain.401 """402 return self._domains.get(domain, self).gettext(message)403 404 def ldgettext(self, domain: str, message: str) -> str:405 """Like ``lgettext()``, but look the message up in the specified406 domain.407 """408 import warnings409 warnings.warn(410 'ldgettext() is deprecated, use dgettext() instead',411 DeprecationWarning,412 stacklevel=2,413 )414 return self._domains.get(domain, self).lgettext(message)415 416 def udgettext(self, domain: str, message: str) -> str:417 """Like ``ugettext()``, but look the message up in the specified418 domain.419 """420 return self._domains.get(domain, self).ugettext(message)421 # backward compatibility with 0.9422 dugettext = udgettext423 424 def dngettext(self, domain: str, singular: str, plural: str, num: int) -> str:425 """Like ``ngettext()``, but look the message up in the specified426 domain.427 """428 return self._domains.get(domain, self).ngettext(singular, plural, num)429 430 def ldngettext(self, domain: str, singular: str, plural: str, num: int) -> str:431 """Like ``lngettext()``, but look the message up in the specified432 domain.433 """434 import warnings435 warnings.warn(436 'ldngettext() is deprecated, use dngettext() instead',437 DeprecationWarning,438 stacklevel=2,439 )440 return self._domains.get(domain, self).lngettext(singular, plural, num)441 442 def udngettext(self, domain: str, singular: str, plural: str, num: int) -> str:443 """Like ``ungettext()`` but look the message up in the specified444 domain.445 """446 return self._domains.get(domain, self).ungettext(singular, plural, num)447 # backward compatibility with 0.9448 dungettext = udngettext449 450 # Most of the downwards code, until it gets included in stdlib, from:451 # https://bugs.python.org/file10036/gettext-pgettext.patch452 #453 # The encoding of a msgctxt and a msgid in a .mo file is454 # msgctxt + "\x04" + msgid (gettext version >= 0.15)455 CONTEXT_ENCODING = '%s\x04%s'456 457 def pgettext(self, context: str, message: str) -> str | object:458 """Look up the `context` and `message` id in the catalog and return the459 corresponding message string, as an 8-bit string encoded with the460 catalog's charset encoding, if known. If there is no entry in the461 catalog for the `message` id and `context` , and a fallback has been462 set, the look up is forwarded to the fallback's ``pgettext()``463 method. Otherwise, the `message` id is returned.464 """465 ctxt_msg_id = self.CONTEXT_ENCODING % (context, message)466 missing = object()467 tmsg = self._catalog.get(ctxt_msg_id, missing)468 if tmsg is missing:469 if self._fallback:470 return self._fallback.pgettext(context, message)471 return message472 return tmsg473 474 def lpgettext(self, context: str, message: str) -> str | bytes | object:475 """Equivalent to ``pgettext()``, but the translation is returned in the476 preferred system encoding, if no other encoding was explicitly set with477 ``bind_textdomain_codeset()``.478 """479 import warnings480 warnings.warn(481 'lpgettext() is deprecated, use pgettext() instead',482 DeprecationWarning,483 stacklevel=2,484 )485 tmsg = self.pgettext(context, message)486 encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding()487 return tmsg.encode(encoding) if isinstance(tmsg, str) else tmsg488 489 def npgettext(self, context: str, singular: str, plural: str, num: int) -> str:490 """Do a plural-forms lookup of a message id. `singular` is used as the491 message id for purposes of lookup in the catalog, while `num` is used to492 determine which plural form to use. The returned message string is an493 8-bit string encoded with the catalog's charset encoding, if known.494 495 If the message id for `context` is not found in the catalog, and a496 fallback is specified, the request is forwarded to the fallback's497 ``npgettext()`` method. Otherwise, when ``num`` is 1 ``singular`` is498 returned, and ``plural`` is returned in all other cases.499 """500 ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)501 try:502 tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]503 return tmsg504 except KeyError:505 if self._fallback:506 return self._fallback.npgettext(context, singular, plural, num)507 if num == 1:508 return singular509 else:510 return plural511 512 def lnpgettext(self, context: str, singular: str, plural: str, num: int) -> str | bytes:513 """Equivalent to ``npgettext()``, but the translation is returned in the514 preferred system encoding, if no other encoding was explicitly set with515 ``bind_textdomain_codeset()``.516 """517 import warnings518 warnings.warn(519 'lnpgettext() is deprecated, use npgettext() instead',520 DeprecationWarning,521 stacklevel=2,522 )523 ctxt_msg_id = self.CONTEXT_ENCODING % (context, singular)524 try:525 tmsg = self._catalog[(ctxt_msg_id, self.plural(num))]526 encoding = getattr(self, "_output_charset", None) or locale.getpreferredencoding()527 return tmsg.encode(encoding)528 except KeyError:529 if self._fallback:530 return self._fallback.lnpgettext(context, singular, plural, num)531 if num == 1:532 return singular533 else:534 return plural535 536 def upgettext(self, context: str, message: str) -> str:537 """Look up the `context` and `message` id in the catalog and return the538 corresponding message string, as a Unicode string. If there is no entry539 in the catalog for the `message` id and `context`, and a fallback has540 been set, the look up is forwarded to the fallback's ``upgettext()``541 method. Otherwise, the `message` id is returned.542 """543 ctxt_message_id = self.CONTEXT_ENCODING % (context, message)544 missing = object()545 tmsg = self._catalog.get(ctxt_message_id, missing)546 if tmsg is missing:547 if self._fallback:548 return self._fallback.upgettext(context, message)549 return str(message)550 assert isinstance(tmsg, str)551 return tmsg552 553 def unpgettext(self, context: str, singular: str, plural: str, num: int) -> str:554 """Do a plural-forms lookup of a message id. `singular` is used as the555 message id for purposes of lookup in the catalog, while `num` is used to556 determine which plural form to use. The returned message string is a557 Unicode string.558 559 If the message id for `context` is not found in the catalog, and a560 fallback is specified, the request is forwarded to the fallback's561 ``unpgettext()`` method. Otherwise, when `num` is 1 `singular` is562 returned, and `plural` is returned in all other cases.563 """564 ctxt_message_id = self.CONTEXT_ENCODING % (context, singular)565 try:566 tmsg = self._catalog[(ctxt_message_id, self.plural(num))]567 except KeyError:568 if self._fallback:569 return self._fallback.unpgettext(context, singular, plural, num)570 tmsg = str(singular) if num == 1 else str(plural)571 return tmsg572 573 def dpgettext(self, domain: str, context: str, message: str) -> str | object:574 """Like `pgettext()`, but look the message up in the specified575 `domain`.576 """577 return self._domains.get(domain, self).pgettext(context, message)578 579 def udpgettext(self, domain: str, context: str, message: str) -> str:580 """Like `upgettext()`, but look the message up in the specified581 `domain`.582 """583 return self._domains.get(domain, self).upgettext(context, message)584 # backward compatibility with 0.9585 dupgettext = udpgettext586 587 def ldpgettext(self, domain: str, context: str, message: str) -> str | bytes | object:588 """Equivalent to ``dpgettext()``, but the translation is returned in the589 preferred system encoding, if no other encoding was explicitly set with590 ``bind_textdomain_codeset()``.591 """592 return self._domains.get(domain, self).lpgettext(context, message)593 594 def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:595 """Like ``npgettext``, but look the message up in the specified596 `domain`.597 """598 return self._domains.get(domain, self).npgettext(context, singular,599 plural, num)600 601 def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:602 """Like ``unpgettext``, but look the message up in the specified603 `domain`.604 """605 return self._domains.get(domain, self).unpgettext(context, singular,606 plural, num)607 # backward compatibility with 0.9608 dunpgettext = udnpgettext609 610 def ldnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str | bytes:611 """Equivalent to ``dnpgettext()``, but the translation is returned in612 the preferred system encoding, if no other encoding was explicitly set613 with ``bind_textdomain_codeset()``.614 """615 return self._domains.get(domain, self).lnpgettext(context, singular,616 plural, num)617 618 ugettext = gettext.NullTranslations.gettext619 ungettext = gettext.NullTranslations.ngettext620 621 622class Translations(NullTranslations, gettext.GNUTranslations):623 """An extended translation catalog class."""624 625 DEFAULT_DOMAIN = 'messages'626 627 def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None):628 """Initialize the translations catalog.629 630 :param fp: the file-like object the translation should be read from631 :param domain: the message domain (default: 'messages')632 """633 super().__init__(fp=fp)634 self.domain = domain or self.DEFAULT_DOMAIN635 636 ugettext = gettext.GNUTranslations.gettext637 ungettext = gettext.GNUTranslations.ngettext638 639 @classmethod640 def load(641 cls,642 dirname: str | os.PathLike[str] | None = None,643 locales: Iterable[str | Locale] | str | Locale | None = None,644 domain: str | None = None,645 ) -> NullTranslations:646 """Load translations from the given directory.647 648 :param dirname: the directory containing the ``MO`` files649 :param locales: the list of locales in order of preference (items in650 this list can be either `Locale` objects or locale651 strings)652 :param domain: the message domain (default: 'messages')653 """654 if not domain:655 domain = cls.DEFAULT_DOMAIN656 filename = gettext.find(domain, dirname, _locales_to_names(locales))657 if not filename:658 return NullTranslations()659 with open(filename, 'rb') as fp:660 return cls(fp=fp, domain=domain)661 662 def __repr__(self) -> str:663 version = self._info.get('project-id-version')664 return f'<{type(self).__name__}: "{version}">'665 666 def add(self, translations: Translations, merge: bool = True):667 """Add the given translations to the catalog.668 669 If the domain of the translations is different than that of the670 current catalog, they are added as a catalog that is only accessible671 by the various ``d*gettext`` functions.672 673 :param translations: the `Translations` instance with the messages to674 add675 :param merge: whether translations for message domains that have676 already been added should be merged with the existing677 translations678 """679 domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)680 if merge and domain == self.domain:681 return self.merge(translations)682 683 existing = self._domains.get(domain)684 if merge and isinstance(existing, Translations):685 existing.merge(translations)686 else:687 translations.add_fallback(self)688 self._domains[domain] = translations689 690 return self691 692 def merge(self, translations: Translations):693 """Merge the given translations into the catalog.694 695 Message translations in the specified catalog override any messages696 with the same identifier in the existing catalog.697 698 :param translations: the `Translations` instance with the messages to699 merge700 """701 if isinstance(translations, gettext.GNUTranslations):702 self._catalog.update(translations._catalog)703 if isinstance(translations, Translations):704 self.files.extend(translations.files)705 706 return self707 708 709def _locales_to_names(710 locales: Iterable[str | Locale] | str | Locale | None,711) -> list[str] | None:712 """Normalize a `locales` argument to a list of locale names.713 714 :param locales: the list of locales in order of preference (items in715 this list can be either `Locale` objects or locale716 strings)717 """718 if locales is None:719 return None720 if isinstance(locales, Locale):721 return [str(locales)]722 if isinstance(locales, str):723 return [locales]724 return [str(locale) for locale in locales]725 