From bc5476798cc30cf11d3a8337187550666af4af8d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 17 Mar 2025 15:16:15 +0200 Subject: [PATCH 1/4] Bump pre-commit tools --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a86ee871..2a3065a2f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.1 + rev: v0.11.0 hooks: - id: ruff args: From ca6c458fa20115d5cc4337e0c2a4c92060d63f85 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Mon, 17 Mar 2025 16:13:04 +0200 Subject: [PATCH 2/4] Set 95 as line-length --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 98ab326ac..167540ce5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ target-version = "py38" extend-exclude = [ "tests/messages/data", ] +line-length = 95 [tool.ruff.format] quote-style = "preserve" From 90800fb5b0b20702ff1fde2e907ff1bbd9a1bd53 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 19 Mar 2025 12:48:41 +0200 Subject: [PATCH 3/4] Apply manual formatting/fmt:skip to things that would be poorly autoformatted --- babel/core.py | 5 +- babel/dates.py | 69 +++++++++-------- babel/localtime/_fallback.py | 2 +- babel/messages/catalog.py | 13 ++-- babel/messages/checkers.py | 18 ++--- babel/messages/extract.py | 13 ++-- babel/messages/frontend.py | 142 ++++++++++++++++++++--------------- babel/messages/jslexer.py | 4 +- babel/messages/mofile.py | 31 ++++---- babel/messages/plurals.py | 2 +- babel/numbers.py | 18 +++-- babel/plural.py | 4 +- babel/support.py | 11 ++- babel/util.py | 10 ++- 14 files changed, 186 insertions(+), 156 deletions(-) diff --git a/babel/core.py b/babel/core.py index 7386fb37d..eb159c310 100644 --- a/babel/core.py +++ b/babel/core.py @@ -119,7 +119,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): @@ -346,7 +346,8 @@ def parse( 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): diff --git a/babel/dates.py b/babel/dates.py index 2ec10b5be..8c6e1fd57 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -41,7 +41,7 @@ _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 @@ -1018,9 +1018,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) @@ -1266,8 +1274,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())) @@ -1637,35 +1648,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) @@ -1739,7 +1739,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) @@ -1967,11 +1967,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) @@ -1982,13 +1982,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/babel/localtime/_fallback.py b/babel/localtime/_fallback.py index fab6867c3..73def22e7 100644 --- a/babel/localtime/_fallback.py +++ b/babel/localtime/_fallback.py @@ -38,7 +38,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/babel/messages/catalog.py b/babel/messages/catalog.py index a35647f0f..0a2319ad9 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -504,7 +504,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)) @@ -726,17 +726,14 @@ 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 = [*distinct(current.locations + message.locations)] + current.auto_comments = [*distinct(current.auto_comments + message.auto_comments)] + current.user_comments = [*distinct(current.user_comments + message.user_comments)] 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)): diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 072a48a98..e56bce9a1 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -27,8 +27,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 +38,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: @@ -137,13 +137,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 diff --git a/babel/messages/extract.py b/babel/messages/extract.py index dcf62f24f..eedc58747 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -62,7 +62,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 +72,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 @@ -696,9 +696,12 @@ 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)] diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 29e5a2aa2..197376bf7 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -173,7 +173,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): @@ -207,30 +207,23 @@ def _run_domain(self, domain): if not self.input_file: 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 = os.path.join(self.directory, self.locale, "LC_MESSAGES") + po_files.append((self.locale, os.path.join(lc_messages_path, f"{domain}.po"))) + mo_files.append(os.path.join(lc_messages_path, f"{domain}.mo")) else: for locale in os.listdir(self.directory): - po_file = os.path.join(self.directory, locale, - 'LC_MESSAGES', f"{domain}.po") + lc_messages_path = os.path.join(self.directory, locale, "LC_MESSAGES") + po_file = os.path.join(lc_messages_path, 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")) + mo_files.append(os.path.join(lc_messages_path, f"{domain}.mo")) else: po_files.append((self.locale, self.input_file)) if self.output_file: mo_files.append(self.output_file) else: - mo_files.append(os.path.join(self.directory, self.locale, - 'LC_MESSAGES', - f"{domain}.mo")) + lc_messages_path = os.path.join(self.directory, self.locale, "LC_MESSAGES") + mo_files.append(os.path.join(lc_messages_path, f"{domain}.mo")) if not po_files: raise OptionError('no message catalogs found') @@ -347,7 +340,7 @@ 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', @@ -435,10 +428,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 = [] @@ -506,18 +498,25 @@ def run(self): 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): @@ -529,12 +528,16 @@ def run(self): 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 = [] @@ -596,7 +599,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,8 +625,8 @@ 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 = os.path.join(self.output_dir, self.locale, "LC_MESSAGES") + self.output_file = os.path.join(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)) @@ -689,7 +692,7 @@ 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', @@ -749,15 +752,12 @@ def run(self): po_files = [] if not self.output_file: 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 = os.path.join(self.output_dir, self.locale, "LC_MESSAGES") + po_files.append((self.locale, os.path.join(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") + lc_messages_path = os.path.join(self.output_dir, locale, 'LC_MESSAGES') + po_file = os.path.join(lc_messages_path, f"{self.domain}.po") if os.path.exists(po_file): po_files.append((locale, po_file)) else: @@ -799,7 +799,8 @@ def run(self): 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, ) @@ -809,10 +810,14 @@ def run(self): 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 @@ -890,15 +895,28 @@ def run(self, argv=None): 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 +931,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: diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index 7a2ba6a43..4cd79d5d7 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -19,7 +19,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'} @@ -59,7 +59,7 @@ class Token(NamedTuple): '(?:[^'\\]*(?:\\.[^'\\]*)*)' | "(?:[^"\\]*(?:\\.[^"\\]*)*)" )''', re.VERBOSE | re.DOTALL)), -] +] # fmt: skip def get_rules(jsx: bool, dotted: bool, template_string: bool) -> list[tuple[str | None, re.Pattern[str]]]: diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 3c9fefc4a..20c67188d 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -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/babel/messages/plurals.py b/babel/messages/plurals.py index da336a7ba..5eae86ddd 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -197,7 +197,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/babel/numbers.py b/babel/numbers.py index 2737a7076..3ea97bcc0 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -432,7 +432,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( @@ -1054,9 +1054,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,9 +1123,10 @@ 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) diff --git a/babel/plural.py b/babel/plural.py index 63a081e85..638dfefe3 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -463,8 +463,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'): diff --git a/babel/support.py b/babel/support.py index b600bfe27..d181fbd1c 100644 --- a/babel/support.py +++ b/babel/support.py @@ -593,19 +593,18 @@ 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 diff --git a/babel/util.py b/babel/util.py index d113982ee..4e90ac5cb 100644 --- a/babel/util.py +++ b/babel/util.py @@ -244,10 +244,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 From a587a5b4b828238ab88d5b801b1d055171f4ff72 Mon Sep 17 00:00:00 2001 From: Ruff Date: Wed, 19 Mar 2025 13:05:15 +0200 Subject: [PATCH 4/4] Run `ruff format babel` --- babel/__init__.py | 22 ++-- babel/core.py | 105 ++++++++++------ babel/dates.py | 130 ++++++++++++------- babel/languages.py | 10 +- babel/lists.py | 29 +++-- babel/localedata.py | 26 ++-- babel/localtime/__init__.py | 12 +- babel/localtime/_fallback.py | 11 +- babel/localtime/_unix.py | 2 +- babel/localtime/_win32.py | 3 +- babel/messages/__init__.py | 10 +- babel/messages/catalog.py | 105 +++++++++++----- babel/messages/checkers.py | 19 +-- babel/messages/extract.py | 119 +++++++++++------- babel/messages/frontend.py | 156 +++++++++++++++-------- babel/messages/jslexer.py | 30 +++-- babel/messages/mofile.py | 22 ++-- babel/messages/plurals.py | 11 +- babel/messages/pofile.py | 119 ++++++++++++------ babel/numbers.py | 238 ++++++++++++++++++++++++----------- babel/plural.py | 50 +++++--- babel/support.py | 131 ++++++++++++++----- babel/units.py | 32 ++++- babel/util.py | 34 +++-- 24 files changed, 942 insertions(+), 484 deletions(-) diff --git a/babel/__init__.py b/babel/__init__.py index 7b2774558..0edfa9a8f 100644 --- a/babel/__init__.py +++ b/babel/__init__.py @@ -1,19 +1,19 @@ """ - babel - ~~~~~ +babel +~~~~~ - Integrated collection of utilities that assist in internationalizing and - localizing applications. +Integrated collection of utilities that assist in internationalizing and +localizing applications. - This package is basically composed of two major parts: +This package is basically composed of two major parts: - * tools to build and work with ``gettext`` message catalogs - * a Python interface to the CLDR (Common Locale Data Repository), providing - access to various locale display names, localized number and date - formatting, etc. + * tools to build and work with ``gettext`` message catalogs + * a Python interface to the CLDR (Common Locale Data Repository), providing + access to various locale display names, localized number and date + formatting, etc. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from babel.core import ( diff --git a/babel/core.py b/babel/core.py index eb159c310..a59e73128 100644 --- a/babel/core.py +++ b/babel/core.py @@ -1,11 +1,11 @@ """ - babel.core - ~~~~~~~~~~ +babel.core +~~~~~~~~~~ - Core locale representation and locale data access. +Core locale representation and locale data access. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -56,12 +56,14 @@ def _raise_no_data_error(): - raise RuntimeError('The babel data files are not available. ' - 'This usually happens because you are using ' - 'a source checkout from Babel and you did ' - 'not build the data files. Just make sure ' - 'to run "python setup.py import_cldr" before ' - 'installing the library.') + raise RuntimeError( + 'The babel data files are not available. ' + 'This usually happens because you are using ' + 'a source checkout from Babel and you did ' + 'not build the data files. Just make sure ' + 'to run "python setup.py import_cldr" before ' + 'installing the library.', + ) def get_global(key: _GLOBAL_KEY) -> Mapping[str, Any]: @@ -216,7 +218,11 @@ 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 +274,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 @@ -421,7 +426,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 @@ -432,19 +439,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 = [''] @@ -455,9 +461,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: @@ -500,7 +506,9 @@ 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 @@ -511,7 +519,8 @@ def get_display_name(self, locale: Locale | str | None = None) -> str | None: u'svenska' :type: `unicode` - """) + """, + ) def get_language_name(self, locale: Locale | str | None = None) -> str | None: """Return the language of this locale in the given locale. @@ -528,12 +537,15 @@ 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' - """) + """, + ) def get_territory_name(self, locale: Locale | str | None = None) -> str | None: """Return the territory name in the given locale.""" @@ -542,12 +554,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' - """) + """, + ) def get_script_name(self, locale: Locale | str | None = None) -> str | None: """Return the script name in the given locale.""" @@ -556,12 +571,15 @@ 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' - """) + """, + ) @property def english_name(self) -> str | None: @@ -785,8 +803,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 @@ -1150,7 +1167,12 @@ def default_locale( return None -def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: str = '_', aliases: Mapping[str, str] = LOCALE_ALIASES) -> str | None: +def negotiate_locale( + preferred: Iterable[str], + available: Iterable[str], + sep: str = '_', + aliases: Mapping[str, str] = LOCALE_ALIASES, +) -> str | None: """Find the best match between available and requested locale strings. >>> negotiate_locale(['de_DE', 'en_US'], ['de_DE', 'de_AT']) @@ -1216,7 +1238,10 @@ def negotiate_locale(preferred: Iterable[str], available: Iterable[str], sep: st def parse_locale( identifier: str, sep: str = '_', -) -> tuple[str, str | None, str | None, str | None] | tuple[str, str | None, str | None, str | None, str | None]: +) -> ( + tuple[str, str | None, str | None, str | None] + | tuple[str, str | None, str | None, str | None, str | None] +): """Parse a locale identifier into a tuple of the form ``(language, territory, script, variant, modifier)``. @@ -1294,8 +1319,10 @@ def parse_locale( territory = parts.pop(0) if parts and ( - len(parts[0]) == 4 and parts[0][0].isdigit() or - len(parts[0]) >= 5 and parts[0][0].isalpha() + len(parts[0]) == 4 + and parts[0][0].isdigit() + or len(parts[0]) >= 5 + and parts[0][0].isalpha() ): variant = parts.pop().upper() diff --git a/babel/dates.py b/babel/dates.py index 8c6e1fd57..70b615321 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -1,18 +1,18 @@ """ - babel.dates - ~~~~~~~~~~~ +babel.dates +~~~~~~~~~~~ - Locale dependent formatting and parsing of dates and times. +Locale dependent formatting and parsing of dates and times. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_TIME``, - * ``LC_ALL``, and - * ``LANG`` + * ``LC_TIME``, + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -38,6 +38,7 @@ if TYPE_CHECKING: from typing_extensions import TypeAlias + _Instant: TypeAlias = datetime.date | datetime.time | float | None _PredefinedTimeFormat: TypeAlias = Literal['full', 'long', 'medium', 'short'] _Context: TypeAlias = Literal['format', 'stand-alone'] @@ -75,7 +76,9 @@ def _localize(tz: datetime.tzinfo, dt: datetime.datetime) -> datetime.datetime: return dt.astimezone(tz) -def _get_dt_and_tzinfo(dt_or_tzinfo: _DtOrTzinfo) -> tuple[datetime.datetime | None, datetime.tzinfo]: +def _get_dt_and_tzinfo( + dt_or_tzinfo: _DtOrTzinfo, +) -> tuple[datetime.datetime | None, datetime.tzinfo]: """ Parse a `dt_or_tzinfo` value into a datetime and a tzinfo. @@ -153,13 +156,16 @@ def _get_datetime(instant: _Instant) -> datetime.datetime: return datetime.datetime.fromtimestamp(instant, UTC).replace(tzinfo=None) elif isinstance(instant, datetime.time): return datetime.datetime.combine(datetime.date.today(), instant) - elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): + elif isinstance(instant, datetime.date) and not isinstance(instant, datetime.datetime): # fmt: skip return datetime.datetime.combine(instant, datetime.time()) # TODO (3.x): Add an assertion/type check for this fallthrough branch: return instant -def _ensure_datetime_tzinfo(dt: datetime.datetime, tzinfo: datetime.tzinfo | None = None) -> datetime.datetime: +def _ensure_datetime_tzinfo( + dt: datetime.datetime, + tzinfo: datetime.tzinfo | None = None, +) -> datetime.datetime: """ Ensure the datetime passed has an attached tzinfo. @@ -524,7 +530,11 @@ def get_timezone_location( if territory not in locale.territories: territory = 'ZZ' # invalid/unknown territory_name = locale.territories[territory] - if not return_city and territory and len(get_global('territory_zones').get(territory, [])) == 1: + if ( + not return_city + and territory + and len(get_global('territory_zones').get(territory, [])) == 1 + ): return region_format % territory_name # Otherwise, include the city in the output @@ -543,10 +553,13 @@ def get_timezone_location( if return_city: return city_name - return region_format % (fallback_format % { - '0': city_name, - '1': territory_name, - }) + return region_format % ( + fallback_format + % { + '0': city_name, + '1': territory_name, + } + ) def get_timezone_name( @@ -744,11 +757,12 @@ def format_datetime( locale = Locale.parse(locale or LC_TIME) if format in ('full', 'long', 'medium', 'short'): - return get_datetime_format(format, locale=locale) \ - .replace("'", "") \ - .replace('{0}', format_time(datetime, format, tzinfo=None, - locale=locale)) \ + return ( + get_datetime_format(format, locale=locale) + .replace("'", "") + .replace('{0}', format_time(datetime, format, tzinfo=None, locale=locale)) .replace('{1}', format_date(datetime, format, locale=locale)) + ) else: return parse_pattern(format).apply(datetime, locale) @@ -890,8 +904,16 @@ 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, @@ -955,8 +977,7 @@ def format_timedelta( raise TypeError('Format must be one of "narrow", "short" or "long"') if format == 'medium': warnings.warn( - '"medium" value for format param of format_timedelta' - ' is deprecated. Use "long" instead', + '"medium" value for format param of format_timedelta is deprecated. Use "long" instead', category=DeprecationWarning, stacklevel=2, ) @@ -973,7 +994,7 @@ def _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: @@ -1040,9 +1061,9 @@ def _format_fallback_interval( return format(start) return ( - locale.interval_formats.get(None, "{0}-{1}"). - replace("{0}", formatted_start). - replace("{1}", formatted_end) + locale.interval_formats.get(None, "{0}-{1}") + .replace("{0}", formatted_start) + .replace("{1}", formatted_end) ) @@ -1142,8 +1163,7 @@ def format_interval( # > format the start and end datetime, as above. return "".join( parse_pattern(pattern).apply(instant, locale) - for pattern, instant - in zip(skel_formats[field], (start, end)) + for pattern, instant in zip(skel_formats[field], (start, end)) ) # > Otherwise, format the start and end datetime using the fallback pattern. @@ -1201,8 +1221,10 @@ def get_period_id( return rule_id else: # e.g. from="21:00" before="06:00" - if rule["from"] <= seconds_past_midnight < 86400 or \ - 0 <= seconds_past_midnight < rule["before"]: + if ( + rule["from"] <= seconds_past_midnight < 86400 + or 0 <= seconds_past_midnight < rule["before"] + ): return rule_id start_ok = end_ok = False @@ -1377,7 +1399,6 @@ def parse_time( class DateTimePattern: - def __init__(self, pattern: str, format: DateTimeFormat): self.pattern = pattern self.format = format @@ -1404,7 +1425,6 @@ def apply( class DateTimeFormat: - def __init__( self, value: datetime.date | datetime.time, @@ -1485,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)] @@ -1599,8 +1621,12 @@ def format_period(self, char: str, num: int) -> str: :param num: count of format character """ - widths = [{3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], - 'wide', 'narrow', 'abbreviated'] + widths = [ + {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)], + 'wide', + 'narrow', + 'abbreviated', + ] if char == 'a': period = 'pm' if self.value.hour >= 12 else 'am' context = 'format' @@ -1623,8 +1649,12 @@ 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: @@ -1681,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 @@ -1715,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 @@ -1919,7 +1949,11 @@ def split_interval_pattern(pattern: str) -> list[str]: return [untokenize_pattern(tokens) for tokens in parts] -def match_skeleton(skeleton: str, options: Iterable[str], allow_different_fields: bool = False) -> str | None: +def match_skeleton( + skeleton: str, + options: Iterable[str], + allow_different_fields: bool = False, +) -> str | None: """ Find the closest match for the given datetime skeleton among the options given. diff --git a/babel/languages.py b/babel/languages.py index 564f555d2..5b2396c84 100644 --- a/babel/languages.py +++ b/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/babel/lists.py b/babel/lists.py index 353171c71..0c17f75bf 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -1,18 +1,19 @@ """ - babel.lists - ~~~~~~~~~~~ +babel.lists +~~~~~~~~~~~ - Locale dependent formatting of lists. +Locale dependent formatting of lists. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_ALL``, and - * ``LANG`` + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2015-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2015-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import warnings @@ -37,7 +38,15 @@ def __getattr__(name): def format_list( lst: Sequence[str], - style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard', + style: Literal[ + 'standard', + 'standard-short', + 'or', + 'or-short', + 'unit', + 'unit-short', + 'unit-narrow', + ] = 'standard', locale: Locale | str | None = None, ) -> str: """ diff --git a/babel/localedata.py b/babel/localedata.py index 59f1db09e..7cff4871e 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -1,14 +1,14 @@ """ - babel.localedata - ~~~~~~~~~~~~~~~~ +babel.localedata +~~~~~~~~~~~~~~~~ - Low-level locale data access. +Low-level locale data access. - :note: The `Locale` class, which uses this module under the hood, provides a - more convenient interface for accessing the locale data. +:note: The `Locale` class, which uses this module under the hood, provides a + more convenient interface for accessing the locale data. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -89,8 +89,9 @@ def locale_identifiers() -> list[str]: """ return [ stem - for stem, extension in - (os.path.splitext(filename) for filename in os.listdir(_dirname)) + for stem, extension in ( + os.path.splitext(filename) for filename in os.listdir(_dirname) + ) if extension == '.dat' and stem != 'root' ] @@ -151,6 +152,7 @@ def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str data = {} else: from babel.core import get_global + parent = get_global('parent_exceptions').get(name) if not parent: if _is_non_likely_script(name): @@ -242,7 +244,11 @@ class LocaleDataDict(abc.MutableMapping): values. """ - def __init__(self, data: MutableMapping[str | int | None, Any], base: Mapping[str | int | None, Any] | None = None): + def __init__( + self, + data: MutableMapping[str | int | None, Any], + base: Mapping[str | int | None, Any] | None = None, + ): self._data = data if base is None: base = data diff --git a/babel/localtime/__init__.py b/babel/localtime/__init__.py index 854c07496..8076516ec 100644 --- a/babel/localtime/__init__.py +++ b/babel/localtime/__init__.py @@ -1,12 +1,12 @@ """ - babel.localtime - ~~~~~~~~~~~~~~~ +babel.localtime +~~~~~~~~~~~~~~~ - Babel specific fork of tzlocal to determine the local timezone - of the system. +Babel specific fork of tzlocal to determine the local timezone +of the system. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ import datetime diff --git a/babel/localtime/_fallback.py b/babel/localtime/_fallback.py index 73def22e7..6cd67b5ce 100644 --- a/babel/localtime/_fallback.py +++ b/babel/localtime/_fallback.py @@ -1,11 +1,11 @@ """ - babel.localtime._fallback - ~~~~~~~~~~~~~~~~~~~~~~~~~ +babel.localtime._fallback +~~~~~~~~~~~~~~~~~~~~~~~~~ - Emulated fallback local timezone when all else fails. +Emulated fallback local timezone when all else fails. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ import datetime @@ -19,7 +19,6 @@ class _FallbackLocalTimezone(datetime.tzinfo): - def utcoffset(self, dt: datetime.datetime) -> datetime.timedelta: if self._isdst(dt): return DSTOFFSET diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py index 782a7d246..70dd2322c 100644 --- a/babel/localtime/_unix.py +++ b/babel/localtime/_unix.py @@ -51,7 +51,7 @@ def _get_localzone(_root: str = '/') -> datetime.tzinfo: # `None` (as a fix for #1092). # Instead, let's just "fix" the double slash symlink by stripping # leading slashes before passing the assumed zone name forward. - zone_name = link_dst[pos + 10:].lstrip("/") + zone_name = link_dst[pos + 10 :].lstrip("/") tzinfo = _get_tzinfo(zone_name) if tzinfo is not None: return tzinfo diff --git a/babel/localtime/_win32.py b/babel/localtime/_win32.py index 1a52567bc..0fb625ba9 100644 --- a/babel/localtime/_win32.py +++ b/babel/localtime/_win32.py @@ -92,7 +92,6 @@ def get_localzone_name() -> str: def _get_localzone() -> datetime.tzinfo: if winreg is None: - raise LookupError( - 'Runtime support not available') + raise LookupError('Runtime support not available') return _get_tzinfo_or_raise(get_localzone_name()) diff --git a/babel/messages/__init__.py b/babel/messages/__init__.py index ca83faa97..6e445e6b0 100644 --- a/babel/messages/__init__.py +++ b/babel/messages/__init__.py @@ -1,11 +1,11 @@ """ - babel.messages - ~~~~~~~~~~~~~~ +babel.messages +~~~~~~~~~~~~~~ - Support for ``gettext`` message catalogs. +Support for ``gettext`` message catalogs. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from babel.messages.catalog import ( diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 0a2319ad9..67828b2b0 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -1,12 +1,13 @@ """ - babel.messages.catalog - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.catalog +~~~~~~~~~~~~~~~~~~~~~~ - Data structures for message catalogs. +Data structures for message catalogs. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import datetime @@ -54,9 +55,11 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): s.set_seq2(word) for x in possibilities: s.set_seq1(x) - if s.real_quick_ratio() >= cutoff and \ - s.quick_ratio() >= cutoff and \ - s.ratio() >= cutoff: + if ( + s.real_quick_ratio() >= cutoff + and s.quick_ratio() >= cutoff + and s.ratio() >= cutoff + ): result.append((s.ratio(), x)) # Move the best scorers to head of list @@ -65,7 +68,8 @@ def get_close_matches(word, possibilities, n=3, cutoff=0.6): return [x for score, x in result] -PYTHON_FORMAT = re.compile(r''' +PYTHON_FORMAT = re.compile( + r''' \% (?:\(([\w]*)\))? ( @@ -74,7 +78,9 @@ 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: @@ -185,10 +191,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: @@ -240,6 +248,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: @@ -322,6 +331,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()) @@ -427,7 +437,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 @@ -443,11 +455,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 @@ -455,7 +469,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', @@ -486,11 +503,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 @@ -554,7 +576,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 @@ -604,7 +629,8 @@ def _set_mime_headers(self, headers: Iterable[tuple[str, str]]) -> None: Generated-By: Babel ... :type: `list` - """) + """, + ) @property def num_plurals(self) -> int: @@ -737,8 +763,9 @@ def __setitem__(self, id: _MessageID, message: Message) -> None: 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( @@ -778,9 +805,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 @@ -893,7 +928,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: @@ -921,7 +960,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] @@ -975,7 +1014,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. @@ -995,10 +1038,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/babel/messages/checkers.py b/babel/messages/checkers.py index e56bce9a1..28081160f 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -1,14 +1,15 @@ """ - babel.messages.checkers - ~~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.checkers +~~~~~~~~~~~~~~~~~~~~~~~ - Various routines that help with validation of translations. +Various routines that help with validation of translations. - :since: version 0.9 +:since: version 0.9 - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations from collections.abc import Callable @@ -115,8 +116,9 @@ def _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 = _parse(format) @@ -161,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/babel/messages/extract.py b/babel/messages/extract.py index eedc58747..00bc35903 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -1,20 +1,21 @@ """ - babel.messages.extract - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.extract +~~~~~~~~~~~~~~~~~~~~~~ - Basic infrastructure for extracting localizable messages from source files. +Basic infrastructure for extracting localizable messages from source files. - This module defines an extensible system for collecting localizable message - strings from a variety of sources. A native extractor for Python source - files is builtin, extractors for other sources can be added using very - simple plugins. +This module defines an extensible system for collecting localizable message +strings from a variety of sources. A native extractor for Python source +files is builtin, extractors for other sources can be added using very +simple plugins. - The main entry points into the extraction functionality are the functions - `extract_from_dir` and `extract_from_file`. +The main entry points into the extraction functionality are the functions +`extract_from_dir` and `extract_from_file`. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import ast @@ -103,11 +104,13 @@ def _strip_comment_tags(comments: MutableSequence[str], tags: Iterable[str]): """Helper function for `extract` that strips comment tags from strings in a list of comment lines. This functions operates in-place. """ + def _strip(line: str): for tag in tags: if line.startswith(tag): - return line[len(tag):].strip() + return line[len(tag) :].strip() return line + comments[:] = [_strip(c) for c in comments] @@ -206,8 +209,7 @@ def extract_from_dir( absname = os.path.abspath(dirname) for root, dirnames, filenames in os.walk(absname): dirnames[:] = [ - subdir for subdir in dirnames - if directory_filter(os.path.join(root, subdir)) + subdir for subdir in dirnames if directory_filter(os.path.join(root, subdir)) ] dirnames.sort() filenames.sort() @@ -280,7 +282,8 @@ def check_and_call_extract_file( if callback: callback(filename, method, options) for message_tuple in extract_from_file( - method, filepath, + method, + filepath, keywords=keywords, comment_tags=comment_tags, options=options, @@ -321,8 +324,9 @@ def extract_from_file( return [] with open(filename, 'rb') as fileobj: - return list(extract(method, fileobj, keywords, comment_tags, - options, strip_comment_tags)) + return list( + extract(method, fileobj, keywords, comment_tags, options, strip_comment_tags), + ) def _match_messages_against_spec( @@ -357,7 +361,7 @@ def _match_messages_against_spec( first_msg_index = spec[0] - 1 # An empty string msgid isn't valid, emit a warning if not messages[first_msg_index]: - filename = (getattr(fileobj, "name", None) or "(unknown)") + filename = getattr(fileobj, "name", None) or "(unknown)" sys.stderr.write( f"{filename}:{lineno}: warning: Empty msgid. It is reserved by GNU gettext: gettext(\"\") " f"returns the header entry with meta information, not the empty string.\n", @@ -431,7 +435,7 @@ def extract( elif ':' in method or '.' in method: if ':' not in method: lastdot = method.rfind('.') - module, attrname = method[:lastdot], method[lastdot + 1:] + module, attrname = method[:lastdot], method[lastdot + 1 :] else: module, attrname = method.split(':', 1) func = getattr(__import__(module, {}, {}, [attrname]), attrname) @@ -445,8 +449,7 @@ def extract( if func is None: raise ValueError(f"Unknown extraction method {method!r}") - results = func(fileobj, keywords.keys(), comment_tags, - options=options or {}) + results = func(fileobj, keywords.keys(), comment_tags, options=options or {}) for lineno, funcname, messages, comments in results: if not isinstance(messages, (list, tuple)): @@ -543,8 +546,7 @@ def extract_python( elif call_stack == -1 and tok == COMMENT: # Strip the comment token from the line value = value[1:].strip() - if in_translator_comments and \ - translator_comments[-1][0] == lineno - 1: + if in_translator_comments and translator_comments[-1][0] == lineno - 1: # We're already inside a translator comment, continue appending translator_comments.append((lineno, value)) continue @@ -556,7 +558,7 @@ def extract_python( translator_comments.append((lineno, value)) break elif funcname and call_stack == 0: - nested = (tok == NAME and value in keywords) + nested = tok == NAME and value in keywords if (tok == OP and value == ')') or nested: if buf: messages.append(''.join(buf)) @@ -567,12 +569,15 @@ def extract_python( 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: + 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]) + yield ( + message_lineno, + funcname, + messages, + [comment[1] for comment in translator_comments], + ) funcname = lineno = message_lineno = None call_stack = -1 @@ -679,6 +684,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 @@ -708,8 +714,18 @@ def extract_javascript( call_stack = 0 token = Token('operator', ')', token.lineno) - if options.get('parse_template_string') and not funcname and token.type == 'template_string': - yield from parse_template_string(token.value, keywords, comment_tags, options, token.lineno) + if ( + options.get('parse_template_string') + and not funcname + and token.type == 'template_string' + ): + yield from parse_template_string( + token.value, + keywords, + comment_tags, + options, + token.lineno, + ) elif token.type == 'operator' and token.value == '(': if funcname: @@ -718,8 +734,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 @@ -739,8 +754,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: @@ -756,13 +770,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 @@ -789,17 +806,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 @@ -823,6 +845,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 @@ -842,7 +865,13 @@ def parse_template_string( if level == 0 and expression_contents: expression_contents = expression_contents[0:-1] fake_file_obj = io.BytesIO(expression_contents.encode()) - yield from extract_javascript(fake_file_obj, keywords, comment_tags, options, lineno) + yield from extract_javascript( + fake_file_obj, + keywords, + comment_tags, + options, + lineno, + ) lineno += len(line_re.findall(expression_contents)) expression_contents = '' prev_character = character diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 197376bf7..d86643d39 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -1,11 +1,11 @@ """ - babel.messages.frontend - ~~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.frontend +~~~~~~~~~~~~~~~~~~~~~~~ - Frontends for the message extraction functionality. +Frontends for the message extraction functionality. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -199,7 +199,7 @@ 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) + return 1 if n_errors else 0 def _run_domain(self, domain): po_files = [] @@ -245,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: @@ -255,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) @@ -275,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 @@ -342,8 +341,13 @@ class ExtractMessages(CommandMixin): 'set the name and email of the last translator in output'), ] # fmt: skip boolean_options = [ - 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', - 'sort-output', 'sort-by-file', 'strip-comments', + 'no-default-keywords', + 'no-location', + 'omit-header', + 'no-wrap', + 'sort-output', + 'sort-by-file', + 'strip-comments', ] as_args = 'input-paths' multiple_value_options = ( @@ -485,13 +489,15 @@ 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) @@ -524,8 +530,13 @@ 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( @@ -557,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)) @@ -638,9 +652,7 @@ def finalize_options(self): self.width = int(self.width) def run(self): - self.log.info( - 'creating catalog %s based on %s', self.output_file, self.input_file, - ) + self.log.info('creating catalog %s based on %s', self.output_file, self.input_file) with open(self.input_file, 'rb') as infile: # Although reading from the catalog template, read_po must be fed @@ -694,9 +706,15 @@ class UpdateCatalog(CommandMixin): 'ignore changes to POT-Creation-Date when updating or checking'), ] # fmt: skip boolean_options = [ - 'omit-header', 'no-wrap', 'ignore-obsolete', 'init-missing', - 'no-fuzzy-matching', 'previous', 'update-header-comment', - 'check', 'ignore-pot-creation-date', + 'omit-header', + 'no-wrap', + 'ignore-obsolete', + 'init-missing', + 'no-fuzzy-matching', + 'previous', + 'update-header-comment', + 'check', + 'ignore-pot-creation-date', ] def initialize_options(self): @@ -727,8 +745,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: @@ -753,7 +770,9 @@ def run(self): if not self.output_file: if self.locale: lc_messages_path = os.path.join(self.output_dir, self.locale, "LC_MESSAGES") - po_files.append((self.locale, os.path.join(lc_messages_path, f"{self.domain}.po"))) + po_files.append( + (self.locale, os.path.join(lc_messages_path, f"{self.domain}.po")), + ) else: for locale in os.listdir(self.output_dir): lc_messages_path = os.path.join(self.output_dir, locale, 'LC_MESSAGES') @@ -778,9 +797,7 @@ def run(self): if self.check: check_status[filename] = False continue - self.log.info( - 'creating catalog %s based on %s', filename, self.input_file, - ) + self.log.info('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 @@ -805,9 +822,10 @@ def run(self): update_creation_date=not self.ignore_pot_creation_date, ) - tmpname = os.path.join(os.path.dirname(filename), - tempfile.gettempprefix() + - os.path.basename(filename)) + tmpname = os.path.join( + os.path.dirname(filename), + tempfile.gettempprefix() + os.path.basename(filename), + ) try: with open(tmpname, 'wb') as tmpfile: write_po( @@ -891,8 +909,10 @@ 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( @@ -1065,40 +1085,58 @@ def _parse_config_object(config: dict, *, filename="(unknown)"): extractors_read = config.get("extractors", {}) if not isinstance(extractors_read, dict): - raise ConfigurationError(f"{filename}: extractors: Expected a dictionary, got {type(extractors_read)!r}") + raise ConfigurationError( + f"{filename}: extractors: Expected a dictionary, got {type(extractors_read)!r}", + ) for method, callable_spec in extractors_read.items(): if not isinstance(method, str): # Impossible via TOML, but could happen with a custom object. - raise ConfigurationError(f"{filename}: extractors: Extraction method must be a string, got {method!r}") + raise ConfigurationError( + f"{filename}: extractors: Extraction method must be a string, got {method!r}", + ) if not isinstance(callable_spec, str): - raise ConfigurationError(f"{filename}: extractors: Callable specification must be a string, got {callable_spec!r}") + raise ConfigurationError( + f"{filename}: extractors: Callable specification must be a string, got {callable_spec!r}", + ) extractors[method] = callable_spec if "mapping" in config: - raise ConfigurationError(f"{filename}: 'mapping' is not a valid key, did you mean 'mappings'?") + raise ConfigurationError( + f"{filename}: 'mapping' is not a valid key, did you mean 'mappings'?", + ) mappings_read = config.get("mappings", []) if not isinstance(mappings_read, list): - raise ConfigurationError(f"{filename}: mappings: Expected a list, got {type(mappings_read)!r}") + raise ConfigurationError( + f"{filename}: mappings: Expected a list, got {type(mappings_read)!r}", + ) for idx, entry in enumerate(mappings_read): if not isinstance(entry, dict): - raise ConfigurationError(f"{filename}: mappings[{idx}]: Expected a dictionary, got {type(entry)!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: Expected a dictionary, got {type(entry)!r}", + ) entry = entry.copy() method = entry.pop("method", None) if not isinstance(method, str): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'method' must be a string, got {method!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'method' must be a string, got {method!r}", + ) method = extractors.get(method, method) # Map the extractor name to the callable now pattern = entry.pop("pattern", None) if not isinstance(pattern, (list, str)): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'pattern' must be a list or a string, got {pattern!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'pattern' must be a list or a string, got {pattern!r}", + ) if not isinstance(pattern, list): pattern = [pattern] for pat in pattern: if not isinstance(pat, str): - raise ConfigurationError(f"{filename}: mappings[{idx}]: 'pattern' elements must be strings, got {pat!r}") + raise ConfigurationError( + f"{filename}: mappings[{idx}]: 'pattern' elements must be strings, got {pat!r}", + ) method_map.append((pat, method)) options_map[pat] = entry @@ -1135,11 +1173,15 @@ def _parse_mapping_toml( try: babel_data = parsed_data["tool"]["babel"] except (TypeError, KeyError) as e: - raise ConfigurationError(f"{filename}: No 'tool.babel' section found in file") from e + raise ConfigurationError( + f"{filename}: No 'tool.babel' section found in file", + ) from e elif style == "standalone": babel_data = parsed_data if "babel" in babel_data: - raise ConfigurationError(f"{filename}: 'babel' should not be present in a stand-alone configuration file") + raise ConfigurationError( + f"{filename}: 'babel' should not be present in a stand-alone configuration file", + ) else: # pragma: no cover raise ValueError(f"Unknown TOML style {style!r}") @@ -1210,7 +1252,13 @@ def parse_keywords(strings: Iterable[str] = ()): def __getattr__(name: str): # Re-exports for backwards compatibility; # `setuptools_frontend` is the canonical import location. - if name in {'check_message_extractors', 'compile_catalog', 'extract_messages', 'init_catalog', 'update_catalog'}: + if name in { + 'check_message_extractors', + 'compile_catalog', + 'extract_messages', + 'init_catalog', + 'update_catalog', + }: from babel.messages import setuptools_frontend return getattr(setuptools_frontend, name) diff --git a/babel/messages/jslexer.py b/babel/messages/jslexer.py index 4cd79d5d7..df503e948 100644 --- a/babel/messages/jslexer.py +++ b/babel/messages/jslexer.py @@ -1,13 +1,14 @@ """ - babel.messages.jslexer - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.jslexer +~~~~~~~~~~~~~~~~~~~~~~ - A simple JavaScript 1.5 lexer which is used for the JavaScript - extractor. +A simple JavaScript 1.5 lexer which is used for the JavaScript +extractor. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import re @@ -62,7 +63,11 @@ class Token(NamedTuple): ] # fmt: skip -def get_rules(jsx: bool, dotted: bool, template_string: bool) -> list[tuple[str | None, re.Pattern[str]]]: +def get_rules( + jsx: bool, + dotted: bool, + template_string: bool, +) -> list[tuple[str | None, re.Pattern[str]]]: """ Get a tokenization rule list given the passed syntax options. @@ -95,8 +100,9 @@ def unquote_string(string: str) -> str: """Unquote a string with JavaScript rules. The string has to start with string delimiters (``'``, ``"`` or the back-tick/grave accent (for template strings).) """ - assert string and string[0] == string[-1] and string[0] in '"\'`', \ + assert string and string[0] == string[-1] and string[0] in '"\'`', ( 'string provided is not properly delimited' + ) string = line_join_re.sub('\\1', string[1:-1]) result: list[str] = [] add = result.append @@ -158,7 +164,13 @@ def unquote_string(string: str) -> str: return ''.join(result) -def tokenize(source: str, jsx: bool = True, dotted: bool = True, template_string: bool = True, lineno: int = 1) -> Generator[Token, None, None]: +def tokenize( + source: str, + jsx: bool = True, + dotted: bool = True, + template_string: bool = True, + lineno: int = 1, +) -> Generator[Token, None, None]: """ Tokenize JavaScript/JSX source. Returns a generator of tokens. diff --git a/babel/messages/mofile.py b/babel/messages/mofile.py index 20c67188d..9ec822b69 100644 --- a/babel/messages/mofile.py +++ b/babel/messages/mofile.py @@ -1,12 +1,13 @@ """ - babel.messages.mofile - ~~~~~~~~~~~~~~~~~~~~~ +babel.messages.mofile +~~~~~~~~~~~~~~~~~~~~~ - Writing of files in the ``gettext`` MO (machine object) format. +Writing of files in the ``gettext`` MO (machine object) format. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import array @@ -18,8 +19,8 @@ if TYPE_CHECKING: from _typeshed import SupportsRead, SupportsWrite -LE_MAGIC: int = 0x950412de -BE_MAGIC: int = 0xde120495 +LE_MAGIC: int = 0x950412DE +BE_MAGIC: int = 0xDE120495 def read_mo(fileobj: SupportsRead[bytes]) -> Catalog: @@ -56,9 +57,9 @@ def read_mo(fileobj: SupportsRead[bytes]) -> Catalog: # Now put all messages from the .mo file buffer into the catalog # dictionary for _i in range(msgcount): - mlen, moff = unpack(ii, buf[origidx:origidx + 8]) + mlen, moff = unpack(ii, buf[origidx : origidx + 8]) mend = moff + mlen - tlen, toff = unpack(ii, buf[transidx:transidx + 8]) + tlen, toff = unpack(ii, buf[transidx : transidx + 8]) tend = toff + tlen if mend < buflen and tend < buflen: msg = buf[moff:mend] @@ -153,8 +154,7 @@ def write_mo(fileobj: SupportsWrite[bytes], catalog: Catalog, use_fuzzy: bool = in the output """ messages = list(catalog) - messages[1:] = [m for m in messages[1:] - if m.string and (use_fuzzy or not m.fuzzy)] + messages[1:] = [m for m in messages[1:] if m.string and (use_fuzzy or not m.fuzzy)] messages.sort() ids = strs = b'' diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 5eae86ddd..a03fb4edd 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -1,12 +1,13 @@ """ - babel.messages.plurals - ~~~~~~~~~~~~~~~~~~~~~~ +babel.messages.plurals +~~~~~~~~~~~~~~~~~~~~~~ - Plural form definitions. +Plural form definitions. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations from babel.core import Locale, default_locale diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 3afdd6061..6f8947ad9 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -1,13 +1,14 @@ """ - babel.messages.pofile - ~~~~~~~~~~~~~~~~~~~~~ +babel.messages.pofile +~~~~~~~~~~~~~~~~~~~~~ - Reading and writing of files in the ``gettext`` PO (portable object) - format. +Reading and writing of files in the ``gettext`` PO (portable object) +format. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import os @@ -35,6 +36,7 @@ def unescape(string: str) -> str: :param string: the string to unescape """ + def replace_escapes(match): m = match.group(1) if m == 'n': @@ -45,6 +47,7 @@ def replace_escapes(match): return '\r' # m is \ or " return m + return re.compile(r'\\([\\trn"])').sub(replace_escapes, string[1:-1]) @@ -94,14 +97,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 == " ": @@ -115,8 +122,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,7 +142,6 @@ def __init__(self, message: str, catalog: Catalog, line: str, lineno: int) -> No class _NormalizedString: - def __init__(self, *args: str) -> None: self._strs: list[str] = [] for arg in args: @@ -190,7 +198,12 @@ class PoFileParser: '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 @@ -225,16 +238,27 @@ def _add_message(self) -> None: string = ['' for _ in range(self.catalog.num_plurals)] for idx, translation in 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: 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, + list(self.locations), + set(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 @@ -246,7 +270,11 @@ 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._invalid_pofile( + "", + self.offset, + f"missing msgstr for msgid '{self.messages[0].denormalize()}'", + ) self.translations.append([0, _NormalizedString("")]) self._add_message() @@ -257,16 +285,19 @@ def _process_message_line(self, lineno, line, obsolete=False) -> None: 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):] + 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.") + self._invalid_pofile( + line, + lineno, + "Start of line didn't match any expected keyword.", + ) return if keyword in ['msgid', 'msgctxt']: @@ -305,12 +336,15 @@ 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) def _process_comment(self, line) -> None: - self._finish_current_message() if line[1:].startswith(':'): @@ -318,7 +352,7 @@ def _process_comment(self, line) -> None: pos = location.rfind(':') if pos >= 0: try: - lineno = int(location[pos + 1:]) + lineno = int(location[pos + 1 :]) except ValueError: continue self.locations.append((location[:pos], lineno)) @@ -436,11 +470,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: @@ -454,11 +490,13 @@ 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: @@ -686,8 +724,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 @@ -725,7 +765,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/babel/numbers.py b/babel/numbers.py index 3ea97bcc0..8a6e02dd8 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -1,20 +1,21 @@ """ - babel.numbers - ~~~~~~~~~~~~~ +babel.numbers +~~~~~~~~~~~~~ - Locale dependent formatting and parsing of numeric data. +Locale dependent formatting and parsing of numeric data. - The default locale for the functions in this module is determined by the - following environment variables, in that order: +The default locale for the functions in this module is determined by the +following environment variables, in that order: - * ``LC_MONETARY`` for currency related functions, - * ``LC_NUMERIC``, and - * ``LC_ALL``, and - * ``LANG`` + * ``LC_MONETARY`` for currency related functions, + * ``LC_NUMERIC``, and + * ``LC_ALL``, and + * ``LANG`` - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + # TODO: # Padding and rounding increments in pattern: # - https://www.unicode.org/reports/tr35/ (Appendix G.6) @@ -34,8 +35,7 @@ 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. """ @@ -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 @@ -481,7 +488,10 @@ def get_infinity_symbol( return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞') -def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = None) -> str: +def format_number( + number: float | decimal.Decimal | str, + locale: Locale | str | None = None, +) -> str: """Return the given number formatted for a specific locale. >>> format_number(1099, locale='en_US') # doctest: +SKIP @@ -498,7 +508,11 @@ def format_number(number: float | decimal.Decimal | str, locale: Locale | str | """ - warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning, stacklevel=2) + warnings.warn( + 'Use babel.numbers.format_decimal() instead.', + DeprecationWarning, + stacklevel=2, + ) return format_decimal(number, locale=locale) @@ -583,7 +597,12 @@ def format_decimal( format = locale.decimal_formats[format] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) def format_compact_decimal( @@ -626,7 +645,12 @@ def format_compact_decimal( if format is None: format = locale.decimal_formats[None] pattern = parse_pattern(format) - return pattern.apply(number, locale, decimal_quantization=False, numbering_system=numbering_system) + return pattern.apply( + number, + locale, + decimal_quantization=False, + numbering_system=numbering_system, + ) def _get_compact_format( @@ -654,7 +678,10 @@ def _get_compact_format( break # otherwise, we need to divide the number by the magnitude but remove zeros # equal to the number of 0's in the pattern minus 1 - number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1)))) + number = cast( + decimal.Decimal, + number / (magnitude // (10 ** (pattern.count("0") - 1))), + ) # round to the number of fraction digits requested rounded = round(number, fraction_digits) # if the remaining number is singular, use the singular format @@ -797,11 +824,19 @@ def format_currency( try: pattern = locale.currency_formats[format_type] except KeyError: - raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None + raise UnknownCurrencyFormatError( + f"{format_type!r} is not a known currency format type", + ) from None return pattern.apply( - number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + currency=currency, + currency_digits=currency_digits, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) def _format_currency_long_name( @@ -839,8 +874,14 @@ def _format_currency_long_name( pattern = parse_pattern(format) number_part = pattern.apply( - number, locale, currency=currency, currency_digits=currency_digits, - decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) + number, + locale, + currency=currency, + currency_digits=currency_digits, + decimal_quantization=decimal_quantization, + group_separator=group_separator, + numbering_system=numbering_system, + ) return unit_pattern.format(number_part, display_name) @@ -877,7 +918,9 @@ def format_compact_currency( try: compact_format = locale.compact_currency_formats[format_type] except KeyError as error: - raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error + raise UnknownCurrencyFormatError( + f"{format_type!r} is not a known compact currency format type", + ) from error number, format = _get_compact_format(number, compact_format, locale, fraction_digits) # Did not find a format, fall back. if format is None or "¤" not in str(format): @@ -894,8 +937,14 @@ def format_compact_currency( if format is None: raise ValueError('No compact currency format found for the given number and locale.') pattern = parse_pattern(format) - return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False, - numbering_system=numbering_system) + return pattern.apply( + number, + locale, + currency=currency, + currency_digits=False, + decimal_quantization=False, + numbering_system=numbering_system, + ) def format_percent( @@ -954,7 +1003,10 @@ def format_percent( format = locale.percent_formats[None] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, + number, + locale, + decimal_quantization=decimal_quantization, + group_separator=group_separator, numbering_system=numbering_system, ) @@ -1002,7 +1054,11 @@ def format_scientific( format = locale.scientific_formats[None] pattern = parse_pattern(format) return pattern.apply( - number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system) + number, + locale, + decimal_quantization=decimal_quantization, + numbering_system=numbering_system, + ) class NumberFormatError(ValueError): @@ -1132,16 +1188,21 @@ def parse_decimal( string = SPACE_CHARS_RE.sub(group_symbol, string) try: - parsed = decimal.Decimal(string.replace(group_symbol, '') - .replace(decimal_symbol, '.')) + parsed = decimal.Decimal(string.replace(group_symbol, '').replace(decimal_symbol, '.')) except decimal.InvalidOperation as exc: raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc if strict and group_symbol in string: - proper = format_decimal(parsed, locale=locale, decimal_quantization=False, numbering_system=numbering_system) - if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): + proper = format_decimal( + parsed, + locale=locale, + decimal_quantization=False, + numbering_system=numbering_system, + ) + if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): # fmt: skip try: - parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') - .replace(group_symbol, '.')) + parsed_alt = decimal.Decimal( + string.replace(decimal_symbol, '').replace(group_symbol, '.'), + ) except decimal.InvalidOperation as exc: raise NumberFormatError( f"{string!r} is not a properly formatted decimal number. " @@ -1230,7 +1291,7 @@ def parse_grouping(p: str) -> tuple[int, int]: if g1 == -1: return 1000, 1000 g1 = width - g1 - 1 - g2 = p[:-g1 - 1].rfind(',') + g2 = p[: -g1 - 1].rfind(',') if g2 == -1: return g1, g1 g2 = width - g1 - g2 - 2 @@ -1296,14 +1357,20 @@ def parse_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, @@ -1352,8 +1419,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) @@ -1430,7 +1496,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. @@ -1443,7 +1513,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 @@ -1463,13 +1533,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: @@ -1477,12 +1545,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)) @@ -1572,8 +1649,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( @@ -1586,7 +1674,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/babel/plural.py b/babel/plural.py index 638dfefe3..e368b1acf 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -1,12 +1,13 @@ """ - babel.numbers - ~~~~~~~~~~~~~ +babel.numbers +~~~~~~~~~~~~~ - CLDR Plural support. See UTS #35. +CLDR Plural support. See UTS #35. - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import decimal @@ -18,7 +19,9 @@ _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[()]]: @@ -566,7 +579,9 @@ class _PythonCompiler(_Compiler): compile_mod = _binary_compiler('MOD(%s, %s)') def compile_relation(self, method, expr, range_list): - ranges = ",".join([f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1]]) + ranges = ",".join( + f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1] + ) return f"{method.upper()}({self.compile(expr)}, [{ranges}])" @@ -604,8 +619,7 @@ class _JavaScriptCompiler(_GettextCompiler): compile_t = compile_zero def compile_relation(self, method, expr, range_list): - code = _GettextCompiler.compile_relation( - self, method, expr, range_list) + code = _GettextCompiler.compile_relation(self, method, expr, range_list) if method == 'in': expr = self.compile(expr) code = f"(parseInt({expr}, 10) == {expr} && {code})" diff --git a/babel/support.py b/babel/support.py index d181fbd1c..dfc7fb6e5 100644 --- a/babel/support.py +++ b/babel/support.py @@ -1,15 +1,16 @@ """ - babel.support - ~~~~~~~~~~~~~ +babel.support +~~~~~~~~~~~~~ - Several classes and functions that help with integrating and using Babel - in applications. +Several classes and functions that help with integrating and using Babel +in applications. - .. note: the code in this module is not used by Babel itself +.. note: the code in this module is not used by Babel itself - :copyright: (c) 2013-2025 by the Babel Team. - :license: BSD, see LICENSE for more details. +:copyright: (c) 2013-2025 by the Babel Team. +:license: BSD, see LICENSE for more details. """ + from __future__ import annotations import gettext @@ -114,7 +115,15 @@ def time( def timedelta( self, delta: _datetime.timedelta | int, - granularity: Literal["year", "month", "week", "day", "hour", "minute", "second"] = "second", + granularity: Literal[ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", + ] = "second", threshold: float = 0.85, format: Literal["narrow", "short", "medium", "long"] = "long", add_direction: bool = False, @@ -126,10 +135,14 @@ def timedelta( >>> fmt.timedelta(timedelta(weeks=11)) u'3 months' """ - return format_timedelta(delta, granularity=granularity, - threshold=threshold, - format=format, add_direction=add_direction, - locale=self.locale) + return format_timedelta( + delta, + granularity=granularity, + threshold=threshold, + format=format, + add_direction=add_direction, + locale=self.locale, + ) def number(self, number: float | Decimal | str) -> str: """Return an integer number formatted for the locale. @@ -138,7 +151,11 @@ def number(self, number: float | Decimal | str) -> str: >>> fmt.number(1099) u'1,099' """ - return format_decimal(number, locale=self.locale, numbering_system=self.numbering_system) + return format_decimal( + number, + locale=self.locale, + numbering_system=self.numbering_system, + ) def decimal(self, number: float | Decimal | str, format: str | None = None) -> str: """Return a decimal number formatted for the locale. @@ -147,7 +164,12 @@ def decimal(self, number: float | Decimal | str, format: str | None = None) -> s >>> fmt.decimal(1.2345) u'1.234' """ - return format_decimal(number, format, locale=self.locale, numbering_system=self.numbering_system) + return format_decimal( + number, + format, + locale=self.locale, + numbering_system=self.numbering_system, + ) def compact_decimal( self, @@ -172,9 +194,13 @@ 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,8 +215,14 @@ 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. @@ -199,12 +231,20 @@ def percent(self, number: float | Decimal | str, format: str | None = None) -> s >>> fmt.percent(0.34) u'34%' """ - return format_percent(number, format, locale=self.locale, numbering_system=self.numbering_system) + return format_percent( + number, + format, + locale=self.locale, + numbering_system=self.numbering_system, + ) def scientific(self, number: float | Decimal | str) -> str: - """Return a number formatted using scientific notation for the locale. - """ - return format_scientific(number, locale=self.locale, numbering_system=self.numbering_system) + """Return a number formatted using scientific notation for the locale.""" + return format_scientific( + number, + locale=self.locale, + numbering_system=self.numbering_system, + ) class LazyProxy: @@ -242,7 +282,15 @@ class LazyProxy: Hello, universe! Hello, world! """ - __slots__ = ['_func', '_args', '_kwargs', '_value', '_is_cache_enabled', '_attribute_error'] + + __slots__ = [ + '_func', + '_args', + '_kwargs', + '_value', + '_is_cache_enabled', + '_attribute_error', + ] if TYPE_CHECKING: _func: Callable[..., Any] @@ -252,7 +300,13 @@ class LazyProxy: _value: Any _attribute_error: AttributeError | None - def __init__(self, func: Callable[..., Any], *args: Any, enable_cache: bool = True, **kwargs: Any) -> None: + def __init__( + self, + func: Callable[..., Any], + *args: Any, + enable_cache: bool = True, + **kwargs: Any, + ) -> None: # Avoid triggering our own __setattr__ implementation object.__setattr__(self, '_func', func) object.__setattr__(self, '_args', args) @@ -362,6 +416,7 @@ 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 @@ -608,13 +669,19 @@ def udnpgettext(self, domain: str, context: str, singular: str, plural: str, 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 @@ -625,7 +692,11 @@ class Translations(NullTranslations, gettext.GNUTranslations): DEFAULT_DOMAIN = 'messages' - def __init__(self, fp: gettext._TranslationsReader | None = None, domain: str | None = None): + def __init__( + self, + fp: gettext._TranslationsReader | None = None, + domain: str | None = None, + ): """Initialize the translations catalog. :param fp: the file-like object the translation should be read from diff --git a/babel/units.py b/babel/units.py index 86ac2abc9..e66e6b47b 100644 --- a/babel/units.py +++ b/babel/units.py @@ -143,7 +143,12 @@ def format_unit( formatted_value = value plural_form = "one" else: - formatted_value = format_decimal(value, format, locale, numbering_system=numbering_system) + formatted_value = format_decimal( + value, + format, + locale, + numbering_system=numbering_system, + ) plural_form = locale.plural_form(value) if plural_form in unit_patterns: @@ -151,7 +156,11 @@ def format_unit( # Fall back to a somewhat bad representation. # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen. - fallback_name = get_unit_name(measurement_unit, length=length, locale=locale) # pragma: no cover + fallback_name = get_unit_name( # pragma: no cover + measurement_unit, + length=length, + locale=locale, + ) return f"{formatted_value} {fallback_name or measurement_unit}" # pragma: no cover @@ -204,7 +213,10 @@ def _find_compound_unit( # Now we can try and rebuild a compound unit specifier, then qualify it: - return _find_unit_pattern(f"{bare_numerator_unit}-per-{bare_denominator_unit}", locale=locale) + return _find_unit_pattern( + f"{bare_numerator_unit}-per-{bare_denominator_unit}", + locale=locale, + ) def format_compound_unit( @@ -310,7 +322,12 @@ def format_compound_unit( elif denominator_unit: # Denominator has unit if denominator_value == 1: # support perUnitPatterns when the denominator is 1 denominator_unit = _find_unit_pattern(denominator_unit, locale=locale) - per_pattern = locale._data["unit_patterns"].get(denominator_unit, {}).get(length, {}).get("per") + per_pattern = ( + locale._data["unit_patterns"] + .get(denominator_unit, {}) + .get(length, {}) + .get("per") + ) if per_pattern: return per_pattern.format(formatted_numerator) # See TR-35's per-unit pattern algorithm, point 3.2. @@ -335,6 +352,11 @@ def format_compound_unit( ) # TODO: this doesn't support "compound_variations" (or "prefix"), and will fall back to the "x/y" representation - per_pattern = locale._data["compound_unit_patterns"].get("per", {}).get(length, {}).get("compound", "{0}/{1}") + per_pattern = ( + locale._data["compound_unit_patterns"] + .get("per", {}) + .get(length, {}) + .get("compound", "{0}/{1}") + ) return per_pattern.format(formatted_numerator, formatted_denominator) diff --git a/babel/util.py b/babel/util.py index 4e90ac5cb..3218bda09 100644 --- a/babel/util.py +++ b/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-2025 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.]+)', + 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. @@ -260,7 +269,6 @@ class FixedOffsetTimezone(datetime.tzinfo): """Fixed offset in minutes east from UTC.""" def __init__(self, offset: float, name: str | None = None) -> None: - self._offset = datetime.timedelta(minutes=offset) if name is None: name = 'Etc/GMT%+d' % offset