diff --git a/addons/source-python/packages/site-packages/babel/__init__.py b/addons/source-python/packages/site-packages/babel/__init__.py
index 7b2774558..2fd88befa 100644
--- a/addons/source-python/packages/site-packages/babel/__init__.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
from babel.core import (
@@ -25,7 +25,7 @@
parse_locale,
)
-__version__ = '2.17.0'
+__version__ = '2.18.0'
__all__ = [
'Locale',
diff --git a/addons/source-python/packages/site-packages/babel/core.py b/addons/source-python/packages/site-packages/babel/core.py
index 5762bbe36..4210b46bb 100644
--- a/addons/source-python/packages/site-packages/babel/core.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
@@ -22,6 +22,7 @@
'Locale',
'UnknownLocaleError',
'default_locale',
+ 'get_cldr_version',
'get_global',
'get_locale_identifier',
'negotiate_locale',
@@ -33,6 +34,7 @@
_GLOBAL_KEY: TypeAlias = Literal[
"all_currencies",
+ "cldr",
"currency_fractions",
"language_aliases",
"likely_subtags",
@@ -56,12 +58,14 @@
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]:
@@ -71,13 +75,14 @@ def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]:
information independent of individual locales.
>>> get_global('zone_aliases')['UTC']
- u'Etc/UTC'
+ 'Etc/UTC'
>>> get_global('zone_territories')['Europe/Berlin']
- u'DE'
+ 'DE'
The keys available are:
- ``all_currencies``
+ - ``cldr`` (metadata)
- ``currency_fractions``
- ``language_aliases``
- ``likely_subtags``
@@ -119,7 +124,7 @@ def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]:
'mk': 'mk_MK', 'nl': 'nl_NL', 'nn': 'nn_NO', 'no': 'nb_NO', 'pl': 'pl_PL',
'pt': 'pt_PT', 'ro': 'ro_RO', 'ru': 'ru_RU', 'sk': 'sk_SK', 'sl': 'sl_SI',
'sv': 'sv_SE', 'th': 'th_TH', 'tr': 'tr_TR', 'uk': 'uk_UA',
-}
+} # fmt: skip
class UnknownLocaleError(Exception):
@@ -145,7 +150,7 @@ class Locale:
>>> repr(locale)
"Locale('en', territory='US')"
>>> locale.display_name
- u'English (United States)'
+ 'English (United States)'
A `Locale` object can also be instantiated from a raw locale string:
@@ -157,7 +162,7 @@ class Locale:
territory and language names, number and date format patterns, and more:
>>> locale.number_symbols['latn']['decimal']
- u'.'
+ '.'
If a locale is requested for which no locale data is available, an
`UnknownLocaleError` is raised:
@@ -216,7 +221,11 @@ def __init__(
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 +277,7 @@ def negotiate(
: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
@@ -285,7 +293,7 @@ def parse(
>>> l = Locale.parse('de-DE', sep='-')
>>> l.display_name
- u'Deutsch (Deutschland)'
+ 'Deutsch (Deutschland)'
If the `identifier` parameter is not a string, but actually a `Locale`
object, that object is returned:
@@ -343,10 +351,11 @@ def parse(
f"Empty locale identifier value: {identifier!r}\n\n"
f"If you didn't explicitly pass an empty value to a Babel function, "
f"this could be caused by there being no suitable locale environment "
- f"variables for the API you tried to use.",
+ f"variables for the API you tried to use."
)
if isinstance(identifier, str):
- raise ValueError(msg) # `parse_locale` would raise a ValueError, so let's do that here
+ # `parse_locale` would raise a ValueError, so let's do that here
+ raise ValueError(msg)
raise TypeError(msg)
if not isinstance(identifier, str):
@@ -420,7 +429,9 @@ def _try_load_reducing(parts):
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
@@ -431,19 +442,18 @@ def __eq__(self, other: object) -> bool:
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 = ['']
@@ -454,9 +464,9 @@ def __repr__(self) -> str:
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:
@@ -471,12 +481,12 @@ def get_display_name(self, locale: Locale | str | None = None) -> str | None:
variant, if those are specified.
>>> Locale('zh', 'CN', script='Hans').get_display_name('en')
- u'Chinese (Simplified, China)'
+ 'Chinese (Simplified, China)'
Modifiers are currently passed through verbatim:
>>> Locale('it', 'IT', modifier='euro').get_display_name('en')
- u'Italian (Italy, euro)'
+ 'Italian (Italy, euro)'
:param locale: the locale to use
"""
@@ -499,24 +509,27 @@ def get_display_name(self, locale: Locale | str | None = None) -> str | None:
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
- u'English'
+ 'English'
>>> Locale('en', 'US').display_name
- u'English (United States)'
+ 'English (United States)'
>>> Locale('sv').display_name
- u'svenska'
+ '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.
>>> Locale('zh', 'CN', script='Hans').get_language_name('de')
- u'Chinesisch'
+ 'Chinesisch'
.. versionadded:: 1.0
@@ -527,12 +540,15 @@ def get_language_name(self, locale: Locale | str | None = None) -> str | None:
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'
- """)
+ 'English'
+ """,
+ )
def get_territory_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the territory name in the given locale."""
@@ -541,12 +557,15 @@ def get_territory_name(self, locale: Locale | str | None = None) -> str | None:
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'
- """)
+ 'Deutschland'
+ """,
+ )
def get_script_name(self, locale: Locale | str | None = None) -> str | None:
"""Return the script name in the given locale."""
@@ -555,21 +574,24 @@ def get_script_name(self, locale: Locale | str | None = None) -> str | None:
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'
- """)
+ 'latinica'
+ """,
+ )
@property
def english_name(self) -> str | None:
"""The english display name of the locale.
>>> Locale('de').english_name
- u'German'
+ 'German'
>>> Locale('de', 'DE').english_name
- u'German (Germany)'
+ 'German (Germany)'
:type: `unicode`"""
return self.get_display_name(Locale('en'))
@@ -581,7 +603,7 @@ def languages(self) -> localedata.LocaleDataDict:
"""Mapping of language codes to translated language names.
>>> Locale('de', 'DE').languages['ja']
- u'Japanisch'
+ 'Japanisch'
See `ISO 639 `_ for
more information.
@@ -593,7 +615,7 @@ def scripts(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('en', 'US').scripts['Hira']
- u'Hiragana'
+ 'Hiragana'
See `ISO 15924 `_
for more information.
@@ -605,7 +627,7 @@ def territories(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('es', 'CO').territories['DE']
- u'Alemania'
+ 'Alemania'
See `ISO 3166 `_
for more information.
@@ -617,7 +639,7 @@ def variants(self) -> localedata.LocaleDataDict:
"""Mapping of script codes to translated script names.
>>> Locale('de', 'DE').variants['1901']
- u'Alte deutsche Rechtschreibung'
+ 'Alte deutsche Rechtschreibung'
"""
return self._data['variants']
@@ -631,9 +653,9 @@ def currencies(self) -> localedata.LocaleDataDict:
:func:`babel.numbers.get_currency_name` function.
>>> Locale('en').currencies['COP']
- u'Colombian Peso'
+ 'Colombian Peso'
>>> Locale('de', 'DE').currencies['COP']
- u'Kolumbianischer Peso'
+ 'Kolumbianischer Peso'
"""
return self._data['currency_names']
@@ -642,9 +664,9 @@ def currency_symbols(self) -> localedata.LocaleDataDict:
"""Mapping of currency codes to symbols.
>>> Locale('en', 'US').currency_symbols['USD']
- u'$'
+ '$'
>>> Locale('es', 'CO').currency_symbols['USD']
- u'US$'
+ 'US$'
"""
return self._data['currency_symbols']
@@ -656,11 +678,11 @@ def number_symbols(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('fr', 'FR').number_symbols["latn"]['decimal']
- u','
+ ','
>>> Locale('fa', 'IR').number_symbols["arabext"]['decimal']
- u'٫'
+ '٫'
>>> Locale('fa', 'IR').number_symbols["latn"]['decimal']
- u'.'
+ '.'
"""
return self._data['number_symbols']
@@ -671,7 +693,7 @@ def other_numbering_systems(self) -> localedata.LocaleDataDict:
See: https://www.unicode.org/reports/tr35/tr35-numbers.html#otherNumberingSystems
>>> Locale('el', 'GR').other_numbering_systems['traditional']
- u'grek'
+ 'grek'
.. note:: The format of the value returned may change between
Babel versions.
@@ -682,7 +704,7 @@ def other_numbering_systems(self) -> localedata.LocaleDataDict:
def default_numbering_system(self) -> str:
"""The default numbering system used by the locale.
>>> Locale('el', 'GR').default_numbering_system
- u'latn'
+ 'latn'
"""
return self._data['default_numbering_system']
@@ -694,7 +716,7 @@ def decimal_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').decimal_formats[None]
-
+
"""
return self._data['decimal_formats']
@@ -706,7 +728,7 @@ def compact_decimal_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').compact_decimal_formats["short"]["one"]["1000"]
-
+
"""
return self._data['compact_decimal_formats']
@@ -718,9 +740,9 @@ def currency_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').currency_formats['standard']
-
+
>>> Locale('en', 'US').currency_formats['accounting']
-
+
"""
return self._data['currency_formats']
@@ -732,7 +754,7 @@ def compact_currency_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').compact_currency_formats["short"]["one"]["1000"]
-
+
"""
return self._data['compact_currency_formats']
@@ -744,7 +766,7 @@ def percent_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').percent_formats[None]
-
+
"""
return self._data['percent_formats']
@@ -756,7 +778,7 @@ def scientific_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').scientific_formats[None]
-
+
"""
return self._data['scientific_formats']
@@ -767,7 +789,7 @@ def periods(self) -> localedata.LocaleDataDict:
"""Locale display names for day periods (AM/PM).
>>> Locale('en', 'US').periods['am']
- u'AM'
+ 'AM'
"""
try:
return self._data['day_periods']['stand-alone']['wide']
@@ -784,8 +806,7 @@ def day_periods(self) -> localedata.LocaleDataDict:
@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
@@ -793,7 +814,7 @@ def days(self) -> localedata.LocaleDataDict:
"""Locale display names for weekdays.
>>> Locale('de', 'DE').days['format']['wide'][3]
- u'Donnerstag'
+ 'Donnerstag'
"""
return self._data['days']
@@ -802,7 +823,7 @@ def months(self) -> localedata.LocaleDataDict:
"""Locale display names for months.
>>> Locale('de', 'DE').months['format']['wide'][10]
- u'Oktober'
+ 'Oktober'
"""
return self._data['months']
@@ -811,7 +832,7 @@ def quarters(self) -> localedata.LocaleDataDict:
"""Locale display names for quarters.
>>> Locale('de', 'DE').quarters['format']['wide'][1]
- u'1. Quartal'
+ '1. Quartal'
"""
return self._data['quarters']
@@ -823,9 +844,9 @@ def eras(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').eras['wide'][1]
- u'Anno Domini'
+ 'Anno Domini'
>>> Locale('en', 'US').eras['abbreviated'][0]
- u'BC'
+ 'BC'
"""
return self._data['eras']
@@ -837,9 +858,9 @@ def time_zones(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').time_zones['Europe/London']['long']['daylight']
- u'British Summer Time'
+ 'British Summer Time'
>>> Locale('en', 'US').time_zones['America/St_Johns']['city']
- u'St. John\u2019s'
+ 'St. John’s'
"""
return self._data['time_zones']
@@ -854,7 +875,7 @@ def meta_zones(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').meta_zones['Europe_Central']['long']['daylight']
- u'Central European Summer Time'
+ 'Central European Summer Time'
.. versionadded:: 0.9
"""
@@ -868,9 +889,9 @@ def zone_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').zone_formats['fallback']
- u'%(1)s (%(0)s)'
+ '%(1)s (%(0)s)'
>>> Locale('pt', 'BR').zone_formats['region']
- u'Hor\\xe1rio %s'
+ 'Horário %s'
.. versionadded:: 0.9
"""
@@ -923,9 +944,9 @@ def date_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').date_formats['short']
-
+
>>> Locale('fr', 'FR').date_formats['long']
-
+
"""
return self._data['date_formats']
@@ -937,9 +958,9 @@ def time_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en', 'US').time_formats['short']
-
+
>>> Locale('fr', 'FR').time_formats['long']
-
+
"""
return self._data['time_formats']
@@ -951,9 +972,9 @@ def datetime_formats(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en').datetime_formats['full']
- u'{1}, {0}'
+ '{1}, {0}'
>>> Locale('th').datetime_formats['medium']
- u'{1} {0}'
+ '{1} {0}'
"""
return self._data['datetime_formats']
@@ -962,11 +983,11 @@ def datetime_skeletons(self) -> localedata.LocaleDataDict:
"""Locale patterns for formatting parts of a datetime.
>>> Locale('en').datetime_skeletons['MEd']
-
+
>>> Locale('fr').datetime_skeletons['MEd']
-
+
>>> Locale('fr').datetime_skeletons['H']
-
+
"""
return self._data['datetime_skeletons']
@@ -981,7 +1002,7 @@ def interval_formats(self) -> localedata.LocaleDataDict:
smallest changing component:
>>> Locale('fi_FI').interval_formats['MEd']['d']
- [u'E d.\u2009\u2013\u2009', u'E d.M.']
+ ['E d.\\u2009–\\u2009', 'E d.M.']
.. seealso::
@@ -1015,11 +1036,11 @@ def list_patterns(self) -> localedata.LocaleDataDict:
Babel versions.
>>> Locale('en').list_patterns['standard']['start']
- u'{0}, {1}'
+ '{0}, {1}'
>>> Locale('en').list_patterns['standard']['end']
- u'{0}, and {1}'
+ '{0}, and {1}'
>>> Locale('en_GB').list_patterns['standard']['end']
- u'{0} and {1}'
+ '{0} and {1}'
"""
return self._data['list_patterns']
@@ -1045,9 +1066,9 @@ def measurement_systems(self) -> localedata.LocaleDataDict:
"""Localized names for various measurement systems.
>>> Locale('fr', 'FR').measurement_systems['US']
- u'am\\xe9ricain'
+ 'américain'
>>> Locale('en', 'US').measurement_systems['US']
- u'US'
+ 'US'
"""
return self._data['measurement_systems']
@@ -1149,7 +1170,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'])
@@ -1215,7 +1241,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)``.
@@ -1293,8 +1322,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()
@@ -1335,3 +1366,19 @@ def get_locale_identifier(
lang, territory, script, variant, modifier = tup + (None,) * (5 - len(tup))
ret = sep.join(filter(None, (lang, script, territory, variant)))
return f'{ret}@{modifier}' if modifier else ret
+
+
+def get_cldr_version() -> str:
+ """Return the Unicode CLDR version used by this Babel installation.
+
+ Generally, you should be able to assume that the return value of this
+ function is a string representing a version number, e.g. '47'.
+
+ >>> get_cldr_version()
+ '47'
+
+ .. versionadded:: 2.18
+
+ :rtype: str
+ """
+ return str(get_global("cldr")["version"])
diff --git a/addons/source-python/packages/site-packages/babel/dates.py b/addons/source-python/packages/site-packages/babel/dates.py
index 355a9236e..69610a7f0 100644
--- a/addons/source-python/packages/site-packages/babel/dates.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
@@ -38,10 +38,11 @@
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']
- _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None
+ _DtOrTzinfo: TypeAlias = datetime.datetime | datetime.tzinfo | str | int | datetime.time | None # fmt: skip
# "If a given short metazone form is known NOT to be understood in a given
# locale and the parent locale has this value such that it would normally
@@ -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.
@@ -260,7 +266,7 @@ def get_period_names(
"""Return the names for day periods (AM/PM) used by the locale.
>>> get_period_names(locale='en_US')['am']
- u'AM'
+ 'AM'
:param width: the width to use, one of "abbreviated", "narrow", or "wide"
:param context: the context, either "format" or "stand-alone"
@@ -277,13 +283,13 @@ def get_day_names(
"""Return the day names used by the locale for the specified format.
>>> get_day_names('wide', locale='en_US')[1]
- u'Tuesday'
+ 'Tuesday'
>>> get_day_names('short', locale='en_US')[1]
- u'Tu'
+ 'Tu'
>>> get_day_names('abbreviated', locale='es')[1]
- u'mar'
+ 'mar'
>>> get_day_names('narrow', context='stand-alone', locale='de_DE')[1]
- u'D'
+ 'D'
:param width: the width to use, one of "wide", "abbreviated", "short" or "narrow"
:param context: the context, either "format" or "stand-alone"
@@ -300,11 +306,11 @@ def get_month_names(
"""Return the month names used by the locale for the specified format.
>>> get_month_names('wide', locale='en_US')[1]
- u'January'
+ 'January'
>>> get_month_names('abbreviated', locale='es')[1]
- u'ene'
+ 'ene'
>>> get_month_names('narrow', context='stand-alone', locale='de_DE')[1]
- u'J'
+ 'J'
:param width: the width to use, one of "wide", "abbreviated", or "narrow"
:param context: the context, either "format" or "stand-alone"
@@ -321,11 +327,11 @@ def get_quarter_names(
"""Return the quarter names used by the locale for the specified format.
>>> get_quarter_names('wide', locale='en_US')[1]
- u'1st quarter'
+ '1st quarter'
>>> get_quarter_names('abbreviated', locale='de_DE')[1]
- u'Q1'
+ 'Q1'
>>> get_quarter_names('narrow', locale='de_DE')[1]
- u'1'
+ '1'
:param width: the width to use, one of "wide", "abbreviated", or "narrow"
:param context: the context, either "format" or "stand-alone"
@@ -341,9 +347,9 @@ def get_era_names(
"""Return the era names used by the locale for the specified format.
>>> get_era_names('wide', locale='en_US')[1]
- u'Anno Domini'
+ 'Anno Domini'
>>> get_era_names('abbreviated', locale='de_DE')[1]
- u'n. Chr.'
+ 'n. Chr.'
:param width: the width to use, either "wide", "abbreviated", or "narrow"
:param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
@@ -359,9 +365,9 @@ def get_date_format(
format.
>>> get_date_format(locale='en_US')
-
+
>>> get_date_format('full', locale='de_DE')
-
+
:param format: the format to use, one of "full", "long", "medium", or
"short"
@@ -378,7 +384,7 @@ def get_datetime_format(
specified format.
>>> get_datetime_format(locale='en_US')
- u'{1}, {0}'
+ '{1}, {0}'
:param format: the format to use, one of "full", "long", "medium", or
"short"
@@ -398,9 +404,9 @@ def get_time_format(
format.
>>> get_time_format(locale='en_US')
-
+
>>> get_time_format('full', locale='de_DE')
-
+
:param format: the format to use, one of "full", "long", "medium", or
"short"
@@ -421,25 +427,25 @@ def get_timezone_gmt(
>>> from datetime import datetime
>>> dt = datetime(2007, 4, 1, 15, 30)
>>> get_timezone_gmt(dt, locale='en')
- u'GMT+00:00'
+ 'GMT+00:00'
>>> get_timezone_gmt(dt, locale='en', return_z=True)
'Z'
>>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
- u'+00'
+ '+00'
>>> tz = get_timezone('America/Los_Angeles')
>>> dt = _localize(tz, datetime(2007, 4, 1, 15, 30))
>>> get_timezone_gmt(dt, locale='en')
- u'GMT-07:00'
+ 'GMT-07:00'
>>> get_timezone_gmt(dt, 'short', locale='en')
- u'-0700'
+ '-0700'
>>> get_timezone_gmt(dt, locale='en', width='iso8601_short')
- u'-07'
+ '-07'
The long format depends on the locale, for example in France the acronym
UTC string is used instead of GMT:
>>> get_timezone_gmt(dt, 'long', locale='fr_FR')
- u'UTC-07:00'
+ 'UTC-07:00'
.. versionadded:: 0.9
@@ -488,14 +494,14 @@ def get_timezone_location(
St. John’s
>>> tz = get_timezone('America/Mexico_City')
>>> get_timezone_location(tz, locale='de_DE')
- u'Mexiko (Mexiko-Stadt) (Ortszeit)'
+ 'Mexiko (Mexiko-Stadt) (Ortszeit)'
If the timezone is associated with a country that uses only a single
timezone, just the localized country name is returned:
>>> tz = get_timezone('Europe/Berlin')
>>> get_timezone_name(tz, locale='de_DE')
- u'Mitteleurop\\xe4ische Zeit'
+ 'Mitteleuropäische Zeit'
.. versionadded:: 0.9
@@ -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(
@@ -563,11 +576,11 @@ def get_timezone_name(
>>> from datetime import time
>>> dt = time(15, 30, tzinfo=get_timezone('America/Los_Angeles'))
>>> get_timezone_name(dt, locale='en_US') # doctest: +SKIP
- u'Pacific Standard Time'
+ 'Pacific Standard Time'
>>> get_timezone_name(dt, locale='en_US', return_zone=True)
'America/Los_Angeles'
>>> get_timezone_name(dt, width='short', locale='en_US') # doctest: +SKIP
- u'PST'
+ 'PST'
If this function gets passed only a `tzinfo` object and no concrete
`datetime`, the returned display name is independent of daylight savings
@@ -576,9 +589,9 @@ def get_timezone_name(
>>> tz = get_timezone('America/Los_Angeles')
>>> get_timezone_name(tz, locale='en_US')
- u'Pacific Time'
+ 'Pacific Time'
>>> get_timezone_name(tz, 'short', locale='en_US')
- u'PT'
+ 'PT'
If no localized display name for the timezone is available, and the timezone
is associated with a country that uses only a single timezone, the name of
@@ -586,16 +599,16 @@ def get_timezone_name(
>>> tz = get_timezone('Europe/Berlin')
>>> get_timezone_name(tz, locale='de_DE')
- u'Mitteleurop\xe4ische Zeit'
+ 'Mitteleuropäische Zeit'
>>> get_timezone_name(tz, locale='pt_BR')
- u'Hor\xe1rio da Europa Central'
+ 'Horário da Europa Central'
On the other hand, if the country uses multiple timezones, the city is also
included in the representation:
>>> tz = get_timezone('America/St_Johns')
>>> get_timezone_name(tz, locale='de_DE')
- u'Neufundland-Zeit'
+ 'Neufundland-Zeit'
Note that short format is currently not supported for all timezones and
all locales. This is partially because not every timezone has a short
@@ -649,7 +662,9 @@ def get_timezone_name(
info = locale.time_zones.get(zone, {})
# Try explicitly translated zone names first
if width in info and zone_variant in info[width]:
- return info[width][zone_variant]
+ value = info[width][zone_variant]
+ if value != NO_INHERITANCE_MARKER:
+ return value
metazone = get_global('meta_zones').get(zone)
if metazone:
@@ -660,7 +675,7 @@ def get_timezone_name(
# If the short form is marked no-inheritance,
# try to fall back to the long name instead.
name = metazone_info.get('long', {}).get(zone_variant)
- if name:
+ if name and name != NO_INHERITANCE_MARKER:
return name
# If we have a concrete datetime, we assume that the result can't be
@@ -681,15 +696,15 @@ def format_date(
>>> from datetime import date
>>> d = date(2007, 4, 1)
>>> format_date(d, locale='en_US')
- u'Apr 1, 2007'
+ 'Apr 1, 2007'
>>> format_date(d, format='full', locale='de_DE')
- u'Sonntag, 1. April 2007'
+ 'Sonntag, 1. April 2007'
If you don't want to use the locale default formats, you can specify a
custom date pattern:
>>> format_date(d, "EEE, MMM d, ''yy", locale='en')
- u"Sun, Apr 1, '07"
+ "Sun, Apr 1, '07"
:param date: the ``date`` or ``datetime`` object; if `None`, the current
date is used
@@ -720,7 +735,7 @@ def format_datetime(
>>> from datetime import datetime
>>> dt = datetime(2007, 4, 1, 15, 30)
>>> format_datetime(dt, locale='en_US')
- u'Apr 1, 2007, 3:30:00\u202fPM'
+ 'Apr 1, 2007, 3:30:00\u202fPM'
For any pattern requiring the display of the timezone:
@@ -729,7 +744,7 @@ def format_datetime(
'dimanche 1 avril 2007, 17:30:00 heure d’été d’Europe centrale'
>>> format_datetime(dt, "yyyy.MM.dd G 'at' HH:mm:ss zzz",
... tzinfo=get_timezone('US/Eastern'), locale='en')
- u'2007.04.01 AD at 11:30:00 EDT'
+ '2007.04.01 AD at 11:30:00 EDT'
:param datetime: the `datetime` object; if `None`, the current date and
time is used
@@ -742,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)
@@ -762,15 +778,15 @@ def format_time(
>>> from datetime import datetime, time
>>> t = time(15, 30)
>>> format_time(t, locale='en_US')
- u'3:30:00\u202fPM'
+ '3:30:00\u202fPM'
>>> format_time(t, format='short', locale='de_DE')
- u'15:30'
+ '15:30'
If you don't want to use the locale default formats, you can specify a
custom time pattern:
>>> format_time(t, "hh 'o''clock' a", locale='en')
- u"03 o'clock PM"
+ "03 o'clock PM"
For any pattern requiring the display of the time-zone a
timezone has to be specified explicitly:
@@ -782,7 +798,7 @@ def format_time(
'15:30:00 heure d’été d’Europe centrale'
>>> format_time(t, "hh 'o''clock' a, zzzz", tzinfo=get_timezone('US/Eastern'),
... locale='en')
- u"09 o'clock AM, Eastern Daylight Time"
+ "09 o'clock AM, Eastern Daylight Time"
As that example shows, when this function gets passed a
``datetime.datetime`` value, the actual time in the formatted string is
@@ -800,10 +816,10 @@ def format_time(
>>> t = time(15, 30)
>>> format_time(t, format='full', tzinfo=get_timezone('Europe/Paris'),
... locale='fr_FR') # doctest: +SKIP
- u'15:30:00 heure normale d\u2019Europe centrale'
+ '15:30:00 heure normale d\u2019Europe centrale'
>>> format_time(t, format='full', tzinfo=get_timezone('US/Eastern'),
... locale='en_US') # doctest: +SKIP
- u'3:30:00\u202fPM Eastern Standard Time'
+ '3:30:00\u202fPM Eastern Standard Time'
:param time: the ``time`` or ``datetime`` object; if `None`, the current
time in UTC is used
@@ -842,11 +858,11 @@ def format_skeleton(
>>> from datetime import datetime
>>> t = datetime(2007, 4, 1, 15, 30)
>>> format_skeleton('MMMEd', t, locale='fr')
- u'dim. 1 avr.'
+ 'dim. 1 avr.'
>>> format_skeleton('MMMEd', t, locale='en')
- u'Sun, Apr 1'
+ 'Sun, Apr 1'
>>> format_skeleton('yMMd', t, locale='fi') # yMMd is not in the Finnish locale; yMd gets used
- u'1.4.2007'
+ '1.4.2007'
>>> format_skeleton('yMMd', t, fuzzy=False, locale='fi') # yMMd is not in the Finnish locale, an error is thrown
Traceback (most recent call last):
...
@@ -888,8 +904,16 @@ def format_skeleton(
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,
@@ -898,39 +922,39 @@ def format_timedelta(
>>> from datetime import timedelta
>>> format_timedelta(timedelta(weeks=12), locale='en_US')
- u'3 months'
+ '3 months'
>>> format_timedelta(timedelta(seconds=1), locale='es')
- u'1 segundo'
+ '1 segundo'
The granularity parameter can be provided to alter the lowest unit
presented, which defaults to a second.
>>> format_timedelta(timedelta(hours=3), granularity='day', locale='en_US')
- u'1 day'
+ '1 day'
The threshold parameter can be used to determine at which value the
presentation switches to the next higher unit. A higher threshold factor
means the presentation will switch later. For example:
>>> format_timedelta(timedelta(hours=23), threshold=0.9, locale='en_US')
- u'1 day'
+ '1 day'
>>> format_timedelta(timedelta(hours=23), threshold=1.1, locale='en_US')
- u'23 hours'
+ '23 hours'
In addition directional information can be provided that informs
the user if the date is in the past or in the future:
>>> format_timedelta(timedelta(hours=1), add_direction=True, locale='en')
- u'in 1 hour'
+ 'in 1 hour'
>>> format_timedelta(timedelta(hours=-1), add_direction=True, locale='en')
- u'1 hour ago'
+ '1 hour ago'
The format parameter controls how compact or wide the presentation is:
>>> format_timedelta(timedelta(hours=3), format='short', locale='en')
- u'3 hr'
+ '3 hr'
>>> format_timedelta(timedelta(hours=3), format='narrow', locale='en')
- u'3h'
+ '3h'
:param delta: a ``timedelta`` object representing the time difference to
format, or the delta in seconds as an `int` value
@@ -953,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,
)
@@ -971,7 +994,7 @@ def _iter_patterns(a_unit):
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:
@@ -1016,9 +1039,17 @@ def _format_fallback_interval(
) -> str:
if skeleton in locale.datetime_skeletons: # Use the given skeleton
format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
- elif all((isinstance(d, datetime.date) and not isinstance(d, datetime.datetime)) for d in (start, end)): # Both are just dates
+ elif all(
+ # Both are just dates
+ (isinstance(d, datetime.date) and not isinstance(d, datetime.datetime))
+ for d in (start, end)
+ ):
format = lambda dt: format_date(dt, locale=locale)
- elif all((isinstance(d, datetime.time) and not isinstance(d, datetime.date)) for d in (start, end)): # Both are times
+ elif all(
+ # Both are times
+ (isinstance(d, datetime.time) and not isinstance(d, datetime.date))
+ for d in (start, end)
+ ):
format = lambda dt: format_time(dt, tzinfo=tzinfo, locale=locale)
else:
format = lambda dt: format_datetime(dt, tzinfo=tzinfo, locale=locale)
@@ -1030,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)
)
@@ -1049,16 +1080,16 @@ def format_interval(
>>> from datetime import date, time
>>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "yMd", locale="fi")
- u'15.\u201317.1.2016'
+ '15.–17.1.2016'
>>> format_interval(time(12, 12), time(16, 16), "Hm", locale="en_GB")
- '12:12\u201316:16'
+ '12:12–16:16'
>>> format_interval(time(5, 12), time(16, 16), "hm", locale="en_US")
- '5:12\u202fAM\u2009–\u20094:16\u202fPM'
+ '5:12\\u202fAM\\u2009–\\u20094:16\\u202fPM'
>>> format_interval(time(16, 18), time(16, 24), "Hm", locale="it")
- '16:18\u201316:24'
+ '16:18–16:24'
If the start instant equals the end instant, the interval is formatted like the instant.
@@ -1068,13 +1099,13 @@ def format_interval(
Unknown skeletons fall back to "default" formatting.
>>> format_interval(date(2015, 1, 1), date(2017, 1, 1), "wzq", locale="ja")
- '2015/01/01\uff5e2017/01/01'
+ '2015/01/01~2017/01/01'
>>> format_interval(time(16, 18), time(16, 24), "xxx", locale="ja")
- '16:18:00\uff5e16:24:00'
+ '16:18:00~16:24:00'
>>> format_interval(date(2016, 1, 15), date(2016, 1, 17), "xxx", locale="de")
- '15.01.2016\u2009–\u200917.01.2016'
+ '15.01.2016\\u2009–\\u200917.01.2016'
:param start: First instant (datetime/date/time)
:param end: Second instant (datetime/date/time)
@@ -1132,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.
@@ -1154,13 +1184,13 @@ def get_period_id(
>>> from datetime import time
>>> get_period_names(locale="de")[get_period_id(time(7, 42), locale="de")]
- u'Morgen'
+ 'Morgen'
>>> get_period_id(time(0), locale="en_US")
- u'midnight'
+ 'midnight'
>>> get_period_id(time(0), type="selection", locale="en_US")
- u'night1'
+ 'morning1'
:param time: The time to inspect.
:param tzinfo: The timezone for the time. See ``format_time``.
@@ -1191,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
@@ -1264,8 +1296,11 @@ def parse_date(
use_predefined_format = format in ('full', 'long', 'medium', 'short')
# we try ISO-8601 format first, meaning similar to formats
# extended YYYY-MM-DD or basic YYYYMMDD
- iso_alike = re.match(r'^(\d{4})-?([01]\d)-?([0-3]\d)$',
- string, flags=re.ASCII) # allow only ASCII digits
+ iso_alike = re.match(
+ r'^(\d{4})-?([01]\d)-?([0-3]\d)$',
+ string,
+ flags=re.ASCII, # allow only ASCII digits
+ )
if iso_alike and use_predefined_format:
try:
return datetime.date(*map(int, iso_alike.groups()))
@@ -1364,7 +1399,6 @@ def parse_time(
class DateTimePattern:
-
def __init__(self, pattern: str, format: DateTimeFormat):
self.pattern = pattern
self.format = format
@@ -1391,7 +1425,6 @@ def apply(
class DateTimeFormat:
-
def __init__(
self,
value: datetime.date | datetime.time,
@@ -1472,7 +1505,9 @@ def extract(self, char: str) -> int:
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)]
@@ -1522,12 +1557,12 @@ def format_weekday(self, char: str = 'E', num: int = 4) -> str:
>>> from datetime import date
>>> format = DateTimeFormat(date(2016, 2, 28), Locale.parse('en_US'))
>>> format.format_weekday()
- u'Sunday'
+ 'Sunday'
'E': Day of week - Use one through three letters for the abbreviated day name, four for the full (wide) name,
five for the narrow name, or six for the short name.
>>> format.format_weekday('E',2)
- u'Sun'
+ 'Sun'
'e': Local day of week. Same as E except adds a numeric value that will depend on the local starting day of the
week, using one or two letters. For this example, Monday is the first day of the week.
@@ -1566,28 +1601,32 @@ def format_period(self, char: str, num: int) -> str:
>>> from datetime import datetime, time
>>> format = DateTimeFormat(time(13, 42), 'fi_FI')
>>> format.format_period('a', 1)
- u'ip.'
+ 'ip.'
>>> format.format_period('b', 1)
- u'iltap.'
+ 'iltap.'
>>> format.format_period('b', 4)
- u'iltapäivä'
+ 'iltapäivä'
>>> format.format_period('B', 4)
- u'iltapäivällä'
+ 'iltapäivällä'
>>> format.format_period('B', 5)
- u'ip.'
+ 'ip.'
>>> format = DateTimeFormat(datetime(2022, 4, 28, 6, 27), 'zh_Hant')
>>> format.format_period('a', 1)
- u'上午'
+ '上午'
>>> format.format_period('B', 1)
- u'清晨'
+ '清晨'
:param char: pattern format character ('a', 'b', 'B')
: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'
@@ -1610,8 +1649,12 @@ def format_frac_seconds(self, num: int) -> str:
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:
@@ -1635,35 +1678,24 @@ def format_timezone(self, char: str, num: int) -> str:
return get_timezone_gmt(value, width, locale=self.locale)
# TODO: To add support for O:1
elif char == 'v':
- return get_timezone_name(value.tzinfo, width,
- locale=self.locale)
+ return get_timezone_name(value.tzinfo, width, locale=self.locale)
elif char == 'V':
if num == 1:
- return get_timezone_name(value.tzinfo, width,
- uncommon=True, locale=self.locale)
+ return get_timezone_name(value.tzinfo, width, locale=self.locale)
elif num == 2:
return get_timezone_name(value.tzinfo, locale=self.locale, return_zone=True)
elif num == 3:
- return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True)
+ return get_timezone_location(value.tzinfo, locale=self.locale, return_city=True) # fmt: skip
return get_timezone_location(value.tzinfo, locale=self.locale)
- # Included additional elif condition to add support for 'Xx' in timezone format
- elif char == 'X':
- if num == 1:
- return get_timezone_gmt(value, width='iso8601_short', locale=self.locale,
- return_z=True)
- elif num in (2, 4):
- return get_timezone_gmt(value, width='short', locale=self.locale,
- return_z=True)
- elif num in (3, 5):
- return get_timezone_gmt(value, width='iso8601', locale=self.locale,
- return_z=True)
- elif char == 'x':
+ elif char in 'Xx':
+ return_z = char == 'X'
if num == 1:
- return get_timezone_gmt(value, width='iso8601_short', locale=self.locale)
+ width = 'iso8601_short'
elif num in (2, 4):
- return get_timezone_gmt(value, width='short', locale=self.locale)
+ width = 'short'
elif num in (3, 5):
- return get_timezone_gmt(value, width='iso8601', locale=self.locale)
+ width = 'iso8601'
+ return get_timezone_gmt(value, width=width, locale=self.locale, return_z=return_z) # fmt: skip
def format(self, value: SupportsInt, length: int) -> str:
return '%0*d' % (length, value)
@@ -1679,12 +1711,13 @@ def get_week_of_year(self) -> int:
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
@@ -1713,8 +1746,7 @@ def get_week_number(self, day_of_period: int, day_of_week: int | None = None) ->
"""
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
@@ -1737,7 +1769,7 @@ def get_week_number(self, day_of_period: int, day_of_week: int | None = None) ->
's': [1, 2], 'S': None, 'A': None, # second
'z': [1, 2, 3, 4], 'Z': [1, 2, 3, 4, 5], 'O': [1, 4], 'v': [1, 4], # zone
'V': [1, 2, 3, 4], 'x': [1, 2, 3, 4, 5], 'X': [1, 2, 3, 4, 5], # zone
-}
+} # fmt: skip
#: The pattern characters declared in the Date Field Symbol Table
#: (https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
@@ -1749,20 +1781,20 @@ def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern:
"""Parse date, time, and datetime format patterns.
>>> parse_pattern("MMMMd").format
- u'%(MMMM)s%(d)s'
+ '%(MMMM)s%(d)s'
>>> parse_pattern("MMM d, yyyy").format
- u'%(MMM)s %(d)s, %(yyyy)s'
+ '%(MMM)s %(d)s, %(yyyy)s'
Pattern can contain literal strings in single quotes:
>>> parse_pattern("H:mm' Uhr 'z").format
- u'%(H)s:%(mm)s Uhr %(z)s'
+ '%(H)s:%(mm)s Uhr %(z)s'
An actual single quote can be used by using two adjacent single quote
characters:
>>> parse_pattern("hh' o''clock'").format
- u"%(hh)s o'clock"
+ "%(hh)s o'clock"
:param pattern: the formatting pattern to parse
"""
@@ -1886,18 +1918,18 @@ def split_interval_pattern(pattern: str) -> list[str]:
> The pattern is then designed to be broken up into two pieces by determining the first repeating field.
- https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
- >>> split_interval_pattern(u'E d.M. \u2013 E d.M.')
- [u'E d.M. \u2013 ', 'E d.M.']
+ >>> split_interval_pattern('E d.M. – E d.M.')
+ ['E d.M. – ', 'E d.M.']
>>> split_interval_pattern("Y 'text' Y 'more text'")
["Y 'text '", "Y 'more text'"]
- >>> split_interval_pattern(u"E, MMM d \u2013 E")
- [u'E, MMM d \u2013 ', u'E']
+ >>> split_interval_pattern('E, MMM d – E')
+ ['E, MMM d – ', 'E']
>>> split_interval_pattern("MMM d")
['MMM d']
>>> split_interval_pattern("y G")
['y G']
- >>> split_interval_pattern(u"MMM d \u2013 d")
- [u'MMM d \u2013 ', u'd']
+ >>> split_interval_pattern('MMM d – d')
+ ['MMM d – ', 'd']
:param pattern: Interval pattern string
:return: list of "subpatterns"
@@ -1917,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.
@@ -1965,11 +2001,11 @@ def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields
if 'b' in skeleton and not any('b' in option for option in options):
skeleton = skeleton.replace('b', '')
- get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get
+ get_input_field_width = dict(t[1] for t in tokenize_pattern(skeleton) if t[0] == "field").get # fmt: skip
best_skeleton = None
best_distance = None
for option in options:
- get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get
+ get_opt_field_width = dict(t[1] for t in tokenize_pattern(option) if t[0] == "field").get # fmt: skip
distance = 0
for field in PATTERN_CHARS:
input_width = get_input_field_width(field, 0)
@@ -1980,13 +2016,18 @@ def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields
if not allow_different_fields: # This one is not okay
option = None
break
- distance += 0x1000 # Magic weight constant for "entirely different fields"
- elif field == 'M' and ((input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)):
- distance += 0x100 # Magic weight for "text turns into a number"
+ # Magic weight constant for "entirely different fields"
+ distance += 0x1000
+ elif field == 'M' and (
+ (input_width > 2 and opt_width <= 2) or (input_width <= 2 and opt_width > 2)
+ ):
+ # Magic weight constant for "text turns into a number"
+ distance += 0x100
else:
distance += abs(input_width - opt_width)
- if not option: # We lost the option along the way (probably due to "allow_different_fields")
+ if not option:
+ # We lost the option along the way (probably due to "allow_different_fields")
continue
if not best_skeleton or distance < best_distance:
diff --git a/addons/source-python/packages/site-packages/babel/global.dat b/addons/source-python/packages/site-packages/babel/global.dat
index 4c7f1506d..225499456 100644
Binary files a/addons/source-python/packages/site-packages/babel/global.dat and b/addons/source-python/packages/site-packages/babel/global.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/languages.py b/addons/source-python/packages/site-packages/babel/languages.py
index 564f555d2..5b2396c84 100644
--- a/addons/source-python/packages/site-packages/babel/languages.py
+++ b/addons/source-python/packages/site-packages/babel/languages.py
@@ -3,7 +3,11 @@
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/addons/source-python/packages/site-packages/babel/lists.py b/addons/source-python/packages/site-packages/babel/lists.py
index 353171c71..b6c859800 100644
--- a/addons/source-python/packages/site-packages/babel/lists.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import warnings
@@ -37,18 +38,26 @@ 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:
"""
Format the items in `lst` as a list.
>>> format_list(['apples', 'oranges', 'pears'], locale='en')
- u'apples, oranges, and pears'
+ 'apples, oranges, and pears'
>>> format_list(['apples', 'oranges', 'pears'], locale='zh')
- u'apples\u3001oranges\u548cpears'
+ 'apples、oranges和pears'
>>> format_list(['omena', 'peruna', 'aplari'], style='or', locale='fi')
- u'omena, peruna tai aplari'
+ 'omena, peruna tai aplari'
Not all styles are necessarily available in all locales.
The function will attempt to fall back to replacement styles according to the rules
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/cop.dat b/addons/source-python/packages/site-packages/babel/locale-data/cop.dat
new file mode 100644
index 000000000..01dd6959a
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/cop.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/cop_EG.dat b/addons/source-python/packages/site-packages/babel/locale-data/cop_EG.dat
new file mode 100644
index 000000000..c6811e715
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/cop_EG.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/da.dat b/addons/source-python/packages/site-packages/babel/locale-data/da.dat
index bb74382c2..43df93f97 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/da.dat and b/addons/source-python/packages/site-packages/babel/locale-data/da.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en.dat b/addons/source-python/packages/site-packages/babel/locale-data/en.dat
index 7a576fcec..10bee2fab 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_001.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_001.dat
index 47db2cc6f..e799826a2 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en_001.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en_001.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_AU.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_AU.dat
index 7c92a1a3b..a27ea0f67 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en_AU.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en_AU.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_CA.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_CA.dat
index 89cab7ceb..4bcd2d24a 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en_CA.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en_CA.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_CZ.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_CZ.dat
new file mode 100644
index 000000000..9fd95110a
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_CZ.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_Dsrt.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_Dsrt.dat
index 114accc10..341ac66dd 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en_Dsrt.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en_Dsrt.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_ES.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_ES.dat
new file mode 100644
index 000000000..aad2b6d97
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_ES.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_FR.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_FR.dat
new file mode 100644
index 000000000..1141d02eb
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_FR.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_GS.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_GS.dat
new file mode 100644
index 000000000..3438bbd55
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_GS.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_HU.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_HU.dat
new file mode 100644
index 000000000..5c6cf3402
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_HU.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_IT.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_IT.dat
new file mode 100644
index 000000000..cd577914d
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_IT.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_NO.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_NO.dat
new file mode 100644
index 000000000..0917f9e98
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_NO.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_PL.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_PL.dat
new file mode 100644
index 000000000..02e4c0f52
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_PL.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_PT.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_PT.dat
new file mode 100644
index 000000000..4d7952e7a
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_PT.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_RO.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_RO.dat
new file mode 100644
index 000000000..ca1e262f1
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_RO.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_SK.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_SK.dat
new file mode 100644
index 000000000..1bda1791f
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/en_SK.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/en_Shaw.dat b/addons/source-python/packages/site-packages/babel/locale-data/en_Shaw.dat
index 6c3943651..60936ea8a 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/en_Shaw.dat and b/addons/source-python/packages/site-packages/babel/locale-data/en_Shaw.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/fi.dat b/addons/source-python/packages/site-packages/babel/locale-data/fi.dat
index 310cd1c4c..113e510da 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/fi.dat and b/addons/source-python/packages/site-packages/babel/locale-data/fi.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/hi.dat b/addons/source-python/packages/site-packages/babel/locale-data/hi.dat
index b2096453b..f4ae1eaea 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/hi.dat and b/addons/source-python/packages/site-packages/babel/locale-data/hi.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/ht.dat b/addons/source-python/packages/site-packages/babel/locale-data/ht.dat
new file mode 100644
index 000000000..153d09f68
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/ht.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/ht_HT.dat b/addons/source-python/packages/site-packages/babel/locale-data/ht_HT.dat
new file mode 100644
index 000000000..48f311697
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/ht_HT.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/la.dat b/addons/source-python/packages/site-packages/babel/locale-data/la.dat
index 36656dc1b..1ccac42fa 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/la.dat and b/addons/source-python/packages/site-packages/babel/locale-data/la.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/nl.dat b/addons/source-python/packages/site-packages/babel/locale-data/nl.dat
index 9284bd748..ae4d85761 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/nl.dat and b/addons/source-python/packages/site-packages/babel/locale-data/nl.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/root.dat b/addons/source-python/packages/site-packages/babel/locale-data/root.dat
index 0557e0d27..441bbdc9a 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/root.dat and b/addons/source-python/packages/site-packages/babel/locale-data/root.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/syr.dat b/addons/source-python/packages/site-packages/babel/locale-data/syr.dat
index 6cfeef618..3b0a2453a 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/syr.dat and b/addons/source-python/packages/site-packages/babel/locale-data/syr.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/yo_BJ.dat b/addons/source-python/packages/site-packages/babel/locale-data/yo_BJ.dat
index b42399595..b9d79c3e1 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/yo_BJ.dat and b/addons/source-python/packages/site-packages/babel/locale-data/yo_BJ.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_CN.dat b/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_CN.dat
index 1ba619717..6ca866c4c 100644
Binary files a/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_CN.dat and b/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_CN.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_MO.dat b/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_MO.dat
new file mode 100644
index 000000000..fca6b29db
Binary files /dev/null and b/addons/source-python/packages/site-packages/babel/locale-data/yue_Hant_MO.dat differ
diff --git a/addons/source-python/packages/site-packages/babel/localedata.py b/addons/source-python/packages/site-packages/babel/localedata.py
index 59f1db09e..2b225a142 100644
--- a/addons/source-python/packages/site-packages/babel/localedata.py
+++ b/addons/source-python/packages/site-packages/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-2026 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'
]
@@ -125,7 +126,7 @@ def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str
>>> d = load('en_US')
>>> d['languages']['sv']
- u'Swedish'
+ 'Swedish'
Note that the results are cached, and subsequent requests for the same
locale return the same dictionary:
@@ -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/addons/source-python/packages/site-packages/babel/localtime/__init__.py b/addons/source-python/packages/site-packages/babel/localtime/__init__.py
index 854c07496..9eb95ab2e 100644
--- a/addons/source-python/packages/site-packages/babel/localtime/__init__.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
import datetime
diff --git a/addons/source-python/packages/site-packages/babel/localtime/_fallback.py b/addons/source-python/packages/site-packages/babel/localtime/_fallback.py
index fab6867c3..218813905 100644
--- a/addons/source-python/packages/site-packages/babel/localtime/_fallback.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
import datetime
@@ -19,7 +19,6 @@
class _FallbackLocalTimezone(datetime.tzinfo):
-
def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta:
if self._isdst(dt):
return DSTOFFSET
@@ -38,7 +37,7 @@ def tzname(self, dt: datetime.datetime) -> str:
def _isdst(self, dt: datetime.datetime) -> bool:
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
- dt.weekday(), 0, -1)
+ dt.weekday(), 0, -1) # fmt: skip
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
diff --git a/addons/source-python/packages/site-packages/babel/localtime/_unix.py b/addons/source-python/packages/site-packages/babel/localtime/_unix.py
index 782a7d246..70dd2322c 100644
--- a/addons/source-python/packages/site-packages/babel/localtime/_unix.py
+++ b/addons/source-python/packages/site-packages/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/addons/source-python/packages/site-packages/babel/localtime/_win32.py b/addons/source-python/packages/site-packages/babel/localtime/_win32.py
index 1a52567bc..0fb625ba9 100644
--- a/addons/source-python/packages/site-packages/babel/localtime/_win32.py
+++ b/addons/source-python/packages/site-packages/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/addons/source-python/packages/site-packages/babel/messages/__init__.py b/addons/source-python/packages/site-packages/babel/messages/__init__.py
index ca83faa97..8dde3f299 100644
--- a/addons/source-python/packages/site-packages/babel/messages/__init__.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
from babel.messages.catalog import (
diff --git a/addons/source-python/packages/site-packages/babel/messages/catalog.py b/addons/source-python/packages/site-packages/babel/messages/catalog.py
index f84a5bd1b..9a9739a72 100644
--- a/addons/source-python/packages/site-packages/babel/messages/catalog.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import datetime
@@ -23,7 +24,7 @@
from babel.core import Locale, UnknownLocaleError
from babel.dates import format_datetime
from babel.messages.plurals import get_plural
-from babel.util import LOCALTZ, FixedOffsetTimezone, _cmp, distinct
+from babel.util import LOCALTZ, _cmp
if TYPE_CHECKING:
from typing_extensions import TypeAlias
@@ -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 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6):
[hlL]?
)
([diouxXeEfFgGcrs%])
-''', re.VERBOSE)
+''',
+ re.VERBOSE,
+)
def _has_python_brace_format(string: str) -> bool:
@@ -118,7 +124,10 @@ def _parse_datetime_header(value: str) -> datetime.datetime:
net_mins_offset *= plus_minus
# Create an offset object
- tzoffset = FixedOffsetTimezone(net_mins_offset)
+ tzoffset = datetime.timezone(
+ offset=datetime.timedelta(minutes=net_mins_offset),
+ name=f'Etc/GMT{net_mins_offset:+d}',
+ )
# Store the offset in a datetime object
dt = dt.replace(tzinfo=tzoffset)
@@ -161,7 +170,7 @@ def __init__(
if not string and self.pluralizable:
string = ('', '')
self.string = string
- self.locations = list(distinct(locations))
+ self.locations = list(dict.fromkeys(locations)) if locations else []
self.flags = set(flags)
if id and self.python_format:
self.flags.add('python-format')
@@ -171,12 +180,15 @@ def __init__(
self.flags.add('python-brace-format')
else:
self.flags.discard('python-brace-format')
- self.auto_comments = list(distinct(auto_comments))
- self.user_comments = list(distinct(user_comments))
- if isinstance(previous_id, str):
- self.previous_id = [previous_id]
+ self.auto_comments = list(dict.fromkeys(auto_comments)) if auto_comments else []
+ self.user_comments = list(dict.fromkeys(user_comments)) if user_comments else []
+ if previous_id:
+ if isinstance(previous_id, str):
+ self.previous_id = [previous_id]
+ else:
+ self.previous_id = list(previous_id)
else:
- self.previous_id = list(previous_id)
+ self.previous_id = []
self.lineno = lineno
self.context = context
@@ -185,10 +197,12 @@ def __repr__(self) -> str:
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:
@@ -217,10 +231,17 @@ def is_identical(self, other: Message) -> bool:
return self.__dict__ == other.__dict__
def clone(self) -> Message:
- return Message(*map(copy, (self.id, self.string, self.locations,
- self.flags, self.auto_comments,
- self.user_comments, self.previous_id,
- self.lineno, self.context)))
+ return Message(
+ id=copy(self.id),
+ string=copy(self.string),
+ locations=copy(self.locations),
+ flags=copy(self.flags),
+ auto_comments=copy(self.auto_comments),
+ user_comments=copy(self.user_comments),
+ previous_id=copy(self.previous_id),
+ lineno=self.lineno, # immutable (str/None)
+ context=self.context, # immutable (str/None)
+ )
def check(self, catalog: Catalog | None = None) -> list[TranslationError]:
"""Run various validation checks on the message. Some validations
@@ -233,6 +254,7 @@ def check(self, catalog: Catalog | None = None) -> list[TranslationError]:
in a catalog.
"""
from babel.messages.checkers import checkers
+
errors: list[TranslationError] = []
for checker in checkers:
try:
@@ -279,9 +301,12 @@ def python_format(self) -> bool:
:type: `bool`"""
ids = self.id
- if not isinstance(ids, (list, tuple)):
- ids = [ids]
- return any(PYTHON_FORMAT.search(id) for id in ids)
+ if isinstance(ids, (list, tuple)):
+ for id in ids: # Explicit loop for performance reasons.
+ if PYTHON_FORMAT.search(id):
+ return True
+ return False
+ return bool(PYTHON_FORMAT.search(ids))
@property
def python_brace_format(self) -> bool:
@@ -294,9 +319,12 @@ def python_brace_format(self) -> bool:
:type: `bool`"""
ids = self.id
- if not isinstance(ids, (list, tuple)):
- ids = [ids]
- return any(_has_python_brace_format(id) for id in ids)
+ if isinstance(ids, (list, tuple)):
+ for id in ids: # Explicit loop for performance reasons.
+ if _has_python_brace_format(id):
+ return True
+ return False
+ return _has_python_brace_format(ids)
class TranslationError(Exception):
@@ -315,6 +343,7 @@ class TranslationError(Exception):
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())
@@ -420,7 +449,9 @@ def _set_locale(self, locale: Locale | str | None) -> None:
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
@@ -436,11 +467,13 @@ def _get_header_comment(self) -> str:
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
@@ -448,7 +481,10 @@ def _get_header_comment(self) -> str:
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',
@@ -479,11 +515,16 @@ def _set_header_comment(self, string: str | None) -> None:
#
: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
@@ -497,7 +538,7 @@ def _get_mime_headers(self) -> list[tuple[str, str]]:
('POT-Creation-Date', format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', locale='en')),
('PO-Revision-Date', revision_date),
('Last-Translator', self.last_translator),
- ]
+ ] # fmt: skip
if self.locale_identifier:
headers.append(('Language', str(self.locale_identifier)))
headers.append(('Language-Team', language_team))
@@ -547,7 +588,10 @@ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None:
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
@@ -597,7 +641,8 @@ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None:
Generated-By: Babel ...
:type: `list`
- """)
+ """,
+ )
@property
def num_plurals(self) -> int:
@@ -693,19 +738,19 @@ def __setitem__(self, id: _MessageID, message: Message) -> None:
"""Add or update the message with the specified ID.
>>> catalog = Catalog()
- >>> catalog[u'foo'] = Message(u'foo')
- >>> catalog[u'foo']
-
+ >>> catalog['foo'] = Message('foo')
+ >>> catalog['foo']
+
If a message with that ID is already in the catalog, it is updated
to include the locations and flags of the new message.
>>> catalog = Catalog()
- >>> catalog[u'foo'] = Message(u'foo', locations=[('main.py', 1)])
- >>> catalog[u'foo'].locations
+ >>> catalog['foo'] = Message('foo', locations=[('main.py', 1)])
+ >>> catalog['foo'].locations
[('main.py', 1)]
- >>> catalog[u'foo'] = Message(u'foo', locations=[('utils.py', 5)])
- >>> catalog[u'foo'].locations
+ >>> catalog['foo'] = Message('foo', locations=[('utils.py', 5)])
+ >>> catalog['foo'].locations
[('main.py', 1), ('utils.py', 5)]
:param id: the message ID
@@ -719,22 +764,20 @@ def __setitem__(self, id: _MessageID, message: Message) -> None:
# The new message adds pluralization
current.id = message.id
current.string = message.string
- current.locations = list(distinct(current.locations +
- message.locations))
- current.auto_comments = list(distinct(current.auto_comments +
- message.auto_comments))
- current.user_comments = list(distinct(current.user_comments +
- message.user_comments))
+ current.locations = list(dict.fromkeys([*current.locations, *message.locations]))
+ current.auto_comments = list(dict.fromkeys([*current.auto_comments, *message.auto_comments])) # fmt:skip
+ current.user_comments = list(dict.fromkeys([*current.user_comments, *message.user_comments])) # fmt:skip
current.flags |= message.flags
elif id == '':
# special treatment for the header message
self.mime_headers = message_from_string(message.string).items()
- self.header_comment = "\n".join([f"# {c}".rstrip() for c in message.user_comments])
+ self.header_comment = "\n".join(f"# {c}".rstrip() for c in message.user_comments)
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(
@@ -752,10 +795,10 @@ def add(
"""Add or update the message with the specified ID.
>>> catalog = Catalog()
- >>> catalog.add(u'foo')
+ >>> catalog.add('foo')
- >>> catalog[u'foo']
-
+ >>> catalog['foo']
+
This method simply constructs a `Message` object with the given
arguments and invokes `__setitem__` with that object.
@@ -774,9 +817,17 @@ def add(
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
@@ -831,11 +882,11 @@ def update(
>>> template.add(('salad', 'salads'), locations=[('util.py', 42)])
>>> catalog = Catalog(locale='de_DE')
- >>> catalog.add('blue', u'blau', locations=[('main.py', 98)])
+ >>> catalog.add('blue', 'blau', locations=[('main.py', 98)])
- >>> catalog.add('head', u'Kopf', locations=[('util.py', 33)])
+ >>> catalog.add('head', 'Kopf', locations=[('util.py', 33)])
- >>> catalog.add(('salad', 'salads'), (u'Salat', u'Salate'),
+ >>> catalog.add(('salad', 'salads'), ('Salat', 'Salate'),
... locations=[('util.py', 38)])
@@ -850,13 +901,13 @@ def update(
>>> msg2 = catalog['blue']
>>> msg2.string
- u'blau'
+ 'blau'
>>> msg2.locations
[('main.py', 100)]
>>> msg3 = catalog['salad']
>>> msg3.string
- (u'Salat', u'Salate')
+ ('Salat', 'Salate')
>>> msg3.locations
[('util.py', 42)]
@@ -889,7 +940,11 @@ def update(
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:
@@ -906,8 +961,8 @@ def _merge(message: Message, oldkey: tuple[str, str] | str, newkey: tuple[str, s
assert oldmsg is not None
message.string = oldmsg.string
- if keep_user_comments:
- message.user_comments = list(distinct(oldmsg.user_comments))
+ if keep_user_comments and oldmsg.user_comments:
+ message.user_comments = list(dict.fromkeys(oldmsg.user_comments))
if isinstance(message.id, (list, tuple)):
if not isinstance(message.string, (list, tuple)):
@@ -917,7 +972,7 @@ def _merge(message: Message, oldkey: tuple[str, str] | str, newkey: tuple[str, s
)
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]
@@ -971,7 +1026,11 @@ def _to_fuzzy_match_key(self, key: tuple[str, str] | str) -> str:
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.
@@ -991,10 +1050,6 @@ def is_identical(self, other: Catalog) -> bool:
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/addons/source-python/packages/site-packages/babel/messages/checkers.py b/addons/source-python/packages/site-packages/babel/messages/checkers.py
index df7c3ca73..4026ab1b3 100644
--- a/addons/source-python/packages/site-packages/babel/messages/checkers.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
from collections.abc import Callable
@@ -27,8 +28,7 @@ def num_plurals(catalog: Catalog | None, message: Message) -> None:
"""Verify the number of plurals in the translation."""
if not message.pluralizable:
if not isinstance(message.string, str):
- raise TranslationError("Found plural forms for non-pluralizable "
- "message")
+ raise TranslationError("Found plural forms for non-pluralizable message")
return
# skip further tests if no catalog is provided.
@@ -39,8 +39,9 @@ def num_plurals(catalog: Catalog | None, message: Message) -> None:
if not isinstance(msgstrs, (list, tuple)):
msgstrs = (msgstrs,)
if len(msgstrs) != catalog.num_plurals:
- raise TranslationError("Wrong number of plural forms (expected %d)" %
- catalog.num_plurals)
+ raise TranslationError(
+ f"Wrong number of plural forms (expected {catalog.num_plurals})",
+ )
def python_format(catalog: Catalog | None, message: Message) -> None:
@@ -54,9 +55,12 @@ def python_format(catalog: Catalog | None, message: Message) -> None:
if not isinstance(msgstrs, (list, tuple)):
msgstrs = (msgstrs,)
- for msgid, msgstr in zip(msgids, msgstrs):
- if msgstr:
- _validate_format(msgid, msgstr)
+ if msgstrs[0]:
+ _validate_format(msgids[0], msgstrs[0])
+ if message.pluralizable:
+ for msgstr in msgstrs[1:]:
+ if msgstr:
+ _validate_format(msgids[1], msgstr)
def _validate_format(format: str, alternative: str) -> None:
@@ -112,17 +116,20 @@ def _check_positional(results: list[tuple[str, str]]) -> bool:
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, b = map(_parse, (format, alternative))
+ a = _parse(format)
+ b = _parse(alternative)
if not a:
return
# now check if both strings are positional or named
- a_positional, b_positional = map(_check_positional, (a, b))
+ a_positional = _check_positional(a)
+ b_positional = _check_positional(b)
if a_positional and not b_positional and not b:
raise TranslationError('placeholders are incompatible')
elif a_positional != b_positional:
@@ -132,13 +139,13 @@ def _check_positional(results: list[tuple[str, str]]) -> bool:
# same number of format chars and those must be compatible
if a_positional:
if len(a) != len(b):
- raise TranslationError('positional format placeholders are '
- 'unbalanced')
+ raise TranslationError('positional format placeholders are unbalanced')
for idx, ((_, first), (_, second)) in enumerate(zip(a, b)):
if not _compatible(first, second):
- raise TranslationError('incompatible format for placeholder '
- '%d: %r and %r are not compatible' %
- (idx + 1, first, second))
+ raise TranslationError(
+ f'incompatible format for placeholder {idx + 1:d}: '
+ f'{first!r} and {second!r} are not compatible',
+ )
# otherwise the second string must not have names the first one
# doesn't have and the types of those included must be compatible
@@ -156,6 +163,7 @@ def _check_positional(results: list[tuple[str, str]]) -> bool:
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/addons/source-python/packages/site-packages/babel/messages/extract.py b/addons/source-python/packages/site-packages/babel/messages/extract.py
index 7f4230f61..6fad84304 100644
--- a/addons/source-python/packages/site-packages/babel/messages/extract.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import ast
@@ -22,6 +23,7 @@
import os
import sys
import tokenize
+import warnings
from collections.abc import (
Callable,
Collection,
@@ -62,7 +64,7 @@ def tell(self) -> int: ...
_Keyword: TypeAlias = dict[int | None, _SimpleKeyword] | _SimpleKeyword
# 5-tuple of (filename, lineno, messages, comments, context)
- _FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None]
+ _FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None] # fmt: skip
# 4-tuple of (lineno, message, comments, context)
_ExtractionResult: TypeAlias = tuple[int, str | tuple[str, ...], list[str], str | None]
@@ -72,7 +74,7 @@ def tell(self) -> int: ...
_CallableExtractionMethod: TypeAlias = Callable[
[_FileObj | IO[bytes], Mapping[str, _Keyword], Collection[str], Mapping[str, Any]],
Iterable[_ExtractionResult],
- ]
+ ] # fmt: skip
_ExtractionMethod: TypeAlias = _CallableExtractionMethod | str
@@ -86,9 +88,11 @@ def tell(self) -> int: ...
'ungettext': (1, 2),
'dgettext': (2,),
'dngettext': (2, 3),
+ 'dpgettext': ((2, 'c'), 3),
'N_': None,
'pgettext': ((1, 'c'), 2),
'npgettext': ((1, 'c'), 2, 3),
+ 'dnpgettext': ((2, 'c'), 3, 4),
}
DEFAULT_MAPPING: list[tuple[str, str]] = [('**.py', 'python')]
@@ -103,15 +107,45 @@ 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[:] = map(_strip, comments)
+ comments[:] = [_strip(c) for c in comments]
+
+
+def _make_default_directory_filter(
+ method_map: Iterable[tuple[str, str]],
+ root_dir: str | os.PathLike[str],
+):
+ method_map = tuple(method_map)
+
+ def directory_filter(dirpath: str | os.PathLike[str]) -> bool:
+ subdir = os.path.basename(dirpath)
+ # Legacy default behavior: ignore dot and underscore directories
+ if subdir.startswith('.') or subdir.startswith('_'):
+ return False
+
+ dir_rel = os.path.relpath(dirpath, root_dir).replace(os.sep, '/')
+
+ for pattern, method in method_map:
+ if method == "ignore" and pathmatch(pattern, dir_rel):
+ return False
-def default_directory_filter(dirpath: str | os.PathLike[str]) -> bool:
+ return True
+
+ return directory_filter
+
+
+def default_directory_filter(dirpath: str | os.PathLike[str]) -> bool: # pragma: no cover
+ warnings.warn(
+ "`default_directory_filter` is deprecated and will be removed in a future version of Babel.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
subdir = os.path.basename(dirpath)
# Legacy default behavior: ignore dot and underscore directories
return not (subdir.startswith('.') or subdir.startswith('_'))
@@ -198,16 +232,21 @@ def extract_from_dir(
"""
if dirname is None:
dirname = os.getcwd()
+
if options_map is None:
options_map = {}
+
+ dirname = os.path.abspath(dirname)
+
if directory_filter is None:
- directory_filter = default_directory_filter
+ directory_filter = _make_default_directory_filter(
+ method_map=method_map,
+ root_dir=dirname,
+ )
- absname = os.path.abspath(dirname)
- for root, dirnames, filenames in os.walk(absname):
+ for root, dirnames, filenames in os.walk(dirname):
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()
@@ -222,7 +261,7 @@ def extract_from_dir(
keywords,
comment_tags,
strip_comment_tags,
- dirpath=absname,
+ dirpath=dirname,
)
@@ -277,12 +316,31 @@ def check_and_call_extract_file(
if pathmatch(opattern, filename):
options = odict
break
+
+ # Merge keywords and comment_tags from per-format options if present.
+ file_keywords = keywords
+ file_comment_tags = comment_tags
+ if keywords_opt := options.get("keywords"):
+ if not isinstance(keywords_opt, dict): # pragma: no cover
+ raise TypeError(
+ f"The `keywords` option must be a dict of parsed keywords, not {keywords_opt!r}",
+ )
+ file_keywords = {**keywords, **keywords_opt}
+
+ if comments_opt := options.get("add_comments"):
+ if not isinstance(comments_opt, (list, tuple, set)): # pragma: no cover
+ raise TypeError(
+ f"The `add_comments` option must be a collection of comment tags, not {comments_opt!r}.",
+ )
+ file_comment_tags = tuple(set(comment_tags) | set(comments_opt))
+
if callback:
callback(filename, method, options)
for message_tuple in extract_from_file(
- method, filepath,
- keywords=keywords,
- comment_tags=comment_tags,
+ method,
+ filepath,
+ keywords=file_keywords,
+ comment_tags=file_comment_tags,
options=options,
strip_comment_tags=strip_comment_tags,
):
@@ -321,8 +379,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 +416,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",
@@ -403,7 +462,7 @@ def extract(
>>> from io import BytesIO
>>> for message in extract('python', BytesIO(source)):
... print(message)
- (3, u'Hello, world!', [], None)
+ (3, 'Hello, world!', [], None)
:param method: an extraction method (a callable), or
a string specifying the extraction method (.e.g. "python");
@@ -431,7 +490,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 +504,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)):
@@ -508,7 +566,7 @@ def extract_python(
:rtype: ``iterator``
"""
funcname = lineno = message_lineno = None
- call_stack = -1
+ call_stack = [] # line numbers of calls
buf = []
messages = []
translator_comments = []
@@ -526,7 +584,7 @@ def extract_python(
current_fstring_start = None
for tok, value, (lineno, _), _, _ in tokens:
- if call_stack == -1 and tok == NAME and value in ('def', 'class'):
+ if not call_stack and tok == NAME and value in ('def', 'class'):
in_def = True
elif tok == OP and value == '(':
if in_def:
@@ -535,16 +593,15 @@ def extract_python(
in_def = False
continue
if funcname:
- call_stack += 1
+ call_stack.append(lineno)
elif in_def and tok == OP and value == ':':
# End of a class definition without parens
in_def = False
continue
- elif call_stack == -1 and tok == COMMENT:
+ 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
@@ -555,8 +612,8 @@ def extract_python(
in_translator_comments = True
translator_comments.append((lineno, value))
break
- elif funcname and call_stack == 0:
- nested = (tok == NAME and value in keywords)
+ elif funcname and len(call_stack) == 1:
+ nested = tok == NAME and value in keywords
if (tok == OP and value == ')') or nested:
if buf:
messages.append(''.join(buf))
@@ -565,17 +622,24 @@ def extract_python(
messages.append(None)
messages = tuple(messages) if len(messages) > 1 else messages[0]
- # Comments don't apply unless they immediately
- # precede the message
- if translator_comments and \
- translator_comments[-1][0] < message_lineno - 1:
- translator_comments = []
- yield (message_lineno, funcname, messages,
- [comment[1] for comment in translator_comments])
+ if translator_comments:
+ last_comment_lineno = translator_comments[-1][0]
+ if last_comment_lineno < min(message_lineno, call_stack[-1]) - 1:
+ # Comments don't apply unless they immediately
+ # precede the message, or the line where the parenthesis token
+ # to start this message's translation call is.
+ translator_comments.clear()
+
+ yield (
+ message_lineno,
+ funcname,
+ messages,
+ [comment[1] for comment in translator_comments],
+ )
funcname = lineno = message_lineno = None
- call_stack = -1
+ call_stack.clear()
messages = []
translator_comments = []
in_translator_comments = False
@@ -619,9 +683,9 @@ def extract_python(
elif tok != NL and not message_lineno:
message_lineno = lineno
- elif call_stack > 0 and tok == OP and value == ')':
- call_stack -= 1
- elif funcname and call_stack == -1:
+ elif len(call_stack) > 1 and tok == OP and value == ')':
+ call_stack.pop()
+ elif funcname and not call_stack:
funcname = None
elif tok == NAME and value in keywords:
funcname = value
@@ -679,6 +743,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
@@ -696,17 +761,30 @@ def extract_javascript(
lineno=lineno,
):
if ( # Turn keyword`foo` expressions into keyword("foo") calls:
- funcname and # have a keyword...
- (last_token and last_token.type == 'name') and # we've seen nothing after the keyword...
- token.type == 'template_string' # this is a template string
+ # have a keyword...
+ funcname
+ # and we've seen nothing after the keyword...
+ and (last_token and last_token.type == 'name')
+ # and this is a template string
+ and token.type == 'template_string'
):
message_lineno = token.lineno
messages = [unquote_string(token.value)]
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:
@@ -715,8 +793,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
@@ -736,8 +813,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:
@@ -753,13 +829,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
@@ -786,17 +865,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
@@ -820,6 +904,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
@@ -839,7 +924,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/addons/source-python/packages/site-packages/babel/messages/frontend.py b/addons/source-python/packages/site-packages/babel/messages/frontend.py
index 29e5a2aa2..f63dd9ded 100644
--- a/addons/source-python/packages/site-packages/babel/messages/frontend.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
@@ -15,6 +15,7 @@
import logging
import optparse
import os
+import pathlib
import re
import shutil
import sys
@@ -22,7 +23,7 @@
import warnings
from configparser import RawConfigParser
from io import StringIO
-from typing import BinaryIO, Iterable, Literal
+from typing import Any, BinaryIO, Iterable, Literal
from babel import Locale, localedata
from babel import __version__ as VERSION
@@ -173,7 +174,7 @@ class CompileCatalog(CommandMixin):
'also include fuzzy translations'),
('statistics', None,
'print statistics about translations'),
- ]
+ ] # fmt: skip
boolean_options = ['use-fuzzy', 'statistics']
def initialize_options(self):
@@ -199,46 +200,38 @@ def run(self):
n_errors += len(errors)
if n_errors:
self.log.error('%d errors encountered.', n_errors)
- return (1 if n_errors else 0)
-
- def _run_domain(self, domain):
- po_files = []
- mo_files = []
+ return 1 if n_errors else 0
+ def _get_po_mo_triples(self, domain: str):
if not self.input_file:
+ dir_path = pathlib.Path(self.directory)
if self.locale:
- po_files.append((self.locale,
- os.path.join(self.directory, self.locale,
- 'LC_MESSAGES',
- f"{domain}.po")))
- mo_files.append(os.path.join(self.directory, self.locale,
- 'LC_MESSAGES',
- f"{domain}.mo"))
+ lc_messages_path = dir_path / self.locale / "LC_MESSAGES"
+ po_file = lc_messages_path / f"{domain}.po"
+ yield self.locale, po_file, po_file.with_suffix(".mo")
else:
- for locale in os.listdir(self.directory):
- po_file = os.path.join(self.directory, locale,
- 'LC_MESSAGES', f"{domain}.po")
- if os.path.exists(po_file):
- po_files.append((locale, po_file))
- mo_files.append(os.path.join(self.directory, locale,
- 'LC_MESSAGES',
- f"{domain}.mo"))
+ for locale_path in dir_path.iterdir():
+ 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:
- po_files.append((self.locale, self.input_file))
+ po_file = pathlib.Path(self.input_file)
if self.output_file:
- mo_files.append(self.output_file)
+ mo_file = pathlib.Path(self.output_file)
else:
- mo_files.append(os.path.join(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
- if not po_files:
- raise OptionError('no message catalogs found')
+ def _run_domain(self, domain):
+ locale_po_mo_triples = list(self._get_po_mo_triples(domain))
+ if not locale_po_mo_triples:
+ raise OptionError(f'no message catalogs found for domain {domain!r}')
catalogs_and_errors = {}
- for idx, (locale, po_file) in enumerate(po_files):
- mo_file = mo_files[idx]
+ for locale, po_file, mo_file in locale_po_mo_triples:
with open(po_file, 'rb') as infile:
catalog = read_po(infile, locale)
@@ -252,7 +245,10 @@ def _run_domain(self, domain):
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:
@@ -262,9 +258,7 @@ def _run_domain(self, domain):
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)
@@ -282,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
@@ -347,10 +339,15 @@ class ExtractMessages(CommandMixin):
'header comment for the catalog'),
('last-translator=', None,
'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 = (
@@ -435,10 +432,9 @@ def finalize_options(self):
if isinstance(self.input_paths, str):
self.input_paths = re.split(r',\s*', self.input_paths)
elif self.distribution is not None:
- self.input_paths = dict.fromkeys([
- k.split('.', 1)[0]
- for k in (self.distribution.packages or ())
- ]).keys()
+ self.input_paths = list(
+ {k.split('.', 1)[0] for k in (self.distribution.packages or ())},
+ )
else:
self.input_paths = []
@@ -493,31 +489,40 @@ def callback(filename: str, method: str, options: dict):
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)
if os.path.isfile(path):
current_dir = os.getcwd()
extracted = check_and_call_extract_file(
- path, method_map, options_map,
- callback, self.keywords, self.add_comments,
- self.strip_comments, current_dir,
+ path,
+ method_map,
+ options_map,
+ callback=callback,
+ comment_tags=self.add_comments,
+ dirpath=current_dir,
+ keywords=self.keywords,
+ strip_comment_tags=self.strip_comments,
)
else:
extracted = extract_from_dir(
- path, method_map, options_map,
- keywords=self.keywords,
- comment_tags=self.add_comments,
+ path,
+ method_map,
+ options_map,
callback=callback,
- strip_comment_tags=self.strip_comments,
+ comment_tags=self.add_comments,
directory_filter=self.directory_filter,
+ keywords=self.keywords,
+ strip_comment_tags=self.strip_comments,
)
for filename, lineno, message, comments, context in extracted:
if os.path.isfile(path):
@@ -525,16 +530,25 @@ def run(self):
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(outfile, catalog, width=self.width,
- no_location=self.no_location,
- omit_header=self.omit_header,
- sort_output=self.sort_output,
- sort_by_file=self.sort_by_file,
- include_lineno=self.include_lineno)
+ write_po(
+ outfile,
+ catalog,
+ include_lineno=self.include_lineno,
+ no_location=self.no_location,
+ omit_header=self.omit_header,
+ sort_by_file=self.sort_by_file,
+ sort_output=self.sort_output,
+ width=self.width,
+ )
def _get_mappings(self):
mappings = []
@@ -554,7 +568,10 @@ def _get_mappings(self):
)
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))
@@ -567,7 +584,7 @@ def _get_mappings(self):
method_map, options_map = [], {}
for pattern, method, options in mapping:
method_map.append((pattern, method))
- options_map[pattern] = options or {}
+ options_map[pattern] = _parse_string_options(options or {})
mappings.append((path, method_map, options_map))
else:
@@ -577,6 +594,23 @@ def _get_mappings(self):
return mappings
+def _init_catalog(*, input_file, output_file, locale: Locale, width: int) -> None:
+ with open(input_file, 'rb') as infile:
+ # Although reading from the catalog template, read_po must be fed
+ # the locale in order to correctly calculate plurals
+ catalog = read_po(infile, locale=locale)
+
+ catalog.locale = locale
+ catalog.revision_date = datetime.datetime.now(LOCALTZ)
+ catalog.fuzzy = False
+
+ if dirname := os.path.dirname(output_file):
+ os.makedirs(dirname, exist_ok=True)
+
+ with open(output_file, 'wb') as outfile:
+ write_po(outfile, catalog, width=width)
+
+
class InitCatalog(CommandMixin):
description = 'create a new catalog based on a POT file'
user_options = [
@@ -596,7 +630,7 @@ class InitCatalog(CommandMixin):
('no-wrap', None,
'do not break long message lines, longer than the output line width, '
'into several lines'),
- ]
+ ] # fmt: skip
boolean_options = ['no-wrap']
def initialize_options(self):
@@ -622,11 +656,9 @@ def finalize_options(self):
if not self.output_file and not self.output_dir:
raise OptionError('you must specify the output directory')
if not self.output_file:
- self.output_file = os.path.join(self.output_dir, self.locale,
- 'LC_MESSAGES', f"{self.domain}.po")
+ lc_messages_path = pathlib.Path(self.output_dir) / self.locale / "LC_MESSAGES"
+ self.output_file = str(lc_messages_path / f"{self.domain}.po")
- if not os.path.exists(os.path.dirname(self.output_file)):
- os.makedirs(os.path.dirname(self.output_file))
if self.no_wrap and self.width:
raise OptionError("'--no-wrap' and '--width' are mutually exclusive")
if not self.no_wrap and not self.width:
@@ -636,20 +668,16 @@ def finalize_options(self):
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,
+ )
+ _init_catalog(
+ input_file=self.input_file,
+ output_file=self.output_file,
+ locale=self._locale,
+ width=self.width,
)
-
- with open(self.input_file, 'rb') as infile:
- # Although reading from the catalog template, read_po must be fed
- # the locale in order to correctly calculate plurals
- catalog = read_po(infile, locale=self.locale)
-
- catalog.locale = self._locale
- catalog.revision_date = datetime.datetime.now(LOCALTZ)
- catalog.fuzzy = False
-
- with open(self.output_file, 'wb') as outfile:
- write_po(outfile, catalog, width=self.width)
class UpdateCatalog(CommandMixin):
@@ -689,11 +717,17 @@ class UpdateCatalog(CommandMixin):
'would be updated'),
('ignore-pot-creation-date=', None,
'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):
@@ -724,8 +758,7 @@ def finalize_options(self):
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:
@@ -744,75 +777,77 @@ def finalize_options(self):
if self.no_fuzzy_matching and self.previous:
self.previous = False
- def run(self):
- check_status = {}
- po_files = []
+ def _get_locale_po_file_tuples(self):
if not self.output_file:
+ output_path = pathlib.Path(self.output_dir)
if self.locale:
- po_files.append((self.locale,
- os.path.join(self.output_dir, self.locale,
- 'LC_MESSAGES',
- f"{self.domain}.po")))
+ lc_messages_path = output_path / self.locale / "LC_MESSAGES"
+ yield self.locale, str(lc_messages_path / f"{self.domain}.po")
else:
- for locale in os.listdir(self.output_dir):
- po_file = os.path.join(self.output_dir, locale,
- 'LC_MESSAGES',
- f"{self.domain}.po")
- if os.path.exists(po_file):
- po_files.append((locale, po_file))
+ for locale_path in output_path.iterdir():
+ po_file = locale_path / "LC_MESSAGES" / f"{self.domain}.po"
+ if po_file.exists():
+ yield locale_path.stem, po_file
else:
- po_files.append((self.locale, self.output_file))
-
- if not po_files:
- raise OptionError('no message catalogs found')
+ yield self.locale, self.output_file
+ def run(self):
domain = self.domain
if not domain:
domain = os.path.splitext(os.path.basename(self.input_file))[0]
+ check_status = {}
+ locale_po_file_tuples = list(self._get_locale_po_file_tuples())
+
+ if not locale_po_file_tuples:
+ raise OptionError(f'no message catalogs found for domain {domain!r}')
+
with open(self.input_file, 'rb') as infile:
template = read_po(infile)
- for locale, filename in po_files:
+ for locale, filename in locale_po_file_tuples:
if self.init_missing and not os.path.exists(filename):
if self.check:
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:
- # Although reading from the catalog template, read_po must
- # be fed the locale in order to correctly calculate plurals
- catalog = read_po(infile, locale=self.locale)
-
- catalog.locale = self._locale
- catalog.revision_date = datetime.datetime.now(LOCALTZ)
- catalog.fuzzy = False
-
- with open(filename, 'wb') as outfile:
- write_po(outfile, catalog)
+ _init_catalog(
+ input_file=self.input_file,
+ output_file=filename,
+ locale=self._locale,
+ width=self.width,
+ )
self.log.info('updating catalog %s based on %s', filename, self.input_file)
with open(filename, 'rb') as infile:
catalog = read_po(infile, locale=locale, domain=domain)
catalog.update(
- template, self.no_fuzzy_matching,
+ template,
+ no_fuzzy_matching=self.no_fuzzy_matching,
update_header_comment=self.update_header_comment,
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(tmpfile, catalog,
- omit_header=self.omit_header,
- ignore_obsolete=self.ignore_obsolete,
- include_previous=self.previous, width=self.width)
+ write_po(
+ tmpfile,
+ catalog,
+ ignore_obsolete=self.ignore_obsolete,
+ include_previous=self.previous,
+ omit_header=self.omit_header,
+ width=self.width,
+ )
except Exception:
os.remove(tmpname)
raise
@@ -886,19 +921,34 @@ def run(self, argv=None):
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('--list-locales', dest='list_locales',
- action='store_true',
- help="print all known locales and exit")
- self.parser.add_option('-v', '--verbose', action='store_const',
- dest='loglevel', const=logging.DEBUG,
- help='print as much as possible')
- self.parser.add_option('-q', '--quiet', action='store_const',
- dest='loglevel', const=logging.ERROR,
- help='print as little as possible')
+ self.parser.add_option(
+ "--list-locales",
+ dest="list_locales",
+ action="store_true",
+ help="print all known locales and exit",
+ )
+ self.parser.add_option(
+ "-v",
+ "--verbose",
+ action="store_const",
+ dest="loglevel",
+ const=logging.DEBUG,
+ help="print as much as possible",
+ )
+ self.parser.add_option(
+ "-q",
+ "--quiet",
+ action="store_const",
+ dest="loglevel",
+ const=logging.ERROR,
+ help="print as little as possible",
+ )
self.parser.set_defaults(list_locales=False, loglevel=logging.INFO)
options, args = self.parser.parse_args(argv[1:])
@@ -913,8 +963,10 @@ def run(self, argv=None):
return 0
if not args:
- self.parser.error('no valid command or option passed. '
- 'Try the -h/--help option for more information.')
+ self.parser.error(
+ "no valid command or option passed. "
+ "Try the -h/--help option for more information.",
+ )
cmdname = args[0]
if cmdname not in self.commands:
@@ -1027,7 +1079,7 @@ def parse_mapping_cfg(fileobj, filename=None):
else:
method, pattern = (part.strip() for part in section.split(':', 1))
method_map.append((pattern, method))
- options_map[pattern] = dict(parser.items(section))
+ options_map[pattern] = _parse_string_options(dict(parser.items(section)))
if extractors:
for idx, (pattern, method) in enumerate(method_map):
@@ -1038,6 +1090,25 @@ def parse_mapping_cfg(fileobj, filename=None):
return method_map, options_map
+def _parse_string_options(options: dict[str, str]) -> dict[str, Any]:
+ """
+ Parse string-formatted options from a mapping configuration.
+
+ The `keywords` and `add_comments` options are parsed into a canonical
+ internal format, so they can be merged with global keywords/comment tags
+ during extraction.
+ """
+ options: dict[str, Any] = options.copy()
+
+ if keywords_val := options.pop("keywords", None):
+ options['keywords'] = parse_keywords(listify_value(keywords_val))
+
+ if comments_val := options.pop("add_comments", None):
+ options['add_comments'] = listify_value(comments_val)
+
+ return options
+
+
def _parse_config_object(config: dict, *, filename="(unknown)"):
extractors = {}
method_map = []
@@ -1045,40 +1116,78 @@ 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]
+ if keywords_val := entry.pop("keywords", None):
+ if isinstance(keywords_val, str):
+ entry["keywords"] = parse_keywords(listify_value(keywords_val))
+ elif isinstance(keywords_val, list):
+ entry["keywords"] = parse_keywords(keywords_val)
+ else:
+ raise ConfigurationError(
+ f"{filename}: mappings[{idx}]: 'keywords' must be a string or list, got {keywords_val!r}",
+ )
+
+ if comments_val := entry.pop("add_comments", None):
+ if isinstance(comments_val, str):
+ entry["add_comments"] = [comments_val]
+ elif isinstance(comments_val, list):
+ entry["add_comments"] = comments_val
+ else:
+ raise ConfigurationError(
+ f"{filename}: mappings[{idx}]: 'add_comments' must be a string or list, got {comments_val!r}",
+ )
+
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
@@ -1115,11 +1224,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}")
@@ -1190,7 +1303,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/addons/source-python/packages/site-packages/babel/messages/jslexer.py b/addons/source-python/packages/site-packages/babel/messages/jslexer.py
index 5fc4956fd..d751b58f7 100644
--- a/addons/source-python/packages/site-packages/babel/messages/jslexer.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import re
@@ -19,7 +20,7 @@
'+=', '-=', '*=', '%=', '<<', '>>', '>>>', '<<=', '>>=',
'>>>=', '&', '&=', '|', '|=', '&&', '||', '^', '^=', '(', ')',
'[', ']', '{', '}', '!', '--', '++', '~', ',', ';', '.', ':',
-], key=len, reverse=True)
+], key=len, reverse=True) # fmt: skip
escapes: dict[str, str] = {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}
@@ -53,16 +54,20 @@ class Token(NamedTuple):
(0x[a-fA-F0-9]+)
)''', re.VERBOSE)),
('jsx_tag', re.compile(r'(?:?[^>\s]+|/>)', re.I)), # May be mangled in `get_rules`
- ('operator', re.compile(r'(%s)' % '|'.join(map(re.escape, operators)))),
+ ('operator', re.compile(r'(%s)' % '|'.join(re.escape(op) for op in operators))),
('template_string', re.compile(r'''`(?:[^`\\]*(?:\\.[^`\\]*)*)`''', re.UNICODE)),
('string', re.compile(r'''(
'(?:[^'\\]*(?:\\.[^'\\]*)*)' |
"(?:[^"\\]*(?:\\.[^"\\]*)*)"
)''', re.VERBOSE | re.DOTALL)),
-]
+] # 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/addons/source-python/packages/site-packages/babel/messages/mofile.py b/addons/source-python/packages/site-packages/babel/messages/mofile.py
index 3c9fefc4a..1a6fedfcb 100644
--- a/addons/source-python/packages/site-packages/babel/messages/mofile.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import array
@@ -18,8 +19,8 @@
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]
@@ -116,7 +117,7 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool =
>>> catalog = Catalog(locale='en_US')
>>> catalog.add('foo', 'Voh')
- >>> catalog.add((u'bar', u'baz'), (u'Bahr', u'Batz'))
+ >>> catalog.add(('bar', 'baz'), ('Bahr', 'Batz'))
>>> catalog.add('fuz', 'Futz', flags=['fuzzy'])
@@ -133,19 +134,19 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool =
... translations.ugettext = translations.gettext
... translations.ungettext = translations.ngettext
>>> translations.ugettext('foo')
- u'Voh'
+ 'Voh'
>>> translations.ungettext('bar', 'baz', 1)
- u'Bahr'
+ 'Bahr'
>>> translations.ungettext('bar', 'baz', 2)
- u'Batz'
+ 'Batz'
>>> translations.ugettext('fuz')
- u'fuz'
+ 'fuz'
>>> translations.ugettext('Fizz')
- u'Fizz'
+ 'Fizz'
>>> translations.ugettext('Fuzz')
- u'Fuzz'
+ 'Fuzz'
>>> translations.ugettext('Fuzzes')
- u'Fuzzes'
+ 'Fuzzes'
:param fileobj: the file-like object to write to
:param catalog: the `Catalog` instance
@@ -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''
@@ -164,24 +164,19 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool =
# For each string, we need size and file offset. Each string is NUL
# terminated; the NUL does not count into the size.
if message.pluralizable:
- msgid = b'\x00'.join([
- msgid.encode(catalog.charset) for msgid in message.id
- ])
+ msgid = b'\x00'.join(msgid.encode(catalog.charset) for msgid in message.id)
msgstrs = []
for idx, string in enumerate(message.string):
if not string:
msgstrs.append(message.id[min(int(idx), 1)])
else:
msgstrs.append(string)
- msgstr = b'\x00'.join([
- msgstr.encode(catalog.charset) for msgstr in msgstrs
- ])
+ msgstr = b'\x00'.join(msgstr.encode(catalog.charset) for msgstr in msgstrs)
else:
msgid = message.id.encode(catalog.charset)
msgstr = message.string.encode(catalog.charset)
if message.context:
- msgid = b'\x04'.join([message.context.encode(catalog.charset),
- msgid])
+ msgid = b'\x04'.join([message.context.encode(catalog.charset), msgid])
offsets.append((len(ids), len(msgid), len(strs), len(msgstr)))
ids += msgid + b'\x00'
strs += msgstr + b'\x00'
@@ -200,11 +195,15 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool =
voffsets += [l2, o2 + valuestart]
offsets = koffsets + voffsets
- fileobj.write(struct.pack('Iiiiiii',
- LE_MAGIC, # magic
- 0, # version
- len(messages), # number of entries
- 7 * 4, # start of key index
- 7 * 4 + len(messages) * 8, # start of value index
- 0, 0, # size and offset of hash table
- ) + array.array.tobytes(array.array("i", offsets)) + ids + strs)
+ header = struct.pack(
+ 'Iiiiiii',
+ LE_MAGIC, # magic
+ 0, # version
+ len(messages), # number of entries
+ 7 * 4, # start of key index
+ 7 * 4 + len(messages) * 8, # start of value index
+ 0,
+ 0, # size and offset of hash table
+ )
+
+ fileobj.write(header + array.array.tobytes(array.array("i", offsets)) + ids + strs)
diff --git a/addons/source-python/packages/site-packages/babel/messages/plurals.py b/addons/source-python/packages/site-packages/babel/messages/plurals.py
index da336a7ba..a66fdfe41 100644
--- a/addons/source-python/packages/site-packages/babel/messages/plurals.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
from babel.core import Locale, default_locale
@@ -197,7 +198,7 @@
'xh': (2, '(n != 1)'),
# Chinese - From Pootle's PO's (modified)
'zh': (1, '0'),
-}
+} # fmt: skip
DEFAULT_PLURAL: tuple[int, str] = (2, '(n != 1)')
diff --git a/addons/source-python/packages/site-packages/babel/messages/pofile.py b/addons/source-python/packages/site-packages/babel/messages/pofile.py
index 2bb0c7741..b9678a924 100644
--- a/addons/source-python/packages/site-packages/babel/messages/pofile.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import os
@@ -17,7 +18,7 @@
from babel.core import Locale
from babel.messages.catalog import Catalog, Message
-from babel.util import TextWrapper, _cmp
+from babel.util import TextWrapper
if TYPE_CHECKING:
from typing import IO, AnyStr
@@ -25,6 +26,9 @@
from _typeshed import SupportsWrite
+_unescape_re = re.compile(r'\\([\\trn"])')
+
+
def unescape(string: str) -> str:
r"""Reverse `escape` the given string.
@@ -35,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':
@@ -45,7 +50,10 @@ def replace_escapes(match):
return '\r'
# m is \ or "
return m
- return re.compile(r'\\([\\trn"])').sub(replace_escapes, string[1:-1])
+
+ if "\\" not in string: # Fast path: there's nothing to unescape
+ return string[1:-1]
+ return _unescape_re.sub(replace_escapes, string[1:-1])
def denormalize(string: str) -> str:
@@ -73,8 +81,7 @@ def denormalize(string: str) -> str:
escaped_lines = string.splitlines()
if string.startswith('""'):
escaped_lines = escaped_lines[1:]
- lines = map(unescape, escaped_lines)
- return ''.join(lines)
+ return ''.join(map(unescape, escaped_lines))
else:
return unescape(string)
@@ -95,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 == " ":
@@ -116,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
@@ -133,48 +146,14 @@ def __init__(self, message: str, catalog: Catalog, line: str, lineno: int) -> No
self.lineno = lineno
-class _NormalizedString:
-
+class _NormalizedString(list):
def __init__(self, *args: str) -> None:
- self._strs: list[str] = []
- for arg in args:
- self.append(arg)
-
- def append(self, s: str) -> None:
- self._strs.append(s.strip())
+ super().__init__(map(str.strip, args))
def denormalize(self) -> str:
- return ''.join(map(unescape, self._strs))
-
- def __bool__(self) -> bool:
- return bool(self._strs)
-
- def __repr__(self) -> str:
- return os.linesep.join(self._strs)
-
- def __cmp__(self, other: object) -> int:
- if not other:
- return 1
-
- return _cmp(str(self), str(other))
-
- def __gt__(self, other: object) -> bool:
- return self.__cmp__(other) > 0
-
- def __lt__(self, other: object) -> bool:
- return self.__cmp__(other) < 0
-
- def __ge__(self, other: object) -> bool:
- return self.__cmp__(other) >= 0
-
- def __le__(self, other: object) -> bool:
- return self.__cmp__(other) <= 0
-
- def __eq__(self, other: object) -> bool:
- return self.__cmp__(other) == 0
-
- def __ne__(self, other: object) -> bool:
- return self.__cmp__(other) != 0
+ if not self:
+ return ""
+ return ''.join(map(unescape, self))
class PoFileParser:
@@ -184,14 +163,12 @@ class PoFileParser:
See `read_po` for simple cases.
"""
- _keywords = [
- 'msgid',
- 'msgstr',
- 'msgctxt',
- 'msgid_plural',
- ]
-
- 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
@@ -217,25 +194,33 @@ def _add_message(self) -> None:
Add a message to the catalog based on the current parser state and
clear the state ready to process the next message.
"""
- self.translations.sort()
if len(self.messages) > 1:
msgid = tuple(m.denormalize() for m in self.messages)
- else:
- msgid = self.messages[0].denormalize()
- if isinstance(msgid, (list, tuple)):
string = ['' for _ in range(self.catalog.num_plurals)]
- for idx, translation in self.translations:
+ 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)
else:
+ msgid = self.messages[0].denormalize()
string = self.translations[0][1].denormalize()
msgctxt = self.context.denormalize() if self.context else None
- message = Message(msgid, string, list(self.locations), set(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
@@ -247,28 +232,24 @@ def _add_message(self) -> None:
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.translations.append([0, _NormalizedString("")])
+ self._invalid_pofile(
+ "",
+ self.offset,
+ f"missing msgstr for msgid '{self.messages[0].denormalize()}'",
+ )
+ self.translations.append([0, _NormalizedString()])
self._add_message()
def _process_message_line(self, lineno, line, obsolete=False) -> None:
- if line.startswith('"'):
+ if not line:
+ return
+ if line[0] == '"':
self._process_string_continuation_line(line, lineno)
else:
self._process_keyword_line(lineno, line, obsolete)
def _process_keyword_line(self, lineno, line, obsolete=False) -> None:
-
- for keyword in self._keywords:
- try:
- if line.startswith(keyword) and line[len(keyword)] in [' ', '[']:
- arg = line[len(keyword):]
- break
- except IndexError:
- self._invalid_pofile(line, lineno, "Keyword must be followed by a string")
- else:
- self._invalid_pofile(line, lineno, "Start of line didn't match any expected keyword.")
- return
+ keyword, _, arg = line.partition(' ')
if keyword in ['msgid', 'msgctxt']:
self._finish_current_message()
@@ -284,19 +265,23 @@ def _process_keyword_line(self, lineno, line, obsolete=False) -> None:
self.in_msgctxt = False
self.in_msgid = True
self.messages.append(_NormalizedString(arg))
+ return
+
+ if keyword == 'msgctxt':
+ self.in_msgctxt = True
+ self.context = _NormalizedString(arg)
+ return
- elif keyword == 'msgstr':
+ if keyword == 'msgstr' or keyword.startswith('msgstr['):
self.in_msgid = False
self.in_msgstr = True
- if arg.startswith('['):
- idx, msg = arg[1:].split(']', 1)
- self.translations.append([int(idx), _NormalizedString(msg)])
- else:
- self.translations.append([0, _NormalizedString(arg)])
+ kwarg, has_bracket, idxarg = keyword.partition('[')
+ idx = int(idxarg[:-1]) if has_bracket else 0
+ s = _NormalizedString(arg) if arg != '""' else _NormalizedString()
+ self.translations.append([idx, s])
+ return
- elif keyword == 'msgctxt':
- self.in_msgctxt = True
- self.context = _NormalizedString(arg)
+ self._invalid_pofile(line, lineno, "Unknown or misformatted keyword")
def _process_string_continuation_line(self, line, lineno) -> None:
if self.in_msgid:
@@ -306,51 +291,68 @@ def _process_string_continuation_line(self, line, lineno) -> None:
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
- s.append(line)
+ # For performance reasons, `NormalizedString` doesn't strip internally
+ s.append(line.strip())
def _process_comment(self, line) -> None:
-
self._finish_current_message()
- if line[1:].startswith(':'):
+ prefix = line[:2]
+ if prefix == '#:':
for location in _extract_locations(line[2:]):
- pos = location.rfind(':')
- if pos >= 0:
+ a, colon, b = location.rpartition(':')
+ if colon:
try:
- lineno = int(location[pos + 1:])
+ self.locations.append((a, int(b)))
except ValueError:
continue
- self.locations.append((location[:pos], lineno))
- else:
+ else: # No line number specified
self.locations.append((location, None))
- elif line[1:].startswith(','):
- for flag in line[2:].lstrip().split(','):
- self.flags.append(flag.strip())
- elif line[1:].startswith('.'):
+ return
+
+ if prefix == '#,':
+ self.flags.extend(flag.strip() for flag in line[2:].lstrip().split(','))
+ return
+
+ if prefix == '#.':
# These are called auto-comments
comment = line[2:].strip()
if comment: # Just check that we're not adding empty comments
self.auto_comments.append(comment)
- else:
- # These are called user comments
- self.user_comments.append(line[1:].strip())
+ return
+
+ # These are called user comments
+ self.user_comments.append(line[1:].strip())
def parse(self, fileobj: IO[AnyStr] | Iterable[AnyStr]) -> None:
"""
- Reads from the file-like object `fileobj` and adds any po file
- units found in it to the `Catalog` supplied to the constructor.
+ Reads from the file-like object (or iterable of string-likes) `fileobj`
+ and adds any po file units found in it to the `Catalog`
+ supplied to the constructor.
+
+ All of the items in the iterable must be the same type; either `str`
+ or `bytes` (decoded with the catalog charset), but not a mixture.
"""
+ needs_decode = None
for lineno, line in enumerate(fileobj):
line = line.strip()
- if not isinstance(line, str):
- line = line.decode(self.catalog.charset)
+ if needs_decode is None:
+ # If we don't yet know whether we need to decode,
+ # let's find out now.
+ needs_decode = not isinstance(line, str)
if not line:
continue
- if line.startswith('#'):
- if line[1:].startswith('~'):
+ if needs_decode:
+ line = line.decode(self.catalog.charset)
+ if line[0] == '#':
+ if line[:2] == '#~':
self._process_message_line(lineno, line[2:].lstrip(), obsolete=True)
else:
try:
@@ -365,8 +367,8 @@ def parse(self, fileobj: IO[AnyStr] | Iterable[AnyStr]) -> None:
# No actual messages found, but there was some info in comments, from which
# we'll construct an empty header message
if not self.counter and (self.flags or self.user_comments or self.auto_comments):
- self.messages.append(_NormalizedString('""'))
- self.translations.append([0, _NormalizedString('""')])
+ self.messages.append(_NormalizedString())
+ self.translations.append([0, _NormalizedString()])
self._add_message()
def _invalid_pofile(self, line, lineno, msg) -> None:
@@ -412,12 +414,12 @@ def read_po(
... print((message.id, message.string))
... print(' ', (message.locations, sorted(list(message.flags))))
... print(' ', (message.user_comments, message.auto_comments))
- (u'foo %(name)s', u'quux %(name)s')
- ([(u'main.py', 1)], [u'fuzzy', u'python-format'])
+ ('foo %(name)s', 'quux %(name)s')
+ ([('main.py', 1)], ['fuzzy', 'python-format'])
([], [])
- ((u'bar', u'baz'), (u'bar', u'baaz'))
- ([(u'main.py', 3)], [])
- ([u'A user comment'], [u'An auto comment'])
+ (('bar', 'baz'), ('bar', 'baaz'))
+ ([('main.py', 3)], [])
+ (['A user comment'], ['An auto comment'])
.. versionadded:: 1.0
Added support for explicit charset argument.
@@ -437,11 +439,13 @@ def read_po(
return catalog
-WORD_SEP = re.compile('('
- r'\s+|' # any whitespace
- r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words
- r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash
- ')')
+WORD_SEP = re.compile(
+ '('
+ r'\s+|' # any whitespace
+ r'[^\s\w]*\w+[a-zA-Z]-(?=\w+[a-zA-Z])|' # hyphenated words
+ r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w)' # em-dash
+ ')',
+)
def escape(string: str) -> str:
@@ -455,11 +459,10 @@ def escape(string: str) -> str:
:param string: the string to escape
"""
- return '"%s"' % string.replace('\\', '\\\\') \
- .replace('\t', '\\t') \
- .replace('\r', '\\r') \
- .replace('\n', '\\n') \
- .replace('\"', '\\"')
+ return '"%s"' % string.replace('\\', '\\\\').replace('\t', '\\t').replace(
+ '\r',
+ '\\r',
+ ).replace('\n', '\\n').replace('"', '\\"')
def normalize(string: str, prefix: str = '', width: int = 76) -> str:
@@ -556,10 +559,10 @@ def write_po(
message catalog to the provided file-like object.
>>> catalog = Catalog()
- >>> catalog.add(u'foo %(name)s', locations=[('main.py', 1)],
+ >>> catalog.add('foo %(name)s', locations=[('main.py', 1)],
... flags=('fuzzy',))
- >>> catalog.add((u'bar', u'baz'), locations=[('main.py', 3)])
+ >>> catalog.add(('bar', 'baz'), locations=[('main.py', 3)])
>>> from io import BytesIO
>>> buf = BytesIO()
@@ -687,8 +690,10 @@ def _format_message(message, prefix=''):
# if no sorting possible, leave unsorted.
# (see issue #606)
try:
- locations = sorted(message.locations,
- key=lambda x: (x[0], isinstance(x[1], int) and x[1] or -1))
+ locations = sorted(
+ message.locations,
+ key=lambda x: (x[0], isinstance(x[1], int) and x[1] or -1),
+ )
except TypeError: # e.g. "TypeError: unorderable types: NoneType() < int()"
locations = message.locations
@@ -726,7 +731,10 @@ def _format_message(message, prefix=''):
yield '\n'
-def _sort_messages(messages: Iterable[Message], sort_by: Literal["message", "location"] | None) -> list[Message]:
+def _sort_messages(
+ messages: Iterable[Message],
+ sort_by: Literal["message", "location"] | None,
+) -> list[Message]:
"""
Sort the given message iterable by the given criteria.
diff --git a/addons/source-python/packages/site-packages/babel/numbers.py b/addons/source-python/packages/site-packages/babel/numbers.py
index 2737a7076..2ef9031aa 100644
--- a/addons/source-python/packages/site-packages/babel/numbers.py
+++ b/addons/source-python/packages/site-packages/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-2026 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 @@
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 @@ def __init__(self, identifier: str) -> None:
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.
"""
@@ -112,7 +112,7 @@ def get_currency_name(
"""Return the name used by the locale for the specified currency.
>>> get_currency_name('USD', locale='en_US')
- u'US Dollar'
+ 'US Dollar'
.. versionadded:: 0.9.4
@@ -142,7 +142,7 @@ def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> st
"""Return the symbol used by the locale for the specified currency.
>>> get_currency_symbol('USD', locale='en_US')
- u'$'
+ '$'
:param currency: the currency code.
:param locale: the `Locale` object or locale identifier.
@@ -178,7 +178,7 @@ def get_currency_unit_pattern(
name should be substituted.
>>> get_currency_unit_pattern('USD', locale='en_US', count=10)
- u'{0} {1}'
+ '{0} {1}'
.. versionadded:: 2.7.0
@@ -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 _is_active(start, end):
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
@@ -351,11 +358,11 @@ def get_decimal_symbol(
"""Return the symbol used by the locale to separate decimal fractions.
>>> get_decimal_symbol('en_US')
- u'.'
+ '.'
>>> get_decimal_symbol('ar_EG', numbering_system='default')
- u'٫'
+ '٫'
>>> get_decimal_symbol('ar_EG', numbering_system='latn')
- u'.'
+ '.'
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -374,11 +381,11 @@ def get_plus_sign_symbol(
"""Return the plus sign symbol used by the current locale.
>>> get_plus_sign_symbol('en_US')
- u'+'
+ '+'
>>> get_plus_sign_symbol('ar_EG', numbering_system='default')
- u'\u061c+'
+ '\\u061c+'
>>> get_plus_sign_symbol('ar_EG', numbering_system='latn')
- u'\u200e+'
+ '\\u200e+'
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -397,11 +404,11 @@ def get_minus_sign_symbol(
"""Return the plus sign symbol used by the current locale.
>>> get_minus_sign_symbol('en_US')
- u'-'
+ '-'
>>> get_minus_sign_symbol('ar_EG', numbering_system='default')
- u'\u061c-'
+ '\\u061c-'
>>> get_minus_sign_symbol('ar_EG', numbering_system='latn')
- u'\u200e-'
+ '\\u200e-'
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -420,11 +427,11 @@ def get_exponential_symbol(
"""Return the symbol used by the locale to separate mantissa and exponent.
>>> get_exponential_symbol('en_US')
- u'E'
+ 'E'
>>> get_exponential_symbol('ar_EG', numbering_system='default')
- u'أس'
+ 'أس'
>>> get_exponential_symbol('ar_EG', numbering_system='latn')
- u'E'
+ 'E'
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -432,7 +439,7 @@ def get_exponential_symbol(
:raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
"""
locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E')
+ return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') # fmt: skip
def get_group_symbol(
@@ -443,11 +450,11 @@ def get_group_symbol(
"""Return the symbol used by the locale to separate groups of thousands.
>>> get_group_symbol('en_US')
- u','
+ ','
>>> get_group_symbol('ar_EG', numbering_system='default')
- u'٬'
+ '٬'
>>> get_group_symbol('ar_EG', numbering_system='latn')
- u','
+ ','
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -466,11 +473,11 @@ def get_infinity_symbol(
"""Return the symbol used by the locale to represent infinity.
>>> get_infinity_symbol('en_US')
- u'∞'
+ '∞'
>>> get_infinity_symbol('ar_EG', numbering_system='default')
- u'∞'
+ '∞'
>>> get_infinity_symbol('ar_EG', numbering_system='latn')
- u'∞'
+ '∞'
:param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
:param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
@@ -481,13 +488,16 @@ 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
- u'1,099'
+ '1,099'
>>> format_number(1099, locale='de_DE') # doctest: +SKIP
- u'1.099'
+ '1.099'
.. deprecated:: 2.6.0
@@ -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)
@@ -534,38 +548,38 @@ def format_decimal(
"""Return the given decimal number formatted for a specific locale.
>>> format_decimal(1.2345, locale='en_US')
- u'1.234'
+ '1.234'
>>> format_decimal(1.2346, locale='en_US')
- u'1.235'
+ '1.235'
>>> format_decimal(-1.2346, locale='en_US')
- u'-1.235'
+ '-1.235'
>>> format_decimal(1.2345, locale='sv_SE')
- u'1,234'
+ '1,234'
>>> format_decimal(1.2345, locale='de')
- u'1,234'
+ '1,234'
>>> format_decimal(1.2345, locale='ar_EG', numbering_system='default')
- u'1٫234'
+ '1٫234'
>>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn')
- u'1.234'
+ '1.234'
The appropriate thousands grouping and the decimal separator are used for
each locale:
>>> format_decimal(12345.5, locale='en_US')
- u'12,345.5'
+ '12,345.5'
By default the locale is allowed to truncate and round a high-precision
number by forcing its format pattern onto the decimal part. You can bypass
this behavior with the `decimal_quantization` parameter:
>>> format_decimal(1.2346, locale='en_US')
- u'1.235'
+ '1.235'
>>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
- u'1.2346'
+ '1.2346'
>>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
- u'12345,67'
+ '12345,67'
>>> format_decimal(12345.67, locale='en_US', group_separator=True)
- u'12,345.67'
+ '12,345.67'
:param number: the number to format
:param format:
@@ -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(
@@ -597,19 +616,19 @@ def format_compact_decimal(
"""Return the given decimal number formatted for a specific locale in compact form.
>>> format_compact_decimal(12345, format_type="short", locale='en_US')
- u'12K'
+ '12K'
>>> format_compact_decimal(12345, format_type="long", locale='en_US')
- u'12 thousand'
+ '12 thousand'
>>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
- u'12.34K'
+ '12.34K'
>>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
- u'123万'
+ '123万'
>>> format_compact_decimal(2345678, format_type="long", locale="mk")
- u'2 милиони'
+ '2 милиони'
>>> format_compact_decimal(21000000, format_type="long", locale="mk")
- u'21 милион'
+ '21 милион'
>>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default')
- u'12٫34\xa0ألف'
+ '12٫34\\xa0ألف'
:param number: the number to format
:param format_type: Compact format to use ("short" or "long")
@@ -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
@@ -663,6 +690,8 @@ def _get_compact_format(
plural_form = "other"
if number == 1 and "1" in compact_format:
plural_form = "1"
+ if str(magnitude) not in compact_format[plural_form]:
+ plural_form = "other" # fall back to other as the implicit default
format = compact_format[plural_form][str(magnitude)]
number = rounded
break
@@ -690,43 +719,43 @@ def format_currency(
>>> format_currency(1099.98, 'USD', locale='en_US')
'$1,099.98'
>>> format_currency(1099.98, 'USD', locale='es_CO')
- u'US$1.099,98'
+ 'US$1.099,98'
>>> format_currency(1099.98, 'EUR', locale='de_DE')
- u'1.099,98\\xa0\\u20ac'
+ '1.099,98\\xa0\\u20ac'
>>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default')
- u'\u200f1٬099٫98\xa0ج.م.\u200f'
+ '\\u200f1٬099٫98\\xa0ج.م.\\u200f'
The format can also be specified explicitly. The currency is
placed with the '¤' sign. As the sign gets repeated the format
expands (¤ being the symbol, ¤¤ is the currency abbreviation and
¤¤¤ is the full name of the currency):
- >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
- u'EUR 1,099.98'
- >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US')
- u'1,099.98 euros'
+ >>> format_currency(1099.98, 'EUR', '\\xa4\\xa4 #,##0.00', locale='en_US')
+ 'EUR 1,099.98'
+ >>> format_currency(1099.98, 'EUR', '#,##0.00 \\xa4\\xa4\\xa4', locale='en_US')
+ '1,099.98 euros'
Currencies usually have a specific number of decimal digits. This function
favours that information over the given format:
>>> format_currency(1099.98, 'JPY', locale='en_US')
- u'\\xa51,100'
- >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES')
- u'1.099,98'
+ '\\xa51,100'
+ >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES')
+ '1.099,98'
However, the number of decimal digits can be overridden from the currency
information, by setting the last parameter to ``False``:
>>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False)
- u'\\xa51,099.98'
- >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES', currency_digits=False)
- u'1.099,98'
+ '\\xa51,099.98'
+ >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES', currency_digits=False)
+ '1.099,98'
If a format is not specified the type of currency format to use
from the locale can be specified:
>>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard')
- u'\\u20ac1,099.98'
+ '\\u20ac1,099.98'
When the given currency format type is not available, an exception is
raised:
@@ -737,30 +766,30 @@ def format_currency(
UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
>>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
- u'$101299.98'
+ '$101299.98'
>>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
- u'$101,299.98'
+ '$101,299.98'
You can also pass format_type='name' to use long display names. The order of
the number and currency name, along with the correct localized plural form
of the currency name, is chosen according to locale:
>>> format_currency(1, 'USD', locale='en_US', format_type='name')
- u'1.00 US dollar'
+ '1.00 US dollar'
>>> format_currency(1099.98, 'USD', locale='en_US', format_type='name')
- u'1,099.98 US dollars'
+ '1,099.98 US dollars'
>>> format_currency(1099.98, 'USD', locale='ee', format_type='name')
- u'us ga dollar 1,099.98'
+ 'us ga dollar 1,099.98'
By default the locale is allowed to truncate and round a high-precision
number by forcing its format pattern onto the decimal part. You can bypass
this behavior with the `decimal_quantization` parameter:
>>> format_currency(1099.9876, 'USD', locale='en_US')
- u'$1,099.99'
+ '$1,099.99'
>>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
- u'$1,099.9876'
+ '$1,099.9876'
:param number: the number to format
:param currency: the currency code
@@ -797,11 +826,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 +876,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)
@@ -857,11 +900,11 @@ def format_compact_currency(
"""Format a number as a currency value in compact form.
>>> format_compact_currency(12345, 'USD', locale='en_US')
- u'$12K'
+ '$12K'
>>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2)
- u'$123.46M'
+ '$123.46M'
>>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1)
- '123,5\xa0Mio.\xa0€'
+ '123,5\\xa0Mio.\\xa0€'
:param number: the number to format
:param currency: the currency code
@@ -877,7 +920,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 +939,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(
@@ -910,33 +961,33 @@ def format_percent(
"""Return formatted percent value for a specific locale.
>>> format_percent(0.34, locale='en_US')
- u'34%'
+ '34%'
>>> format_percent(25.1234, locale='en_US')
- u'2,512%'
+ '2,512%'
>>> format_percent(25.1234, locale='sv_SE')
- u'2\\xa0512\\xa0%'
+ '2\\xa0512\\xa0%'
>>> format_percent(25.1234, locale='ar_EG', numbering_system='default')
- u'2٬512%'
+ '2٬512%'
The format pattern can also be specified explicitly:
- >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
- u'25,123\u2030'
+ >>> format_percent(25.1234, '#,##0\\u2030', locale='en_US')
+ '25,123‰'
By default the locale is allowed to truncate and round a high-precision
number by forcing its format pattern onto the decimal part. You can bypass
this behavior with the `decimal_quantization` parameter:
>>> format_percent(23.9876, locale='en_US')
- u'2,399%'
+ '2,399%'
>>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
- u'2,398.76%'
+ '2,398.76%'
>>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
- u'22929112%'
+ '22929112%'
>>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
- u'22.929.112%'
+ '22.929.112%'
:param number: the percent number to format
:param format:
@@ -954,7 +1005,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,
)
@@ -970,23 +1024,23 @@ def format_scientific(
"""Return value formatted in scientific notation for a specific locale.
>>> format_scientific(10000, locale='en_US')
- u'1E4'
+ '1E4'
>>> format_scientific(10000, locale='ar_EG', numbering_system='default')
- u'1أس4'
+ '1أس4'
The format pattern can also be specified explicitly:
- >>> format_scientific(1234567, u'##0.##E00', locale='en_US')
- u'1.23E06'
+ >>> format_scientific(1234567, '##0.##E00', locale='en_US')
+ '1.23E06'
By default the locale is allowed to truncate and round a high-precision
number by forcing its format pattern onto the decimal part. You can bypass
this behavior with the `decimal_quantization` parameter:
- >>> format_scientific(1234.9876, u'#.##E0', locale='en_US')
- u'1.23E3'
- >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False)
- u'1.2349876E3'
+ >>> format_scientific(1234.9876, '#.##E0', locale='en_US')
+ '1.23E3'
+ >>> format_scientific(1234.9876, '#.##E0', locale='en_US', decimal_quantization=False)
+ '1.2349876E3'
:param number: the number to format
:param format:
@@ -1002,7 +1056,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):
@@ -1054,9 +1112,12 @@ def parse_number(
group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
if (
- group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space,
- group_symbol not in string and # and the string to be parsed does not contain it,
- SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead,
+ # if the grouping symbol is a kind of space,
+ group_symbol in SPACE_CHARS
+ # and the string to be parsed does not contain it,
+ and group_symbol not in string
+ # but it does contain any other kind of space instead,
+ and SPACE_CHARS_RE.search(string)
):
# ... it's reasonable to assume it is taking the place of the grouping symbol.
string = SPACE_CHARS_RE.sub(group_symbol, string)
@@ -1120,24 +1181,30 @@ def parse_decimal(
decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
if not strict and (
- group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space,
- group_symbol not in string and # and the string to be parsed does not contain it,
- SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead,
+ group_symbol in SPACE_CHARS # if the grouping symbol is a kind of space,
+ and group_symbol not in string # and the string to be parsed does not contain it,
+ # but it does contain any other kind of space instead,
+ and SPACE_CHARS_RE.search(string)
):
# ... it's reasonable to assume it is taking the place of the grouping symbol.
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. "
@@ -1201,14 +1268,11 @@ def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> st
return string
-PREFIX_END = r'[^0-9@#.,]'
-NUMBER_TOKEN = r'[0-9@#.,E+]'
-
-PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END
-NUMBER_PATTERN = r"(?P%s*)" % NUMBER_TOKEN
-SUFFIX_PATTERN = r"(?P.*)"
-
-number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}")
+_number_pattern_re = re.compile(
+ r"(?P(?:[^'0-9@#.,]|'[^']*')*)"
+ r"(?P[0-9@#.,E+]*)"
+ r"(?P.*)",
+)
def parse_grouping(p: str) -> tuple[int, int]:
@@ -1226,7 +1290,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
@@ -1239,7 +1303,7 @@ def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
return pattern
def _match_number(pattern):
- rv = number_re.search(pattern)
+ rv = _number_pattern_re.search(pattern)
if rv is None:
raise ValueError(f"Invalid number pattern {pattern!r}")
return rv.groups()
@@ -1292,14 +1356,20 @@ def parse_precision(p):
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,
@@ -1348,8 +1418,7 @@ def scientific_notation_elements(
*,
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)
@@ -1426,7 +1495,11 @@ def apply(
# 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.
@@ -1439,7 +1512,7 @@ def apply(
)
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
@@ -1459,13 +1532,11 @@ def apply(
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:
@@ -1473,12 +1544,21 @@ def apply(
# 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))
@@ -1568,8 +1648,19 @@ def _quantize_value(
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(
@@ -1582,7 +1673,7 @@ def _format_frac(
) -> 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/addons/source-python/packages/site-packages/babel/plural.py b/addons/source-python/packages/site-packages/babel/plural.py
index 085209e9d..90aa4952d 100644
--- a/addons/source-python/packages/site-packages/babel/plural.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import decimal
@@ -18,7 +19,9 @@
_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 @@ def __init__(self, rules: Mapping[str, str] | Iterable[tuple[str, str]]) -> None
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 @@ class RuleError(Exception):
_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[()]]:
@@ -463,8 +476,8 @@ def and_condition(self):
def relation(self):
left = self.expr()
if skip_token(self.tokens, 'word', 'is'):
- return skip_token(self.tokens, 'word', 'not') and 'isnot' or 'is', \
- (left, self.value())
+ op = 'isnot' if skip_token(self.tokens, 'word', 'not') else 'is'
+ return op, (left, self.value())
negated = skip_token(self.tokens, 'word', 'not')
method = 'in'
if skip_token(self.tokens, 'word', 'within'):
@@ -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}])"
@@ -586,7 +601,8 @@ def compile_relation(self, method, expr, range_list):
if item[0] == item[1]:
rv.append(f"({expr} == {self.compile(item[0])})")
else:
- min, max = map(self.compile, item)
+ min = self.compile(item[0])
+ max = self.compile(item[1])
rv.append(f"({expr} >= {min} && {expr} <= {max})")
return f"({' || '.join(rv)})"
@@ -603,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/addons/source-python/packages/site-packages/babel/support.py b/addons/source-python/packages/site-packages/babel/support.py
index b600bfe27..8cc2492e8 100644
--- a/addons/source-python/packages/site-packages/babel/support.py
+++ b/addons/source-python/packages/site-packages/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-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import gettext
@@ -44,9 +45,9 @@ class Format:
>>> from datetime import date
>>> fmt = Format('en_US', UTC)
>>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
+ 'Apr 1, 2007'
>>> fmt.decimal(1.2345)
- u'1.234'
+ '1.234'
"""
def __init__(
@@ -77,7 +78,7 @@ def date(
>>> from datetime import date
>>> fmt = Format('en_US')
>>> fmt.date(date(2007, 4, 1))
- u'Apr 1, 2007'
+ 'Apr 1, 2007'
"""
return format_date(date, format, locale=self.locale)
@@ -92,7 +93,7 @@ def datetime(
>>> from babel.dates import get_timezone
>>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
>>> fmt.datetime(datetime(2007, 4, 1, 15, 30))
- u'Apr 1, 2007, 11:30:00\u202fAM'
+ 'Apr 1, 2007, 11:30:00\\u202fAM'
"""
return format_datetime(datetime, format, tzinfo=self.tzinfo, locale=self.locale)
@@ -107,14 +108,22 @@ def time(
>>> from babel.dates import get_timezone
>>> fmt = Format('en_US', tzinfo=get_timezone('US/Eastern'))
>>> fmt.time(datetime(2007, 4, 1, 15, 30))
- u'11:30:00\u202fAM'
+ '11:30:00\\u202fAM'
"""
return format_time(time, format, tzinfo=self.tzinfo, locale=self.locale)
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,
@@ -124,30 +133,43 @@ def timedelta(
>>> from datetime import timedelta
>>> fmt = Format('en_US')
>>> 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)
+ '3 months'
+ """
+ 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.
>>> fmt = Format('en_US')
>>> fmt.number(1099)
- u'1,099'
+ '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.
>>> fmt = Format('en_US')
>>> fmt.decimal(1.2345)
- u'1.234'
+ '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,
@@ -159,7 +181,7 @@ def compact_decimal(
>>> fmt = Format('en_US')
>>> fmt.compact_decimal(123456789)
- u'123M'
+ '123M'
>>> fmt.compact_decimal(1234567, format_type='long', fraction_digits=2)
'1.23 million'
"""
@@ -172,9 +194,13 @@ def compact_decimal(
)
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,22 +215,36 @@ def compact_currency(
>>> 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.
>>> fmt = Format('en_US')
>>> fmt.percent(0.34)
- u'34%'
+ '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:
@@ -216,10 +256,10 @@ class LazyProxy:
>>> lazy_greeting = LazyProxy(greeting, name='Joe')
>>> print(lazy_greeting)
Hello, Joe!
- >>> u' ' + lazy_greeting
- u' Hello, Joe!'
- >>> u'(%s)' % lazy_greeting
- u'(Hello, Joe!)'
+ >>> ' ' + lazy_greeting
+ ' Hello, Joe!'
+ >>> '(%s)' % lazy_greeting
+ '(Hello, Joe!)'
This can be used, for example, to implement lazy translation functions that
delay the actual translation until the string is actually used. The
@@ -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 @@ def __copy__(self) -> 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 @@ def __deepcopy__(self, memo: Any) -> LazyProxy:
class NullTranslations(gettext.NullTranslations):
-
if TYPE_CHECKING:
_info: dict[str, str]
_fallback: NullTranslations | None
@@ -406,6 +460,7 @@ def ldgettext(self, domain: str, message: str) -> str:
domain.
"""
import warnings
+
warnings.warn(
'ldgettext() is deprecated, use dgettext() instead',
DeprecationWarning,
@@ -418,6 +473,7 @@ def udgettext(self, domain: str, message: str) -> str:
domain.
"""
return self._domains.get(domain, self).ugettext(message)
+
# backward compatibility with 0.9
dugettext = udgettext
@@ -432,6 +488,7 @@ def ldngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
domain.
"""
import warnings
+
warnings.warn(
'ldngettext() is deprecated, use dngettext() instead',
DeprecationWarning,
@@ -444,6 +501,7 @@ def udngettext(self, domain: str, singular: str, plural: str, num: int) -> str:
domain.
"""
return self._domains.get(domain, self).ungettext(singular, plural, num)
+
# backward compatibility with 0.9
dungettext = udngettext
@@ -479,6 +537,7 @@ def lpgettext(self, context: str, message: str) -> str | bytes | object:
``bind_textdomain_codeset()``.
"""
import warnings
+
warnings.warn(
'lpgettext() is deprecated, use pgettext() instead',
DeprecationWarning,
@@ -517,6 +576,7 @@ def lnpgettext(self, context: str, singular: str, plural: str, num: int) -> str
``bind_textdomain_codeset()``.
"""
import warnings
+
warnings.warn(
'lnpgettext() is deprecated, use npgettext() instead',
DeprecationWarning,
@@ -583,6 +643,7 @@ def udpgettext(self, domain: str, context: str, message: str) -> str:
`domain`.
"""
return self._domains.get(domain, self).upgettext(context, message)
+
# backward compatibility with 0.9
dupgettext = udpgettext
@@ -593,29 +654,34 @@ def ldpgettext(self, domain: str, context: str, message: str) -> str | bytes | o
"""
return self._domains.get(domain, self).lpgettext(context, message)
- def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
+ def dnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: # fmt: skip
"""Like ``npgettext``, but look the message up in the specified
`domain`.
"""
- return self._domains.get(domain, self).npgettext(context, singular,
- plural, num)
+ return self._domains.get(domain, self).npgettext(context, singular, plural, num)
- def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str:
+ def udnpgettext(self, domain: str, context: str, singular: str, plural: str, num: int) -> str: # fmt: skip
"""Like ``unpgettext``, but look the message up in the specified
`domain`.
"""
- return self._domains.get(domain, self).unpgettext(context, singular,
- plural, num)
+ return self._domains.get(domain, self).unpgettext(context, singular, plural, num)
+
# 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
@@ -626,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/addons/source-python/packages/site-packages/babel/units.py b/addons/source-python/packages/site-packages/babel/units.py
index 86ac2abc9..88ebb909c 100644
--- a/addons/source-python/packages/site-packages/babel/units.py
+++ b/addons/source-python/packages/site-packages/babel/units.py
@@ -87,32 +87,32 @@ def format_unit(
and number formats.
>>> format_unit(12, 'length-meter', locale='ro_RO')
- u'12 metri'
+ '12 metri'
>>> format_unit(15.5, 'length-mile', locale='fi_FI')
- u'15,5 mailia'
+ '15,5 mailia'
>>> format_unit(1200, 'pressure-millimeter-ofhg', locale='nb')
- u'1\\xa0200 millimeter kvikks\\xf8lv'
+ '1\\xa0200 millimeter kvikks\\xf8lv'
>>> format_unit(270, 'ton', locale='en')
- u'270 tons'
+ '270 tons'
>>> format_unit(1234.5, 'kilogram', locale='ar_EG', numbering_system='default')
- u'1٬234٫5 كيلوغرام'
+ '1٬234٫5 كيلوغرام'
Number formats may be overridden with the ``format`` parameter.
>>> import decimal
>>> format_unit(decimal.Decimal("-42.774"), 'temperature-celsius', 'short', format='#.0', locale='fr')
- u'-42,8\\u202f\\xb0C'
+ '-42,8\\u202f\\xb0C'
The locale's usual pluralization rules are respected.
>>> format_unit(1, 'length-meter', locale='ro_RO')
- u'1 metru'
+ '1 metru'
>>> format_unit(0, 'length-mile', locale='cy')
- u'0 mi'
+ '0 mi'
>>> format_unit(1, 'length-mile', locale='cy')
- u'1 filltir'
+ '1 filltir'
>>> format_unit(3, 'length-mile', locale='cy')
- u'3 milltir'
+ '3 milltir'
>>> format_unit(15, 'length-horse', locale='fi')
Traceback (most recent call last):
@@ -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/addons/source-python/packages/site-packages/babel/util.py b/addons/source-python/packages/site-packages/babel/util.py
index d113982ee..a2bf728cc 100644
--- a/addons/source-python/packages/site-packages/babel/util.py
+++ b/addons/source-python/packages/site-packages/babel/util.py
@@ -1,12 +1,13 @@
"""
- babel.util
- ~~~~~~~~~~
+babel.util
+~~~~~~~~~~
- Various utility classes and functions.
+Various utility classes and functions.
- :copyright: (c) 2013-2025 by the Babel Team.
- :license: BSD, see LICENSE for more details.
+:copyright: (c) 2013-2026 by the Babel Team.
+:license: BSD, see LICENSE for more details.
"""
+
from __future__ import annotations
import codecs
@@ -47,7 +48,9 @@ def distinct(iterable: Iterable[_T]) -> Generator[_T, None, None]:
# Regexp to match python magic encoding line
PYTHON_MAGIC_COMMENT_re = re.compile(
- br'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', re.VERBOSE)
+ rb'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)',
+ flags=re.VERBOSE,
+)
def parse_encoding(fp: IO[bytes]) -> str | None:
@@ -67,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
@@ -98,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:
@@ -107,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
@@ -201,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'
@@ -226,7 +230,12 @@ def _split(self, text):
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.
@@ -244,10 +253,12 @@ def wraptext(text: str, width: int = 70, initial_indent: str = '', subsequent_in
DeprecationWarning,
stacklevel=2,
)
- wrapper = TextWrapper(width=width, initial_indent=initial_indent,
- subsequent_indent=subsequent_indent,
- break_long_words=False)
- return wrapper.wrap(text)
+ return TextWrapper(
+ width=width,
+ initial_indent=initial_indent,
+ subsequent_indent=subsequent_indent,
+ break_long_words=False,
+ ).wrap(text)
# TODO (Babel 3.x): Remove this re-export
@@ -255,10 +266,21 @@ def wraptext(text: str, width: int = 70, initial_indent: str = '', subsequent_in
class FixedOffsetTimezone(datetime.tzinfo):
- """Fixed offset in minutes east from UTC."""
+ """
+ Fixed offset in minutes east from UTC.
- def __init__(self, offset: float, name: str | None = None) -> None:
+ 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:
+ warnings.warn(
+ "`FixedOffsetTimezone` is deprecated and will be removed in a future version of Babel. "
+ "Use the standard library `datetime.timezone` class.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
self._offset = datetime.timedelta(minutes=offset)
if name is None:
name = 'Etc/GMT%+d' % offset
diff --git a/addons/source-python/packages/site-packages/certifi/__init__.py b/addons/source-python/packages/site-packages/certifi/__init__.py
index 177082e0f..16c0c7c26 100644
--- a/addons/source-python/packages/site-packages/certifi/__init__.py
+++ b/addons/source-python/packages/site-packages/certifi/__init__.py
@@ -1,4 +1,4 @@
from .core import contents, where
__all__ = ["contents", "where"]
-__version__ = "2025.01.31"
+__version__ = "2026.02.25"
diff --git a/addons/source-python/packages/site-packages/certifi/cacert.pem b/addons/source-python/packages/site-packages/certifi/cacert.pem
index 860f259bd..5ec1afe02 100644
--- a/addons/source-python/packages/site-packages/certifi/cacert.pem
+++ b/addons/source-python/packages/site-packages/certifi/cacert.pem
@@ -1,163 +1,4 @@
-# Issuer: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
-# Subject: CN=GlobalSign Root CA O=GlobalSign nv-sa OU=Root CA
-# Label: "GlobalSign Root CA"
-# Serial: 4835703278459707669005204
-# MD5 Fingerprint: 3e:45:52:15:09:51:92:e1:b7:5d:37:9f:b1:87:29:8a
-# SHA1 Fingerprint: b1:bc:96:8b:d4:f4:9d:62:2a:a8:9a:81:f2:15:01:52:a4:1d:82:9c
-# SHA256 Fingerprint: eb:d4:10:40:e4:bb:3e:c7:42:c9:e3:81:d3:1e:f2:a4:1a:48:b6:68:5c:96:e7:ce:f3:c1:df:6c:d4:33:1c:99
------BEGIN CERTIFICATE-----
-MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG
-A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv
-b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw
-MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i
-YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT
-aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ
-jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp
-xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp
-1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG
-snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ
-U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8
-9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
-BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B
-AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz
-yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE
-38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP
-AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad
-DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME
-HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==
------END CERTIFICATE-----
-
-# Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
-# Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited
-# Label: "Entrust.net Premium 2048 Secure Server CA"
-# Serial: 946069240
-# MD5 Fingerprint: ee:29:31:bc:32:7e:9a:e6:e8:b5:f7:51:b4:34:71:90
-# SHA1 Fingerprint: 50:30:06:09:1d:97:d4:f5:ae:39:f7:cb:e7:92:7d:7d:65:2d:34:31
-# SHA256 Fingerprint: 6d:c4:71:72:e0:1c:bc:b0:bf:62:58:0d:89:5f:e2:b8:ac:9a:d4:f8:73:80:1e:0c:10:b9:c8:37:d2:1e:b1:77
------BEGIN CERTIFICATE-----
-MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChML
-RW50cnVzdC5uZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBp
-bmNvcnAuIGJ5IHJlZi4gKGxpbWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5
-IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNVBAMTKkVudHJ1c3QubmV0IENlcnRp
-ZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQxNzUwNTFaFw0yOTA3
-MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3d3d3
-LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxp
-YWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEG
-A1UEAxMqRW50cnVzdC5uZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgp
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArU1LqRKGsuqjIAcVFmQq
-K0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOLGp18EzoOH1u3Hs/lJBQe
-sYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSrhRSGlVuX
-MlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVT
-XTzWnLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/
-HoZdenoVve8AjhUiVBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH
-4QIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
-HQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJKoZIhvcNAQEFBQADggEBADub
-j1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPyT/4xmf3IDExo
-U8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf
-zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5b
-u/8j72gZyxKTJ1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+
-bYQLCIt+jerXmCHG8+c8eS9enNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/Er
-fF6adulZkMV8gzURZVE=
------END CERTIFICATE-----
-
-# Issuer: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
-# Subject: CN=Baltimore CyberTrust Root O=Baltimore OU=CyberTrust
-# Label: "Baltimore CyberTrust Root"
-# Serial: 33554617
-# MD5 Fingerprint: ac:b6:94:a5:9c:17:e0:d7:91:52:9b:b1:97:06:a6:e4
-# SHA1 Fingerprint: d4:de:20:d0:5e:66:fc:53:fe:1a:50:88:2c:78:db:28:52:ca:e4:74
-# SHA256 Fingerprint: 16:af:57:a9:f6:76:b0:ab:12:60:95:aa:5e:ba:de:f2:2a:b3:11:19:d6:44:ac:95:cd:4b:93:db:f3:f2:6a:eb
------BEGIN CERTIFICATE-----
-MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
-RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
-VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
-DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
-ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
-VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
-mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
-IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
-mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
-XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
-dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
-jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
-BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
-DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
-9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
-jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
-Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
-ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
-R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
------END CERTIFICATE-----
-
-# Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
-# Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc.
-# Label: "Entrust Root Certification Authority"
-# Serial: 1164660820
-# MD5 Fingerprint: d6:a5:c3:ed:5d:dd:3e:00:c1:3d:87:92:1f:1d:3f:e4
-# SHA1 Fingerprint: b3:1e:b1:b7:40:e3:6c:84:02:da:dc:37:d4:4d:f5:d4:67:49:52:f9
-# SHA256 Fingerprint: 73:c1:76:43:4f:1b:c6:d5:ad:f4:5b:0e:76:e7:27:28:7c:8d:e5:76:16:c1:e6:e6:14:1a:2b:2c:bc:7d:8e:4c
------BEGIN CERTIFICATE-----
-MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC
-VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0
-Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW
-KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl
-cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw
-NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw
-NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy
-ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV
-BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ
-KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo
-Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4
-4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9
-KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI
-rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi
-94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB
-sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi
-gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo
-kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE
-vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA
-A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t
-O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua
-AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP
-9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/
-eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m
-0vdXcDazv/wor3ElhVsT/h5/WrQ8
------END CERTIFICATE-----
-
-# Issuer: CN=AAA Certificate Services O=Comodo CA Limited
-# Subject: CN=AAA Certificate Services O=Comodo CA Limited
-# Label: "Comodo AAA Services root"
-# Serial: 1
-# MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0
-# SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49
-# SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4
------BEGIN CERTIFICATE-----
-MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
-MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
-GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
-YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
-MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
-BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
-GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
-ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
-BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
-3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
-YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
-rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
-ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
-oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
-MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
-QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
-b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
-AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
-GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
-Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
-G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
-l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
-smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
------END CERTIFICATE-----
-
# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited
# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited
# Label: "QuoVadis Root CA 2"
@@ -245,103 +86,6 @@ mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
4SVhM7JZG+Ju1zdXtg2pEto=
-----END CERTIFICATE-----
-# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
-# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
-# Label: "XRamp Global CA Root"
-# Serial: 107108908803651509692980124233745014957
-# MD5 Fingerprint: a1:0b:44:b3:ca:10:d8:00:6e:9d:0f:d8:0f:92:0a:d1
-# SHA1 Fingerprint: b8:01:86:d1:eb:9c:86:a5:41:04:cf:30:54:f3:4c:52:b7:e5:58:c6
-# SHA256 Fingerprint: ce:cd:dc:90:50:99:d8:da:df:c5:b1:d2:09:b7:37:cb:e2:c1:8c:fb:2c:10:c0:ff:0b:cf:0d:32:86:fc:1a:a2
------BEGIN CERTIFICATE-----
-MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCB
-gjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEk
-MCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRY
-UmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQxMTAxMTcx
-NDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMxHjAcBgNVBAsTFXd3
-dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkgU2Vy
-dmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB
-dXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS6
-38eMpSe2OAtp87ZOqCwuIR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCP
-KZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMxfoArtYzAQDsRhtDLooY2YKTVMIJt2W7Q
-DxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FEzG+gSqmUsE3a56k0enI4
-qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqsAxcZZPRa
-JSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNVi
-PvryxS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0P
-BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASs
-jVy16bYbMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0
-eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQEwDQYJKoZIhvcNAQEFBQAD
-ggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc/Kh4ZzXxHfAR
-vbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt
-qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLa
-IR9NmXmd4c8nnxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSy
-i6mx5O+aGtA9aZnuqCij4Tyz8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQ
-O+7ETPTsJ3xCwnR8gooJybQDJbw=
------END CERTIFICATE-----
-
-# Issuer: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
-# Subject: O=The Go Daddy Group, Inc. OU=Go Daddy Class 2 Certification Authority
-# Label: "Go Daddy Class 2 CA"
-# Serial: 0
-# MD5 Fingerprint: 91:de:06:25:ab:da:fd:32:17:0c:bb:25:17:2a:84:67
-# SHA1 Fingerprint: 27:96:ba:e6:3f:18:01:e2:77:26:1b:a0:d7:77:70:02:8f:20:ee:e4
-# SHA256 Fingerprint: c3:84:6b:f2:4b:9e:93:ca:64:27:4c:0e:c6:7c:1e:cc:5e:02:4f:fc:ac:d2:d7:40:19:35:0e:81:fe:54:6a:e4
------BEGIN CERTIFICATE-----
-MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh
-MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE
-YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3
-MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo
-ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg
-MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN
-ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA
-PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w
-wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi
-EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY
-avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+
-YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE
-sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h
-/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5
-IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj
-YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD
-ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy
-OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P
-TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ
-HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER
-dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf
-ReYNnyicsbkqWletNw+vHX/bvZ8=
------END CERTIFICATE-----
-
-# Issuer: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
-# Subject: O=Starfield Technologies, Inc. OU=Starfield Class 2 Certification Authority
-# Label: "Starfield Class 2 CA"
-# Serial: 0
-# MD5 Fingerprint: 32:4a:4b:bb:c8:63:69:9b:be:74:9a:c6:dd:1d:46:24
-# SHA1 Fingerprint: ad:7e:1c:28:b0:64:ef:8f:60:03:40:20:14:c3:d0:e3:37:0e:b5:8a
-# SHA256 Fingerprint: 14:65:fa:20:53:97:b8:76:fa:a6:f0:a9:95:8e:55:90:e4:0f:cc:7f:aa:4f:b7:c2:c8:67:75:21:fb:5f:b6:58
------BEGIN CERTIFICATE-----
-MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzEl
-MCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMp
-U3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQw
-NjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBoMQswCQYDVQQGEwJVUzElMCMGA1UE
-ChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZp
-ZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqGSIb3
-DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf
-8MOh2tTYbitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN
-+lq2cwQlZut3f+dZxkqZJRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0
-X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVmepsZGD3/cVE8MC5fvj13c7JdBmzDI1aa
-K4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSNF4Azbl5KXZnJHoe0nRrA
-1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HFMIHCMB0G
-A1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fR
-zt0fhvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0
-YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBD
-bGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8w
-DQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGsafPzWdqbAYcaT1epoXkJKtv3
-L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLMPUxA2IGvd56D
-eruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl
-xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynp
-VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY
-WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q=
------END CERTIFICATE-----
-
# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com
# Label: "DigiCert Assured ID Root CA"
@@ -919,122 +663,6 @@ iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn
sSi6
-----END CERTIFICATE-----
-# Issuer: CN=AffirmTrust Commercial O=AffirmTrust
-# Subject: CN=AffirmTrust Commercial O=AffirmTrust
-# Label: "AffirmTrust Commercial"
-# Serial: 8608355977964138876
-# MD5 Fingerprint: 82:92:ba:5b:ef:cd:8a:6f:a6:3d:55:f9:84:f6:d6:b7
-# SHA1 Fingerprint: f9:b5:b6:32:45:5f:9c:be:ec:57:5f:80:dc:e9:6e:2c:c7:b2:78:b7
-# SHA256 Fingerprint: 03:76:ab:1d:54:c5:f9:80:3c:e4:b2:e2:01:a0:ee:7e:ef:7b:57:b6:36:e8:a9:3c:9b:8d:48:60:c9:6f:5f:a7
------BEGIN CERTIFICATE-----
-MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UE
-BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
-dCBDb21tZXJjaWFsMB4XDTEwMDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDEL
-MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
-cm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
-AQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6EqdbDuKP
-Hx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yr
-ba0F8PrVC8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPAL
-MeIrJmqbTFeurCA+ukV6BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1
-yHp52UKqK39c/s4mT6NmgTWvRLpUHhwwMmWd5jyTXlBOeuM61G7MGvv50jeuJCqr
-VwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNVHQ4EFgQUnZPGU4teyq8/
-nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
-KoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYG
-XUPGhi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNj
-vbz4YYCanrHOQnDiqX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivt
-Z8SOyUOyXGsViQK8YvxO8rUzqrJv0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9g
-N53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0khsUlHRUe072o0EclNmsxZt9YC
-nlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8=
------END CERTIFICATE-----
-
-# Issuer: CN=AffirmTrust Networking O=AffirmTrust
-# Subject: CN=AffirmTrust Networking O=AffirmTrust
-# Label: "AffirmTrust Networking"
-# Serial: 8957382827206547757
-# MD5 Fingerprint: 42:65:ca:be:01:9a:9a:4c:a9:8c:41:49:cd:c0:d5:7f
-# SHA1 Fingerprint: 29:36:21:02:8b:20:ed:02:f5:66:c5:32:d1:d6:ed:90:9f:45:00:2f
-# SHA256 Fingerprint: 0a:81:ec:5a:92:97:77:f1:45:90:4a:f3:8d:5d:50:9f:66:b5:e2:c5:8f:cd:b5:31:05:8b:0e:17:f3:f0:b4:1b
------BEGIN CERTIFICATE-----
-MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UE
-BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVz
-dCBOZXR3b3JraW5nMB4XDTEwMDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDEL
-MAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZp
-cm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
-AQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SEHi3y
-YJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbua
-kCNrmreIdIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRL
-QESxG9fhwoXA3hA/Pe24/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp
-6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gbh+0t+nvujArjqWaJGctB+d1ENmHP4ndG
-yH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNVHQ4EFgQUBx/S55zawm6i
-QLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJ
-KoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfO
-tDIuUFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzu
-QY0x2+c06lkh1QF612S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZ
-Lgo/bNjR9eUJtGxUAArgFU2HdW23WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4u
-olu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9/ZFvgrG+CJPbFEfxojfHRZ48
-x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s=
------END CERTIFICATE-----
-
-# Issuer: CN=AffirmTrust Premium O=AffirmTrust
-# Subject: CN=AffirmTrust Premium O=AffirmTrust
-# Label: "AffirmTrust Premium"
-# Serial: 7893706540734352110
-# MD5 Fingerprint: c4:5d:0e:48:b6:ac:28:30:4e:0a:bc:f9:38:16:87:57
-# SHA1 Fingerprint: d8:a6:33:2c:e0:03:6f:b1:85:f6:63:4f:7d:6a:06:65:26:32:28:27
-# SHA256 Fingerprint: 70:a7:3f:7f:37:6b:60:07:42:48:90:45:34:b1:14:82:d5:bf:0e:69:8e:cc:49:8d:f5:25:77:eb:f2:e9:3b:9a
------BEGIN CERTIFICATE-----
-MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UE
-BhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVz
-dCBQcmVtaXVtMB4XDTEwMDEyOTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkG
-A1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1U
-cnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxBLf
-qV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtnBKAQ
-JG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ
-+jjeRFcV5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrS
-s8PhaJyJ+HoAVt70VZVs+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5
-HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmdGPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d7
-70O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5Rp9EixAqnOEhss/n/fauG
-V+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NIS+LI+H+S
-qHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S
-5u046uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4Ia
-C1nEWTJ3s7xgaVY5/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TX
-OwF0lkLgAOIua+rF7nKsu7/+6qqo+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYE
-FJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
-BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByvMiPIs0laUZx2
-KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg
-Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B
-8OWycvpEgjNC6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQ
-MKSOyARiqcTtNd56l+0OOF6SL5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc
-0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK+4w1IX2COPKpVJEZNZOUbWo6xbLQ
-u4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmVBtWVyuEklut89pMF
-u+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFgIxpH
-YoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8
-GKa1qF60g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaO
-RtGdFNrHF+QFlozEJLUbzxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6e
-KeC2uAloGRwYQw==
------END CERTIFICATE-----
-
-# Issuer: CN=AffirmTrust Premium ECC O=AffirmTrust
-# Subject: CN=AffirmTrust Premium ECC O=AffirmTrust
-# Label: "AffirmTrust Premium ECC"
-# Serial: 8401224907861490260
-# MD5 Fingerprint: 64:b0:09:55:cf:b1:d5:99:e2:be:13:ab:a6:5d:ea:4d
-# SHA1 Fingerprint: b8:23:6b:00:2f:1d:16:86:53:01:55:6c:11:a4:37:ca:eb:ff:c3:bb
-# SHA256 Fingerprint: bd:71:fd:f6:da:97:e4:cf:62:d1:64:7a:dd:25:81:b0:7d:79:ad:f8:39:7e:b4:ec:ba:9c:5e:84:88:82:14:23
------BEGIN CERTIFICATE-----
-MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMC
-VVMxFDASBgNVBAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQ
-cmVtaXVtIEVDQzAeFw0xMDAxMjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJ
-BgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1UcnVzdDEgMB4GA1UEAwwXQWZmaXJt
-VHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQNMF4bFZ0D
-0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQN8O9
-ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0G
-A1UdDgQWBBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4G
-A1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/Vs
-aobgxCd05DhT1wV/GzTjxi+zygk8N53X57hG8f2h4nECMEJZh0PUUd+60wkyWs6I
-flc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKMeQ==
------END CERTIFICATE-----
-
# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority
# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority
# Label: "Certum Trusted Network CA"
@@ -2038,65 +1666,6 @@ GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv
8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c
-----END CERTIFICATE-----
-# Issuer: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only
-# Subject: CN=Entrust Root Certification Authority - G2 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2009 Entrust, Inc. - for authorized use only
-# Label: "Entrust Root Certification Authority - G2"
-# Serial: 1246989352
-# MD5 Fingerprint: 4b:e2:c9:91:96:65:0c:f4:0e:5a:93:92:a0:0a:fe:b2
-# SHA1 Fingerprint: 8c:f4:27:fd:79:0c:3a:d1:66:06:8d:e8:1e:57:ef:bb:93:22:72:d4
-# SHA256 Fingerprint: 43:df:57:74:b0:3e:7f:ef:5f:e4:0d:93:1a:7b:ed:f1:bb:2e:6b:42:73:8c:4e:6d:38:41:10:3d:3a:a7:f3:39
------BEGIN CERTIFICATE-----
-MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC
-VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50
-cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs
-IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz
-dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy
-NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu
-dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt
-dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0
-aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj
-YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
-AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T
-RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN
-cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW
-wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1
-U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0
-jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP
-BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN
-BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/
-jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ
-Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v
-1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R
-nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH
-VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==
------END CERTIFICATE-----
-
-# Issuer: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only
-# Subject: CN=Entrust Root Certification Authority - EC1 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2012 Entrust, Inc. - for authorized use only
-# Label: "Entrust Root Certification Authority - EC1"
-# Serial: 51543124481930649114116133369
-# MD5 Fingerprint: b6:7e:1d:f0:58:c5:49:6c:24:3b:3d:ed:98:18:ed:bc
-# SHA1 Fingerprint: 20:d8:06:40:df:9b:25:f5:12:25:3a:11:ea:f7:59:8a:eb:14:b5:47
-# SHA256 Fingerprint: 02:ed:0e:b2:8c:14:da:45:16:5c:56:67:91:70:0d:64:51:d7:fb:56:f0:b2:ab:1d:3b:8e:b0:70:e5:6e:df:f5
------BEGIN CERTIFICATE-----
-MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkG
-A1UEBhMCVVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3
-d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVu
-dHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEzMDEGA1UEAxMq
-RW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRUMxMB4XDTEy
-MTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYwFAYD
-VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
-L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0g
-Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBD
-ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEVDMTB2MBAGByqGSM49AgEGBSuBBAAi
-A2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHyAsWfoPZb1YsGGYZPUxBt
-ByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef9eNi1KlH
-Bz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
-BBYEFLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVC
-R98crlOZF7ZvHH3hvxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nX
-hTcGtXsI/esni0qU+eH6p44mCOh8kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G
------END CERTIFICATE-----
-
# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority
# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority
# Label: "CFCA EV ROOT"
@@ -3371,46 +2940,6 @@ DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ
+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A=
-----END CERTIFICATE-----
-# Issuer: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH
-# Subject: CN=GLOBALTRUST 2020 O=e-commerce monitoring GmbH
-# Label: "GLOBALTRUST 2020"
-# Serial: 109160994242082918454945253
-# MD5 Fingerprint: 8a:c7:6f:cb:6d:e3:cc:a2:f1:7c:83:fa:0e:78:d7:e8
-# SHA1 Fingerprint: d0:67:c1:13:51:01:0c:aa:d0:c7:6a:65:37:31:16:26:4f:53:71:a2
-# SHA256 Fingerprint: 9a:29:6a:51:82:d1:d4:51:a2:e3:7f:43:9b:74:da:af:a2:67:52:33:29:f9:0f:9a:0d:20:07:c3:34:e2:3c:9a
------BEGIN CERTIFICATE-----
-MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkG
-A1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkw
-FwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYx
-MDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAhBgNVBAoTGmUtY29tbWVyY2UgbW9u
-aXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAyMDIwMIICIjANBgkq
-hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWiD59b
-RatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9Z
-YybNpyrOVPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3
-QWPKzv9pj2gOlTblzLmMCcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPw
-yJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCmfecqQjuCgGOlYx8ZzHyyZqjC0203b+J+
-BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKAA1GqtH6qRNdDYfOiaxaJ
-SaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9ORJitHHmkH
-r96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj0
-4KlGDfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9Me
-dKZssCz3AwyIDMvUclOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIw
-q7ejMZdnrY8XD2zHc+0klGvIg5rQmjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2
-nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
-AQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1UdIwQYMBaAFNwu
-H9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA
-VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJC
-XtzoRlgHNQIw4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd
-6IwPS3BD0IL/qMy/pJTAvoe9iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf
-+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS8cE54+X1+NZK3TTN+2/BT+MAi1bi
-kvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2HcqtbepBEX4tdJP7
-wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxSvTOB
-TI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6C
-MUO+1918oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn
-4rnvyOL2NSl6dPrFf4IFYqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+I
-aFvowdlxfv1k7/9nR4hYJS8+hge9+6jlgqispdNpQ80xiEmEU5LAsTkbOYMBMMTy
-qfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg==
------END CERTIFICATE-----
-
# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz
# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz
# Label: "ANF Secure Server Root CA"
@@ -4473,128 +4002,6 @@ UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj
/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA==
-----END CERTIFICATE-----
-# Issuer: CN=CommScope Public Trust ECC Root-01 O=CommScope
-# Subject: CN=CommScope Public Trust ECC Root-01 O=CommScope
-# Label: "CommScope Public Trust ECC Root-01"
-# Serial: 385011430473757362783587124273108818652468453534
-# MD5 Fingerprint: 3a:40:a7:fc:03:8c:9c:38:79:2f:3a:a2:6c:b6:0a:16
-# SHA1 Fingerprint: 07:86:c0:d8:dd:8e:c0:80:98:06:98:d0:58:7a:ef:de:a6:cc:a2:5d
-# SHA256 Fingerprint: 11:43:7c:da:7b:b4:5e:41:36:5f:45:b3:9a:38:98:6b:0d:e0:0d:ef:34:8e:0c:7b:b0:87:36:33:80:0b:c3:8b
------BEGIN CERTIFICATE-----
-MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMw
-TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t
-bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNa
-Fw00NjA0MjgxNzM1NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv
-cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEw
-djAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16ocNfQj3Rid8NeeqrltqLxeP0C
-flfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOcw5tjnSCDPgYLpkJE
-hRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
-VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq
-hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg
-2NED3W3ROT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uS
-Um9poIyNStDuiw7LR47QjRE=
------END CERTIFICATE-----
-
-# Issuer: CN=CommScope Public Trust ECC Root-02 O=CommScope
-# Subject: CN=CommScope Public Trust ECC Root-02 O=CommScope
-# Label: "CommScope Public Trust ECC Root-02"
-# Serial: 234015080301808452132356021271193974922492992893
-# MD5 Fingerprint: 59:b0:44:d5:65:4d:b8:5c:55:19:92:02:b6:d1:94:b2
-# SHA1 Fingerprint: 3c:3f:ef:57:0f:fe:65:93:86:9e:a0:fe:b0:f6:ed:8e:d1:13:c7:e5
-# SHA256 Fingerprint: 2f:fb:7f:81:3b:bb:b3:c8:9a:b4:e8:16:2d:0f:16:d7:15:09:a8:30:cc:9d:73:c2:62:e5:14:08:75:d1:ad:4a
------BEGIN CERTIFICATE-----
-MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMw
-TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t
-bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRa
-Fw00NjA0MjgxNzQ0NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv
-cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIw
-djAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l63FRD/cHB8o5mXxO1Q/MMDAL
-j2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/ubCK1sK9IRQq9qEmU
-v4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
-VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq
-hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/n
-ich/m35rChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AV
-mkzw5l4lIhVtwodZ0LKOag==
------END CERTIFICATE-----
-
-# Issuer: CN=CommScope Public Trust RSA Root-01 O=CommScope
-# Subject: CN=CommScope Public Trust RSA Root-01 O=CommScope
-# Label: "CommScope Public Trust RSA Root-01"
-# Serial: 354030733275608256394402989253558293562031411421
-# MD5 Fingerprint: 0e:b4:15:bc:87:63:5d:5d:02:73:d4:26:38:68:73:d8
-# SHA1 Fingerprint: 6d:0a:5f:f7:b4:23:06:b4:85:b3:b7:97:64:fc:ac:75:f5:33:f2:93
-# SHA256 Fingerprint: 02:bd:f9:6e:2a:45:dd:9b:f1:8f:c7:e1:db:df:21:a0:37:9b:a3:c9:c2:61:03:44:cf:d8:d6:06:fe:c1:ed:81
------BEGIN CERTIFICATE-----
-MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQEL
-BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi
-Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1
-NTRaFw00NjA0MjgxNjQ1NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t
-U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt
-MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwSGWjDR1C45FtnYSk
-YZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2bKOx7dAvnQmtVzslh
-suitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBWBB0HW0al
-DrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj
-WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFl
-P8GMicWWMJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547
-KI2DAg+pn3LiLCuz3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7p
-UcZsBSjBAg/pGG3svZwG1KdJ9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/
-kQO9lLvkuI6cMmPNn7togbGEW682v3fuHX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JO
-Hg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcmAyejOQoBqsCyMWCDIqFPEgkB
-Ea801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8eKg1mp9BFM6ltM6U
-CBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
-A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ
-KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6
-NWPxzIHIxgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQ
-nmhUQo8mUuJM3y+Xpi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+
-QgvfKNmwrZggvkN80V4aCRckjXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2v
-trV0KnahP/t1MJ+UXjulYPPLXAziDslg+MkfFoom3ecnf+slpoq9uC02EJqxWE2a
-aE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0DLwAHb/WNyVntHKLr4W96ioD
-j8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/xBpMu95yo9GA+o/E4
-Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054A5U+1C0w
-lREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn
-YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVoc
-icCMb3SgazNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw
------END CERTIFICATE-----
-
-# Issuer: CN=CommScope Public Trust RSA Root-02 O=CommScope
-# Subject: CN=CommScope Public Trust RSA Root-02 O=CommScope
-# Label: "CommScope Public Trust RSA Root-02"
-# Serial: 480062499834624527752716769107743131258796508494
-# MD5 Fingerprint: e1:29:f9:62:7b:76:e2:96:6d:f3:d4:d7:0f:ae:1f:aa
-# SHA1 Fingerprint: ea:b0:e2:52:1b:89:93:4c:11:68:f2:d8:9a:ac:22:4c:a3:8a:57:ae
-# SHA256 Fingerprint: ff:e9:43:d7:93:42:4b:4f:7c:44:0c:1c:3d:64:8d:53:63:f3:4b:82:dc:87:aa:7a:9f:11:8f:c5:de:e1:01:f1
------BEGIN CERTIFICATE-----
-MIIFbDCCA1SgAwIBAgIUVBa/O345lXGN0aoApYYNK496BU4wDQYJKoZIhvcNAQEL
-BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi
-Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMjAeFw0yMTA0MjgxNzE2
-NDNaFw00NjA0MjgxNzE2NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t
-U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt
-MDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDh+g77aAASyE3VrCLE
-NQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mnG2I81lDnNJUDMrG0
-kyI9p+Kx7eZ7Ti6Hmw0zdQreqjXnfuU2mKKuJZ6VszKWpCtYHu8//mI0SFHRtI1C
-rWDaSWqVcN3SAOLMV2MCe5bdSZdbkk6V0/nLKR8YSvgBKtJjCW4k6YnS5cciTNxz
-hkcAqg2Ijq6FfUrpuzNPDlJwnZXjfG2WWy09X6GDRl224yW4fKcZgBzqZUPckXk2
-LHR88mcGyYnJ27/aaL8j7dxrrSiDeS/sOKUNNwFnJ5rpM9kzXzehxfCrPfp4sOcs
-n/Y+n2Dg70jpkEUeBVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIfhs1w/tku
-FT0du7jyU1fbzMZ0KZwYszZ1OC4PVKH4kh+Jlk+71O6d6Ts2QrUKOyrUZHk2EOH5
-kQMreyBUzQ0ZGshBMjTRsJnhkB4BQDa1t/qp5Xd1pCKBXbCL5CcSD1SIxtuFdOa3
-wNemKfrb3vOTlycEVS8KbzfFPROvCgCpLIscgSjX74Yxqa7ybrjKaixUR9gqiC6v
-wQcQeKwRoi9C8DfF8rhW3Q5iLc4tVn5V8qdE9isy9COoR+jUKgF4z2rDN6ieZdIs
-5fq6M8EGRPbmz6UNp2YINIos8wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
-A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUR9DnsSL/nSz12Vdgs7GxcJXvYXowDQYJ
-KoZIhvcNAQELBQADggIBAIZpsU0v6Z9PIpNojuQhmaPORVMbc0RTAIFhzTHjCLqB
-KCh6krm2qMhDnscTJk3C2OVVnJJdUNjCK9v+5qiXz1I6JMNlZFxHMaNlNRPDk7n3
-+VGXu6TwYofF1gbTl4MgqX67tiHCpQ2EAOHyJxCDut0DgdXdaMNmEMjRdrSzbyme
-APnCKfWxkxlSaRosTKCL4BWaMS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3Nyq
-pgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv41xdgSGn2rtO/+YHqP65DSdsu3BaVXoT
-6fEqSWnHX4dXTEN5bTpl6TBcQe7rd6VzEojov32u5cSoHw2OHG1QAk8mGEPej1WF
-sQs3BWDJVTkSBKEqz3EWnzZRSb9wO55nnPt7eck5HHisd5FUmrh1CoFSl+NmYWvt
-PjgelmFV4ZFUjO2MJB+ByRCac5krFk5yAD9UG/iNuovnFNa2RU9g7Jauwy8CTl2d
-lklyALKrdVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670
-v64fG9PiO/yzcnMcmyiQiRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17O
-rg3bhzjlP1v9mxnhMUF6cKojawHhRUzNlM47ni3niAIi9G7oyOzWPPO5std3eqx7
------END CERTIFICATE-----
-
# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH
# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH
# Label: "Telekom Security TLS ECC Root 2020"
@@ -4855,6 +4262,68 @@ knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ
hJ65bvspmZDogNOfJA==
-----END CERTIFICATE-----
+# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc.
+# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc.
+# Label: "TrustAsia TLS ECC Root CA"
+# Serial: 310892014698942880364840003424242768478804666567
+# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c
+# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36
+# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11
+-----BEGIN CERTIFICATE-----
+MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw
+WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
+IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw
+NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE
+ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB
+c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/
+AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp
+guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw
+DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01
+L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR
+OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ==
+-----END CERTIFICATE-----
+
+# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc.
+# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc.
+# Label: "TrustAsia TLS RSA Root CA"
+# Serial: 160405846464868906657516898462547310235378010780
+# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12
+# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa
+# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3
+-----BEGIN CERTIFICATE-----
+MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM
+BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp
+ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN
+MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG
+A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1
+c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
+AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+
+NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ
+Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561
+HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32
+ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb
+xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX
+i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ
+UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j
+TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT
+bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8
+S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA
+MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT
+MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3
+Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4
+iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt
+7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp
+2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ
+g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj
+pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M
+pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP
+XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe
+SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0
+ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy
+323imttUQ/hHWKNddBWcwauwxzQ=
+-----END CERTIFICATE-----
+
# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH
# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH
# Label: "D-TRUST EV Root CA 2 2023"
@@ -4895,3 +4364,131 @@ gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst
Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh
XBxvWHZks/wCuPWdCg==
-----END CERTIFICATE-----
+
+# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG
+# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG
+# Label: "SwissSign RSA TLS Root CA 2022 - 1"
+# Serial: 388078645722908516278762308316089881486363258315
+# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39
+# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce
+# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41
+-----BEGIN CERTIFICATE-----
+MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL
+BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE
+AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx
+MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT
+d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg
+MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX
+vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7
+LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX
+5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE
+EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt
+/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x
+0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5
+KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM
+0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd
+OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta
+clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK
+wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD
+AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4
+DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL
+BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3
+10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz
+Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ
+iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc
+gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM
+ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF
+LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp
+zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td
+Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0
+rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO
+gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ
+-----END CERTIFICATE-----
+
+# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation
+# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation
+# Label: "OISTE Server Root ECC G1"
+# Serial: 47819833811561661340092227008453318557
+# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4
+# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2
+# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49
+-----BEGIN CERTIFICATE-----
+MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw
+CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY
+T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy
+NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp
+b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49
+AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy
+cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N
+2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3
+TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C
+tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR
+QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD
+YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c=
+-----END CERTIFICATE-----
+
+# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation
+# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation
+# Label: "OISTE Server Root RSA G1"
+# Serial: 113845518112613905024960613408179309848
+# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68
+# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e
+# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6
+-----BEGIN CERTIFICATE-----
+MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL
+MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE
+AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4
+MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k
+YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ
+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM
+vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b
+rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk
+ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z
+O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R
+tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS
+jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh
+sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho
+mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu
++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR
+i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT
+kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
+8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2
+zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33
+I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG
+5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8
+qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP
+AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk
+gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs
+YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1
+9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome
+/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3
+J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2
+wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy
+BiElxky8j3C7DOReIoMt0r7+hVu05L0=
+-----END CERTIFICATE-----
+
+# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd.
+# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd.
+# Label: "e-Szigno TLS Root CA 2023"
+# Serial: 71934828665710877219916191754
+# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71
+# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe
+# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4
+-----BEGIN CERTIFICATE-----
+MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD
+VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0
+ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU
+TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow
+dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy
+b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T
+emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE
+AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS
+AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v
+SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB
+Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K
+ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI
+zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt
+y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl
+C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6
+uWWL
+-----END CERTIFICATE-----
diff --git a/addons/source-python/packages/site-packages/certifi/core.py b/addons/source-python/packages/site-packages/certifi/core.py
index 91f538bb1..1c9661cc7 100644
--- a/addons/source-python/packages/site-packages/certifi/core.py
+++ b/addons/source-python/packages/site-packages/certifi/core.py
@@ -46,7 +46,7 @@ def where() -> str:
def contents() -> str:
return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii")
-elif sys.version_info >= (3, 7):
+else:
from importlib.resources import path as get_path, read_text
@@ -81,34 +81,3 @@ def where() -> str:
def contents() -> str:
return read_text("certifi", "cacert.pem", encoding="ascii")
-
-else:
- import os
- import types
- from typing import Union
-
- Package = Union[types.ModuleType, str]
- Resource = Union[str, "os.PathLike"]
-
- # This fallback will work for Python versions prior to 3.7 that lack the
- # importlib.resources module but relies on the existing `where` function
- # so won't address issues with environments like PyOxidizer that don't set
- # __file__ on modules.
- def read_text(
- package: Package,
- resource: Resource,
- encoding: str = 'utf-8',
- errors: str = 'strict'
- ) -> str:
- with open(where(), encoding=encoding) as data:
- return data.read()
-
- # If we don't have importlib.resources, then we will just do the old logic
- # of assuming we're on the filesystem and munge the path directly.
- def where() -> str:
- f = os.path.dirname(__file__)
-
- return os.path.join(f, "cacert.pem")
-
- def contents() -> str:
- return read_text("certifi", "cacert.pem", encoding="ascii")
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/api.py b/addons/source-python/packages/site-packages/charset_normalizer/api.py
index 2c8c0618c..1f32091f2 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/api.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/api.py
@@ -10,7 +10,13 @@
mb_encoding_languages,
merge_coherence_ratios,
)
-from .constant import IANA_SUPPORTED, TOO_BIG_SEQUENCE, TOO_SMALL_SEQUENCE, TRACE
+from .constant import (
+ IANA_SUPPORTED,
+ IANA_SUPPORTED_SIMILAR,
+ TOO_BIG_SEQUENCE,
+ TOO_SMALL_SEQUENCE,
+ TRACE,
+)
from .md import mess_ratio
from .models import CharsetMatch, CharsetMatches
from .utils import (
@@ -18,7 +24,6 @@
cut_sequence_chunks,
iana_name,
identify_sig_or_bom,
- is_cp_similar,
is_multi_byte_encoding,
should_strip_sig_or_bom,
)
@@ -29,6 +34,25 @@
logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
)
+# Pre-compute a reordered encoding list: multibyte first, then single-byte.
+# This allows the mb_definitive_match optimization to fire earlier, skipping
+# all single-byte encodings for genuine CJK content. Multibyte codecs
+# hard-fail (UnicodeDecodeError) on single-byte data almost instantly, so
+# testing them first costs negligible time for non-CJK files.
+_mb_supported: list[str] = []
+_sb_supported: list[str] = []
+
+for _supported_enc in IANA_SUPPORTED:
+ try:
+ if is_multi_byte_encoding(_supported_enc):
+ _mb_supported.append(_supported_enc)
+ else:
+ _sb_supported.append(_supported_enc)
+ except ImportError:
+ _sb_supported.append(_supported_enc)
+
+IANA_SUPPORTED_MB_FIRST: list[str] = _mb_supported + _sb_supported
+
def from_bytes(
sequences: bytes | bytearray,
@@ -78,7 +102,7 @@ def from_bytes(
logger.debug("Encoding detection on empty bytes, assuming utf_8 intention.")
if explain: # Defensive: ensure exit path clean handler
logger.removeHandler(explain_handler)
- logger.setLevel(previous_logger_level or logging.WARNING)
+ logger.setLevel(previous_logger_level)
return CharsetMatches([CharsetMatch(sequences, "utf_8", 0.0, False, [], "")])
if cp_isolation is not None:
@@ -152,6 +176,40 @@ def from_bytes(
tested: set[str] = set()
tested_but_hard_failure: list[str] = []
tested_but_soft_failure: list[str] = []
+ soft_failure_skip: set[str] = set()
+ success_fast_tracked: set[str] = set()
+
+ # Cache for decoded payload deduplication: hash(decoded_payload) -> (mean_mess_ratio, cd_ratios_merged, passed)
+ # When multiple encodings decode to the exact same string, we can skip the expensive
+ # mess_ratio and coherence_ratio analysis and reuse the results from the first encoding.
+ payload_result_cache: dict[int, tuple[float, list[tuple[str, float]], bool]] = {}
+
+ # When a definitive result (chaos=0.0 and good coherence) is found after testing
+ # the prioritized encodings (ascii, utf_8), we can significantly reduce the remaining
+ # work. Encodings that target completely different language families (e.g., Cyrillic
+ # when the definitive match is Latin) are skipped entirely.
+ # Additionally, for same-family encodings that pass chaos probing, we reuse the
+ # definitive match's coherence ratios instead of recomputing them — a major savings
+ # since coherence_ratio accounts for ~30% of total time on slow Latin files.
+ definitive_match_found: bool = False
+ definitive_target_languages: set[str] = set()
+ # After the definitive match fires, we cap the number of additional same-family
+ # single-byte encodings that pass chaos probing. Once we've accumulated enough
+ # good candidates (N), further same-family SB encodings are unlikely to produce
+ # a better best() result and just waste mess_ratio + coherence_ratio time.
+ # The first encoding to trigger the definitive match is NOT counted (it's already in).
+ post_definitive_sb_success_count: int = 0
+ POST_DEFINITIVE_SB_CAP: int = 7
+
+ # When a non-UTF multibyte encoding passes chaos probing with significant multibyte
+ # content (decoded length < 98% of raw length), skip all remaining single-byte encodings.
+ # Rationale: multi-byte decoders (CJK) have strict byte-sequence validation — if they
+ # decode without error AND pass chaos probing with substantial multibyte content, the
+ # data is genuinely multibyte encoded. Single-byte encodings will always decode (every
+ # byte maps to something) but waste time on mess_ratio before failing.
+ # The 98% threshold prevents false triggers on files that happen to have a few valid
+ # multibyte pairs (e.g., cp424/_ude_1.txt where big5 decodes with 99% ratio).
+ mb_definitive_match_found: bool = False
fallback_ascii: CharsetMatch | None = None
fallback_u8: CharsetMatch | None = None
@@ -177,7 +235,7 @@ def from_bytes(
if "utf_8" not in prioritized_encodings:
prioritized_encodings.append("utf_8")
- for encoding_iana in prioritized_encodings + IANA_SUPPORTED:
+ for encoding_iana in prioritized_encodings + IANA_SUPPORTED_MB_FIRST:
if cp_isolation and encoding_iana not in cp_isolation:
continue
@@ -210,9 +268,28 @@ def from_bytes(
)
continue
+ # Skip encodings similar to ones that already soft-failed (high mess ratio).
+ # Checked BEFORE the expensive decode attempt.
+ if encoding_iana in soft_failure_skip:
+ logger.log(
+ TRACE,
+ "%s is deemed too similar to a code page that was already considered unsuited. Continuing!",
+ encoding_iana,
+ )
+ continue
+
+ # Skip encodings that were already fast-tracked from a similar successful encoding.
+ if encoding_iana in success_fast_tracked:
+ logger.log(
+ TRACE,
+ "Skipping %s: already fast-tracked from a similar successful encoding.",
+ encoding_iana,
+ )
+ continue
+
try:
is_multi_byte_decoder: bool = is_multi_byte_encoding(encoding_iana)
- except (ModuleNotFoundError, ImportError):
+ except (ModuleNotFoundError, ImportError): # Defensive:
logger.log(
TRACE,
"Encoding %s does not provide an IncrementalDecoder",
@@ -220,6 +297,55 @@ def from_bytes(
)
continue
+ # When we've already found a definitive match (chaos=0.0 with good coherence)
+ # after testing the prioritized encodings, skip encodings that target
+ # completely different language families. This avoids running expensive
+ # mess_ratio + coherence_ratio on clearly unrelated candidates (e.g., Cyrillic
+ # when the definitive match is Latin-based).
+ if definitive_match_found:
+ if not is_multi_byte_decoder:
+ enc_languages = set(encoding_languages(encoding_iana))
+ else:
+ enc_languages = set(mb_encoding_languages(encoding_iana))
+ if not enc_languages.intersection(definitive_target_languages):
+ logger.log(
+ TRACE,
+ "Skipping %s: definitive match already found, this encoding targets different languages (%s vs %s).",
+ encoding_iana,
+ enc_languages,
+ definitive_target_languages,
+ )
+ continue
+
+ # After the definitive match, cap the number of additional same-family
+ # single-byte encodings that pass chaos probing. This avoids testing the
+ # tail of rare, low-value same-family encodings (mac_iceland, cp860, etc.)
+ # that almost never change best() but each cost ~1-2ms of mess_ratio + coherence.
+ if (
+ definitive_match_found
+ and not is_multi_byte_decoder
+ and post_definitive_sb_success_count >= POST_DEFINITIVE_SB_CAP
+ ):
+ logger.log(
+ TRACE,
+ "Skipping %s: already accumulated %d same-family results after definitive match (cap=%d).",
+ encoding_iana,
+ post_definitive_sb_success_count,
+ POST_DEFINITIVE_SB_CAP,
+ )
+ continue
+
+ # When a multibyte encoding with significant multibyte content has already
+ # passed chaos probing, skip all single-byte encodings. They will either fail
+ # chaos probing (wasting mess_ratio time) or produce inferior results.
+ if mb_definitive_match_found and not is_multi_byte_decoder:
+ logger.log(
+ TRACE,
+ "Skipping single-byte %s: multi-byte definitive match already found.",
+ encoding_iana,
+ )
+ continue
+
try:
if is_too_large_sequence and is_multi_byte_decoder is False:
str(
@@ -250,22 +376,6 @@ def from_bytes(
tested_but_hard_failure.append(encoding_iana)
continue
- similar_soft_failure_test: bool = False
-
- for encoding_soft_failed in tested_but_soft_failure:
- if is_cp_similar(encoding_iana, encoding_soft_failed):
- similar_soft_failure_test = True
- break
-
- if similar_soft_failure_test:
- logger.log(
- TRACE,
- "%s is deemed too similar to code page %s and was consider unsuited already. Continuing!",
- encoding_iana,
- encoding_soft_failed,
- )
- continue
-
r_ = range(
0 if not bom_or_sig_available else len(sig_payload),
length,
@@ -286,6 +396,108 @@ def from_bytes(
encoding_iana,
)
+ # Payload-hash deduplication: if another encoding already decoded to the
+ # exact same string, reuse its mess_ratio and coherence results entirely.
+ # This is strictly more general than the old IANA_SUPPORTED_SIMILAR approach
+ # because it catches ALL identical decoding, not just pre-mapped ones.
+ if decoded_payload is not None and not is_multi_byte_decoder:
+ payload_hash: int = hash(decoded_payload)
+ cached = payload_result_cache.get(payload_hash)
+ if cached is not None:
+ cached_mess, cached_cd, cached_passed = cached
+ if cached_passed:
+ # The previous encoding with identical output passed chaos probing.
+ fast_match = CharsetMatch(
+ sequences,
+ encoding_iana,
+ cached_mess,
+ bom_or_sig_available,
+ cached_cd,
+ (
+ decoded_payload
+ if (
+ is_too_large_sequence is False
+ or encoding_iana
+ in [specified_encoding, "ascii", "utf_8"]
+ )
+ else None
+ ),
+ preemptive_declaration=specified_encoding,
+ )
+ results.append(fast_match)
+ success_fast_tracked.add(encoding_iana)
+ logger.log(
+ TRACE,
+ "%s fast-tracked (identical decoded payload to a prior encoding, chaos=%f %%).",
+ encoding_iana,
+ round(cached_mess * 100, ndigits=3),
+ )
+
+ if (
+ encoding_iana in [specified_encoding, "ascii", "utf_8"]
+ and cached_mess < 0.1
+ ):
+ if cached_mess == 0.0:
+ logger.debug(
+ "Encoding detection: %s is most likely the one.",
+ fast_match.encoding,
+ )
+ if explain:
+ logger.removeHandler(explain_handler)
+ logger.setLevel(previous_logger_level)
+ return CharsetMatches([fast_match])
+ early_stop_results.append(fast_match)
+
+ if (
+ len(early_stop_results)
+ and (specified_encoding is None or specified_encoding in tested)
+ and "ascii" in tested
+ and "utf_8" in tested
+ ):
+ probable_result: CharsetMatch = early_stop_results.best() # type: ignore[assignment]
+ logger.debug(
+ "Encoding detection: %s is most likely the one.",
+ probable_result.encoding,
+ )
+ if explain:
+ logger.removeHandler(explain_handler)
+ logger.setLevel(previous_logger_level)
+ return CharsetMatches([probable_result])
+
+ continue
+ else:
+ # The previous encoding with identical output failed chaos probing.
+ tested_but_soft_failure.append(encoding_iana)
+ logger.log(
+ TRACE,
+ "%s fast-skipped (identical decoded payload to a prior encoding that failed chaos probing).",
+ encoding_iana,
+ )
+ # Prepare fallbacks for special encodings even when skipped.
+ if enable_fallback and encoding_iana in [
+ "ascii",
+ "utf_8",
+ specified_encoding,
+ "utf_16",
+ "utf_32",
+ ]:
+ fallback_entry = CharsetMatch(
+ sequences,
+ encoding_iana,
+ threshold,
+ bom_or_sig_available,
+ [],
+ decoded_payload,
+ preemptive_declaration=specified_encoding,
+ )
+ if encoding_iana == specified_encoding:
+ fallback_specified = fallback_entry
+ elif encoding_iana == "ascii":
+ fallback_ascii = fallback_entry
+ else:
+ fallback_u8 = fallback_entry
+ continue
+
max_chunk_gave_up: int = int(len(r_) / 4)
max_chunk_gave_up = max(max_chunk_gave_up, 2)
@@ -358,6 +570,14 @@ def from_bytes(
mean_mess_ratio: float = sum(md_ratios) / len(md_ratios) if md_ratios else 0.0
if mean_mess_ratio >= threshold or early_stop_count >= max_chunk_gave_up:
tested_but_soft_failure.append(encoding_iana)
+ if encoding_iana in IANA_SUPPORTED_SIMILAR:
+ soft_failure_skip.update(IANA_SUPPORTED_SIMILAR[encoding_iana])
+ # Cache this soft-failure so identical decoding from other encodings
+ # can be skipped immediately.
+ if decoded_payload is not None and not is_multi_byte_decoder:
+ payload_result_cache.setdefault(
+ hash(decoded_payload), (mean_mess_ratio, [], False)
+ )
logger.log(
TRACE,
"%s was excluded because of initial chaos probing. Gave up %i time(s). "
@@ -369,14 +589,15 @@ def from_bytes(
# Preparing those fallbacks in case we got nothing.
if (
enable_fallback
- and encoding_iana in ["ascii", "utf_8", specified_encoding]
+ and encoding_iana
+ in ["ascii", "utf_8", specified_encoding, "utf_16", "utf_32"]
and not lazy_str_hard_failure
):
fallback_entry = CharsetMatch(
sequences,
encoding_iana,
threshold,
- False,
+ bom_or_sig_available,
[],
decoded_payload,
preemptive_declaration=specified_encoding,
@@ -411,9 +632,14 @@ def from_bytes(
cd_ratios = []
- # We shall skip the CD when its about ASCII
- # Most of the time its not relevant to run "language-detection" on it.
+ # Run coherence detection on all chunks. We previously tried limiting to
+ # 1-2 chunks for post-definitive encodings to save time, but this caused
+ # coverage regressions by producing unrepresentative coherence scores.
+ # The SB cap and language-family skip optimizations provide sufficient
+ # speedup without sacrificing coherence accuracy.
if encoding_iana != "ascii":
+ # We shall skip the CD when its about ASCII
+ # Most of the time its not relevant to run "language-detection" on it.
for chunk in md_chunks:
chunk_languages = coherence_ratio(
chunk,
@@ -422,8 +648,9 @@ def from_bytes(
)
cd_ratios.append(chunk_languages)
-
- cd_ratios_merged = merge_coherence_ratios(cd_ratios)
+ cd_ratios_merged = merge_coherence_ratios(cd_ratios)
+ else:
+ cd_ratios_merged = merge_coherence_ratios(cd_ratios)
if cd_ratios_merged:
logger.log(
@@ -452,6 +679,25 @@ def from_bytes(
results.append(current_match)
+ # Cache the successful result for payload-hash deduplication.
+ if decoded_payload is not None and not is_multi_byte_decoder:
+ payload_result_cache.setdefault(
+ hash(decoded_payload),
+ (mean_mess_ratio, cd_ratios_merged, True),
+ )
+
+ # Count post-definitive same-family SB successes for the early termination cap.
+ # Only count low-mess encodings (< 2%) toward the cap. High-mess encodings are
+ # marginal results that shouldn't prevent better-quality candidates from being
+ # tested. For example, iso8859_4 (mess=0%) should not be skipped just because
+ # 7 high-mess Latin encodings (cp1252 at 8%, etc.) were tried first.
+ if (
+ definitive_match_found
+ and not is_multi_byte_decoder
+ and mean_mess_ratio < 0.02
+ ):
+ post_definitive_sb_success_count += 1
+
if (
encoding_iana in [specified_encoding, "ascii", "utf_8"]
and mean_mess_ratio < 0.1
@@ -475,10 +721,10 @@ def from_bytes(
and "ascii" in tested
and "utf_8" in tested
):
- probable_result: CharsetMatch = early_stop_results.best() # type: ignore[assignment]
+ probable_result = early_stop_results.best() # type: ignore[assignment]
logger.debug(
"Encoding detection: %s is most likely the one.",
- probable_result.encoding,
+ probable_result.encoding, # type: ignore[union-attr]
)
if explain: # Defensive: ensure exit path clean handler
logger.removeHandler(explain_handler)
@@ -486,6 +732,66 @@ def from_bytes(
return CharsetMatches([probable_result])
+ # Once we find a result with good coherence (>= 0.5) after testing the
+ # prioritized encodings (ascii, utf_8), activate "definitive mode": skip
+ # encodings that target completely different language families. This avoids
+ # running expensive mess_ratio + coherence_ratio on clearly unrelated
+ # candidates (e.g., Cyrillic encodings when the match is Latin-based).
+ # We require coherence >= 0.5 to avoid false positives (e.g., cp1251 decoding
+ # Hebrew text with 0.0 chaos but wrong language detection at coherence 0.33).
+ if not definitive_match_found and not is_multi_byte_decoder:
+ best_coherence = (
+ max((v for _, v in cd_ratios_merged), default=0.0)
+ if cd_ratios_merged
+ else 0.0
+ )
+ if best_coherence >= 0.5 and "ascii" in tested and "utf_8" in tested:
+ definitive_match_found = True
+ definitive_target_languages.update(target_languages)
+ logger.log(
+ TRACE,
+ "Definitive match found: %s (chaos=%.3f, coherence=%.2f). Encodings targeting different language families will be skipped.",
+ encoding_iana,
+ mean_mess_ratio,
+ best_coherence,
+ )
+
+ # When a non-UTF multibyte encoding passes chaos probing with significant
+ # multibyte content (decoded < 98% of raw), activate mb_definitive_match.
+ # This skips all remaining single-byte encodings which would either soft-fail
+ # (running expensive mess_ratio for nothing) or produce inferior results.
+ if (
+ not mb_definitive_match_found
+ and is_multi_byte_decoder
+ and multi_byte_bonus
+ and decoded_payload is not None
+ and len(decoded_payload) < length * 0.98
+ and encoding_iana
+ not in {
+ "utf_8",
+ "utf_8_sig",
+ "utf_16",
+ "utf_16_be",
+ "utf_16_le",
+ "utf_32",
+ "utf_32_be",
+ "utf_32_le",
+ "utf_7",
+ }
+ and "ascii" in tested
+ and "utf_8" in tested
+ ):
+ mb_definitive_match_found = True
+ logger.log(
+ TRACE,
+ "Multi-byte definitive match: %s (chaos=%.3f, decoded=%d/%d=%.1f%%). Single-byte encodings will be skipped.",
+ encoding_iana,
+ mean_mess_ratio,
+ len(decoded_payload),
+ length,
+ len(decoded_payload) / length * 100,
+ )
+
if encoding_iana == sig_encoding:
logger.debug(
"Encoding detection: %s is most likely the one as we detected a BOM or SIG within "
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/cd.cp313-win_amd64.pyd b/addons/source-python/packages/site-packages/charset_normalizer/cd.cp313-win_amd64.pyd
new file mode 100644
index 000000000..cdb777a32
Binary files /dev/null and b/addons/source-python/packages/site-packages/charset_normalizer/cd.cp313-win_amd64.pyd differ
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/cd.py b/addons/source-python/packages/site-packages/charset_normalizer/cd.py
index 71a3ed519..9545d35d1 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/cd.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/cd.py
@@ -12,6 +12,8 @@
LANGUAGE_SUPPORTED_COUNT,
TOO_SMALL_SEQUENCE,
ZH_NAMES,
+ _FREQUENCIES_SET,
+ _FREQUENCIES_RANK,
)
from .md import is_suspiciously_successive_range
from .models import CoherenceMatches
@@ -29,7 +31,9 @@ def encoding_unicode_range(iana_name: str) -> list[str]:
Return associated unicode ranges in a single byte code page.
"""
if is_multi_byte_encoding(iana_name):
- raise OSError("Function not supported on multi-byte code page")
+ raise OSError( # Defensive:
+ "Function not supported on multi-byte code page"
+ )
decoder = importlib.import_module(f"encodings.{iana_name}").IncrementalDecoder
@@ -142,6 +146,7 @@ def alphabet_languages(
"""
languages: list[tuple[str, float]] = []
+ characters_set: frozenset[str] = frozenset(characters)
source_have_accents = any(is_accentuated(character) for character in characters)
for language, language_characters in FREQUENCIES.items():
@@ -155,9 +160,7 @@ def alphabet_languages(
character_count: int = len(language_characters)
- character_match_count: int = len(
- [c for c in language_characters if c in characters]
- )
+ character_match_count: int = len(_FREQUENCIES_SET[language] & characters_set)
ratio: float = character_match_count / character_count
@@ -178,26 +181,46 @@ def characters_popularity_compare(
Beware that is function is not strict on the match in order to ease the detection. (Meaning close match is 1.)
"""
if language not in FREQUENCIES:
- raise ValueError(f"{language} not available")
+ raise ValueError(f"{language} not available") # Defensive:
character_approved_count: int = 0
- FREQUENCIES_language_set = set(FREQUENCIES[language])
+ frequencies_language_set: frozenset[str] = _FREQUENCIES_SET[language]
+ lang_rank: dict[str, int] = _FREQUENCIES_RANK[language]
ordered_characters_count: int = len(ordered_characters)
target_language_characters_count: int = len(FREQUENCIES[language])
large_alphabet: bool = target_language_characters_count > 26
+ expected_projection_ratio: float = (
+ target_language_characters_count / ordered_characters_count
+ )
+
+ # Pre-built rank dict for ordered_characters (avoids repeated list slicing).
+ ordered_rank: dict[str, int] = {
+ char: rank for rank, char in enumerate(ordered_characters)
+ }
+
+ # Pre-compute characters common to both orderings.
+ # Avoids repeated `c in ordered_rank` dict lookups in the inner counts.
+ common_chars: list[tuple[int, int]] = [
+ (lr, ordered_rank[c]) for c, lr in lang_rank.items() if c in ordered_rank
+ ]
+
+ # Pre-extract lr and orr arrays for faster iteration in the inner loop.
+ # Plain integer loops with local arrays are much faster under mypyc than
+ # generator expression sums over a list of tuples.
+ common_count: int = len(common_chars)
+ common_lr: list[int] = [p[0] for p in common_chars]
+ common_orr: list[int] = [p[1] for p in common_chars]
+
for character, character_rank in zip(
ordered_characters, range(0, ordered_characters_count)
):
- if character not in FREQUENCIES_language_set:
+ if character not in frequencies_language_set:
continue
- character_rank_in_language: int = FREQUENCIES[language].index(character)
- expected_projection_ratio: float = (
- target_language_characters_count / ordered_characters_count
- )
+ character_rank_in_language: int = lang_rank[character]
character_rank_projection: int = int(character_rank * expected_projection_ratio)
if (
@@ -214,35 +237,36 @@ def characters_popularity_compare(
character_approved_count += 1
continue
- characters_before_source: list[str] = FREQUENCIES[language][
- 0:character_rank_in_language
- ]
- characters_after_source: list[str] = FREQUENCIES[language][
- character_rank_in_language:
- ]
- characters_before: list[str] = ordered_characters[0:character_rank]
- characters_after: list[str] = ordered_characters[character_rank:]
-
- before_match_count: int = len(
- set(characters_before) & set(characters_before_source)
- )
-
- after_match_count: int = len(
- set(characters_after) & set(characters_after_source)
- )
-
- if len(characters_before_source) == 0 and before_match_count <= 4:
+ # Count how many characters appear "before" in both orderings,
+ # and how many appear "at or after" in both orderings.
+ # Single pass over pre-extracted arrays — much faster under mypyc
+ # than two generator expression sums.
+ before_match_count: int = 0
+ after_match_count: int = 0
+ for i in range(common_count):
+ lr_i: int = common_lr[i]
+ orr_i: int = common_orr[i]
+ if lr_i < character_rank_in_language:
+ if orr_i < character_rank:
+ before_match_count += 1
+ else:
+ if orr_i >= character_rank:
+ after_match_count += 1
+
+ after_len: int = target_language_characters_count - character_rank_in_language
+
+ if character_rank_in_language == 0 and before_match_count <= 4:
character_approved_count += 1
continue
- if len(characters_after_source) == 0 and after_match_count <= 4:
+ if after_len == 0 and after_match_count <= 4:
character_approved_count += 1
continue
if (
- before_match_count / len(characters_before_source) >= 0.4
- or after_match_count / len(characters_after_source) >= 0.4
- ):
+ character_rank_in_language > 0
+ and before_match_count / character_rank_in_language >= 0.4
+ ) or (after_len > 0 and after_match_count / after_len >= 0.4):
character_approved_count += 1
continue
@@ -255,37 +279,72 @@ def alpha_unicode_split(decoded_sequence: str) -> list[str]:
Ex. a text containing English/Latin with a bit a Hebrew will return two items in the resulting list;
One containing the latin letters and the other hebrew.
"""
- layers: dict[str, str] = {}
+ layers: dict[str, list[str]] = {}
+
+ # Fast path: track single-layer key to skip dict iteration for single-script text.
+ single_layer_key: str | None = None
+ multi_layer: bool = False
+
+ # Cache the last character_range and its resolved layer to avoid repeated
+ # is_suspiciously_successive_range calls for consecutive same-range chars.
+ prev_character_range: str | None = None
+ prev_layer_target: str | None = None
for character in decoded_sequence:
if character.isalpha() is False:
continue
- character_range: str | None = unicode_range(character)
+ # ASCII fast-path: a-z and A-Z are always "Basic Latin".
+ # Avoids unicode_range() function call overhead for the most common case.
+ character_ord: int = ord(character)
+ if character_ord < 128:
+ character_range: str | None = "Basic Latin"
+ else:
+ character_range = unicode_range(character)
if character_range is None:
continue
+ # Fast path: same range as previous character → reuse cached layer target.
+ if character_range == prev_character_range:
+ if prev_layer_target is not None:
+ layers[prev_layer_target].append(character)
+ continue
+
layer_target_range: str | None = None
- for discovered_range in layers:
+ if multi_layer:
+ for discovered_range in layers:
+ if (
+ is_suspiciously_successive_range(discovered_range, character_range)
+ is False
+ ):
+ layer_target_range = discovered_range
+ break
+ elif single_layer_key is not None:
if (
- is_suspiciously_successive_range(discovered_range, character_range)
+ is_suspiciously_successive_range(single_layer_key, character_range)
is False
):
- layer_target_range = discovered_range
- break
+ layer_target_range = single_layer_key
if layer_target_range is None:
layer_target_range = character_range
if layer_target_range not in layers:
- layers[layer_target_range] = character.lower()
- continue
+ layers[layer_target_range] = []
+ if single_layer_key is None:
+ single_layer_key = layer_target_range
+ else:
+ multi_layer = True
+
+ layers[layer_target_range].append(character)
- layers[layer_target_range] += character.lower()
+ # Cache for next iteration
+ prev_character_range = character_range
+ prev_layer_target = layer_target_range
- return list(layers.values())
+ return ["".join(chars).lower() for chars in layers.values()]
def merge_coherence_ratios(results: list[CoherenceMatches]) -> CoherenceMatches:
@@ -366,7 +425,7 @@ def coherence_ratio(
sequence_frequencies: TypeCounter[str] = Counter(layer)
most_common = sequence_frequencies.most_common()
- character_count: int = sum(o for c, o in most_common)
+ character_count: int = len(layer)
if character_count <= TOO_SMALL_SEQUENCE:
continue
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/cli/__main__.py b/addons/source-python/packages/site-packages/charset_normalizer/cli/__main__.py
index 64a290f2f..ad843c1d0 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/cli/__main__.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/cli/__main__.py
@@ -2,6 +2,7 @@
import argparse
import sys
+import typing
from json import dumps
from os.path import abspath, basename, dirname, join, realpath
from platform import python_version
@@ -13,37 +14,78 @@
from charset_normalizer.version import __version__
-def query_yes_no(question: str, default: str = "yes") -> bool:
- """Ask a yes/no question via input() and return their answer.
+def query_yes_no(question: str, default: str = "yes") -> bool: # Defensive:
+ """Ask a yes/no question via input() and return the answer as a bool."""
+ prompt = " [Y/n] " if default == "yes" else " [y/N] "
- "question" is a string that is presented to the user.
- "default" is the presumed answer if the user just hits .
- It must be "yes" (the default), "no" or None (meaning
- an answer is required of the user).
+ while True:
+ choice = input(question + prompt).strip().lower()
+ if not choice:
+ return default == "yes"
+ if choice in ("y", "yes"):
+ return True
+ if choice in ("n", "no"):
+ return False
+ print("Please respond with 'y' or 'n'.")
+
+
+class FileType:
+ """Factory for creating file object types
- The "answer" return value is True for "yes" or False for "no".
+ Instances of FileType are typically passed as type= arguments to the
+ ArgumentParser add_argument() method.
- Credit goes to (c) https://stackoverflow.com/questions/3041986/apt-command-line-interface-like-yes-no-input
+ Keyword Arguments:
+ - mode -- A string indicating how the file is to be opened. Accepts the
+ same values as the builtin open() function.
+ - bufsize -- The file's desired buffer size. Accepts the same values as
+ the builtin open() function.
+ - encoding -- The file's encoding. Accepts the same values as the
+ builtin open() function.
+ - errors -- A string indicating how encoding and decoding errors are to
+ be handled. Accepts the same value as the builtin open() function.
+
+ Backported from CPython 3.12
"""
- valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
- if default is None:
- prompt = " [y/n] "
- elif default == "yes":
- prompt = " [Y/n] "
- elif default == "no":
- prompt = " [y/N] "
- else:
- raise ValueError("invalid default answer: '%s'" % default)
- while True:
- sys.stdout.write(question + prompt)
- choice = input().lower()
- if default is not None and choice == "":
- return valid[default]
- elif choice in valid:
- return valid[choice]
- else:
- sys.stdout.write("Please respond with 'yes' or 'no' " "(or 'y' or 'n').\n")
+ def __init__(
+ self,
+ mode: str = "r",
+ bufsize: int = -1,
+ encoding: str | None = None,
+ errors: str | None = None,
+ ):
+ self._mode = mode
+ self._bufsize = bufsize
+ self._encoding = encoding
+ self._errors = errors
+
+ def __call__(self, string: str) -> typing.IO: # type: ignore[type-arg]
+ # the special argument "-" means sys.std{in,out}
+ if string == "-":
+ if "r" in self._mode:
+ return sys.stdin.buffer if "b" in self._mode else sys.stdin
+ elif any(c in self._mode for c in "wax"):
+ return sys.stdout.buffer if "b" in self._mode else sys.stdout
+ else:
+ msg = f'argument "-" with mode {self._mode}'
+ raise ValueError(msg)
+
+ # all other arguments are used as file names
+ try:
+ return open(string, self._mode, self._bufsize, self._encoding, self._errors)
+ except OSError as e:
+ message = f"can't open '{string}': {e}"
+ raise argparse.ArgumentTypeError(message)
+
+ def __repr__(self) -> str:
+ args = self._mode, self._bufsize
+ kwargs = [("encoding", self._encoding), ("errors", self._errors)]
+ args_str = ", ".join(
+ [repr(arg) for arg in args if arg != -1]
+ + [f"{kw}={arg!r}" for kw, arg in kwargs if arg is not None]
+ )
+ return f"{type(self).__name__}({args_str})"
def cli_detect(argv: list[str] | None = None) -> int:
@@ -59,7 +101,7 @@ def cli_detect(argv: list[str] | None = None) -> int:
)
parser.add_argument(
- "files", type=argparse.FileType("rb"), nargs="+", help="File(s) to be analysed"
+ "files", type=FileType("rb"), nargs="+", help="File(s) to be analysed"
)
parser.add_argument(
"-v",
@@ -202,25 +244,24 @@ def cli_detect(argv: list[str] | None = None) -> int:
)
)
else:
- x_.append(
- CliDetectionResult(
- abspath(my_file.name),
- best_guess.encoding,
- best_guess.encoding_aliases,
- [
- cp
- for cp in best_guess.could_be_from_charset
- if cp != best_guess.encoding
- ],
- best_guess.language,
- best_guess.alphabets,
- best_guess.bom,
- best_guess.percent_chaos,
- best_guess.percent_coherence,
- None,
- True,
- )
+ cli_result = CliDetectionResult(
+ abspath(my_file.name),
+ best_guess.encoding,
+ best_guess.encoding_aliases,
+ [
+ cp
+ for cp in best_guess.could_be_from_charset
+ if cp != best_guess.encoding
+ ],
+ best_guess.language,
+ best_guess.alphabets,
+ best_guess.bom,
+ best_guess.percent_chaos,
+ best_guess.percent_coherence,
+ None,
+ True,
)
+ x_.append(cli_result)
if len(matches) > 1 and args.alternatives:
for el in matches:
@@ -281,11 +322,11 @@ def cli_detect(argv: list[str] | None = None) -> int:
continue
try:
- x_[0].unicode_path = join(dir_path, ".".join(o_))
+ cli_result.unicode_path = join(dir_path, ".".join(o_))
- with open(x_[0].unicode_path, "wb") as fp:
+ with open(cli_result.unicode_path, "wb") as fp:
fp.write(best_guess.output())
- except OSError as e:
+ except OSError as e: # Defensive:
print(str(e), file=sys.stderr)
if my_file.closed is False:
my_file.close()
@@ -317,5 +358,5 @@ def cli_detect(argv: list[str] | None = None) -> int:
return 0
-if __name__ == "__main__":
+if __name__ == "__main__": # Defensive:
cli_detect()
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/constant.py b/addons/source-python/packages/site-packages/charset_normalizer/constant.py
index 1fb9508d2..6ed779567 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/constant.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/constant.py
@@ -25,7 +25,7 @@
UTF8_MAXIMAL_ALLOCATION: int = 1_112_064
-# Up-to-date Unicode ucd/15.0.0
+# Up-to-date Unicode ucd/17.0.0
UNICODE_RANGES_COMBINED: dict[str, range] = {
"Control character": range(32),
"Basic Latin": range(32, 128),
@@ -213,6 +213,7 @@
"Elbasan": range(66816, 66864),
"Caucasian Albanian": range(66864, 66928),
"Vithkuqi": range(66928, 67008),
+ "Todhri": range(67008, 67072),
"Linear A": range(67072, 67456),
"Latin Extended-F": range(67456, 67520),
"Cypriot Syllabary": range(67584, 67648),
@@ -222,6 +223,7 @@
"Hatran": range(67808, 67840),
"Phoenician": range(67840, 67872),
"Lydian": range(67872, 67904),
+ "Sidetic": range(67904, 67936),
"Meroitic Hieroglyphs": range(67968, 68000),
"Meroitic Cursive": range(68000, 68096),
"Kharoshthi": range(68096, 68192),
@@ -235,6 +237,7 @@
"Old Turkic": range(68608, 68688),
"Old Hungarian": range(68736, 68864),
"Hanifi Rohingya": range(68864, 68928),
+ "Garay": range(68928, 69008),
"Rumi Numeral Symbols": range(69216, 69248),
"Yezidi": range(69248, 69312),
"Arabic Extended-C": range(69312, 69376),
@@ -254,12 +257,14 @@
"Multani": range(70272, 70320),
"Khudawadi": range(70320, 70400),
"Grantha": range(70400, 70528),
+ "Tulu-Tigalari": range(70528, 70656),
"Newa": range(70656, 70784),
"Tirhuta": range(70784, 70880),
"Siddham": range(71040, 71168),
"Modi": range(71168, 71264),
"Mongolian Supplement": range(71264, 71296),
"Takri": range(71296, 71376),
+ "Myanmar Extended-C": range(71376, 71424),
"Ahom": range(71424, 71504),
"Dogra": range(71680, 71760),
"Warang Citi": range(71840, 71936),
@@ -270,10 +275,13 @@
"Unified Canadian Aboriginal Syllabics Extended-A": range(72368, 72384),
"Pau Cin Hau": range(72384, 72448),
"Devanagari Extended-A": range(72448, 72544),
+ "Sharada Supplement": range(72544, 72576),
+ "Sunuwar": range(72640, 72704),
"Bhaiksuki": range(72704, 72816),
"Marchen": range(72816, 72896),
"Masaram Gondi": range(72960, 73056),
"Gunjala Gondi": range(73056, 73136),
+ "Tolong Siki": range(73136, 73200),
"Makasar": range(73440, 73472),
"Kawi": range(73472, 73568),
"Lisu Supplement": range(73648, 73664),
@@ -284,19 +292,24 @@
"Cypro-Minoan": range(77712, 77824),
"Egyptian Hieroglyphs": range(77824, 78896),
"Egyptian Hieroglyph Format Controls": range(78896, 78944),
+ "Egyptian Hieroglyphs Extended-A": range(78944, 82944),
"Anatolian Hieroglyphs": range(82944, 83584),
+ "Gurung Khema": range(90368, 90432),
"Bamum Supplement": range(92160, 92736),
"Mro": range(92736, 92784),
"Tangsa": range(92784, 92880),
"Bassa Vah": range(92880, 92928),
"Pahawh Hmong": range(92928, 93072),
+ "Kirat Rai": range(93504, 93568),
"Medefaidrin": range(93760, 93856),
+ "Beria Erfe": range(93856, 93920),
"Miao": range(93952, 94112),
"Ideographic Symbols and Punctuation": range(94176, 94208),
"Tangut": range(94208, 100352),
"Tangut Components": range(100352, 101120),
"Khitan Small Script": range(101120, 101632),
"Tangut Supplement": range(101632, 101760),
+ "Tangut Components Supplement": range(101760, 101888),
"Kana Extended-B": range(110576, 110592),
"Kana Supplement": range(110592, 110848),
"Kana Extended-A": range(110848, 110896),
@@ -304,6 +317,8 @@
"Nushu": range(110960, 111360),
"Duployan": range(113664, 113824),
"Shorthand Format Controls": range(113824, 113840),
+ "Symbols for Legacy Computing Supplement": range(117760, 118464),
+ "Miscellaneous Symbols Supplement": range(118464, 118528),
"Znamenny Musical Notation": range(118528, 118736),
"Byzantine Musical Symbols": range(118784, 119040),
"Musical Symbols": range(119040, 119296),
@@ -321,6 +336,8 @@
"Toto": range(123536, 123584),
"Wancho": range(123584, 123648),
"Nag Mundari": range(124112, 124160),
+ "Ol Onal": range(124368, 124416),
+ "Tai Yo": range(124608, 124672),
"Ethiopic Extended-B": range(124896, 124928),
"Mende Kikakui": range(124928, 125152),
"Adlam": range(125184, 125280),
@@ -333,7 +350,7 @@
"Enclosed Alphanumeric Supplement": range(127232, 127488),
"Enclosed Ideographic Supplement": range(127488, 127744),
"Miscellaneous Symbols and Pictographs": range(127744, 128512),
- "Emoticons range(Emoji)": range(128512, 128592),
+ "Emoticons": range(128512, 128592),
"Ornamental Dingbats": range(128592, 128640),
"Transport and Map Symbols": range(128640, 128768),
"Alchemical Symbols": range(128768, 128896),
@@ -348,9 +365,11 @@
"CJK Unified Ideographs Extension D": range(177984, 178208),
"CJK Unified Ideographs Extension E": range(178208, 183984),
"CJK Unified Ideographs Extension F": range(183984, 191472),
+ "CJK Unified Ideographs Extension I": range(191472, 192096),
"CJK Compatibility Ideographs Supplement": range(194560, 195104),
"CJK Unified Ideographs Extension G": range(196608, 201552),
"CJK Unified Ideographs Extension H": range(201552, 205744),
+ "CJK Unified Ideographs Extension J": range(205744, 210048),
"Tags": range(917504, 917632),
"Variation Selectors Supplement": range(917760, 918000),
"Supplementary Private Use Area-A": range(983040, 1048576),
@@ -529,29 +548,48 @@
}
-COMMON_SAFE_ASCII_CHARACTERS: set[str] = {
- "<",
- ">",
- "=",
- ":",
- "/",
- "&",
- ";",
- "{",
- "}",
- "[",
- "]",
- ",",
- "|",
- '"',
- "-",
- "(",
- ")",
-}
+COMMON_SAFE_ASCII_CHARACTERS: frozenset[str] = frozenset(
+ {
+ "<",
+ ">",
+ "=",
+ ":",
+ "/",
+ "&",
+ ";",
+ "{",
+ "}",
+ "[",
+ "]",
+ ",",
+ "|",
+ '"',
+ "-",
+ "(",
+ ")",
+ }
+)
+
+# Sample character sets — replace with full lists if needed
+COMMON_CHINESE_CHARACTERS = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞"
+COMMON_JAPANESE_CHARACTERS = "日一国年大十二本中長出三時行見月分後前生五間上東四今金九入学高円子外八六下来気小七山話女北午百書先名川千水半男西電校語土木聞食車何南万毎白天母火右読友左休父雨"
-KO_NAMES: set[str] = {"johab", "cp949", "euc_kr"}
-ZH_NAMES: set[str] = {"big5", "cp950", "big5hkscs", "hz"}
+COMMON_KOREAN_CHARACTERS = "一二三四五六七八九十百千萬上下左右中人女子大小山川日月火水木金土父母天地國名年時文校學生"
+
+# Combine all into a frozenset
+COMMON_CJK_CHARACTERS = frozenset(
+ "".join(
+ [
+ COMMON_CHINESE_CHARACTERS,
+ COMMON_JAPANESE_CHARACTERS,
+ COMMON_KOREAN_CHARACTERS,
+ ]
+ )
+)
+
+KO_NAMES: frozenset[str] = frozenset({"johab", "cp949", "euc_kr"})
+ZH_NAMES: frozenset[str] = frozenset({"big5", "cp950", "big5hkscs", "hz"})
# Logging LEVEL below DEBUG
TRACE: int = 5
@@ -786,12 +824,12 @@
],
"Russian": [
"о",
- "а",
"е",
+ "а",
"и",
"н",
- "с",
"т",
+ "с",
"р",
"в",
"л",
@@ -814,106 +852,85 @@
],
# Jap-Kanji
"Japanese": [
- "人",
+ "日",
"一",
+ "人",
+ "年",
"大",
- "亅",
- "丁",
- "丨",
- "竹",
- "笑",
- "口",
- "日",
- "今",
- "二",
- "彳",
- "行",
"十",
- "土",
- "丶",
- "寸",
- "寺",
+ "二",
+ "本",
+ "中",
+ "長",
+ "出",
+ "三",
"時",
- "乙",
- "丿",
- "乂",
- "气",
- "気",
- "冂",
- "巾",
- "亠",
- "市",
- "目",
- "儿",
+ "行",
"見",
- "八",
- "小",
- "凵",
- "県",
"月",
- "彐",
- "門",
- "間",
- "木",
- "東",
- "山",
- "出",
- "本",
- "中",
- "刀",
"分",
- "耳",
- "又",
- "取",
- "最",
- "言",
- "田",
- "心",
- "思",
- "刂",
+ "後",
"前",
- "京",
- "尹",
- "事",
"生",
- "厶",
- "云",
- "会",
- "未",
- "来",
- "白",
- "冫",
- "楽",
- "灬",
- "馬",
- "尸",
- "尺",
- "駅",
- "明",
- "耂",
- "者",
- "了",
- "阝",
- "都",
- "高",
- "卜",
- "占",
- "厂",
- "广",
- "店",
- "子",
- "申",
- "奄",
- "亻",
- "俺",
+ "五",
+ "間",
"上",
- "方",
- "冖",
+ "東",
+ "四",
+ "今",
+ "金",
+ "九",
+ "入",
"学",
- "衣",
- "艮",
+ "高",
+ "円",
+ "子",
+ "外",
+ "八",
+ "六",
+ "下",
+ "来",
+ "気",
+ "小",
+ "七",
+ "山",
+ "話",
+ "女",
+ "北",
+ "午",
+ "百",
+ "書",
+ "先",
+ "名",
+ "川",
+ "千",
+ "水",
+ "半",
+ "男",
+ "西",
+ "電",
+ "校",
+ "語",
+ "土",
+ "木",
+ "聞",
"食",
- "自",
+ "車",
+ "何",
+ "南",
+ "万",
+ "毎",
+ "白",
+ "天",
+ "母",
+ "火",
+ "右",
+ "読",
+ "友",
+ "左",
+ "休",
+ "父",
+ "雨",
],
# Jap-Katakana
"Japanese—": [
@@ -1996,3 +2013,38 @@
}
LANGUAGE_SUPPORTED_COUNT: int = len(FREQUENCIES)
+
+# Bit flags for unified character classification.
+# A single unicodedata.name() call sets all relevant flags at once.
+_LATIN: int = 1
+_ACCENTUATED: int = 1 << 1
+_CJK: int = 1 << 2
+_HANGUL: int = 1 << 3
+_KATAKANA: int = 1 << 4
+_HIRAGANA: int = 1 << 5
+_THAI: int = 1 << 6
+_ARABIC: int = 1 << 7
+_ARABIC_ISOLATED_FORM: int = 1 << 8
+
+_ACCENT_KEYWORDS: tuple[str, ...] = (
+ "WITH GRAVE",
+ "WITH ACUTE",
+ "WITH CEDILLA",
+ "WITH DIAERESIS",
+ "WITH CIRCUMFLEX",
+ "WITH TILDE",
+ "WITH MACRON",
+ "WITH RING ABOVE",
+)
+
+# Pre-built lookup structures for FREQUENCIES (computed once at import time).
+# character -> rank mapping per language (replaces list .index() calls).
+_FREQUENCIES_RANK: dict[str, dict[str, int]] = {
+ lang: {char: rank for rank, char in enumerate(chars)}
+ for lang, chars in FREQUENCIES.items()
+}
+
+# frozenset per language (avoids rebuilding set() per call).
+_FREQUENCIES_SET: dict[str, frozenset[str]] = {
+ lang: frozenset(chars) for lang, chars in FREQUENCIES.items()
+}
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/legacy.py b/addons/source-python/packages/site-packages/charset_normalizer/legacy.py
index a2f534514..293c1efaf 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/legacy.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/legacy.py
@@ -4,11 +4,10 @@
from warnings import warn
from .api import from_bytes
-from .constant import CHARDET_CORRESPONDENCE
+from .constant import CHARDET_CORRESPONDENCE, TOO_SMALL_SEQUENCE
-# TODO: remove this check when dropping Python 3.7 support
if TYPE_CHECKING:
- from typing_extensions import TypedDict
+ from typing import TypedDict
class ResultDict(TypedDict):
encoding: str | None
@@ -37,9 +36,7 @@ def detect(
if not isinstance(byte_str, (bytearray, bytes)):
raise TypeError( # pragma: nocover
- "Expected object of type bytes or bytearray, got: " "{}".format(
- type(byte_str)
- )
+ f"Expected object of type bytes or bytearray, got: {type(byte_str)}"
)
if isinstance(byte_str, bytearray):
@@ -51,6 +48,22 @@ def detect(
language = r.language if r is not None and r.language != "Unknown" else ""
confidence = 1.0 - r.chaos if r is not None else None
+ # automatically lower confidence
+ # on small bytes samples.
+ # https://github.com/jawah/charset_normalizer/issues/391
+ if (
+ confidence is not None
+ and confidence >= 0.9
+ and encoding
+ not in {
+ "utf_8",
+ "ascii",
+ }
+ and r.bom is False # type: ignore[union-attr]
+ and len(byte_str) < TOO_SMALL_SEQUENCE
+ ):
+ confidence -= 0.2
+
# Note: CharsetNormalizer does not return 'UTF-8-SIG' as the sig get stripped in the detection/normalization process
# but chardet does return 'utf-8-sig' and it is a valid codec name.
if r is not None and encoding == "utf_8" and r.bom:
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/md.cp313-win_amd64.pyd b/addons/source-python/packages/site-packages/charset_normalizer/md.cp313-win_amd64.pyd
new file mode 100644
index 000000000..feee0f3c5
Binary files /dev/null and b/addons/source-python/packages/site-packages/charset_normalizer/md.cp313-win_amd64.pyd differ
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/md.py b/addons/source-python/packages/site-packages/charset_normalizer/md.py
index 9ed59a868..b41d9cfc5 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/md.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/md.py
@@ -1,33 +1,207 @@
from __future__ import annotations
+import sys
from functools import lru_cache
from logging import getLogger
+if sys.version_info >= (3, 8):
+ from typing import final
+else:
+ try:
+ from typing_extensions import final
+ except ImportError:
+
+ def final(cls): # type: ignore[misc,no-untyped-def]
+ return cls
+
+
from .constant import (
+ COMMON_CJK_CHARACTERS,
COMMON_SAFE_ASCII_CHARACTERS,
TRACE,
UNICODE_SECONDARY_RANGE_KEYWORD,
+ _ACCENTUATED,
+ _ARABIC,
+ _ARABIC_ISOLATED_FORM,
+ _CJK,
+ _HANGUL,
+ _HIRAGANA,
+ _KATAKANA,
+ _LATIN,
+ _THAI,
)
from .utils import (
- is_accentuated,
- is_arabic,
- is_arabic_isolated_form,
- is_case_variable,
- is_cjk,
+ _character_flags,
is_emoticon,
- is_hangul,
- is_hiragana,
- is_katakana,
- is_latin,
is_punctuation,
is_separator,
is_symbol,
- is_thai,
- is_unprintable,
remove_accent,
unicode_range,
)
+# Combined bitmask for CJK/Hangul/Katakana/Hiragana/Thai glyph detection.
+_GLYPH_MASK: int = _CJK | _HANGUL | _KATAKANA | _HIRAGANA | _THAI
+
+
+@final
+class CharInfo:
+ """Pre-computed character properties shared across all detectors.
+
+ Instantiated once and reused via :meth:`update` on every character
+ in the hot loop so that redundant calls to str methods
+ (``isalpha``, ``isupper``, …) and cached utility functions
+ (``_character_flags``, ``is_punctuation``, …) are avoided when
+ several plugins need the same information.
+ """
+
+ __slots__ = (
+ "character",
+ "printable",
+ "alpha",
+ "upper",
+ "lower",
+ "space",
+ "digit",
+ "is_ascii",
+ "case_variable",
+ "flags",
+ "accentuated",
+ "latin",
+ "is_cjk",
+ "is_arabic",
+ "is_glyph",
+ "punct",
+ "sym",
+ )
+
+ def __init__(self) -> None:
+ self.character: str = ""
+ self.printable: bool = False
+ self.alpha: bool = False
+ self.upper: bool = False
+ self.lower: bool = False
+ self.space: bool = False
+ self.digit: bool = False
+ self.is_ascii: bool = False
+ self.case_variable: bool = False
+ self.flags: int = 0
+ self.accentuated: bool = False
+ self.latin: bool = False
+ self.is_cjk: bool = False
+ self.is_arabic: bool = False
+ self.is_glyph: bool = False
+ self.punct: bool = False
+ self.sym: bool = False
+
+ def update(self, character: str) -> None:
+ """Update all properties for *character* (called once per character)."""
+ self.character = character
+
+ # ASCII fast-path: for characters with ord < 128, we can skip
+ # _character_flags() entirely and derive most properties from ord.
+ o: int = ord(character)
+ if o < 128:
+ self.is_ascii = True
+ self.accentuated = False
+ self.is_cjk = False
+ self.is_arabic = False
+ self.is_glyph = False
+ # ASCII alpha: a-z (97-122) or A-Z (65-90)
+ if 65 <= o <= 90:
+ # Uppercase ASCII letter
+ self.alpha = True
+ self.upper = True
+ self.lower = False
+ self.space = False
+ self.digit = False
+ self.printable = True
+ self.case_variable = True
+ self.flags = _LATIN
+ self.latin = True
+ self.punct = False
+ self.sym = False
+ elif 97 <= o <= 122:
+ # Lowercase ASCII letter
+ self.alpha = True
+ self.upper = False
+ self.lower = True
+ self.space = False
+ self.digit = False
+ self.printable = True
+ self.case_variable = True
+ self.flags = _LATIN
+ self.latin = True
+ self.punct = False
+ self.sym = False
+ elif 48 <= o <= 57:
+ # ASCII digit 0-9
+ self.alpha = False
+ self.upper = False
+ self.lower = False
+ self.space = False
+ self.digit = True
+ self.printable = True
+ self.case_variable = False
+ self.flags = 0
+ self.latin = False
+ self.punct = False
+ self.sym = False
+ elif o == 32 or (9 <= o <= 13):
+ # Space, tab, newline, etc.
+ self.alpha = False
+ self.upper = False
+ self.lower = False
+ self.space = True
+ self.digit = False
+ self.printable = o == 32
+ self.case_variable = False
+ self.flags = 0
+ self.latin = False
+ self.punct = False
+ self.sym = False
+ else:
+ # Other ASCII (punctuation, symbols, control chars)
+ self.printable = character.isprintable()
+ self.alpha = False
+ self.upper = False
+ self.lower = False
+ self.space = False
+ self.digit = False
+ self.case_variable = False
+ self.flags = 0
+ self.latin = False
+ self.punct = is_punctuation(character) if self.printable else False
+ self.sym = is_symbol(character) if self.printable else False
+ else:
+ # Non-ASCII path
+ self.is_ascii = False
+ self.printable = character.isprintable()
+ self.alpha = character.isalpha()
+ self.upper = character.isupper()
+ self.lower = character.islower()
+ self.space = character.isspace()
+ self.digit = character.isdigit()
+ self.case_variable = self.lower != self.upper
+
+ # Flag-based classification (single unicodedata.name() call, lru-cached)
+ flags: int
+ if self.alpha:
+ flags = _character_flags(character)
+ else:
+ flags = 0
+ self.flags = flags
+ self.accentuated = bool(flags & _ACCENTUATED)
+ self.latin = bool(flags & _LATIN)
+ self.is_cjk = bool(flags & _CJK)
+ self.is_arabic = bool(flags & _ARABIC)
+ self.is_glyph = bool(flags & _GLYPH_MASK)
+
+ # Eagerly compute punct and sym (avoids property dispatch overhead
+ # on 300K+ accesses in the hot loop).
+ self.punct = is_punctuation(character) if self.printable else False
+ self.sym = is_symbol(character) if self.printable else False
+
class MessDetectorPlugin:
"""
@@ -35,20 +209,16 @@ class MessDetectorPlugin:
All detectors MUST extend and implement given methods.
"""
- def eligible(self, character: str) -> bool:
- """
- Determine if given character should be fed in.
- """
- raise NotImplementedError # pragma: nocover
+ __slots__ = ()
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
"""
The main routine to be executed upon character.
Insert the logic in witch the text would be considered chaotic.
"""
- raise NotImplementedError # pragma: nocover
+ raise NotImplementedError # Defensive:
- def reset(self) -> None: # pragma: no cover
+ def reset(self) -> None: # Defensive:
"""
Permit to reset the plugin to the initial state.
"""
@@ -60,10 +230,19 @@ def ratio(self) -> float:
Compute the chaos ratio based on what your feed() has seen.
Must NOT be lower than 0.; No restriction gt 0.
"""
- raise NotImplementedError # pragma: nocover
+ raise NotImplementedError # Defensive:
+@final
class TooManySymbolOrPunctuationPlugin(MessDetectorPlugin):
+ __slots__ = (
+ "_punctuation_count",
+ "_symbol_count",
+ "_character_count",
+ "_last_printable_char",
+ "_frenzy_symbol_in_word",
+ )
+
def __init__(self) -> None:
self._punctuation_count: int = 0
self._symbol_count: int = 0
@@ -72,23 +251,17 @@ def __init__(self) -> None:
self._last_printable_char: str | None = None
self._frenzy_symbol_in_word: bool = False
- def eligible(self, character: str) -> bool:
- return character.isprintable()
-
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
self._character_count += 1
if (
character != self._last_printable_char
and character not in COMMON_SAFE_ASCII_CHARACTERS
):
- if is_punctuation(character):
+ if info.punct:
self._punctuation_count += 1
- elif (
- character.isdigit() is False
- and is_symbol(character)
- and is_emoticon(character) is False
- ):
+ elif not info.digit and info.sym and not is_emoticon(character):
self._symbol_count += 2
self._last_printable_char = character
@@ -110,18 +283,19 @@ def ratio(self) -> float:
return ratio_of_punctuation if ratio_of_punctuation >= 0.3 else 0.0
+@final
class TooManyAccentuatedPlugin(MessDetectorPlugin):
+ __slots__ = ("_character_count", "_accentuated_count")
+
def __init__(self) -> None:
self._character_count: int = 0
self._accentuated_count: int = 0
- def eligible(self, character: str) -> bool:
- return character.isalpha()
-
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
self._character_count += 1
- if is_accentuated(character):
+ if info.accentuated:
self._accentuated_count += 1
def reset(self) -> None: # Abstract
@@ -137,16 +311,22 @@ def ratio(self) -> float:
return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0
+@final
class UnprintablePlugin(MessDetectorPlugin):
+ __slots__ = ("_unprintable_count", "_character_count")
+
def __init__(self) -> None:
self._unprintable_count: int = 0
self._character_count: int = 0
- def eligible(self, character: str) -> bool:
- return True
-
- def feed(self, character: str) -> None:
- if is_unprintable(character):
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
+ if (
+ not info.space
+ and not info.printable
+ and character != "\x1a"
+ and character != "\ufeff"
+ ):
self._unprintable_count += 1
self._character_count += 1
@@ -155,40 +335,48 @@ def reset(self) -> None: # Abstract
@property
def ratio(self) -> float:
- if self._character_count == 0:
+ if self._character_count == 0: # Defensive:
return 0.0
return (self._unprintable_count * 8) / self._character_count
+@final
class SuspiciousDuplicateAccentPlugin(MessDetectorPlugin):
+ __slots__ = (
+ "_successive_count",
+ "_character_count",
+ "_last_latin_character",
+ "_last_was_accentuated",
+ )
+
def __init__(self) -> None:
self._successive_count: int = 0
self._character_count: int = 0
self._last_latin_character: str | None = None
+ self._last_was_accentuated: bool = False
- def eligible(self, character: str) -> bool:
- return character.isalpha() and is_latin(character)
-
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
self._character_count += 1
if (
self._last_latin_character is not None
- and is_accentuated(character)
- and is_accentuated(self._last_latin_character)
+ and info.accentuated
+ and self._last_was_accentuated
):
- if character.isupper() and self._last_latin_character.isupper():
+ if info.upper and self._last_latin_character.isupper():
self._successive_count += 1
- # Worse if its the same char duplicated with different accent.
if remove_accent(character) == remove_accent(self._last_latin_character):
self._successive_count += 1
self._last_latin_character = character
+ self._last_was_accentuated = info.accentuated
def reset(self) -> None: # Abstract
self._successive_count = 0
self._character_count = 0
self._last_latin_character = None
+ self._last_was_accentuated = False
@property
def ratio(self) -> float:
@@ -198,42 +386,49 @@ def ratio(self) -> float:
return (self._successive_count * 2) / self._character_count
+@final
class SuspiciousRange(MessDetectorPlugin):
+ __slots__ = (
+ "_suspicious_successive_range_count",
+ "_character_count",
+ "_last_printable_seen",
+ "_last_printable_range",
+ )
+
def __init__(self) -> None:
self._suspicious_successive_range_count: int = 0
self._character_count: int = 0
self._last_printable_seen: str | None = None
+ self._last_printable_range: str | None = None
- def eligible(self, character: str) -> bool:
- return character.isprintable()
-
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
self._character_count += 1
- if (
- character.isspace()
- or is_punctuation(character)
- or character in COMMON_SAFE_ASCII_CHARACTERS
- ):
+ if info.space or info.punct or character in COMMON_SAFE_ASCII_CHARACTERS:
self._last_printable_seen = None
+ self._last_printable_range = None
return
if self._last_printable_seen is None:
self._last_printable_seen = character
+ self._last_printable_range = unicode_range(character)
return
- unicode_range_a: str | None = unicode_range(self._last_printable_seen)
+ unicode_range_a: str | None = self._last_printable_range
unicode_range_b: str | None = unicode_range(character)
if is_suspiciously_successive_range(unicode_range_a, unicode_range_b):
self._suspicious_successive_range_count += 1
self._last_printable_seen = character
+ self._last_printable_range = unicode_range_b
def reset(self) -> None: # Abstract
self._character_count = 0
self._suspicious_successive_range_count = 0
self._last_printable_seen = None
+ self._last_printable_range = None
@property
def ratio(self) -> float:
@@ -247,7 +442,24 @@ def ratio(self) -> float:
return ratio_of_suspicious_range_usage
+@final
class SuperWeirdWordPlugin(MessDetectorPlugin):
+ __slots__ = (
+ "_word_count",
+ "_bad_word_count",
+ "_foreign_long_count",
+ "_is_current_word_bad",
+ "_foreign_long_watch",
+ "_character_count",
+ "_bad_character_count",
+ "_buffer_length",
+ "_buffer_last_char",
+ "_buffer_last_char_accentuated",
+ "_buffer_accent_count",
+ "_buffer_glyph_count",
+ "_buffer_upper_count",
+ )
+
def __init__(self) -> None:
self._word_count: int = 0
self._bad_word_count: int = 0
@@ -259,56 +471,50 @@ def __init__(self) -> None:
self._character_count: int = 0
self._bad_character_count: int = 0
- self._buffer: str = ""
+ self._buffer_length: int = 0
+ self._buffer_last_char: str | None = None
+ self._buffer_last_char_accentuated: bool = False
self._buffer_accent_count: int = 0
self._buffer_glyph_count: int = 0
+ self._buffer_upper_count: int = 0
- def eligible(self, character: str) -> bool:
- return True
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
+ if info.alpha:
+ self._buffer_length += 1
+ self._buffer_last_char = character
+
+ if info.upper:
+ self._buffer_upper_count += 1
- def feed(self, character: str) -> None:
- if character.isalpha():
- self._buffer += character
- if is_accentuated(character):
+ self._buffer_last_char_accentuated = info.accentuated
+
+ if info.accentuated:
self._buffer_accent_count += 1
if (
- self._foreign_long_watch is False
- and (is_latin(character) is False or is_accentuated(character))
- and is_cjk(character) is False
- and is_hangul(character) is False
- and is_katakana(character) is False
- and is_hiragana(character) is False
- and is_thai(character) is False
+ not self._foreign_long_watch
+ and (not info.latin or info.accentuated)
+ and not info.is_glyph
):
self._foreign_long_watch = True
- if (
- is_cjk(character)
- or is_hangul(character)
- or is_katakana(character)
- or is_hiragana(character)
- or is_thai(character)
- ):
+ if info.is_glyph:
self._buffer_glyph_count += 1
return
- if not self._buffer:
+ if not self._buffer_length:
return
- if (
- character.isspace() or is_punctuation(character) or is_separator(character)
- ) and self._buffer:
+ if info.space or info.punct or is_separator(character):
self._word_count += 1
- buffer_length: int = len(self._buffer)
+ buffer_length: int = self._buffer_length
self._character_count += buffer_length
if buffer_length >= 4:
if self._buffer_accent_count / buffer_length >= 0.5:
self._is_current_word_bad = True
- # Word/Buffer ending with an upper case accentuated letter are so rare,
- # that we will consider them all as suspicious. Same weight as foreign_long suspicious.
elif (
- is_accentuated(self._buffer[-1])
- and self._buffer[-1].isupper()
- and all(_.isupper() for _ in self._buffer) is False
+ self._buffer_last_char_accentuated
+ and self._buffer_last_char.isupper() # type: ignore[union-attr]
+ and self._buffer_upper_count != buffer_length
):
self._foreign_long_count += 1
self._is_current_word_bad = True
@@ -316,15 +522,10 @@ def feed(self, character: str) -> None:
self._is_current_word_bad = True
self._foreign_long_count += 1
if buffer_length >= 24 and self._foreign_long_watch:
- camel_case_dst = [
- i
- for c, i in zip(self._buffer, range(0, buffer_length))
- if c.isupper()
- ]
- probable_camel_cased: bool = False
-
- if camel_case_dst and (len(camel_case_dst) / buffer_length <= 0.3):
- probable_camel_cased = True
+ probable_camel_cased: bool = (
+ self._buffer_upper_count > 0
+ and self._buffer_upper_count / buffer_length <= 0.3
+ )
if not probable_camel_cased:
self._foreign_long_count += 1
@@ -332,23 +533,30 @@ def feed(self, character: str) -> None:
if self._is_current_word_bad:
self._bad_word_count += 1
- self._bad_character_count += len(self._buffer)
+ self._bad_character_count += buffer_length
self._is_current_word_bad = False
self._foreign_long_watch = False
- self._buffer = ""
+ self._buffer_length = 0
+ self._buffer_last_char = None
+ self._buffer_last_char_accentuated = False
self._buffer_accent_count = 0
self._buffer_glyph_count = 0
+ self._buffer_upper_count = 0
elif (
character not in {"<", ">", "-", "=", "~", "|", "_"}
- and character.isdigit() is False
- and is_symbol(character)
+ and not info.digit
+ and info.sym
):
self._is_current_word_bad = True
- self._buffer += character
+ self._buffer_length += 1
+ self._buffer_last_char = character
+ self._buffer_last_char_accentuated = False
def reset(self) -> None: # Abstract
- self._buffer = ""
+ self._buffer_length = 0
+ self._buffer_last_char = None
+ self._buffer_last_char_accentuated = False
self._is_current_word_bad = False
self._foreign_long_watch = False
self._bad_word_count = 0
@@ -356,6 +564,9 @@ def reset(self) -> None: # Abstract
self._character_count = 0
self._bad_character_count = 0
self._foreign_long_count = 0
+ self._buffer_accent_count = 0
+ self._buffer_glyph_count = 0
+ self._buffer_upper_count = 0
@property
def ratio(self) -> float:
@@ -365,38 +576,55 @@ def ratio(self) -> float:
return self._bad_character_count / self._character_count
-class CjkInvalidStopPlugin(MessDetectorPlugin):
+@final
+class CjkUncommonPlugin(MessDetectorPlugin):
"""
- GB(Chinese) based encoding often render the stop incorrectly when the content does not fit and
- can be easily detected. Searching for the overuse of '丅' and '丄'.
+ Detect messy CJK text that probably means nothing.
"""
+ __slots__ = ("_character_count", "_uncommon_count")
+
def __init__(self) -> None:
- self._wrong_stop_count: int = 0
- self._cjk_character_count: int = 0
+ self._character_count: int = 0
+ self._uncommon_count: int = 0
- def eligible(self, character: str) -> bool:
- return True
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
+ self._character_count += 1
- def feed(self, character: str) -> None:
- if character in {"丅", "丄"}:
- self._wrong_stop_count += 1
- return
- if is_cjk(character):
- self._cjk_character_count += 1
+ if character not in COMMON_CJK_CHARACTERS:
+ self._uncommon_count += 1
def reset(self) -> None: # Abstract
- self._wrong_stop_count = 0
- self._cjk_character_count = 0
+ self._character_count = 0
+ self._uncommon_count = 0
@property
def ratio(self) -> float:
- if self._cjk_character_count < 16:
+ if self._character_count < 8:
return 0.0
- return self._wrong_stop_count / self._cjk_character_count
+ uncommon_form_usage: float = self._uncommon_count / self._character_count
+ # we can be pretty sure it's garbage when uncommon characters are widely
+ # used. otherwise it could just be traditional chinese for example.
+ return uncommon_form_usage / 10 if uncommon_form_usage > 0.5 else 0.0
+
+
+@final
class ArchaicUpperLowerPlugin(MessDetectorPlugin):
+ __slots__ = (
+ "_buf",
+ "_character_count_since_last_sep",
+ "_successive_upper_lower_count",
+ "_successive_upper_lower_count_final",
+ "_character_count",
+ "_last_alpha_seen",
+ "_last_alpha_seen_upper",
+ "_last_alpha_seen_lower",
+ "_current_ascii_only",
+ )
+
def __init__(self) -> None:
self._buf: bool = False
@@ -408,20 +636,20 @@ def __init__(self) -> None:
self._character_count: int = 0
self._last_alpha_seen: str | None = None
+ self._last_alpha_seen_upper: bool = False
+ self._last_alpha_seen_lower: bool = False
self._current_ascii_only: bool = True
- def eligible(self, character: str) -> bool:
- return True
-
- def feed(self, character: str) -> None:
- is_concerned = character.isalpha() and is_case_variable(character)
- chunk_sep = is_concerned is False
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
+ is_concerned: bool = info.alpha and info.case_variable
+ chunk_sep: bool = not is_concerned
if chunk_sep and self._character_count_since_last_sep > 0:
if (
self._character_count_since_last_sep <= 64
- and character.isdigit() is False
- and self._current_ascii_only is False
+ and not info.digit
+ and not self._current_ascii_only
):
self._successive_upper_lower_count_final += (
self._successive_upper_lower_count
@@ -436,14 +664,14 @@ def feed(self, character: str) -> None:
return
- if self._current_ascii_only is True and character.isascii() is False:
+ if self._current_ascii_only and not info.is_ascii:
self._current_ascii_only = False
if self._last_alpha_seen is not None:
- if (character.isupper() and self._last_alpha_seen.islower()) or (
- character.islower() and self._last_alpha_seen.isupper()
+ if (info.upper and self._last_alpha_seen_lower) or (
+ info.lower and self._last_alpha_seen_upper
):
- if self._buf is True:
+ if self._buf:
self._successive_upper_lower_count += 2
self._buf = False
else:
@@ -454,6 +682,8 @@ def feed(self, character: str) -> None:
self._character_count += 1
self._character_count_since_last_sep += 1
self._last_alpha_seen = character
+ self._last_alpha_seen_upper = info.upper
+ self._last_alpha_seen_lower = info.lower
def reset(self) -> None: # Abstract
self._character_count = 0
@@ -461,18 +691,23 @@ def reset(self) -> None: # Abstract
self._successive_upper_lower_count = 0
self._successive_upper_lower_count_final = 0
self._last_alpha_seen = None
+ self._last_alpha_seen_upper = False
+ self._last_alpha_seen_lower = False
self._buf = False
self._current_ascii_only = True
@property
def ratio(self) -> float:
- if self._character_count == 0:
+ if self._character_count == 0: # Defensive:
return 0.0
return self._successive_upper_lower_count_final / self._character_count
+@final
class ArabicIsolatedFormPlugin(MessDetectorPlugin):
+ __slots__ = ("_character_count", "_isolated_form_count")
+
def __init__(self) -> None:
self._character_count: int = 0
self._isolated_form_count: int = 0
@@ -481,13 +716,11 @@ def reset(self) -> None: # Abstract
self._character_count = 0
self._isolated_form_count = 0
- def eligible(self, character: str) -> bool:
- return is_arabic(character)
-
- def feed(self, character: str) -> None:
+ def feed_info(self, character: str, info: CharInfo) -> None:
+ """Optimized feed using pre-computed character info."""
self._character_count += 1
- if is_arabic_isolated_form(character):
+ if info.flags & _ARABIC_ISOLATED_FORM:
self._isolated_form_count += 1
@property
@@ -582,49 +815,122 @@ def mess_ratio(
Compute a mess ratio given a decoded bytes sequence. The maximum threshold does stop the computation earlier.
"""
- detectors: list[MessDetectorPlugin] = [
- md_class() for md_class in MessDetectorPlugin.__subclasses__()
- ]
+ seq_len: int = len(decoded_sequence)
- length: int = len(decoded_sequence) + 1
-
- mean_mess_ratio: float = 0.0
-
- if length < 512:
- intermediary_mean_mess_ratio_calc: int = 32
- elif length <= 1024:
- intermediary_mean_mess_ratio_calc = 64
+ if seq_len < 511:
+ step: int = 32
+ elif seq_len < 1024:
+ step = 64
else:
- intermediary_mean_mess_ratio_calc = 128
-
- for character, index in zip(decoded_sequence + "\n", range(length)):
- for detector in detectors:
- if detector.eligible(character):
- detector.feed(character)
-
- if (
- index > 0 and index % intermediary_mean_mess_ratio_calc == 0
- ) or index == length - 1:
- mean_mess_ratio = sum(dt.ratio for dt in detectors)
+ step = 128
+
+ # Create each detector as a named local variable (unrolled from the generic loop).
+ # This eliminates per-character iteration over the detector list and
+ # per-character eligible() virtual dispatch, while keeping every plugin class
+ # intact and fully readable.
+ d_sp: TooManySymbolOrPunctuationPlugin = TooManySymbolOrPunctuationPlugin()
+ d_ta: TooManyAccentuatedPlugin = TooManyAccentuatedPlugin()
+ d_up: UnprintablePlugin = UnprintablePlugin()
+ d_sda: SuspiciousDuplicateAccentPlugin = SuspiciousDuplicateAccentPlugin()
+ d_sr: SuspiciousRange = SuspiciousRange()
+ d_sw: SuperWeirdWordPlugin = SuperWeirdWordPlugin()
+ d_cu: CjkUncommonPlugin = CjkUncommonPlugin()
+ d_au: ArchaicUpperLowerPlugin = ArchaicUpperLowerPlugin()
+ d_ai: ArabicIsolatedFormPlugin = ArabicIsolatedFormPlugin()
+
+ # Local references for feed_info methods called in the hot loop.
+ d_sp_feed = d_sp.feed_info
+ d_ta_feed = d_ta.feed_info
+ d_up_feed = d_up.feed_info
+ d_sda_feed = d_sda.feed_info
+ d_sr_feed = d_sr.feed_info
+ d_sw_feed = d_sw.feed_info
+ d_cu_feed = d_cu.feed_info
+ d_au_feed = d_au.feed_info
+ d_ai_feed = d_ai.feed_info
+
+ # Single reusable CharInfo object (avoids per-character allocation).
+ info: CharInfo = CharInfo()
+ info_update = info.update
+
+ mean_mess_ratio: float
+
+ for block_start in range(0, seq_len, step):
+ for character in decoded_sequence[block_start : block_start + step]:
+ # Pre-compute all character properties once (shared across all plugins).
+ info_update(character)
+
+ # Detectors with eligible() == always True
+ d_up_feed(character, info)
+ d_sw_feed(character, info)
+ d_au_feed(character, info)
+
+ # Detectors with eligible() == isprintable
+ if info.printable:
+ d_sp_feed(character, info)
+ d_sr_feed(character, info)
+
+ # Detectors with eligible() == isalpha
+ if info.alpha:
+ d_ta_feed(character, info)
+ # SuspiciousDuplicateAccent: isalpha() and is_latin()
+ if info.latin:
+ d_sda_feed(character, info)
+ # CjkUncommon: is_cjk()
+ if info.is_cjk:
+ d_cu_feed(character, info)
+ # ArabicIsolatedForm: is_arabic()
+ if info.is_arabic:
+ d_ai_feed(character, info)
+
+ mean_mess_ratio = (
+ d_sp.ratio
+ + d_ta.ratio
+ + d_up.ratio
+ + d_sda.ratio
+ + d_sr.ratio
+ + d_sw.ratio
+ + d_cu.ratio
+ + d_au.ratio
+ + d_ai.ratio
+ )
- if mean_mess_ratio >= maximum_threshold:
- break
+ if mean_mess_ratio >= maximum_threshold:
+ break
+ else:
+ # Flush last word buffer in SuperWeirdWordPlugin via trailing newline.
+ info_update("\n")
+ d_sw_feed("\n", info)
+ d_au_feed("\n", info)
+ d_up_feed("\n", info)
+
+ mean_mess_ratio = (
+ d_sp.ratio
+ + d_ta.ratio
+ + d_up.ratio
+ + d_sda.ratio
+ + d_sr.ratio
+ + d_sw.ratio
+ + d_cu.ratio
+ + d_au.ratio
+ + d_ai.ratio
+ )
- if debug:
+ if debug: # Defensive:
logger = getLogger("charset_normalizer")
logger.log(
TRACE,
"Mess-detector extended-analysis start. "
- f"intermediary_mean_mess_ratio_calc={intermediary_mean_mess_ratio_calc} mean_mess_ratio={mean_mess_ratio} "
+ f"intermediary_mean_mess_ratio_calc={step} mean_mess_ratio={mean_mess_ratio} "
f"maximum_threshold={maximum_threshold}",
)
- if len(decoded_sequence) > 16:
+ if seq_len > 16:
logger.log(TRACE, f"Starting with: {decoded_sequence[:16]}")
logger.log(TRACE, f"Ending with: {decoded_sequence[-16::]}")
- for dt in detectors:
+ for dt in [d_sp, d_ta, d_up, d_sda, d_sr, d_sw, d_cu, d_au, d_ai]:
logger.log(TRACE, f"{dt.__class__}: {dt.ratio}")
return round(mean_mess_ratio, 3)
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/models.py b/addons/source-python/packages/site-packages/charset_normalizer/models.py
index 1042758f8..30e8a163e 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/models.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/models.py
@@ -1,7 +1,6 @@
from __future__ import annotations
from encodings.aliases import aliases
-from hashlib import sha256
from json import dumps
from re import sub
from typing import Any, Iterator, List, Tuple
@@ -13,7 +12,7 @@
class CharsetMatch:
def __init__(
self,
- payload: bytes,
+ payload: bytes | bytearray,
guessed_encoding: str,
mean_mess_ratio: float,
has_sig_or_bom: bool,
@@ -21,7 +20,7 @@ def __init__(
decoded_payload: str | None = None,
preemptive_declaration: str | None = None,
):
- self._payload: bytes = payload
+ self._payload: bytes | bytearray = payload
self._encoding: str = guessed_encoding
self._mean_mess_ratio: float = mean_mess_ratio
@@ -56,10 +55,10 @@ def __lt__(self, other: object) -> bool:
chaos_difference: float = abs(self.chaos - other.chaos)
coherence_difference: float = abs(self.coherence - other.coherence)
- # Below 1% difference --> Use Coherence
- if chaos_difference < 0.01 and coherence_difference > 0.02:
+ # Below 0.5% difference --> Use Coherence
+ if chaos_difference < 0.005 and coherence_difference > 0.02:
return self.coherence > other.coherence
- elif chaos_difference < 0.01 and coherence_difference <= 0.02:
+ elif chaos_difference < 0.005 and coherence_difference <= 0.02:
# When having a difficult decision, use the result that decoded as many multi-byte as possible.
# preserve RAM usage!
if len(self._payload) >= TOO_BIG_SEQUENCE:
@@ -79,7 +78,7 @@ def __str__(self) -> str:
return self._string
def __repr__(self) -> str:
- return f""
+ return f""
def add_submatch(self, other: CharsetMatch) -> None:
if not isinstance(other, CharsetMatch) or other == self:
@@ -172,7 +171,7 @@ def percent_coherence(self) -> float:
return round(self.coherence * 100, ndigits=3)
@property
- def raw(self) -> bytes:
+ def raw(self) -> bytes | bytearray:
"""
Original untouched bytes.
"""
@@ -235,11 +234,11 @@ def output(self, encoding: str = "utf_8") -> bytes:
return self._output_payload # type: ignore
@property
- def fingerprint(self) -> str:
+ def fingerprint(self) -> int:
"""
- Retrieve the unique SHA256 computed using the transformed (re-encoded) payload. Not the original one.
+ Retrieve a hash fingerprint of the decoded payload, used for deduplication.
"""
- return sha256(self.output()).hexdigest()
+ return hash(str(self))
class CharsetMatches:
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/utils.py b/addons/source-python/packages/site-packages/charset_normalizer/utils.py
index 0175e0a96..0f529b59c 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/utils.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/utils.py
@@ -3,6 +3,7 @@
import importlib
import logging
import unicodedata
+from bisect import bisect_right
from codecs import IncrementalDecoder
from encodings.aliases import aliases
from functools import lru_cache
@@ -20,25 +21,58 @@
UNICODE_RANGES_COMBINED,
UNICODE_SECONDARY_RANGE_KEYWORD,
UTF8_MAXIMAL_ALLOCATION,
+ COMMON_CJK_CHARACTERS,
+ _LATIN,
+ _CJK,
+ _HANGUL,
+ _KATAKANA,
+ _HIRAGANA,
+ _THAI,
+ _ARABIC,
+ _ARABIC_ISOLATED_FORM,
+ _ACCENT_KEYWORDS,
+ _ACCENTUATED,
)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
-def is_accentuated(character: str) -> bool:
+def _character_flags(character: str) -> int:
+ """Compute all name-based classification flags with a single unicodedata.name() call."""
try:
- description: str = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
- return (
- "WITH GRAVE" in description
- or "WITH ACUTE" in description
- or "WITH CEDILLA" in description
- or "WITH DIAERESIS" in description
- or "WITH CIRCUMFLEX" in description
- or "WITH TILDE" in description
- or "WITH MACRON" in description
- or "WITH RING ABOVE" in description
- )
+ desc: str = unicodedata.name(character)
+ except ValueError:
+ return 0
+
+ flags: int = 0
+
+ if "LATIN" in desc:
+ flags |= _LATIN
+ if "CJK" in desc:
+ flags |= _CJK
+ if "HANGUL" in desc:
+ flags |= _HANGUL
+ if "KATAKANA" in desc:
+ flags |= _KATAKANA
+ if "HIRAGANA" in desc:
+ flags |= _HIRAGANA
+ if "THAI" in desc:
+ flags |= _THAI
+ if "ARABIC" in desc:
+ flags |= _ARABIC
+ if "ISOLATED FORM" in desc:
+ flags |= _ARABIC_ISOLATED_FORM
+
+ for kw in _ACCENT_KEYWORDS:
+ if kw in desc:
+ flags |= _ACCENTUATED
+ break
+
+ return flags
+
+
+@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
+def is_accentuated(character: str) -> bool:
+ return bool(_character_flags(character) & _ACCENTUATED)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
@@ -52,6 +86,15 @@ def remove_accent(character: str) -> str:
return chr(int(codes[0], 16))
+# Pre-built sorted lookup table for O(log n) binary search in unicode_range().
+# Each entry is (range_start, range_end_exclusive, range_name).
+_UNICODE_RANGES_SORTED: list[tuple[int, int, str]] = sorted(
+ (ord_range.start, ord_range.stop, name)
+ for name, ord_range in UNICODE_RANGES_COMBINED.items()
+)
+_UNICODE_RANGE_STARTS: list[int] = [e[0] for e in _UNICODE_RANGES_SORTED]
+
+
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def unicode_range(character: str) -> str | None:
"""
@@ -59,20 +102,19 @@ def unicode_range(character: str) -> str | None:
"""
character_ord: int = ord(character)
- for range_name, ord_range in UNICODE_RANGES_COMBINED.items():
- if character_ord in ord_range:
- return range_name
+ # Binary search: find the rightmost range whose start <= character_ord
+ idx = bisect_right(_UNICODE_RANGE_STARTS, character_ord) - 1
+ if idx >= 0:
+ start, stop, name = _UNICODE_RANGES_SORTED[idx]
+ if character_ord < stop:
+ return name
return None
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_latin(character: str) -> bool:
- try:
- description: str = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
- return "LATIN" in description
+ return bool(_character_flags(character) & _LATIN)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
@@ -132,72 +174,42 @@ def is_case_variable(character: str) -> bool:
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_cjk(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "CJK" in character_name
+ return bool(_character_flags(character) & _CJK)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_hiragana(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "HIRAGANA" in character_name
+ return bool(_character_flags(character) & _HIRAGANA)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_katakana(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "KATAKANA" in character_name
+ return bool(_character_flags(character) & _KATAKANA)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_hangul(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "HANGUL" in character_name
+ return bool(_character_flags(character) & _HANGUL)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_thai(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "THAI" in character_name
+ return bool(_character_flags(character) & _THAI)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_arabic(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
-
- return "ARABIC" in character_name
+ return bool(_character_flags(character) & _ARABIC)
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
def is_arabic_isolated_form(character: str) -> bool:
- try:
- character_name = unicodedata.name(character)
- except ValueError: # Defensive: unicode database outdated?
- return False
+ return bool(_character_flags(character) & _ARABIC_ISOLATED_FORM)
- return "ARABIC" in character_name and "ISOLATED FORM" in character_name
+
+@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
+def is_cjk_uncommon(character: str) -> bool:
+ return character not in COMMON_CJK_CHARACTERS
@lru_cache(maxsize=len(UNICODE_RANGES_COMBINED))
@@ -216,11 +228,13 @@ def is_unprintable(character: str) -> bool:
)
-def any_specified_encoding(sequence: bytes, search_zone: int = 8192) -> str | None:
+def any_specified_encoding(
+ sequence: bytes | bytearray, search_zone: int = 8192
+) -> str | None:
"""
Extract using ASCII-only decoder any specified encoding in the first n-bytes.
"""
- if not isinstance(sequence, bytes):
+ if not isinstance(sequence, (bytes, bytearray)):
raise TypeError
seq_len: int = len(sequence)
@@ -269,7 +283,7 @@ def is_multi_byte_encoding(name: str) -> bool:
)
-def identify_sig_or_bom(sequence: bytes) -> tuple[str | None, bytes]:
+def identify_sig_or_bom(sequence: bytes | bytearray) -> tuple[str | None, bytes]:
"""
Identify and extract SIG/BOM in given sequence.
"""
@@ -320,12 +334,12 @@ def cp_similarity(iana_name_a: str, iana_name_b: str) -> float:
character_match_count: int = 0
- for i in range(255):
+ for i in range(256):
to_be_decoded: bytes = bytes([i])
if id_a.decode(to_be_decoded) == id_b.decode(to_be_decoded):
character_match_count += 1
- return character_match_count / 254
+ return character_match_count / 256
def is_cp_similar(iana_name_a: str, iana_name_b: str) -> bool:
@@ -353,7 +367,7 @@ def set_logging_handler(
def cut_sequence_chunks(
- sequences: bytes,
+ sequences: bytes | bytearray,
encoding_iana: str,
offsets: range,
chunk_size: int,
diff --git a/addons/source-python/packages/site-packages/charset_normalizer/version.py b/addons/source-python/packages/site-packages/charset_normalizer/version.py
index f85e8929e..a80346fc7 100644
--- a/addons/source-python/packages/site-packages/charset_normalizer/version.py
+++ b/addons/source-python/packages/site-packages/charset_normalizer/version.py
@@ -4,5 +4,5 @@
from __future__ import annotations
-__version__ = "3.4.1"
+__version__ = "3.4.6"
VERSION = __version__.split(".")
diff --git a/addons/source-python/packages/site-packages/docutils/__init__.py b/addons/source-python/packages/site-packages/docutils/__init__.py
index 16af4108e..e27f1266d 100644
--- a/addons/source-python/packages/site-packages/docutils/__init__.py
+++ b/addons/source-python/packages/site-packages/docutils/__init__.py
@@ -1,4 +1,4 @@
-# $Id: __init__.py 9649 2024-04-23 18:54:26Z grubert $
+# $Id: __init__.py 10275 2025-12-18 18:44:54Z grubert $
# Author: David Goodger
# Copyright: This module has been placed in the public domain.
@@ -50,11 +50,42 @@
- writers: Format-specific output translators.
"""
+from __future__ import annotations
+
from collections import namedtuple
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+ from typing import Any, ClassVar, Literal, Protocol, Union
+
+ from docutils.nodes import Element
+ from docutils.transforms import Transform
+
+ _Components = Literal['reader', 'parser', 'writer', 'input', 'output']
+ _OptionTuple = tuple[str, list[str], dict[str, Any]]
+ _ReleaseLevels = Literal['alpha', 'beta', 'candidate', 'final']
+ _SettingsSpecTuple = Union[
+ tuple[str|None, str|None, Sequence[_OptionTuple]],
+ tuple[str|None, str|None, Sequence[_OptionTuple],
+ str|None, str|None, Sequence[_OptionTuple]],
+ tuple[str|None, str|None, Sequence[_OptionTuple],
+ str|None, str|None, Sequence[_OptionTuple],
+ str|None, str|None, Sequence[_OptionTuple]],
+ ]
+
+ class _UnknownReferenceResolver(Protocol):
+ """Deprecated. Will be removed in Docutils 1.0."""
+ # See `TransformSpec.unknown_reference_resolvers`.
+
+ priority: int
+
+ def __call__(self, node: Element, /) -> bool:
+ ...
+
__docformat__ = 'reStructuredText'
-__version__ = '0.21.2'
+__version__ = '0.22.4'
"""Docutils version identifier (complies with PEP 440)::
major.minor[.micro][releaselevel[serial]][.dev]
@@ -74,9 +105,20 @@
class VersionInfo(namedtuple('VersionInfo',
'major minor micro releaselevel serial release')):
-
- def __new__(cls, major=0, minor=0, micro=0,
- releaselevel='final', serial=0, release=True):
+ __slots__ = ()
+
+ major: int
+ minor: int
+ micro: int
+ releaselevel: _ReleaseLevels
+ serial: int
+ release: bool
+
+ def __new__(cls,
+ major: int = 0, minor: int = 0, micro: int = 0,
+ releaselevel: _ReleaseLevels = 'final',
+ serial: int = 0, release: bool = True,
+ ) -> VersionInfo:
releaselevels = ('alpha', 'beta', 'candidate', 'final')
if releaselevel not in releaselevels:
raise ValueError('releaselevel must be one of %r.'
@@ -86,29 +128,29 @@ def __new__(cls, major=0, minor=0, micro=0,
raise ValueError('releaselevel "final" must not be used '
'with development versions (leads to wrong '
'version ordering of the related __version__')
- # cf. https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering # noqa
+ # cf. https://peps.python.org/pep-0440/#summary-of-permitted-suffixes-and-relative-ordering # NoQA: E501
if serial != 0:
raise ValueError('"serial" must be 0 for final releases')
return super().__new__(cls, major, minor, micro,
releaselevel, serial, release)
- def __lt__(self, other):
+ def __lt__(self, other: object) -> bool:
if isinstance(other, tuple):
other = VersionInfo(*other)
return tuple.__lt__(self, other)
- def __gt__(self, other):
+ def __gt__(self, other: object) -> bool:
if isinstance(other, tuple):
other = VersionInfo(*other)
return tuple.__gt__(self, other)
- def __le__(self, other):
+ def __le__(self, other: object) -> bool:
if isinstance(other, tuple):
other = VersionInfo(*other)
return tuple.__le__(self, other)
- def __ge__(self, other):
+ def __ge__(self, other: object) -> bool:
if isinstance(other, tuple):
other = VersionInfo(*other)
return tuple.__ge__(self, other)
@@ -116,8 +158,8 @@ def __ge__(self, other):
__version_info__ = VersionInfo(
major=0,
- minor=21,
- micro=2,
+ minor=22,
+ micro=4,
releaselevel='final', # one of 'alpha', 'beta', 'candidate', 'final'
serial=0, # pre-release number (0 for final releases and snapshots)
release=True # True for official releases and pre-releases
@@ -151,7 +193,7 @@ class SettingsSpec:
# https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/writers/html.py
# This should be changed (before retiring the old format)
# to use `settings_default_overrides` instead.
- settings_spec = ()
+ settings_spec: ClassVar[_SettingsSpecTuple] = ()
"""Runtime settings specification. Override in subclasses.
Defines runtime settings and associated command-line options, as used by
@@ -190,25 +232,25 @@ class SettingsSpec:
needed. Thus, `settings_spec` tuples can be simply concatenated.
"""
- settings_defaults = None
+ settings_defaults: ClassVar[dict[str, Any] | None] = None
"""A dictionary of defaults for settings not in `settings_spec` (internal
settings, intended to be inaccessible by command-line and config file).
Override in subclasses."""
- settings_default_overrides = None
+ settings_default_overrides: ClassVar[dict[str, Any] | None] = None
"""A dictionary of auxiliary defaults, to override defaults for settings
defined in other components' `setting_specs`. Override in subclasses."""
- relative_path_settings = ()
+ relative_path_settings: ClassVar[tuple[str, ...]] = ()
"""Settings containing filesystem paths. Override in subclasses.
Settings listed here are to be interpreted relative to the current working
directory."""
- config_section = None
+ config_section: ClassVar[str | None] = None
"""The name of the config file section specific to this component
(lowercase, no brackets). Override in subclasses."""
- config_section_dependencies = None
+ config_section_dependencies: ClassVar[tuple[str, ...] | None] = None
"""A list of names of config file sections that are to be applied before
`config_section`, in order (from general to specific). In other words,
the settings in `config_section` are to be overlaid on top of the settings
@@ -226,7 +268,7 @@ class TransformSpec:
https://docutils.sourceforge.io/docs/ref/transforms.html
"""
- def get_transforms(self):
+ def get_transforms(self) -> list[type[Transform]]:
"""Transforms required by this class. Override in subclasses."""
if self.default_transforms != ():
import warnings
@@ -238,50 +280,58 @@ def get_transforms(self):
return []
# Deprecated; for compatibility.
- default_transforms = ()
-
- unknown_reference_resolvers = ()
- """List of functions to try to resolve unknown references.
-
- Unknown references have a 'refname' attribute which doesn't correspond
- to any target in the document. Called when the transforms in
- `docutils.transforms.references` are unable to find a correct target.
-
- The list should contain functions which will try to resolve unknown
- references, with the following signature::
-
- def reference_resolver(node):
- '''Returns boolean: true if resolved, false if not.'''
-
- If the function is able to resolve the reference, it should also remove
- the 'refname' attribute and mark the node as resolved::
+ default_transforms: ClassVar[tuple[()]] = ()
- del node['refname']
- node.resolved = 1
+ unknown_reference_resolvers: Sequence[_UnknownReferenceResolver] = ()
+ """List of hook functions which assist in resolving references.
- Each function must have a "priority" attribute which will affect the order
- the unknown_reference_resolvers are run::
-
- reference_resolver.priority = 100
-
- This hook is provided for 3rd party extensions.
- Example use case: the `MoinMoin - ReStructured Text Parser`
- in ``sandbox/mmgilbe/rst.py``.
+ Deprecated. Will be removed in Docutils 1.0
"""
+ # Override in subclasses to implement component-specific resolving of
+ # unknown references.
+ #
+ # Unknown references have a 'refname' attribute which doesn't correspond
+ # to any target in the document. Called when the transforms in
+ # `docutils.transforms.references` are unable to find a correct target.
+ #
+ # The list should contain functions which will try to resolve unknown
+ # references, with the following signature::
+ #
+ # def reference_resolver(node: nodes.Element) -> bool:
+ # '''Returns boolean: true if resolved, false if not.'''
+ #
+ # If the function is able to resolve the reference, it should also remove
+ # the 'refname' attribute and mark the node as resolved::
+ #
+ # del node['refname']
+ # node.resolved = True
+ #
+ # Each function must have a "priority" attribute which will affect the
+ # order the unknown_reference_resolvers are run
+ # cf. ../docs/api/transforms.html#transform-priority-range-categories ::
+ #
+ # reference_resolver.priority = 500
+ #
+ # Examples:
+ # The `MoinMoin ReStructured Text Parser`__ provided a resolver for
+ # "WikiWiki links" in the 1.9 version.
+ #
+ # __ https://github.com/moinwiki/moin-1.9/blob/1.9.11/MoinMoin/parser/
+ # text_rst.py
class Component(SettingsSpec, TransformSpec):
"""Base class for Docutils components."""
- component_type = None
- """Name of the component type ('reader', 'parser', 'writer'). Override in
- subclasses."""
+ component_type: ClassVar[_Components | None] = None
+ """Name of the component type ('reader', 'parser', 'writer').
+ Override in subclasses."""
- supported = ()
+ supported: ClassVar[tuple[str, ...]] = ()
"""Name and aliases for this component. Override in subclasses."""
- def supports(self, format):
+ def supports(self, format: str) -> bool:
"""
Is `format` supported by this component?
diff --git a/addons/source-python/packages/site-packages/docutils/__main__.py b/addons/source-python/packages/site-packages/docutils/__main__.py
index ce614891f..6c4ce42d7 100644
--- a/addons/source-python/packages/site-packages/docutils/__main__.py
+++ b/addons/source-python/packages/site-packages/docutils/__main__.py
@@ -9,8 +9,8 @@
#
# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
#
-# Revision: $Revision: 9107 $
-# Date: $Date: 2022-07-06 15:59:57 +0200 (Mi, 06. Jul 2022) $
+# Revision: $Revision: 10136 $
+# Date: $Date: 2025-05-20 17:48:27 +0200 (Di, 20. Mai 2025) $
"""Generic command line interface for the `docutils` package.
@@ -18,6 +18,10 @@
https://docs.python.org/3/library/__main__.html#main-py-in-python-packages
"""
+from __future__ import annotations
+
+__docformat__ = 'reStructuredText'
+
import argparse
import locale
import sys
@@ -53,10 +57,13 @@ class CliSettingsSpec(docutils.SettingsSpec):
'applications')
-def main():
+def main() -> None:
"""Generic command line interface for the Docutils Publisher.
"""
- locale.setlocale(locale.LC_ALL, '')
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except locale.Error as e:
+ sys.stderr.write(f'WARNING: Cannot set the default locale: {e}.\n')
description = ('Convert documents into useful formats. '
+ default_description)
@@ -75,9 +82,9 @@ def main():
CliSettingsSpec.settings_default_overrides = args.__dict__
try:
- publish_cmdline(reader_name=args.reader,
- parser_name=args.parser,
- writer_name=args.writer,
+ publish_cmdline(reader=args.reader,
+ parser=args.parser,
+ writer=args.writer,
settings_spec=CliSettingsSpec,
description=description,
argv=remainder)
diff --git a/addons/source-python/packages/site-packages/docutils/core.py b/addons/source-python/packages/site-packages/docutils/core.py
index adf807592..54ecbe397 100644
--- a/addons/source-python/packages/site-packages/docutils/core.py
+++ b/addons/source-python/packages/site-packages/docutils/core.py
@@ -1,4 +1,4 @@
-# $Id: core.py 9369 2023-05-02 23:04:27Z milde $
+# $Id: core.py 10267 2025-12-01 22:43:32Z milde $
# Author: David Goodger
# Copyright: This module has been placed in the public domain.
@@ -13,6 +13,8 @@
https://docutils.sourceforge.io/docs/api/publisher.html
"""
+from __future__ import annotations
+
__docformat__ = 'reStructuredText'
import locale
@@ -22,10 +24,15 @@
import warnings
from docutils import (__version__, __version_details__, SettingsSpec,
- io, utils, readers, writers)
+ io, utils, readers, parsers, writers)
from docutils.frontend import OptionParser
from docutils.readers import doctree
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import TextIO
+ from docutils.nodes import StrPath
+
class Publisher:
@@ -36,13 +43,25 @@ class Publisher:
def __init__(self, reader=None, parser=None, writer=None,
source=None, source_class=io.FileInput,
destination=None, destination_class=io.FileOutput,
- settings=None):
+ settings=None) -> None:
"""
- Initial setup. If any of `reader`, `parser`, or `writer` are not
- specified, ``set_components()`` or the corresponding ``set_...()``
- method should be called with component names
- (`set_reader` sets the parser as well).
+ Initial setup.
+
+ The components `reader`, `parser`, or `writer` should all be
+ specified, either as instances or via their names.
"""
+ # get component instances from their names:
+ if isinstance(reader, str):
+ reader = readers.get_reader_class(reader)(parser)
+ if isinstance(parser, str):
+ if isinstance(reader, readers.Reader):
+ if reader.parser is None:
+ reader.set_parser(parser)
+ parser = reader.parser
+ else:
+ parser = parsers.get_parser_class(parser)()
+ if isinstance(writer, str):
+ writer = writers.get_writer_class(writer)()
self.document = None
"""The document tree (`docutils.nodes` objects)."""
@@ -56,13 +75,6 @@ def __init__(self, reader=None, parser=None, writer=None,
self.writer = writer
"""A `docutils.writers.Writer` instance."""
- for component in 'reader', 'parser', 'writer':
- assert not isinstance(getattr(self, component), str), (
- 'passed string "%s" as "%s" parameter; pass an instance, '
- 'or use the "%s_name" parameter instead (in '
- 'docutils.core.publish_* convenience functions).'
- % (getattr(self, component), component, component))
-
self.source = source
"""The source of input data, a `docutils.io.Input` instance."""
@@ -82,18 +94,29 @@ def __init__(self, reader=None, parser=None, writer=None,
self._stderr = io.ErrorOutput()
- def set_reader(self, reader_name, parser, parser_name):
- """Set `self.reader` by name."""
- reader_class = readers.get_reader_class(reader_name)
+ def set_reader(self, reader, parser=None, parser_name=None) -> None:
+ """Set `self.reader` by name.
+
+ The "paser_name" argument is deprecated,
+ use "parser" with parser name or instance.
+ """
+ reader_class = readers.get_reader_class(reader)
self.reader = reader_class(parser, parser_name)
- self.parser = self.reader.parser
+ if self.reader.parser is not None:
+ self.parser = self.reader.parser
+ elif self.parser is not None:
+ self.reader.parser = self.parser
- def set_writer(self, writer_name):
+ def set_writer(self, writer_name) -> None:
"""Set `self.writer` by name."""
writer_class = writers.get_writer_class(writer_name)
self.writer = writer_class()
- def set_components(self, reader_name, parser_name, writer_name):
+ def set_components(self, reader_name, parser_name, writer_name) -> None:
+ warnings.warn('`Publisher.set_components()` will be removed in '
+ 'Docutils 2.0. Specify component names '
+ 'at instantiation.',
+ PendingDeprecationWarning, stacklevel=2)
if self.reader is None:
self.set_reader(reader_name, self.parser, parser_name)
if self.parser is None:
@@ -103,32 +126,27 @@ def set_components(self, reader_name, parser_name, writer_name):
if self.writer is None:
self.set_writer(writer_name)
- def setup_option_parser(self, usage=None, description=None,
- settings_spec=None, config_section=None,
- **defaults):
- warnings.warn('Publisher.setup_option_parser is deprecated, '
- 'and will be removed in Docutils 0.21.',
- DeprecationWarning, stacklevel=2)
- if config_section:
- if not settings_spec:
- settings_spec = SettingsSpec()
- settings_spec.config_section = config_section
- parts = config_section.split()
- if len(parts) > 1 and parts[-1] == 'application':
- settings_spec.config_section_dependencies = ['applications']
- # @@@ Add self.source & self.destination to components in future?
- return OptionParser(
- components=(self.parser, self.reader, self.writer, settings_spec),
- defaults=defaults, read_config_files=True,
- usage=usage, description=description)
-
- def _setup_settings_parser(self, *args, **kwargs):
+ def _setup_settings_parser(self, usage=None, description=None,
+ settings_spec=None, config_section=None,
+ **defaults):
# Provisional: will change (docutils.frontend.OptionParser will
# be replaced by a parser based on arparse.ArgumentParser)
# and may be removed later.
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
- return self.setup_option_parser(*args, **kwargs)
+ if config_section:
+ if not settings_spec:
+ settings_spec = SettingsSpec()
+ settings_spec.config_section = config_section
+ parts = config_section.split()
+ if len(parts) > 1 and parts[-1] == 'application':
+ settings_spec.config_section_dependencies = ['applications'] # noqa: E501
+ # @@@ Add self.source & self.destination to components in future?
+ return OptionParser(
+ components=(self.parser, self.reader, self.writer,
+ settings_spec),
+ defaults=defaults, read_config_files=True,
+ usage=usage, description=description)
def get_settings(self, usage=None, description=None,
settings_spec=None, config_section=None, **defaults):
@@ -149,7 +167,7 @@ def get_settings(self, usage=None, description=None,
def process_programmatic_settings(self, settings_spec,
settings_overrides,
- config_section):
+ config_section) -> None:
if self.settings is None:
defaults = settings_overrides.copy() if settings_overrides else {}
# Propagate exceptions by default when used programmatically:
@@ -160,7 +178,7 @@ def process_programmatic_settings(self, settings_spec,
def process_command_line(self, argv=None, usage=None, description=None,
settings_spec=None, config_section=None,
- **defaults):
+ **defaults) -> None:
"""
Parse command line arguments and set ``self.settings``.
@@ -175,41 +193,58 @@ def process_command_line(self, argv=None, usage=None, description=None,
argv = sys.argv[1:]
self.settings = option_parser.parse_args(argv)
- def set_io(self, source_path=None, destination_path=None):
+ def set_io(self, source_path=None, destination_path=None) -> None:
if self.source is None:
self.set_source(source_path=source_path)
if self.destination is None:
self.set_destination(destination_path=destination_path)
- def set_source(self, source=None, source_path=None):
+ def set_source(self,
+ source: str | None = None,
+ source_path: StrPath | None = None,
+ ) -> None:
if source_path is None:
source_path = self.settings._source
else:
+ source_path = os.fspath(source_path)
self.settings._source = source_path
self.source = self.source_class(
source=source, source_path=source_path,
encoding=self.settings.input_encoding,
error_handler=self.settings.input_encoding_error_handler)
- def set_destination(self, destination=None, destination_path=None):
- if destination_path is None:
- if (self.settings.output and self.settings._destination
- and self.settings.output != self.settings._destination):
- raise SystemExit('The positional argument is '
- 'obsoleted by the --output option. '
+ def set_destination(self,
+ destination: TextIO | None = None,
+ destination_path: StrPath | None = None,
+ ) -> None:
+ # Provisional: the "_destination" and "output" settings
+ # are deprecated and will be ignored in Docutils 2.0.
+ if destination_path is not None:
+ self.settings.output_path = os.fspath(destination_path)
+ else:
+ # check 'output_path' and legacy settings
+ if getattr(self.settings, 'output', None
+ ) and not self.settings.output_path:
+ self.settings.output_path = self.settings.output
+ if (self.settings.output_path and self.settings._destination
+ and self.settings.output_path != self.settings._destination):
+ raise SystemExit('The --output-path option obsoletes the '
+ 'second positional argument (DESTINATION). '
'You cannot use them together.')
- if self.settings.output == '-': # means stdout
- self.settings.output = None
- destination_path = (self.settings.output
- or self.settings._destination)
- self.settings._destination = destination_path
+ if self.settings.output_path is None:
+ self.settings.output_path = self.settings._destination
+ if self.settings.output_path == '-': # use stdout
+ self.settings.output_path = None
+ self.settings._destination = self.settings.output \
+ = self.settings.output_path
+
self.destination = self.destination_class(
destination=destination,
- destination_path=destination_path,
+ destination_path=self.settings.output_path,
encoding=self.settings.output_encoding,
error_handler=self.settings.output_encoding_error_handler)
- def apply_transforms(self):
+ def apply_transforms(self) -> None:
self.document.transformer.populate_from_components(
(self.source, self.reader, self.reader.parser, self.writer,
self.destination))
@@ -223,7 +258,7 @@ def publish(self, argv=None, usage=None, description=None,
already set), run `self.reader` and then `self.writer`. Return
`self.writer`'s output.
"""
- exit = None
+ exit_ = None
try:
if self.settings is None:
self.process_command_line(
@@ -237,7 +272,7 @@ def publish(self, argv=None, usage=None, description=None,
output = self.writer.write(self.document, self.destination)
self.writer.assemble_parts()
except SystemExit as error:
- exit = True
+ exit_ = True
exit_status = error.code
except Exception as error:
if not self.settings: # exception too early to report nicely
@@ -246,18 +281,18 @@ def publish(self, argv=None, usage=None, description=None,
self.debugging_dumps()
raise
self.report_Exception(error)
- exit = True
+ exit_ = True
exit_status = 1
self.debugging_dumps()
if (enable_exit_status and self.document
and (self.document.reporter.max_level
>= self.settings.exit_status_level)):
sys.exit(self.document.reporter.max_level + 10)
- elif exit:
+ elif exit_:
sys.exit(exit_status)
return output
- def debugging_dumps(self):
+ def debugging_dumps(self) -> None:
if not self.document:
return
if self.settings.dump_settings:
@@ -280,7 +315,7 @@ def debugging_dumps(self):
print(self.document.pformat().encode(
'raw_unicode_escape'), file=self._stderr)
- def prompt(self):
+ def prompt(self) -> None:
"""Print info and prompt when waiting for input from a terminal."""
try:
if not (self.source.isatty() and self._stderr.isatty()):
@@ -302,7 +337,7 @@ def prompt(self):
'on an empty line):',
file=self._stderr)
- def report_Exception(self, error):
+ def report_Exception(self, error) -> None:
if isinstance(error, utils.SystemMessage):
self.report_SystemMessage(error)
elif isinstance(error, UnicodeEncodeError):
@@ -324,12 +359,12 @@ def report_Exception(self, error):
Python version ({sys.version.split()[0]}), your OS type & version, \
and the command line used.""", file=self._stderr)
- def report_SystemMessage(self, error):
+ def report_SystemMessage(self, error) -> None:
print('Exiting due to level-%s (%s) system message.' % (
error.level, utils.Reporter.levels[error.level]),
file=self._stderr)
- def report_UnicodeError(self, error):
+ def report_UnicodeError(self, error) -> None:
data = error.object[error.start:error.end]
self._stderr.write(
'%s\n'
@@ -376,9 +411,9 @@ def report_UnicodeError(self, error):
# Chain several args as input and use --output or redirection for output:
# argparser.add_argument('source', nargs='+')
#
-def publish_cmdline(reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
- writer=None, writer_name='pseudoxml',
+def publish_cmdline(reader=None, reader_name=None,
+ parser=None, parser_name=None,
+ writer=None, writer_name=None,
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=True, argv=None,
@@ -397,8 +432,13 @@ def publish_cmdline(reader=None, reader_name='standalone',
- `description`: Program description, output for the "--help" option
(along with command-line option descriptions).
"""
+ # The "*_name" arguments are deprecated.
+ _name_arg_warning(reader_name, parser_name, writer_name)
+ # The default is only used if both arguments are empty
+ reader = reader or reader_name or 'standalone'
+ parser = parser or parser_name or 'restructuredtext'
+ writer = writer or writer_name or 'pseudoxml'
publisher = Publisher(reader, parser, writer, settings=settings)
- publisher.set_components(reader_name, parser_name, writer_name)
output = publisher.publish(
argv, usage, description, settings_spec, settings_overrides,
config_section=config_section, enable_exit_status=enable_exit_status)
@@ -407,9 +447,9 @@ def publish_cmdline(reader=None, reader_name='standalone',
def publish_file(source=None, source_path=None,
destination=None, destination_path=None,
- reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
- writer=None, writer_name='pseudoxml',
+ reader=None, reader_name=None,
+ parser=None, parser_name=None,
+ writer=None, writer_name=None,
settings=None, settings_spec=None, settings_overrides=None,
config_section=None, enable_exit_status=False):
"""
@@ -419,7 +459,10 @@ def publish_file(source=None, source_path=None,
Parameters: see `publish_programmatically()`.
"""
- output, publisher = publish_programmatically(
+ # The "*_name" arguments are deprecated.
+ _name_arg_warning(reader_name, parser_name, writer_name)
+ # The default is set in publish_programmatically().
+ output, _publisher = publish_programmatically(
source_class=io.FileInput, source=source, source_path=source_path,
destination_class=io.FileOutput,
destination=destination, destination_path=destination_path,
@@ -434,9 +477,9 @@ def publish_file(source=None, source_path=None,
def publish_string(source, source_path=None, destination_path=None,
- reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
- writer=None, writer_name='pseudoxml',
+ reader=None, reader_name=None,
+ parser=None, parser_name=None,
+ writer=None, writer_name=None,
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=False):
@@ -449,7 +492,8 @@ def publish_string(source, source_path=None, destination_path=None,
the return value is a `bytes` instance (unless `output_encoding`_ is
"unicode", cf. `docutils.io.StringOutput.write()`).
- Parameters: see `publish_programmatically()`.
+ Parameters: see `publish_programmatically()` or
+ https://docutils.sourceforge.io/docs/api/publisher.html#publish-string
This function is provisional because in Python 3 name and behaviour
no longer match.
@@ -457,7 +501,10 @@ def publish_string(source, source_path=None, destination_path=None,
.. _output_encoding:
https://docutils.sourceforge.io/docs/user/config.html#output-encoding
"""
- output, publisher = publish_programmatically(
+ # The "*_name" arguments are deprecated.
+ _name_arg_warning(reader_name, parser_name, writer_name)
+ # The default is set in publish_programmatically().
+ output, _publisher = publish_programmatically(
source_class=io.StringInput, source=source, source_path=source_path,
destination_class=io.StringOutput,
destination=None, destination_path=destination_path,
@@ -473,9 +520,9 @@ def publish_string(source, source_path=None, destination_path=None,
def publish_parts(source, source_path=None, source_class=io.StringInput,
destination_path=None,
- reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
- writer=None, writer_name='pseudoxml',
+ reader=None, reader_name=None,
+ parser=None, parser_name=None,
+ writer=None, writer_name=None,
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=False):
@@ -495,7 +542,10 @@ def publish_parts(source, source_path=None, source_class=io.StringInput,
__ https://docutils.sourceforge.io/docs/api/publisher.html#publish-parts
"""
- output, publisher = publish_programmatically(
+ # The "*_name" arguments are deprecated.
+ _name_arg_warning(reader_name, parser_name, writer_name)
+ # The default is set in publish_programmatically().
+ _output, publisher = publish_programmatically(
source=source, source_path=source_path, source_class=source_class,
destination_class=io.StringOutput,
destination=None, destination_path=destination_path,
@@ -511,8 +561,8 @@ def publish_parts(source, source_path=None, source_class=io.StringInput,
def publish_doctree(source, source_path=None,
source_class=io.StringInput,
- reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
+ reader=None, reader_name=None,
+ parser=None, parser_name=None,
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=False):
@@ -521,6 +571,9 @@ def publish_doctree(source, source_path=None,
Parameters: see `publish_programmatically()`.
"""
+ # The "*_name" arguments are deprecated.
+ _name_arg_warning(reader_name, parser_name, None)
+ # The default is set in publish_programmatically().
_output, publisher = publish_programmatically(
source=source, source_path=source_path,
source_class=source_class,
@@ -528,7 +581,7 @@ def publish_doctree(source, source_path=None,
destination_class=io.NullOutput,
reader=reader, reader_name=reader_name,
parser=parser, parser_name=parser_name,
- writer=None, writer_name='null',
+ writer='null', writer_name=None,
settings=settings, settings_spec=settings_spec,
settings_overrides=settings_overrides, config_section=config_section,
enable_exit_status=enable_exit_status)
@@ -536,7 +589,7 @@ def publish_doctree(source, source_path=None,
def publish_from_doctree(document, destination_path=None,
- writer=None, writer_name='pseudoxml',
+ writer=None, writer_name=None,
settings=None, settings_spec=None,
settings_overrides=None, config_section=None,
enable_exit_status=False):
@@ -559,13 +612,13 @@ def publish_from_doctree(document, destination_path=None,
This function is provisional because in Python 3 name and behaviour
of the `io.StringOutput` class no longer match.
"""
- reader = doctree.Reader(parser_name='null')
- publisher = Publisher(reader, None, writer,
+ # The "writer_name" argument is deprecated.
+ _name_arg_warning(None, None, writer_name)
+ publisher = Publisher(reader=doctree.Reader(),
+ writer=writer or writer_name or 'pseudoxml',
source=io.DocTreeInput(document),
destination_class=io.StringOutput,
settings=settings)
- if not writer and writer_name:
- publisher.set_writer(writer_name)
publisher.process_programmatic_settings(
settings_spec, settings_overrides, config_section)
publisher.set_destination(None, destination_path)
@@ -600,7 +653,13 @@ def publish_cmdline_to_binary(reader=None, reader_name='standalone',
line.
- `description`: Program description, output for the "--help" option
(along with command-line option descriptions).
+
+ Deprecated. Use `publish_cmdline()` (works with `bytes` since
+ Docutils 0.20). Will be removed in Docutils 0.24.
"""
+ warnings.warn('"publish_cmdline_to_binary()" is obsoleted'
+ ' by "publish_cmdline()" and will be removed'
+ ' in Docutils 0.24.', DeprecationWarning, stacklevel=2)
publisher = Publisher(reader, parser, writer, settings=settings,
destination_class=destination_class)
publisher.set_components(reader_name, parser_name, writer_name)
@@ -610,6 +669,15 @@ def publish_cmdline_to_binary(reader=None, reader_name='standalone',
return output
+def _name_arg_warning(*name_args) -> None:
+ for component, name_arg in zip(('reader', 'parser', 'writer'), name_args):
+ if name_arg is not None:
+ warnings.warn(f'Argument "{component}_name" will be removed in '
+ f'Docutils 2.0. Specify {component} name '
+ f'in the "{component}" argument.',
+ PendingDeprecationWarning, stacklevel=3)
+
+
def publish_programmatically(source_class, source, source_path,
destination_class, destination, destination_path,
reader, reader_name,
@@ -624,7 +692,8 @@ def publish_programmatically(source_class, source, source_path,
Return the output (as `str` or `bytes`, depending on `destination_class`,
writer, and the "output_encoding" setting) and the Publisher object.
- Applications should not need to call this function directly. If it does
+ Internal:
+ Applications should not call this function directly. If it does
seem to be necessary to call this function directly, please write to the
Docutils-develop mailing list
.
@@ -650,7 +719,7 @@ def publish_programmatically(source_class, source, source_path,
- `io.FileInput`: Path to the input file, opened if no `source`
supplied.
- - `io.StringInput`: Optional. Path to the file or name of the
+ - `io.StringInput`: Optional. Path to the file or description of the
object that produced `source`. Only used for diagnostic output.
* `destination_class` **required**: The class for dynamically created
@@ -674,20 +743,20 @@ def publish_programmatically(source_class, source, source_path,
output; optional. Used for determining relative paths (stylesheets,
source links, etc.).
- * `reader`: A `docutils.readers.Reader` object.
+ * `reader`: A `docutils.readers.Reader` instance, name, or alias.
+ Default: "standalone".
- * `reader_name`: Name or alias of the Reader class to be instantiated if
- no `reader` supplied.
+ * `reader_name`: Deprecated. Use `reader`.
- * `parser`: A `docutils.parsers.Parser` object.
+ * `parser`: A `docutils.parsers.Parser` instance, name, or alias.
+ Default: "restructuredtext".
- * `parser_name`: Name or alias of the Parser class to be instantiated if
- no `parser` supplied.
+ * `parser_name`: Deprecated. Use `parser`.
- * `writer`: A `docutils.writers.Writer` object.
+ * `writer`: A `docutils.writers.Writer` instance, name, or alias.
+ Default: "pseudoxml".
- * `writer_name`: Name or alias of the Writer class to be instantiated if
- no `writer` supplied.
+ * `writer_name`: Deprecated. Use `writer`.
* `settings`: A runtime settings (`docutils.frontend.Values`) object, for
dotted-attribute access to runtime settings. It's the end result of the
@@ -711,10 +780,13 @@ def publish_programmatically(source_class, source, source_path,
* `enable_exit_status`: Boolean; enable exit status at end of processing?
"""
+ reader = reader or reader_name or 'standalone'
+ parser = parser or parser_name or 'restructuredtext'
+ writer = writer or writer_name or 'pseudoxml'
+
publisher = Publisher(reader, parser, writer, settings=settings,
source_class=source_class,
destination_class=destination_class)
- publisher.set_components(reader_name, parser_name, writer_name)
publisher.process_programmatic_settings(
settings_spec, settings_overrides, config_section)
publisher.set_source(source, source_path)
@@ -726,8 +798,8 @@ def publish_programmatically(source_class, source, source_path,
# "Entry points" with functionality of the "tools/rst2*.py" scripts
# cf. https://packaging.python.org/en/latest/specifications/entry-points/
-def rst2something(writer, documenttype, doc_path=''):
- # Helper function for the common parts of rst2...
+def rst2something(writer, documenttype, doc_path='') -> None:
+ # Helper function for the common parts of `rst2*()`
# writer: writer name
# documenttype: output document type
# doc_path: documentation path (relative to the documentation root)
@@ -736,45 +808,48 @@ def rst2something(writer, documenttype, doc_path=''):
'from standalone reStructuredText sources '
f'. '
+ default_description)
- locale.setlocale(locale.LC_ALL, '')
- publish_cmdline(writer_name=writer, description=description)
+ try:
+ locale.setlocale(locale.LC_ALL, '')
+ except locale.Error as e:
+ sys.stderr.write(f'WARNING: Cannot set the default locale: {e}.\n')
+ publish_cmdline(writer=writer, description=description)
-def rst2html():
+def rst2html() -> None:
rst2something('html', 'HTML', 'user/html.html#html')
-def rst2html4():
+def rst2html4() -> None:
rst2something('html4', 'XHTML 1.1', 'user/html.html#html4css1')
-def rst2html5():
+def rst2html5() -> None:
rst2something('html5', 'HTML5', 'user/html.html#html5-polyglot')
-def rst2latex():
+def rst2latex() -> None:
rst2something('latex', 'LaTeX', 'user/latex.html')
-def rst2man():
+def rst2man() -> None:
rst2something('manpage', 'Unix manual (troff)', 'user/manpage.html')
-def rst2odt():
+def rst2odt() -> None:
rst2something('odt', 'OpenDocument text (ODT)', 'user/odt.html')
-def rst2pseudoxml():
+def rst2pseudoxml() -> None:
rst2something('pseudoxml', 'pseudo-XML (test)', 'ref/doctree.html')
-def rst2s5():
+def rst2s5() -> None:
rst2something('s5', 'S5 HTML slideshow', 'user/slide-shows.html')
-def rst2xetex():
+def rst2xetex() -> None:
rst2something('xetex', 'LaTeX (XeLaTeX/LuaLaTeX)', 'user/latex.html')
-def rst2xml():
+def rst2xml() -> None:
rst2something('xml', 'Docutils-native XML', 'ref/doctree.html')
diff --git a/addons/source-python/packages/site-packages/docutils/examples.py b/addons/source-python/packages/site-packages/docutils/examples.py
index c27ab70d9..7a2480aa7 100644
--- a/addons/source-python/packages/site-packages/docutils/examples.py
+++ b/addons/source-python/packages/site-packages/docutils/examples.py
@@ -1,4 +1,4 @@
-# $Id: examples.py 9026 2022-03-04 15:57:13Z milde $
+# $Id: examples.py 10045 2025-03-09 01:02:23Z aa-turner $
# Author: David Goodger
# Copyright: This module has been placed in the public domain.
@@ -11,12 +11,27 @@
necessary.
"""
+from __future__ import annotations
+
+
from docutils import core, io
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Any, Literal
+
+ from docutils import nodes
+ from docutils.nodes import StrPath
+ from docutils.core import Publisher
-def html_parts(input_string, source_path=None, destination_path=None,
- input_encoding='unicode', doctitle=True,
- initial_header_level=1):
+
+def html_parts(input_string: str | bytes,
+ source_path: StrPath | None = None,
+ destination_path: StrPath | None = None,
+ input_encoding: Literal['unicode'] | str = 'unicode',
+ doctitle: bool = True,
+ initial_header_level: int = 1,
+ ) -> dict[str, str]:
"""
Given an input string, returns a dictionary of HTML document parts.
@@ -46,13 +61,18 @@ def html_parts(input_string, source_path=None, destination_path=None,
parts = core.publish_parts(
source=input_string, source_path=source_path,
destination_path=destination_path,
- writer_name='html', settings_overrides=overrides)
+ writer='html', settings_overrides=overrides)
return parts
-def html_body(input_string, source_path=None, destination_path=None,
- input_encoding='unicode', output_encoding='unicode',
- doctitle=True, initial_header_level=1):
+def html_body(input_string: str | bytes,
+ source_path: StrPath | None = None,
+ destination_path: StrPath | None = None,
+ input_encoding: Literal['unicode'] | str = 'unicode',
+ output_encoding: Literal['unicode'] | str = 'unicode',
+ doctitle: bool = True,
+ initial_header_level: int = 1,
+ ) -> str | bytes:
"""
Given an input string, returns an HTML fragment as a string.
@@ -74,26 +94,30 @@ def html_body(input_string, source_path=None, destination_path=None,
return fragment
-def internals(input_string, source_path=None, destination_path=None,
- input_encoding='unicode', settings_overrides=None):
+def internals(source: str,
+ source_path: StrPath | None = None,
+ input_encoding: Literal['unicode'] | str = 'unicode',
+ settings_overrides: dict[str, Any] | None = None,
+ ) -> tuple[nodes.document, Publisher]:
"""
Return the document tree and publisher, for exploring Docutils internals.
Parameters: see `html_parts()`.
"""
- if settings_overrides:
- overrides = settings_overrides.copy()
- else:
- overrides = {}
- overrides['input_encoding'] = input_encoding
- output, pub = core.publish_programmatically(
- source_class=io.StringInput, source=input_string,
- source_path=source_path,
- destination_class=io.NullOutput, destination=None,
- destination_path=destination_path,
- reader=None, reader_name='standalone',
- parser=None, parser_name='restructuredtext',
- writer=None, writer_name='null',
- settings=None, settings_spec=None, settings_overrides=overrides,
- config_section=None, enable_exit_status=None)
- return pub.writer.document, pub
+ if settings_overrides is None:
+ settings_overrides = {}
+ overrides = settings_overrides | {'input_encoding': input_encoding}
+
+ publisher = core.Publisher('standalone', 'rst', 'null',
+ source_class=io.StringInput,
+ destination_class=io.NullOutput)
+ publisher.process_programmatic_settings(settings_spec=None,
+ settings_overrides=overrides,
+ config_section=None)
+ publisher.set_source(source, source_path)
+ publisher.publish()
+ return publisher.document, publisher
+
+
+if __name__ == '__main__':
+ print(internals('test')[0])
diff --git a/addons/source-python/packages/site-packages/docutils/frontend.py b/addons/source-python/packages/site-packages/docutils/frontend.py
index 2499c628c..d5415f4fd 100644
--- a/addons/source-python/packages/site-packages/docutils/frontend.py
+++ b/addons/source-python/packages/site-packages/docutils/frontend.py
@@ -1,4 +1,4 @@
-# $Id: frontend.py 9540 2024-02-17 10:36:59Z milde $
+# $Id: frontend.py 10196 2025-08-07 06:35:37Z milde $
# Author: David Goodger
# Copyright: This module has been placed in the public domain.
@@ -6,8 +6,8 @@
Command-line and common processing for Docutils front-end tools.
This module is provisional.
-Major changes will happen with the switch from the deprecated
-"optparse" module to "arparse".
+Major changes will happen with the transition from the
+"optparse" module to "arparse" in Docutils 2.0 or later.
Applications should use the high-level API provided by `docutils.core`.
See https://docutils.sourceforge.io/docs/api/runtime-settings.html.
@@ -30,7 +30,7 @@
`get_default_settings()`. New in 0.19.
Option callbacks:
- `store_multiple()`, `read_config_file()`. Deprecated.
+ `store_multiple()`, `read_config_file()`. Deprecated. To be removed.
Setting validators:
`validate_encoding()`, `validate_encoding_error_handler()`,
@@ -50,29 +50,59 @@
`make_paths_absolute()`, `filter_settings_spec()`. Provisional.
"""
+from __future__ import annotations
+
__docformat__ = 'reStructuredText'
import codecs
import configparser
import optparse
-from optparse import SUPPRESS_HELP
import os
import os.path
-from pathlib import Path
import sys
import warnings
+from optparse import SUPPRESS_HELP
+from pathlib import Path
import docutils
from docutils import io, utils
-
-def store_multiple(option, opt, value, parser, *args, **kwargs):
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Iterable, Mapping, Sequence
+ from typing import Any, ClassVar, Literal, Protocol
+
+ from docutils import SettingsSpec, _OptionTuple, _SettingsSpecTuple
+ from docutils.io import StrPath
+
+ class _OptionValidator(Protocol):
+ def __call__(
+ self,
+ setting: str,
+ value: str | None,
+ option_parser: OptionParser,
+ /,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> Any:
+ ...
+
+
+def store_multiple(option: optparse.Option,
+ opt: str,
+ value: Any,
+ parser: OptionParser,
+ *args: str,
+ **kwargs: Any,
+ ) -> None:
"""
Store multiple values in `parser.values`. (Option callback.)
Store `None` for each attribute named in `args`, and store the value for
each key (attribute name) in `kwargs`.
+
+ Deprecated. Will be removed with the switch to from optparse to argparse.
"""
for attribute in args:
setattr(parser.values, attribute, None)
@@ -80,9 +110,15 @@ def store_multiple(option, opt, value, parser, *args, **kwargs):
setattr(parser.values, key, value)
-def read_config_file(option, opt, value, parser):
+def read_config_file(option: optparse.Option,
+ opt: str,
+ value: Any,
+ parser: OptionParser,
+ ) -> None:
"""
Read a configuration file during option processing. (Option callback.)
+
+ Deprecated. Will be removed with the switch to from optparse to argparse.
"""
try:
new_settings = parser.get_config_file_settings(value)
@@ -91,25 +127,37 @@ def read_config_file(option, opt, value, parser):
parser.values.update(new_settings, parser)
-def validate_encoding(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_encoding(setting: str,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> str | None:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
if value is None:
value = setting
if value == '':
- return None # allow overwriting a config file value
+ warnings.warn('Input encoding detection will be removed and the '
+ 'special encoding values None and "" become invalid '
+ 'in Docutils 1.0.', FutureWarning, stacklevel=2)
+ return None
try:
codecs.lookup(value)
except LookupError:
- raise LookupError('setting "%s": unknown encoding: "%s"'
- % (setting, value))
+ prefix = f'setting "{setting}":' if setting else ''
+ raise LookupError(f'{prefix} unknown encoding: "{value}"')
return value
-def validate_encoding_error_handler(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_encoding_error_handler(
+ setting: str,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> str:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -127,10 +175,20 @@ def validate_encoding_error_handler(setting, value=None, option_parser=None,
def validate_encoding_and_error_handler(
- setting, value, option_parser, config_parser=None, config_section=None):
- """
+ setting: str,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> str:
+ """Check/normalize encoding settings
+
Side-effect: if an error handler is included in the value, it is inserted
into the appropriate place as if it were a separate setting/option.
+
+ All arguments except `value` are ignored
+ (kept for compatibility with "optparse" module).
+ If there is only one positional argument, it is interpreted as `value`.
"""
if ':' in value:
encoding, handler = value.split(':')
@@ -145,11 +203,16 @@ def validate_encoding_and_error_handler(
return validate_encoding(encoding)
-def validate_boolean(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_boolean(setting: str | bool,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> bool:
"""Check/normalize boolean settings:
- True: '1', 'on', 'yes', 'true'
- False: '0', 'off', 'no','false', ''
+
+ :True: '1', 'on', 'yes', 'true'
+ :False: '0', 'off', 'no','false', ''
All arguments except `value` are ignored
(kept for compatibility with "optparse" module).
@@ -165,12 +228,17 @@ def validate_boolean(setting, value=None, option_parser=None,
raise LookupError('unknown boolean value: "%s"' % value)
-def validate_ternary(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_ternary(setting: str | bool,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> str | bool | None:
"""Check/normalize three-value settings:
- True: '1', 'on', 'yes', 'true'
- False: '0', 'off', 'no','false', ''
- any other value: returned as-is.
+
+ :True: '1', 'on', 'yes', 'true'
+ :False: '0', 'off', 'no','false', ''
+ :any other value: returned as-is.
All arguments except `value` are ignored
(kept for compatibility with "optparse" module).
@@ -186,8 +254,12 @@ def validate_ternary(setting, value=None, option_parser=None,
return value
-def validate_nonnegative_int(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_nonnegative_int(setting: str | int,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> int:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -199,8 +271,12 @@ def validate_nonnegative_int(setting, value=None, option_parser=None,
return value
-def validate_threshold(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_threshold(setting: str | int,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> int:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -216,8 +292,12 @@ def validate_threshold(setting, value=None, option_parser=None,
def validate_colon_separated_string_list(
- setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+ setting: str | list[str],
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> list[str]:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -231,8 +311,13 @@ def validate_colon_separated_string_list(
return value
-def validate_comma_separated_list(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_comma_separated_list(
+ setting: str | list[str],
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> list[str]:
"""Check/normalize list arguments (split at "," and strip whitespace).
All arguments except `value` are ignored
@@ -253,8 +338,12 @@ def validate_comma_separated_list(setting, value=None, option_parser=None,
return value
-def validate_math_output(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_math_output(setting: str,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> tuple[()] | tuple[str, str]:
"""Check "math-output" setting, return list with "format" and "options".
See also https://docutils.sourceforge.io/docs/user/config.html#math-output
@@ -270,7 +359,7 @@ def validate_math_output(setting, value=None, option_parser=None,
tex2mathml_converters = ('', 'latexml', 'ttm', 'blahtexml', 'pandoc')
if not value:
- return []
+ return ()
values = value.split(maxsplit=1)
format = values[0].lower()
try:
@@ -286,11 +375,15 @@ def validate_math_output(setting, value=None, option_parser=None,
raise LookupError(f'MathML converter "{options}" not supported,\n'
f' choose from {tex2mathml_converters}.')
options = converter
- return [format, options]
+ return format, options
-def validate_url_trailing_slash(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_url_trailing_slash(setting: str | None,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> str:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -304,8 +397,12 @@ def validate_url_trailing_slash(setting, value=None, option_parser=None,
return value + '/'
-def validate_dependency_file(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_dependency_file(setting: str | None,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> utils.DependencyList:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -318,8 +415,12 @@ def validate_dependency_file(setting, value=None, option_parser=None,
return utils.DependencyList(None)
-def validate_strip_class(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_strip_class(setting: str,
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> list[str]:
# All arguments except `value` are ignored
# (kept for compatibility with "optparse" module).
# If there is only one positional argument, it is interpreted as `value`.
@@ -336,8 +437,13 @@ def validate_strip_class(setting, value=None, option_parser=None,
return value
-def validate_smartquotes_locales(setting, value=None, option_parser=None,
- config_parser=None, config_section=None):
+def validate_smartquotes_locales(
+ setting: str | list[str | tuple[str, str]],
+ value: str | None = None,
+ option_parser: OptionParser | None = None,
+ config_parser: ConfigParser | None = None,
+ config_section: str | None = None,
+ ) -> list[tuple[str, Sequence[str]]]:
"""Check/normalize a comma separated list of smart quote definitions.
Return a list of (language-tag, quotes) string tuples.
@@ -377,7 +483,10 @@ def validate_smartquotes_locales(setting, value=None, option_parser=None,
return lc_quotes
-def make_paths_absolute(pathdict, keys, base_path=None):
+def make_paths_absolute(pathdict: dict[str, list[StrPath] | StrPath],
+ keys: tuple[str],
+ base_path: StrPath | None = None,
+ ) -> None:
"""
Interpret filesystem path settings relative to the `base_path` given.
@@ -388,24 +497,30 @@ def make_paths_absolute(pathdict, keys, base_path=None):
base_path = Path.cwd()
else:
base_path = Path(base_path)
+ if sys.platform == 'win32' and sys.version_info[:2] <= (3, 9):
+ base_path = base_path.absolute()
for key in keys:
if key in pathdict:
value = pathdict[key]
- if isinstance(value, list):
+ if isinstance(value, (list, tuple)):
value = [str((base_path/path).resolve()) for path in value]
elif value:
value = str((base_path/value).resolve())
pathdict[key] = value
-def make_one_path_absolute(base_path, path):
+def make_one_path_absolute(base_path: StrPath, path: StrPath) -> str:
# deprecated, will be removed
warnings.warn('frontend.make_one_path_absolute() will be removed '
- 'in Docutils 0.23.', DeprecationWarning, stacklevel=2)
+ 'in Docutils 2.0 or later.',
+ DeprecationWarning, stacklevel=2)
return os.path.abspath(os.path.join(base_path, path))
-def filter_settings_spec(settings_spec, *exclude, **replace):
+def filter_settings_spec(settings_spec: _SettingsSpecTuple,
+ *exclude: str,
+ **replace: _OptionTuple,
+ ) -> _SettingsSpecTuple:
"""Return a copy of `settings_spec` excluding/replacing some settings.
`settings_spec` is a tuple of configuration settings
@@ -418,7 +533,7 @@ def filter_settings_spec(settings_spec, *exclude, **replace):
settings = list(settings_spec)
# every third item is a sequence of option tuples
for i in range(2, len(settings), 3):
- newopts = []
+ newopts: list[_OptionTuple] = []
for opt_spec in settings[i]:
# opt_spec is ("", [