From a45e25e3125f6ee0a9f32387545df318b0b3b2d0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 23 Nov 2022 13:33:10 +0200 Subject: Replace %/.format/concatenation with f-strings where feasible (#927) Original conversion suggestions via flynt, edited by hand. --- babel/core.py | 15 +++++----- babel/dates.py | 27 ++++++++---------- babel/lists.py | 9 +++--- babel/localedata.py | 6 ++-- babel/localtime/_win32.py | 4 +-- babel/messages/catalog.py | 32 ++++++++++----------- babel/messages/checkers.py | 2 +- babel/messages/extract.py | 13 ++++----- babel/messages/frontend.py | 36 ++++++++++++------------ babel/messages/pofile.py | 36 ++++++++---------------- babel/numbers.py | 47 ++++++++++++++++--------------- babel/plural.py | 62 +++++++++++++++-------------------------- babel/support.py | 4 +-- babel/units.py | 10 +++---- babel/util.py | 8 ++---- scripts/generate_authors.py | 2 +- scripts/import_cldr.py | 17 +++++------ setup.py | 2 +- tests/messages/test_catalog.py | 4 +-- tests/messages/test_frontend.py | 13 +++++---- tests/messages/test_pofile.py | 2 +- tests/test_core.py | 3 +- tests/test_plural.py | 2 +- tests/test_support.py | 9 +++--- 24 files changed, 161 insertions(+), 204 deletions(-) diff --git a/babel/core.py b/babel/core.py index 2a01c30..825af81 100644 --- a/babel/core.py +++ b/babel/core.py @@ -98,7 +98,7 @@ class UnknownLocaleError(Exception): :param identifier: the identifier string of the unsupported locale """ - Exception.__init__(self, 'unknown locale %r' % identifier) + Exception.__init__(self, f"unknown locale {identifier!r}") #: The identifier of the locale that could not be found. self.identifier = identifier @@ -262,7 +262,7 @@ class Locale: elif isinstance(identifier, Locale): return identifier elif not isinstance(identifier, str): - raise TypeError('Unexpected value for identifier: %r' % (identifier,)) + raise TypeError(f"Unexpected value for identifier: {identifier!r}") parts = parse_locale(identifier, sep=sep) input_id = get_locale_identifier(parts) @@ -349,9 +349,8 @@ class Locale: for key in ('territory', 'script', 'variant'): value = getattr(self, key) if value is not None: - parameters.append('%s=%r' % (key, value)) - parameter_string = '%r' % self.language + ', '.join(parameters) - return 'Locale(%s)' % parameter_string + parameters.append(f"{key}={value!r}") + return f"Locale({self.language!r}{', '.join(parameters)})" def __str__(self): return get_locale_identifier((self.language, self.territory, @@ -388,7 +387,7 @@ class Locale: details.append(locale.variants.get(self.variant)) details = filter(None, details) if details: - retval += ' (%s)' % u', '.join(details) + retval += f" ({', '.join(details)})" return retval display_name = property(get_display_name, doc="""\ @@ -1120,7 +1119,7 @@ def parse_locale(identifier, sep='_'): parts = identifier.split(sep) lang = parts.pop(0).lower() if not lang.isalpha(): - raise ValueError('expected only letters, got %r' % lang) + raise ValueError(f"expected only letters, got {lang!r}") script = territory = variant = None if parts: @@ -1139,7 +1138,7 @@ def parse_locale(identifier, sep='_'): variant = parts.pop().upper() if parts: - raise ValueError('%r is not a valid locale identifier' % identifier) + raise ValueError(f"{identifier!r} is not a valid locale identifier") return lang, territory, script, variant diff --git a/babel/dates.py b/babel/dates.py index 8228bef..e9f6f6d 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -203,7 +203,7 @@ def get_timezone(zone=None): try: return _pytz.timezone(zone) except _pytz.UnknownTimeZoneError: - raise LookupError('Unknown timezone %s' % zone) + raise LookupError(f"Unknown timezone {zone}") def get_next_timezone_transition(zone=None, dt=None): @@ -312,11 +312,7 @@ class TimezoneTransition: return int(self.to_tzinfo._utcoffset.total_seconds()) def __repr__(self): - return ' %s (%s)>' % ( - self.from_tz, - self.to_tz, - self.activates, - ) + return f" {self.to_tz} ({self.activates})>" def get_period_names(width='wide', context='stand-alone', locale=LC_TIME): @@ -958,7 +954,7 @@ def format_timedelta(delta, granularity='second', threshold=.85, yield unit_rel_patterns['future'] else: yield unit_rel_patterns['past'] - a_unit = 'duration-' + a_unit + a_unit = f"duration-{a_unit}" yield locale._data['unit_patterns'].get(a_unit, {}).get(format) for unit, secs_per_unit in TIMEDELTA_UNITS: @@ -1293,7 +1289,7 @@ class DateTimePattern: self.format = format def __repr__(self): - return '<%s %r>' % (type(self).__name__, self.pattern) + return f"<{type(self).__name__} {self.pattern!r}>" def __str__(self): pat = self.pattern @@ -1365,7 +1361,7 @@ class DateTimeFormat: elif char in ('z', 'Z', 'v', 'V', 'x', 'X', 'O'): return self.format_timezone(char, num) else: - raise KeyError('Unsupported date/time field %r' % char) + raise KeyError(f"Unsupported date/time field {char!r}") def extract(self, char): char = str(char)[0] @@ -1384,7 +1380,7 @@ class DateTimeFormat: elif char == 'a': return int(self.value.hour >= 12) # 0 for am, 1 for pm else: - raise NotImplementedError("Not implemented: extracting %r from %r" % (char, self.value)) + raise NotImplementedError(f"Not implemented: extracting {char!r} from {self.value!r}") def format_era(self, char, num): width = {3: 'abbreviated', 4: 'wide', 5: 'narrow'}[max(3, num)] @@ -1429,7 +1425,7 @@ class DateTimeFormat: if week == 0: date = self.value - timedelta(days=self.value.day) week = self.get_week_number(date.day, date.weekday()) - return '%d' % week + return str(week) def format_weekday(self, char='E', num=4): """ @@ -1475,7 +1471,7 @@ class DateTimeFormat: return self.format(self.get_day_of_year(), num) def format_day_of_week_in_month(self): - return '%d' % ((self.value.day - 1) // 7 + 1) + return str((self.value.day - 1) // 7 + 1) def format_period(self, char, num): """ @@ -1517,7 +1513,7 @@ class DateTimeFormat: period_names = get_period_names(context=context, width=width, locale=self.locale) if period in period_names: return period_names[period] - raise ValueError('Could not format period %s in %s' % (period, self.locale)) + raise ValueError(f"Could not format period {period} in {self.locale}") def format_frac_seconds(self, num): """ Return fractional seconds. @@ -1689,11 +1685,10 @@ def parse_pattern(pattern): fieldchar, fieldnum = tok_value limit = PATTERN_CHARS[fieldchar] if limit and fieldnum not in limit: - raise ValueError('Invalid length for field: %r' - % (fieldchar * fieldnum)) + raise ValueError(f"Invalid length for field: {fieldchar * fieldnum!r}") result.append('%%(%s)s' % (fieldchar * fieldnum)) else: - raise NotImplementedError("Unknown token type: %s" % tok_type) + raise NotImplementedError(f"Unknown token type: {tok_type}") _pattern_cache[pattern] = pat = DateTimePattern(pattern, u''.join(result)) return pat diff --git a/babel/lists.py b/babel/lists.py index 11cc7d7..ea983ef 100644 --- a/babel/lists.py +++ b/babel/lists.py @@ -68,11 +68,10 @@ def format_list(lst, style='standard', locale=DEFAULT_LOCALE): return lst[0] if style not in locale.list_patterns: - raise ValueError('Locale %s does not support list formatting style %r (supported are %s)' % ( - locale, - style, - list(sorted(locale.list_patterns)), - )) + raise ValueError( + f'Locale {locale} does not support list formatting style {style!r} ' + f'(supported are {sorted(locale.list_patterns)})' + ) patterns = locale.list_patterns[style] if len(lst) == 2: diff --git a/babel/localedata.py b/babel/localedata.py index 14e6bcd..8ec8f4a 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -50,10 +50,10 @@ def resolve_locale_filename(name): # Ensure we're not left with one of the Windows reserved names. if sys.platform == "win32" and _windows_reserved_name_re.match(os.path.splitext(name)[0]): - raise ValueError("Name %s is invalid on Windows" % name) + raise ValueError(f"Name {name} is invalid on Windows") # Build the path. - return os.path.join(_dirname, '%s.dat' % name) + return os.path.join(_dirname, f"{name}.dat") def exists(name): @@ -194,7 +194,7 @@ class Alias: self.keys = tuple(keys) def __repr__(self): - return '<%s %r>' % (type(self).__name__, self.keys) + return f"<{type(self).__name__} {self.keys!r}>" def resolve(self, data): """Resolve the alias based on the given data. diff --git a/babel/localtime/_win32.py b/babel/localtime/_win32.py index 09b87b1..a4f6d55 100644 --- a/babel/localtime/_win32.py +++ b/babel/localtime/_win32.py @@ -77,11 +77,11 @@ def get_localzone_name(): if timezone is None: # Nope, that didn't work. Try adding 'Standard Time', # it seems to work a lot of times: - timezone = tz_names.get(tzkeyname + ' Standard Time') + timezone = tz_names.get(f"{tzkeyname} Standard Time") # Return what we have. if timezone is None: - raise pytz.UnknownTimeZoneError('Can not find timezone ' + tzkeyname) + raise pytz.UnknownTimeZoneError(f"Can not find timezone {tzkeyname}") return timezone diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index e43a28c..22ce660 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -49,7 +49,7 @@ def _parse_datetime_header(value): hours_offset_s, mins_offset_s = rest[:2], rest[2:] # Make them all integers - plus_minus = int(plus_minus_s + '1') + plus_minus = int(f"{plus_minus_s}1") hours_offset = int(hours_offset_s) mins_offset = int(mins_offset_s) @@ -108,8 +108,7 @@ class Message: self.context = context def __repr__(self): - return '<%s %r (flags: %r)>' % (type(self).__name__, self.id, - list(self.flags)) + return f"<{type(self).__name__} {self.id!r} (flags: {list(self.flags)!r})>" def __cmp__(self, other): """Compare Messages, taking into account plural ids""" @@ -312,7 +311,7 @@ class Catalog: self._locale = None return - raise TypeError('`locale` must be a Locale, a locale identifier string, or None; got %r' % locale) + raise TypeError(f"`locale` must be a Locale, a locale identifier string, or None; got {locale!r}") def _get_locale(self): return self._locale @@ -334,7 +333,7 @@ class Catalog: .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', '%s translations' % locale_name) + comment = comment.replace("Translations template", f"{locale_name} translations") return comment def _set_header_comment(self, string): @@ -375,8 +374,7 @@ class Catalog: def _get_mime_headers(self): headers = [] - headers.append(('Project-Id-Version', - '%s %s' % (self.project, self.version))) + headers.append(("Project-Id-Version", f"{self.project} {self.version}")) headers.append(('Report-Msgid-Bugs-To', self.msgid_bugs_address)) headers.append(('POT-Creation-Date', format_datetime(self.creation_date, 'yyyy-MM-dd HH:mmZ', @@ -399,10 +397,9 @@ class Catalog: if self.locale is not None: headers.append(('Plural-Forms', self.plural_forms)) headers.append(('MIME-Version', '1.0')) - headers.append(('Content-Type', - 'text/plain; charset=%s' % self.charset)) + headers.append(("Content-Type", f"text/plain; charset={self.charset}")) headers.append(('Content-Transfer-Encoding', '8bit')) - headers.append(('Generated-By', 'Babel %s\n' % VERSION)) + headers.append(("Generated-By", f"Babel {VERSION}\n")) return headers def _force_text(self, s, encoding='utf-8', errors='strict'): @@ -434,7 +431,7 @@ class Catalog: if 'charset' in params: self.charset = params['charset'].lower() elif name == 'plural-forms': - params = parse_separated_header(' ;' + value) + params = parse_separated_header(f" ;{value}") self._num_plurals = int(params.get('nplurals', 2)) self._plural_expr = params.get('plural', '(n != 1)') elif name == 'pot-creation-date': @@ -541,7 +538,7 @@ class Catalog: 'nplurals=2; plural=(n > 1);' :type: `str`""" - return 'nplurals=%s; plural=%s;' % (self.num_plurals, self.plural_expr) + return f"nplurals={self.num_plurals}; plural={self.plural_expr};" def __contains__(self, id): """Return whether the catalog has a message with the specified ID.""" @@ -560,7 +557,7 @@ class Catalog: :rtype: ``iterator``""" buf = [] for name, value in self.mime_headers: - buf.append('%s: %s' % (name, value)) + buf.append(f"{name}: {value}") flags = set() if self.fuzzy: flags |= {'fuzzy'} @@ -571,8 +568,8 @@ class Catalog: def __repr__(self): locale = '' if self.locale: - locale = ' %s' % self.locale - return '<%s %r%s>' % (type(self).__name__, self.domain, locale) + locale = f" {self.locale}" + return f"<{type(self).__name__} {self.domain!r}{locale}>" def __delitem__(self, id): """Delete the message with the specified ID.""" @@ -626,13 +623,12 @@ class Catalog: elif id == '': # special treatment for the header message self.mime_headers = message_from_string(message.string).items() - self.header_comment = '\n'.join([('# %s' % c).rstrip() for c - in message.user_comments]) + self.header_comment = "\n".join([f"# {c}".rstrip() for c in message.user_comments]) self.fuzzy = message.fuzzy else: if isinstance(id, (list, tuple)): assert isinstance(message.string, (list, tuple)), \ - 'Expected sequence but got %s' % type(message.string) + f"Expected sequence but got {type(message.string)}" self._messages[key] = message def add(self, id, string=None, locations=(), flags=(), auto_comments=(), diff --git a/babel/messages/checkers.py b/babel/messages/checkers.py index 4292c02..2706c5b 100644 --- a/babel/messages/checkers.py +++ b/babel/messages/checkers.py @@ -144,7 +144,7 @@ def _validate_format(format, alternative): type_map = dict(a) for name, typechar in b: if name not in type_map: - raise TranslationError('unknown named placeholder %r' % name) + raise TranslationError(f'unknown named placeholder {name!r}') elif not _compatible(typechar, type_map[name]): raise TranslationError('incompatible format for ' 'placeholder %r: ' diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 74e57a1..4f0f649 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -42,9 +42,6 @@ DEFAULT_KEYWORDS = { DEFAULT_MAPPING = [('**.py', 'python')] -empty_msgid_warning = ( - '%s: warning: Empty msgid. It is reserved by GNU gettext: gettext("") ' - 'returns the header entry with meta information, not the empty string.') def _strip_comment_tags(comments, tags): @@ -332,7 +329,7 @@ def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), func = builtin.get(method) if func is None: - raise ValueError('Unknown extraction method %r' % method) + raise ValueError(f"Unknown extraction method {method!r}") results = func(fileobj, keywords.keys(), comment_tags, options=options or {}) @@ -377,9 +374,11 @@ def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(), first_msg_index = spec[0] - 1 if not messages[first_msg_index]: # An empty string msgid isn't valid, emit a warning - where = '%s:%i' % (hasattr(fileobj, 'name') and - fileobj.name or '(unknown)', lineno) - sys.stderr.write((empty_msgid_warning % where) + '\n') + 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" + ) continue messages = tuple(msgs) diff --git a/babel/messages/frontend.py b/babel/messages/frontend.py index 6e09d10..c42cdc2 100644 --- a/babel/messages/frontend.py +++ b/babel/messages/frontend.py @@ -191,7 +191,7 @@ class compile_catalog(Command): for catalog, errors in self._run_domain(domain).items(): n_errors += len(errors) if n_errors: - self.log.error('%d errors encountered.' % n_errors) + self.log.error('%d errors encountered.', n_errors) return (1 if n_errors else 0) def _run_domain(self, domain): @@ -203,19 +203,19 @@ class compile_catalog(Command): po_files.append((self.locale, os.path.join(self.directory, self.locale, 'LC_MESSAGES', - domain + '.po'))) + f"{domain}.po"))) mo_files.append(os.path.join(self.directory, self.locale, 'LC_MESSAGES', - domain + '.mo')) + f"{domain}.mo")) else: for locale in os.listdir(self.directory): po_file = os.path.join(self.directory, locale, - 'LC_MESSAGES', domain + '.po') + 'LC_MESSAGES', f"{domain}.po") if os.path.exists(po_file): po_files.append((locale, po_file)) mo_files.append(os.path.join(self.directory, locale, 'LC_MESSAGES', - domain + '.mo')) + f"{domain}.mo")) else: po_files.append((self.locale, self.input_file)) if self.output_file: @@ -223,7 +223,7 @@ class compile_catalog(Command): else: mo_files.append(os.path.join(self.directory, self.locale, 'LC_MESSAGES', - domain + '.mo')) + f"{domain}.mo")) if not po_files: raise OptionError('no message catalogs found') @@ -451,7 +451,7 @@ class extract_messages(Command): for path in self.input_paths: if not os.path.exists(path): - raise OptionError("Input path: %s does not exist" % path) + raise OptionError(f"Input path: {path} does not exist") self.add_comments = listify_value(self.add_comments or (), ",") @@ -498,8 +498,8 @@ class extract_messages(Command): optstr = '' if options: - optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for - k, v in options.items()]) + opt_values = ", ".join(f'{k}="{v}"' for k, v in options.items()) + optstr = f" ({opt_values})" self.log.info('extracting messages from %s%s', filepath, optstr) if os.path.isfile(path): @@ -640,7 +640,7 @@ class init_catalog(Command): 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', self.domain + '.po') + 'LC_MESSAGES', f"{self.domain}.po") if not os.path.exists(os.path.dirname(self.output_file)): os.makedirs(os.path.dirname(self.output_file)) @@ -782,12 +782,12 @@ class update_catalog(Command): po_files.append((self.locale, os.path.join(self.output_dir, self.locale, 'LC_MESSAGES', - self.domain + '.po'))) + f"{self.domain}.po"))) else: for locale in os.listdir(self.output_dir): po_file = os.path.join(self.output_dir, locale, 'LC_MESSAGES', - self.domain + '.po') + f"{self.domain}.po") if os.path.exists(po_file): po_files.append((locale, po_file)) else: @@ -889,7 +889,7 @@ class CommandLineInterface: """ usage = '%%prog %s [options] %s' - version = '%%prog %s' % VERSION + version = f'%prog {VERSION}' commands = { 'compile': 'compile message catalogs to MO files', 'extract': 'extract messages from source files and generate a POT file', @@ -949,7 +949,7 @@ class CommandLineInterface: cmdname = args[0] if cmdname not in self.commands: - self.parser.error('unknown command "%s"' % cmdname) + self.parser.error(f'unknown command "{cmdname}"') cmdinst = self._configure_command(cmdname, args[1:]) return cmdinst.run() @@ -997,14 +997,14 @@ class CommandLineInterface: as_args = getattr(cmdclass, "as_args", ()) for long, short, help in cmdclass.user_options: name = long.strip("=") - default = getattr(cmdinst, name.replace('-', '_')) - strs = ["--%s" % name] + default = getattr(cmdinst, name.replace("-", "_")) + strs = [f"--{name}"] if short: - strs.append("-%s" % short) + strs.append(f"-{short}") strs.extend(cmdclass.option_aliases.get(name, ())) choices = cmdclass.option_choices.get(name, None) if name == as_args: - parser.usage += "<%s>" % name + parser.usage += f"<{name}>" elif name in cmdclass.boolean_options: parser.add_option(*strs, action="store_true", help=help) elif name in cmdclass.multiple_value_options: diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py index 00e0844..a213b22 100644 --- a/babel/messages/pofile.py +++ b/babel/messages/pofile.py @@ -318,10 +318,7 @@ class PoFileParser: if self.abort_invalid: raise PoFileError(msg, self.catalog, line, lineno) print("WARNING:", msg) - # `line` is guaranteed to be unicode so u"{}"-interpolating would always - # succeed, but on Python < 2 if not in a TTY, `sys.stdout.encoding` - # is `None`, unicode may not be printable so we `repr()` to ASCII. - print(u"WARNING: Problem on line {0}: {1}".format(lineno + 1, repr(line))) + print(f"WARNING: Problem on line {lineno + 1}: {line!r}") def read_po(fileobj, locale=None, domain=None, ignore_obsolete=False, charset=None, abort_invalid=False): @@ -525,34 +522,26 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, else: _width = 76 for line in wraptext(comment, _width): - _write('#%s %s\n' % (prefix, line.strip())) + _write(f"#{prefix} {line.strip()}\n") def _write_message(message, prefix=''): if isinstance(message.id, (list, tuple)): if message.context: - _write('%smsgctxt %s\n' % (prefix, - _normalize(message.context, prefix))) - _write('%smsgid %s\n' % (prefix, _normalize(message.id[0], prefix))) - _write('%smsgid_plural %s\n' % ( - prefix, _normalize(message.id[1], prefix) - )) + _write(f"{prefix}msgctxt {_normalize(message.context, prefix)}\n") + _write(f"{prefix}msgid {_normalize(message.id[0], prefix)}\n") + _write(f"{prefix}msgid_plural {_normalize(message.id[1], prefix)}\n") for idx in range(catalog.num_plurals): try: string = message.string[idx] except IndexError: string = '' - _write('%smsgstr[%d] %s\n' % ( - prefix, idx, _normalize(string, prefix) - )) + _write(f"{prefix}msgstr[{idx:d}] {_normalize(string, prefix)}\n") else: if message.context: - _write('%smsgctxt %s\n' % (prefix, - _normalize(message.context, prefix))) - _write('%smsgid %s\n' % (prefix, _normalize(message.id, prefix))) - _write('%smsgstr %s\n' % ( - prefix, _normalize(message.string or '', prefix) - )) + _write(f"{prefix}msgctxt {_normalize(message.context, prefix)}\n") + _write(f"{prefix}msgid {_normalize(message.id, prefix)}\n") + _write(f"{prefix}msgstr {_normalize(message.string or '', prefix)}\n") sort_by = None if sort_output: @@ -571,7 +560,7 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, lines += wraptext(line, width=width, subsequent_indent='# ') comment_header = u'\n'.join(lines) - _write(comment_header + u'\n') + _write(f"{comment_header}\n") for comment in message.user_comments: _write_comment(comment) @@ -592,10 +581,9 @@ def write_po(fileobj, catalog, width=76, no_location=False, omit_header=False, locations = message.locations for filename, lineno in locations: + location = filename.replace(os.sep, '/') if lineno and include_lineno: - location = u'%s:%d' % (filename.replace(os.sep, '/'), lineno) - else: - location = u'%s' % filename.replace(os.sep, '/') + location = f"{location}:{lineno:d}" if location not in locs: locs.append(location) _write_comment(' '.join(locs), prefix=':') diff --git a/babel/numbers.py b/babel/numbers.py index 6c9ab24..2221e95 100644 --- a/babel/numbers.py +++ b/babel/numbers.py @@ -36,7 +36,7 @@ class UnknownCurrencyError(Exception): """Create the exception. :param identifier: the identifier string of the unsupported currency """ - Exception.__init__(self, 'Unknown currency %r.' % identifier) + Exception.__init__(self, f"Unknown currency {identifier!r}.") #: The identifier of the locale that could not be found. self.identifier = identifier @@ -583,8 +583,7 @@ def format_currency( try: pattern = locale.currency_formats[format_type] except KeyError: - raise UnknownCurrencyFormatError( - "%r is not a known currency format type" % format_type) + raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") return pattern.apply( number, locale, currency=currency, currency_digits=currency_digits, @@ -779,7 +778,7 @@ def parse_number(string, locale=LC_NUMERIC): try: return int(string.replace(get_group_symbol(locale), '')) except ValueError: - raise NumberFormatError('%r is not a valid number' % string) + raise NumberFormatError(f"{string!r} is not a valid number") def parse_decimal(string, locale=LC_NUMERIC, strict=False): @@ -835,7 +834,7 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): parsed = decimal.Decimal(string.replace(group_symbol, '') .replace(decimal_symbol, '.')) except decimal.InvalidOperation: - raise NumberFormatError('%r is not a valid decimal number' % string) + raise NumberFormatError(f"{string!r} is not a valid decimal number") if strict and group_symbol in string: proper = format_decimal(parsed, locale=locale, decimal_quantization=False) if string != proper and string.rstrip('0') != (proper + decimal_symbol): @@ -843,22 +842,25 @@ def parse_decimal(string, locale=LC_NUMERIC, strict=False): parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') .replace(group_symbol, '.')) except decimal.InvalidOperation: - raise NumberFormatError(( - "%r is not a properly formatted decimal number. Did you mean %r?" % - (string, proper) - ), suggestions=[proper]) + raise NumberFormatError( + f"{string!r} is not a properly formatted decimal number. " + f"Did you mean {proper!r}?", + suggestions=[proper], + ) else: proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) if proper_alt == proper: - raise NumberFormatError(( - "%r is not a properly formatted decimal number. Did you mean %r?" % - (string, proper) - ), suggestions=[proper]) + raise NumberFormatError( + f"{string!r} is not a properly formatted decimal number. " + f"Did you mean {proper!r}?", + suggestions=[proper], + ) else: - raise NumberFormatError(( - "%r is not a properly formatted decimal number. Did you mean %r? Or maybe %r?" % - (string, proper, proper_alt) - ), suggestions=[proper, proper_alt]) + raise NumberFormatError( + f"{string!r} is not a properly formatted decimal number. " + f"Did you mean {proper!r}? Or maybe {proper_alt!r}?", + suggestions=[proper, proper_alt], + ) return parsed @@ -869,8 +871,7 @@ PREFIX_PATTERN = r"(?P(?:'[^']*'|%s)*)" % PREFIX_END NUMBER_PATTERN = r"(?P%s*)" % NUMBER_TOKEN SUFFIX_PATTERN = r"(?P.*)" -number_re = re.compile(r"%s%s%s" % (PREFIX_PATTERN, NUMBER_PATTERN, - SUFFIX_PATTERN)) +number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}") def parse_grouping(p): @@ -903,7 +904,7 @@ def parse_pattern(pattern): def _match_number(pattern): rv = number_re.search(pattern) if rv is None: - raise ValueError('Invalid number pattern %r' % pattern) + raise ValueError(f"Invalid number pattern {pattern!r}") return rv.groups() pos_pattern = pattern @@ -915,7 +916,7 @@ def parse_pattern(pattern): neg_prefix, _, neg_suffix = _match_number(neg_pattern) else: pos_prefix, number, pos_suffix = _match_number(pos_pattern) - neg_prefix = '-' + pos_prefix + neg_prefix = f"-{pos_prefix}" neg_suffix = pos_suffix if 'E' in number: number, exp = number.split('E', 1) @@ -978,7 +979,7 @@ class NumberPattern: self.scale = self.compute_scale() def __repr__(self): - return '<%s %r>' % (type(self).__name__, self.pattern) + return f"<{type(self).__name__} {self.pattern!r}>" def compute_scale(self): """Return the scaling factor to apply to the number before rendering. @@ -1184,7 +1185,7 @@ class NumberPattern: def _quantize_value(self, value, locale, frac_prec, group_separator): quantum = get_decimal_quantum(frac_prec[1]) rounded = value.quantize(quantum) - a, sep, b = "{:f}".format(rounded).partition(".") + 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) diff --git a/babel/plural.py b/babel/plural.py index d3dc22d..712acec 100644 --- a/babel/plural.py +++ b/babel/plural.py @@ -111,9 +111,9 @@ class PluralRule: self.abstract = [] for key, expr in sorted(list(rules)): if key not in _plural_tags: - raise ValueError('unknown tag %r' % key) + raise ValueError(f"unknown tag {key!r}") elif key in found: - raise ValueError('tag %r defined twice' % key) + raise ValueError(f"tag {key!r} defined twice") found.add(key) ast = _Parser(expr).ast if ast: @@ -121,11 +121,8 @@ class PluralRule: def __repr__(self): rules = self.rules - return '<%s %r>' % ( - type(self).__name__, - ', '.join(['%s: %s' % (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): @@ -185,7 +182,7 @@ def to_javascript(rule): to_js = _JavaScriptCompiler().compile result = ['(function(n) { return '] for tag, ast in PluralRule.parse(rule).abstract: - result.append('%s ? %r : ' % (to_js(ast), tag)) + result.append(f"{to_js(ast)} ? {tag!r} : ") result.append('%r; })' % _fallback_tag) return ''.join(result) @@ -223,8 +220,8 @@ def to_python(rule): for tag, ast in PluralRule.parse(rule).abstract: # the str() call is to coerce the tag to the native string. It's # a limited ascii restricted set of tags anyways so that is fine. - result.append(' if (%s): return %r' % (to_python_func(ast), str(tag))) - result.append(' return %r' % _fallback_tag) + result.append(f" if ({to_python_func(ast)}): return {str(tag)!r}") + result.append(f" return {_fallback_tag!r}") code = compile('\n'.join(result), '', 'exec') eval(code, namespace) return namespace['evaluate'] @@ -246,10 +243,10 @@ def to_gettext(rule): _compile = _GettextCompiler().compile _get_index = [tag for tag in _plural_tags if tag in used_tags].index - result = ['nplurals=%d; plural=(' % len(used_tags)] + result = [f"nplurals={len(used_tags)}; plural=("] for tag, ast in rule.abstract: - result.append('%s ? %d : ' % (_compile(ast), _get_index(tag))) - result.append('%d);' % _get_index(_fallback_tag)) + result.append(f"{_compile(ast)} ? {_get_index(tag)} : ") + result.append(f"{_get_index(_fallback_tag)});") return ''.join(result) @@ -427,8 +424,7 @@ class _Parser: return self.ast = self.condition() if self.tokens: - raise RuleError('Expected end of rule, got %r' % - self.tokens[-1][1]) + raise RuleError(f"Expected end of rule, got {self.tokens[-1][1]!r}") def expect(self, type_, value=None, term=None): token = skip_token(self.tokens, type_, value) @@ -437,8 +433,8 @@ class _Parser: if term is None: term = repr(value is None and type_ or value) if not self.tokens: - raise RuleError('expected %s but end of rule reached' % term) - raise RuleError('expected %s but got %r' % (term, self.tokens[-1][1])) + raise RuleError(f"expected {term} but end of rule reached") + raise RuleError(f"expected {term} but got {self.tokens[-1][1]!r}") def condition(self): op = self.and_condition() @@ -527,7 +523,7 @@ class _Compiler: def compile(self, arg): op, args = arg - return getattr(self, 'compile_' + op)(*args) + return getattr(self, f"compile_{op}")(*args) compile_n = lambda x: 'n' compile_i = lambda x: 'i' @@ -558,11 +554,8 @@ class _PythonCompiler(_Compiler): compile_mod = _binary_compiler('MOD(%s, %s)') def compile_relation(self, method, expr, range_list): - compile_range_list = '[%s]' % ','.join( - ['(%s, %s)' % tuple(map(self.compile, range_)) - for range_ in range_list[1]]) - return '%s(%s, %s)' % (method.upper(), self.compile(expr), - compile_range_list) + ranges = ",".join([f"({self.compile(a)}, {self.compile(b)})" for (a, b) in range_list[1]]) + return f"{method.upper()}({self.compile(expr)}, [{ranges}])" class _GettextCompiler(_Compiler): @@ -579,19 +572,11 @@ class _GettextCompiler(_Compiler): expr = self.compile(expr) for item in range_list[1]: if item[0] == item[1]: - rv.append('(%s == %s)' % ( - expr, - self.compile(item[0]) - )) + rv.append(f"({expr} == {self.compile(item[0])})") else: min, max = map(self.compile, item) - rv.append('(%s >= %s && %s <= %s)' % ( - expr, - min, - expr, - max - )) - return '(%s)' % ' || '.join(rv) + rv.append(f"({expr} >= {min} && {expr} <= {max})") + return f"({' || '.join(rv)})" class _JavaScriptCompiler(_GettextCompiler): @@ -610,7 +595,7 @@ class _JavaScriptCompiler(_GettextCompiler): self, method, expr, range_list) if method == 'in': expr = self.compile(expr) - code = '(parseInt(%s, 10) == %s && %s)' % (expr, expr, code) + code = f"(parseInt({expr}, 10) == {expr} && {code})" return code @@ -636,8 +621,5 @@ class _UnicodeCompiler(_Compiler): if item[0] == item[1]: ranges.append(self.compile(item[0])) else: - ranges.append('%s..%s' % tuple(map(self.compile, item))) - return '%s%s %s %s' % ( - self.compile(expr), negated and ' not' or '', - method, ','.join(ranges) - ) + ranges.append(f"{self.compile(item[0])}..{self.compile(item[1])}") + return f"{self.compile(expr)}{' not' if negated else ''} {method} {','.join(ranges)}" diff --git a/babel/support.py b/babel/support.py index 50f2752..8cebd7d 100644 --- a/babel/support.py +++ b/babel/support.py @@ -577,8 +577,8 @@ class Translations(NullTranslations, gettext.GNUTranslations): return cls(fp=fp, domain=domain) def __repr__(self): - return '<%s: "%s">' % (type(self).__name__, - self._info.get('project-id-version')) + version = self._info.get('project-id-version') + return f'<{type(self).__name__}: "{version}">' def add(self, translations, merge=True): """Add the given translations to the catalog. diff --git a/babel/units.py b/babel/units.py index 8a9ec7d..f8f2675 100644 --- a/babel/units.py +++ b/babel/units.py @@ -4,7 +4,7 @@ from babel.numbers import format_decimal, LC_NUMERIC class UnknownUnitError(ValueError): def __init__(self, unit, locale): - ValueError.__init__(self, "%s is not a known unit in %s" % (unit, locale)) + ValueError.__init__(self, f"{unit} is not a known unit in {locale}") def get_unit_name(measurement_unit, length='long', locale=LC_NUMERIC): @@ -128,10 +128,8 @@ def format_unit(value, measurement_unit, length='long', format=None, locale=LC_N # 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. - return '%s %s' % ( # pragma: no cover - formatted_value, - (get_unit_name(measurement_unit, length=length, locale=locale) or measurement_unit) - ) + fallback_name = get_unit_name(measurement_unit, length=length, locale=locale) # pragma: no cover + return f"{formatted_value} {fallback_name or measurement_unit}" # pragma: no cover def _find_compound_unit(numerator_unit, denominator_unit, locale=LC_NUMERIC): @@ -179,7 +177,7 @@ def _find_compound_unit(numerator_unit, denominator_unit, locale=LC_NUMERIC): # Now we can try and rebuild a compound unit specifier, then qualify it: - return _find_unit_pattern("%s-per-%s" % (bare_numerator_unit, bare_denominator_unit), locale=locale) + return _find_unit_pattern(f"{bare_numerator_unit}-per-{bare_denominator_unit}", locale=locale) def format_compound_unit( diff --git a/babel/util.py b/babel/util.py index f628844..0436b9e 100644 --- a/babel/util.py +++ b/babel/util.py @@ -82,9 +82,7 @@ def parse_encoding(fp): if m: magic_comment_encoding = m.group(1).decode('latin-1') if magic_comment_encoding != 'utf-8': - raise SyntaxError( - 'encoding problem: {0} with BOM'.format( - magic_comment_encoding)) + raise SyntaxError(f"encoding problem: {magic_comment_encoding} with BOM") return 'utf-8' elif m: return m.group(1).decode('latin-1') @@ -191,7 +189,7 @@ def pathmatch(pattern, filename): buf.append(symbols[part]) elif part: buf.append(re.escape(part)) - match = re.match(''.join(buf) + '$', filename.replace(os.sep, '/')) + match = re.match(f"{''.join(buf)}$", filename.replace(os.sep, "/")) return match is not None @@ -236,7 +234,7 @@ class FixedOffsetTimezone(tzinfo): return self.zone def __repr__(self): - return '' % (self.zone, self._offset) + return f'' def utcoffset(self, dt): return self._offset diff --git a/scripts/generate_authors.py b/scripts/generate_authors.py index 409f24e..e2e3add 100644 --- a/scripts/generate_authors.py +++ b/scripts/generate_authors.py @@ -13,7 +13,7 @@ def get_sorted_authors_list(): def get_authors_file_content(): - author_list = '\n'.join('- %s' % a for a in get_sorted_authors_list()) + author_list = "\n".join(f"- {a}" for a in get_sorted_authors_list()) return ''' Babel is written and maintained by the Babel team and various contributors: diff --git a/scripts/import_cldr.py b/scripts/import_cldr.py index 097840c..5de707c 100755 --- a/scripts/import_cldr.py +++ b/scripts/import_cldr.py @@ -156,7 +156,8 @@ def write_datafile(path, data, dump_json=False): pickle.dump(data, outfile, 2) if dump_json: import json - with open(path + '.json', 'w') as outfile: + + with open(f"{path}.json", "w") as outfile: json.dump(data, outfile, indent=4, default=debug_repr) @@ -358,8 +359,8 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): if ext != '.xml': continue - full_filename = os.path.join(srcdir, 'main', filename) - data_filename = os.path.join(destdir, 'locale-data', stem + '.dat') + full_filename = os.path.join(srcdir, "main", filename) + data_filename = os.path.join(destdir, "locale-data", f"{stem}.dat") data = {} if not (force or need_conversion(data_filename, data, full_filename)): @@ -434,10 +435,10 @@ def _process_local_datas(sup, srcdir, destdir, force=False, dump_json=False): unsupported_number_systems_string = ', '.join(sorted(data.pop('unsupported_number_systems'))) if unsupported_number_systems_string: - log.warning('%s: unsupported number systems were ignored: %s' % ( - locale_id, - unsupported_number_systems_string, - )) + log.warning( + f"{locale_id}: unsupported number systems were ignored: " + f"{unsupported_number_systems_string}" + ) write_datafile(data_filename, data, dump_json=dump_json) @@ -902,7 +903,7 @@ def parse_interval_formats(data, tree): if item_sub.tag == "greatestDifference": skel_data[item_sub.attrib["id"]] = split_interval_pattern(item_sub.text) else: - raise NotImplementedError("Not implemented: %s(%r)" % (item_sub.tag, item_sub.attrib)) + raise NotImplementedError(f"Not implemented: {item_sub.tag}({item_sub.attrib!r})") def parse_currency_formats(data, tree): diff --git a/setup.py b/setup.py index 06caa6a..157f7c1 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup, Command try: from babel import __version__ except SyntaxError as exc: - sys.stderr.write("Unable to import Babel (%s). Are you running a supported version of Python?\n" % exc) + sys.stderr.write(f"Unable to import Babel ({exc}). Are you running a supported version of Python?\n") sys.exit(1) diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index cc5fb49..e9b15a8 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -357,7 +357,7 @@ def test_catalog_mime_headers(): ('MIME-Version', '1.0'), ('Content-Type', 'text/plain; charset=utf-8'), ('Content-Transfer-Encoding', '8bit'), - ('Generated-By', 'Babel %s\n' % catalog.VERSION), + ('Generated-By', f'Babel {catalog.VERSION}\n'), ] @@ -380,7 +380,7 @@ def test_catalog_mime_headers_set_locale(): ('MIME-Version', '1.0'), ('Content-Type', 'text/plain; charset=utf-8'), ('Content-Transfer-Encoding', '8bit'), - ('Generated-By', 'Babel %s\n' % catalog.VERSION), + ('Generated-By', f'Babel {catalog.VERSION}\n'), ] diff --git a/tests/messages/test_frontend.py b/tests/messages/test_frontend.py index 0eb7e8f..9e3ac9f 100644 --- a/tests/messages/test_frontend.py +++ b/tests/messages/test_frontend.py @@ -130,7 +130,7 @@ class ExtractMessagesTestCase(unittest.TestCase): assert ('file1.py' in msg.locations[0][0]) def test_input_paths_handle_spaces_after_comma(self): - self.cmd.input_paths = '%s, %s' % (this_dir, data_dir) + self.cmd.input_paths = f"{this_dir}, {data_dir}" self.cmd.output_file = pot_file self.cmd.finalize_options() assert self.cmd.input_paths == [this_dir, data_dir] @@ -1118,7 +1118,7 @@ msgstr[2] "" self.cli.run(sys.argv + ['compile', '--locale', 'de_DE', '-d', i18n_dir]) - assert not os.path.isfile(mo_file), 'Expected no file at %r' % mo_file + assert not os.path.isfile(mo_file), f'Expected no file at {mo_file!r}' assert sys.stderr.getvalue() == f'catalog {po_file} is marked as fuzzy, skipping\n' def test_compile_fuzzy_catalog(self): @@ -1397,14 +1397,15 @@ def test_extract_keyword_args_384(split, arg_name): ] if split: # Generate a command line with multiple -ks - kwarg_text = " ".join("%s %s" % (arg_name, kwarg_spec) for kwarg_spec in kwarg_specs) + kwarg_text = " ".join(f"{arg_name} {kwarg_spec}" for kwarg_spec in kwarg_specs) else: # Generate a single space-separated -k - kwarg_text = "%s \"%s\"" % (arg_name, " ".join(kwarg_specs)) + specs = ' '.join(kwarg_specs) + kwarg_text = f'{arg_name} "{specs}"' # (Both of those invocation styles should be equivalent, so there is no parametrization from here on out) cmdinst = configure_cli_command( - "extract -F babel-django.cfg --add-comments Translators: -o django232.pot %s ." % kwarg_text + f"extract -F babel-django.cfg --add-comments Translators: -o django232.pot {kwarg_text} ." ) assert isinstance(cmdinst, extract_messages) assert set(cmdinst.keywords.keys()) == {'_', 'dgettext', 'dngettext', @@ -1489,7 +1490,7 @@ def test_extract_error_code(monkeypatch, capsys): def test_extract_ignore_dirs(monkeypatch, capsys, tmp_path, with_underscore_ignore): pot_file = tmp_path / 'temp.pot' monkeypatch.chdir(project_dir) - cmd = "extract . -o '{}' --ignore-dirs '*ignored*' ".format(pot_file) + cmd = f"extract . -o '{pot_file}' --ignore-dirs '*ignored*' " if with_underscore_ignore: # This also tests that multiple arguments are supported. cmd += "--ignore-dirs '_*'" diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py index 99e59ba..a72368b 100644 --- a/tests/messages/test_pofile.py +++ b/tests/messages/test_pofile.py @@ -859,7 +859,7 @@ class PofileFunctionsTestCase(unittest.TestCase): expected_denormalized = u'multi-line\n translation' assert expected_denormalized == pofile.denormalize(msgstr) - assert expected_denormalized == pofile.denormalize('""\n' + msgstr) + assert expected_denormalized == pofile.denormalize(f'""\n{msgstr}') def test_unknown_language_roundtrip(): diff --git a/tests/test_core.py b/tests/test_core.py index 2de79e2..605bf5c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -309,8 +309,7 @@ def test_compatible_classes_in_global_and_localedata(filename): # *.dat files must have compatible classes between Python 2 and 3 if module.split('.')[0] == 'babel': return pickle.Unpickler.find_class(self, module, name) - raise pickle.UnpicklingError("global '%s.%s' is forbidden" % - (module, name)) + raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") with open(filename, 'rb') as f: assert Unpickler(f).load() diff --git a/tests/test_plural.py b/tests/test_plural.py index 4210033..16601cf 100644 --- a/tests/test_plural.py +++ b/tests/test_plural.py @@ -273,7 +273,7 @@ def test_gettext_compilation(locale): chars = 'ivwft' # Test that these rules are valid for this test; i.e. that they contain at least one # of the gettext-unsupported characters. - assert any((" " + ch + " ") in rule for ch in chars for rule in ru_rules.values()) + assert any(f" {ch} " in rule for ch in chars for rule in ru_rules.values()) # Then test that the generated value indeed does not contain these. ru_rules_gettext = plural.to_gettext(ru_rules) assert not any(ch in ru_rules_gettext for ch in chars) diff --git a/tests/test_support.py b/tests/test_support.py index 9447107..c73e53b 100644 --- a/tests/test_support.py +++ b/tests/test_support.py @@ -63,8 +63,7 @@ class TranslationsTestCase(unittest.TestCase): def assertEqualTypeToo(self, expected, result): assert expected == result - assert type(expected) == type(result), "instance type's do not " + \ - "match: %r!=%r" % (type(expected), type(result)) + assert type(expected) == type(result), f"instance types do not match: {type(expected)!r}!={type(result)!r}" def test_pgettext(self): self.assertEqualTypeToo('Voh', self.translations.gettext('foo')) @@ -210,7 +209,7 @@ class NullTranslationsTestCase(unittest.TestCase): def test_same_methods(self): for name in self.method_names(): if not hasattr(self.null_translations, name): - self.fail('NullTranslations does not provide method %r' % name) + self.fail(f"NullTranslations does not provide method {name!r}") def test_method_signature_compatibility(self): for name in self.method_names(): @@ -346,11 +345,13 @@ def test_format_percent(): def test_lazy_proxy(): def greeting(name='world'): - return u'Hello, %s!' % name + return f"Hello, {name}!" + lazy_greeting = support.LazyProxy(greeting, name='Joe') assert str(lazy_greeting) == u"Hello, Joe!" assert u' ' + lazy_greeting == u' Hello, Joe!' assert u'(%s)' % lazy_greeting == u'(Hello, Joe!)' + assert f"[{lazy_greeting}]" == "[Hello, Joe!]" greetings = [ support.LazyProxy(greeting, 'world'), -- cgit v1.2.1