From efb695e240f9bb3d5376e6dc9eb5dac58f2d62b8 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Wed, 30 Apr 2025 10:23:16 +0900 Subject: [PATCH 1/5] Replace format for f-strings Babel does not support f-strings, which prevents from localising a few strings in the code, like "Usage: " and "Try". The changes in this commit rewrites f-strings into the format syntax. Please note some f-strings remain as they are not expected to be translatable. This commit has to deactivate the UP032 rule on a few files. There seems to be an unreported bug in either Ruff or pyupgrade: The UP032 rule is not deactivated on multi-line commands, including multi-line strings. --- src/click/_winconsole.py | 4 ++-- src/click/core.py | 3 +-- src/click/formatting.py | 26 +++++++++++++++++++++----- src/click/parser.py | 18 ++++++++++++++---- src/click/shell_completion.py | 16 ++++++++++++++-- src/click/termui.py | 4 ++-- src/click/types.py | 27 +++++++++++++++++++++------ src/click/utils.py | 12 ++++++++++-- 8 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index b01035b29..9b062f9dc 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -151,7 +151,7 @@ def readinto(self, b: Buffer) -> int: # wait for KeyboardInterrupt time.sleep(0.1) if not rv: - raise OSError(f"Windows error: {GetLastError()}") + raise OSError("Windows error: {error}".format(error=GetLastError())) # noqa: UP032 if buffer[0] == EOF: return 0 @@ -168,7 +168,7 @@ def _get_error_message(errno: int) -> str: return "ERROR_SUCCESS" elif errno == ERROR_NOT_ENOUGH_MEMORY: return "ERROR_NOT_ENOUGH_MEMORY" - return f"Windows error {errno}" + return "Windows error {errno}".format(errno=errno) # noqa: UP032 def write(self, b: Buffer) -> int: bytes_to_be_written = len(b) diff --git a/src/click/core.py b/src/click/core.py index fe77cb985..4f1a38c2a 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1218,8 +1218,7 @@ def invoke(self, ctx: Context) -> t.Any: f" {self.deprecated}" if isinstance(self.deprecated, str) else "" ) message = _( - "DeprecationWarning: The command {name!r} is deprecated." - "{extra_message}" + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" ).format(name=self.name, extra_message=extra_message) echo(style(message, fg="red"), err=True) diff --git a/src/click/formatting.py b/src/click/formatting.py index a6e78fe04..f9b09c180 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -153,9 +153,11 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N ``"Usage: "``. """ if prefix is None: - prefix = f"{_('Usage:')} " + prefix = "{usage} ".format(usage=_("Usage:")) - usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + usage_prefix = "{prefix:>{indent}}{prog} ".format( + prefix=prefix, indent=self.current_indent, prog=prog + ) text_width = self.width - self.current_indent if text_width >= (term_len(usage_prefix) + 20): @@ -184,7 +186,11 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N def write_heading(self, heading: str) -> None: """Writes a heading into the buffer.""" - self.write(f"{'':>{self.current_indent}}{heading}:\n") + self.write( + "{prefix:>{indent}}{message}:\n".format( + prefix="", indent=self.current_indent, message=heading + ) + ) def write_paragraph(self) -> None: """Writes a paragraph into the buffer.""" @@ -229,7 +235,11 @@ def write_dl( first_col = min(widths[0], col_max) + col_spacing for first, second in iter_rows(rows, len(widths)): - self.write(f"{'':>{self.current_indent}}{first}") + self.write( + "{prefix:>{indent}}{message}".format( + prefix="", indent=self.current_indent, message=first + ) + ) if not second: self.write("\n") continue @@ -247,7 +257,13 @@ def write_dl( self.write(f"{lines[0]}\n") for line in lines[1:]: - self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + self.write( + "{prefix:>{indent}}{message}\n".format( + prefix="", + indent=first_col + self.current_indent, + message=line, + ) + ) else: self.write("\n") diff --git a/src/click/parser.py b/src/click/parser.py index a8b7d2634..20fa33eef 100644 --- a/src/click/parser.py +++ b/src/click/parser.py @@ -18,6 +18,14 @@ Copyright 2002-2006 Python Software Foundation. All rights reserved. """ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + # This code uses parts of optparse written by Gregory P. Ward and # maintained by the Python Software Foundation. # Copyright 2001-2006 Gregory P. Ward @@ -142,7 +150,9 @@ def __init__( for opt in opts: prefix, value = _split_opt(opt) if not prefix: - raise ValueError(f"Invalid start character for option ({opt})") + raise ValueError( + "Invalid start character for option ({option})".format(option=opt) + ) # noqa: UP032 self.prefixes.add(prefix[0]) if len(prefix) == 1 and len(value) == 1: self._short_opts.append(opt) @@ -175,7 +185,7 @@ def process(self, value: t.Any, state: _ParsingState) -> None: elif self.action == "count": state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore else: - raise ValueError(f"unknown action '{self.action}'") + raise ValueError("unknown action '{action}'".format(action=self.action)) # noqa: UP032 state.order.append(self.obj) @@ -511,8 +521,8 @@ def __getattr__(name: str) -> object: "ParsingState", }: warnings.warn( - f"'parser.{name}' is deprecated and will be removed in Click 9.0." - " The old parser is available in 'optparse'.", + "'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'.".format(name=name), # noqa: UP032 DeprecationWarning, stacklevel=2, ) diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index cdb58222c..890bef29e 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -373,7 +381,9 @@ def get_completion_args(self) -> tuple[list[str], str]: return args, incomplete def format_completion(self, item: CompletionItem) -> str: - return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" + return "{type}\n{value}\n{help}".format( + type=item.type, value=item.value, help=item.help if item.help else "_" + ) class FishComplete(ShellComplete): @@ -396,7 +406,9 @@ def get_completion_args(self) -> tuple[list[str], str]: def format_completion(self, item: CompletionItem) -> str: if item.help: - return f"{item.type},{item.value}\t{item.help}" + return "{type},{value}\t{help}".format( + type=item.type, value=item.value, help=item.help + ) # noqa: UP032 return f"{item.type},{item.value}" diff --git a/src/click/termui.py b/src/click/termui.py index dcbb22216..ece09f91b 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -608,13 +608,13 @@ def style( try: bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError(f"Unknown color {fg!r}") from None + raise TypeError("Unknown color {colour!r}".format(colour=fg)) from None # noqa: UP032 if bg: try: bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError(f"Unknown color {bg!r}") from None + raise TypeError("Unknown color {colour!r}".format(colour=bg)) from None # noqa: UP032 if bold is not None: bits.append(f"\033[{1 if bold else 22}m") diff --git a/src/click/types.py b/src/click/types.py index 23da68d39..a996b082a 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -318,10 +326,10 @@ def get_metavar(self, param: Parameter, ctx: Context) -> str | None: # Use curly braces to indicate a required argument. if param.required and param.param_type_name == "argument": - return f"{{{choices_str}}}" + return "{{{choices}}}".format(choices=choices_str) # noqa: UP032 # Use square braces to indicate an option or optional argument. - return f"[{choices_str}]" + return "[{choices}]".format(choices=choices_str) # noqa: UP032 def get_missing_message(self, param: Parameter, ctx: Context | None) -> str: """ @@ -372,7 +380,7 @@ def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str: ).format(value=value, choice=choices_str, choices=choices_str) def __repr__(self) -> str: - return f"Choice({list(self.choices)})" + return "Choice({choices})".format(choices=list(self.choices)) # noqa: UP032 def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -434,7 +442,7 @@ def to_info_dict(self) -> dict[str, t.Any]: return info_dict def get_metavar(self, param: Parameter, ctx: Context) -> str | None: - return f"[{'|'.join(self.formats)}]" + return "[{formats}]".format(formats="|".join(self.formats)) # noqa: UP032 def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None: try: @@ -809,7 +817,13 @@ def convert( return f except OSError as e: - self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + self.fail( + "'{filename}': {message}".format( + filename=format_filename(value), message=e.strerror + ), + param, + ctx, + ) # noqa: UP032 def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -1114,7 +1128,8 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: try: if issubclass(ty, ParamType): raise AssertionError( - f"Attempted to use an uninstantiated parameter type ({ty})." + "Attempted to use an uninstantiated parameter " + "type ({type}).".format(type=ty) # noqa: UP032 ) except TypeError: # ty is an instance (correct), so issubclass fails. diff --git a/src/click/utils.py b/src/click/utils.py index ab2fe5889..459f8eeee 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -330,7 +338,7 @@ def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryI """ opener = binary_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError("Unknown standard stream '{name}'".format(name=name)) # noqa: UP032 return opener() @@ -351,7 +359,7 @@ def get_text_stream( """ opener = text_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError("Unknown standard stream '{name}'".format(name=name)) # noqa: UP032 return opener(encoding, errors) From 0bbb222b83f535b67d4c9ec2a569ec2b446ae497 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Wed, 30 Apr 2025 10:23:16 +0900 Subject: [PATCH 2/5] Replace format for f-strings Babel does not support f-strings, which prevents from localising a few strings in the code, like "Usage: " and "Try". The changes in this commit rewrites f-strings into the format syntax. Please note some f-strings remain as they are not expected to be translatable. This commit has to deactivate the UP032 rule on a few files. There seems to be an unreported bug in either Ruff or pyupgrade: The UP032 rule is not deactivated on multi-line commands, including multi-line strings. --- src/click/_termui_impl.py | 8 +++- src/click/_winconsole.py | 4 +- src/click/core.py | 90 +++++++++++++++++++++++------------ src/click/decorators.py | 30 ++++++++---- src/click/formatting.py | 2 +- src/click/parser.py | 28 ++++++++--- src/click/shell_completion.py | 16 ++++++- src/click/termui.py | 4 +- src/click/types.py | 20 ++++++-- src/click/utils.py | 13 ++++- 10 files changed, 157 insertions(+), 58 deletions(-) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index bbf2386cc..59567c627 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -174,7 +174,13 @@ def format_eta(self) -> str: hours = t % 24 t //= 24 if t > 0: - return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + return "{d}{day_label} {h:02}:{m:02}:{s:02}".format( + d=t, + day_label=_("d"), + h=hours, + m=minutes, + s=seconds, + ) else: return f"{hours:02}:{minutes:02}:{seconds:02}" return "" diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index b01035b29..9b062f9dc 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -151,7 +151,7 @@ def readinto(self, b: Buffer) -> int: # wait for KeyboardInterrupt time.sleep(0.1) if not rv: - raise OSError(f"Windows error: {GetLastError()}") + raise OSError("Windows error: {error}".format(error=GetLastError())) # noqa: UP032 if buffer[0] == EOF: return 0 @@ -168,7 +168,7 @@ def _get_error_message(errno: int) -> str: return "ERROR_SUCCESS" elif errno == ERROR_NOT_ENOUGH_MEMORY: return "ERROR_NOT_ENOUGH_MEMORY" - return f"Windows error {errno}" + return "Windows error {errno}".format(errno=errno) # noqa: UP032 def write(self, b: Buffer) -> int: bytes_to_be_written = len(b) diff --git a/src/click/core.py b/src/click/core.py index fe77cb985..b5a7ca6c9 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -77,13 +85,17 @@ def _check_nested_chain( if register: message = ( - f"It is not possible to add the group {cmd_name!r} to another" - f" group {base_command.name!r} that is in chain mode." + "It is not possible to add the group {cmd_name!r} to another" + " group {base_cmd_name!r} that is in chain mode.".format( + cmd_name=cmd_name, base_cmd_name=base_command.name + ) # noqa: UP032 ) else: message = ( - f"Found the group {cmd_name!r} as subcommand to another group " - f" {base_command.name!r} that is in chain mode. This is not supported." + "Found the group {cmd_name!r} as subcommand to another group " + " {base_cmd_name!r} that is in chain mode. This is not supported.".format( + cmd_name=cmd_name, base_cmd_name=base_command.name + ) # noqa: UP032 ) raise RuntimeError(message) @@ -986,8 +998,10 @@ def get_params(self, ctx: Context) -> list[Parameter]: for duplicate_opt in duplicate_opts: warnings.warn( ( - f"The parameter {duplicate_opt} is used more than once. " - "Remove its duplicate as parameters should be unique." + _( + "The parameter {param} is used more than once. " + "Remove its duplicate as parameters should be unique." + ).format(param=duplicate_opt) ), stacklevel=3, ) @@ -1077,9 +1091,9 @@ def get_short_help_str(self, limit: int = 45) -> str: if self.deprecated: deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" + _("(DEPRECATED: {target})".format(target=self.deprecated)) if isinstance(self.deprecated, str) - else "(DEPRECATED)" + else _("(DEPRECATED)") ) text = _("{text} {deprecated_message}").format( text=text, deprecated_message=deprecated_message @@ -1114,9 +1128,9 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: if self.deprecated: deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" + _("(DEPRECATED: {target})".format(target=self.deprecated)) if isinstance(self.deprecated, str) - else "(DEPRECATED)" + else _("(DEPRECATED)") ) text = _("{text} {deprecated_message}").format( text=text, deprecated_message=deprecated_message @@ -1218,8 +1232,7 @@ def invoke(self, ctx: Context) -> t.Any: f" {self.deprecated}" if isinstance(self.deprecated, str) else "" ) message = _( - "DeprecationWarning: The command {name!r} is deprecated." - "{extra_message}" + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" ).format(name=self.name, extra_message=extra_message) echo(style(message, fg="red"), err=True) @@ -2124,8 +2137,10 @@ def __init__( if __debug__: if self.type.is_composite and nargs != self.type.arity: raise ValueError( - f"'nargs' must be {self.type.arity} (or None) for" - f" type {self.type!r}, but it was {nargs}." + "'nargs' must be {arity} (or None) for" + " type {type!r}, but it was {nargs}.".format( + arity=self.type.arity, type=self.type, nargs=nargs + ) ) # Skip no default or callable default. @@ -2159,14 +2174,21 @@ def __init__( if nargs > 1 and len(check_default) != nargs: subject = "item length" if multiple else "length" raise ValueError( - f"'default' {subject} must match nargs={nargs}." + _("'default' {subject} must match nargs={nargs}.").format( + subject=subject, nargs=nargs + ) ) if required and deprecated: raise ValueError( - f"The {self.param_type_name} '{self.human_readable_name}' " - "is deprecated and still required. A deprecated " - f"{self.param_type_name} cannot be required." + _( + "The {type_name} '{readable_name}' " + "is deprecated and still required. A deprecated " + "{type_name} cannot be required." + ).format( + type_name=self.param_type_name, + readable_name=self.human_readable_name, + ) ) def to_info_dict(self) -> dict[str, t.Any]: @@ -2572,9 +2594,9 @@ def __init__( if deprecated: deprecated_message = ( - f"(DEPRECATED: {deprecated})" + _("(DEPRECATED: {target})".format(target=deprecated)) if isinstance(deprecated, str) - else "(DEPRECATED)" + else _("(DEPRECATED)") ) help = help + deprecated_message if help is not None else deprecated_message @@ -2677,7 +2699,7 @@ def to_info_dict(self) -> dict[str, t.Any]: def get_error_hint(self, ctx: Context) -> str: result = super().get_error_hint(ctx) if self.show_envvar: - result += f" (env var: '{self.envvar}')" + result += _(" (env var: '{var}')").format(var=self.envvar) # noqa: UP032 return result def _parse_decls( @@ -2691,7 +2713,7 @@ def _parse_decls( for decl in decls: if decl.isidentifier(): if name is not None: - raise TypeError(f"Name '{name}' defined twice") + raise TypeError(_("Name '{name}' defined twice").format(name=name)) # noqa: UP032 name = decl else: split_char = ";" if decl[:1] == "/" else "/" @@ -2706,8 +2728,10 @@ def _parse_decls( secondary_opts.append(second.lstrip()) if first == second: raise ValueError( - f"Boolean option {decl!r} cannot use the" - " same flag for true/false." + _( + "Boolean option {decl!r} cannot use the" + " same flag for true/false." + ).format(decl=decl) ) else: possible_names.append(_split_opt(decl)) @@ -2723,14 +2747,18 @@ def _parse_decls( if not expose_value: return None, opts, secondary_opts raise TypeError( - f"Could not determine name for option with declarations {decls!r}" + _( + "Could not determine name for option with declarations {decls!r}" + ).format(decls=decls) ) if not opts and not secondary_opts: raise TypeError( - f"No options defined but a name was passed ({name})." - " Did you mean to declare an argument instead? Did" - f" you mean to pass '--{name}'?" + _( + "No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + " you mean to pass '--{name}'?" + ).format(name=name) ) return name, opts, secondary_opts @@ -3096,8 +3124,10 @@ def _parse_decls( name = name.replace("-", "_").lower() else: raise TypeError( - "Arguments take exactly one parameter declaration, got" - f" {len(decls)}: {decls}." + _( + "Arguments take exactly one parameter declaration, got" + " {length}: {decls}." + ).format(length=len(decls), decls=decls) ) return name, [arg], [] diff --git a/src/click/decorators.py b/src/click/decorators.py index 21f4c3422..d6d646593 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import inspect @@ -86,9 +94,9 @@ def new_func(*args: P.args, **kwargs: P.kwargs) -> R: if obj is None: raise RuntimeError( "Managed to invoke callback without a context" - f" object of type {object_type.__name__!r}" - " existing." - ) + " object of type {type!r}" + " existing.".format(type=object_type.__name__) # noqa: UP032 + ) # noqa: UP032 return ctx.invoke(f, obj, *args, **kwargs) @@ -121,11 +129,13 @@ def new_func(*args: P.args, **kwargs: P.kwargs) -> R: return update_wrapper(new_func, f) if doc_description is None: - doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + doc_description = "the {key!r} key from :attr:`click.Context.meta`".format( + key=key + ) # noqa: UP032 decorator.__doc__ = ( - f"Decorator that passes {doc_description} as the first argument" - " to the decorated function." + "Decorator that passes {description} as the first argument" + " to the decorated function.".format(description=doc_description) # noqa: UP032 ) return decorator @@ -498,13 +508,15 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: version = importlib.metadata.version(package_name) except importlib.metadata.PackageNotFoundError: raise RuntimeError( - f"{package_name!r} is not installed. Try passing" - " 'package_name' instead." + "{name!r} is not installed. Try passing" + " 'package_name' instead.".format(name=package_name) # noqa: UP032 ) from None if version is None: raise RuntimeError( - f"Could not determine the version for {package_name!r} automatically." + "Could not determine the version for {name!r} automatically.".format( + name=package_name + ) # noqa: UP032 ) echo( diff --git a/src/click/formatting.py b/src/click/formatting.py index a6e78fe04..4307d8dfd 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -153,7 +153,7 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N ``"Usage: "``. """ if prefix is None: - prefix = f"{_('Usage:')} " + prefix = "{usage} ".format(usage=_("Usage:")) usage_prefix = f"{prefix:>{self.current_indent}}{prog} " text_width = self.width - self.current_indent diff --git a/src/click/parser.py b/src/click/parser.py index a8b7d2634..8a02d7845 100644 --- a/src/click/parser.py +++ b/src/click/parser.py @@ -18,6 +18,14 @@ Copyright 2002-2006 Python Software Foundation. All rights reserved. """ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + # This code uses parts of optparse written by Gregory P. Ward and # maintained by the Python Software Foundation. # Copyright 2001-2006 Gregory P. Ward @@ -142,7 +150,11 @@ def __init__( for opt in opts: prefix, value = _split_opt(opt) if not prefix: - raise ValueError(f"Invalid start character for option ({opt})") + raise ValueError( + _("Invalid start character for option ({option})").format( + option=opt + ) + ) # noqa: UP032 self.prefixes.add(prefix[0]) if len(prefix) == 1 and len(value) == 1: self._short_opts.append(opt) @@ -175,7 +187,7 @@ def process(self, value: t.Any, state: _ParsingState) -> None: elif self.action == "count": state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore else: - raise ValueError(f"unknown action '{self.action}'") + raise ValueError(_("unknown action '{action}'").format(action=self.action)) # noqa: UP032 state.order.append(self.obj) @@ -511,8 +523,10 @@ def __getattr__(name: str) -> object: "ParsingState", }: warnings.warn( - f"'parser.{name}' is deprecated and will be removed in Click 9.0." - " The old parser is available in 'optparse'.", + _( + "'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'." + ).format(name=name), # noqa: UP032 DeprecationWarning, stacklevel=2, ) @@ -522,8 +536,10 @@ def __getattr__(name: str) -> object: from .shell_completion import split_arg_string warnings.warn( - "Importing 'parser.split_arg_string' is deprecated, it will only be" - " available in 'shell_completion' in Click 9.0.", + _( + "Importing 'parser.split_arg_string' is deprecated, it will only be" + " available in 'shell_completion' in Click 9.0." + ), DeprecationWarning, stacklevel=2, ) diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index cdb58222c..890bef29e 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -373,7 +381,9 @@ def get_completion_args(self) -> tuple[list[str], str]: return args, incomplete def format_completion(self, item: CompletionItem) -> str: - return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" + return "{type}\n{value}\n{help}".format( + type=item.type, value=item.value, help=item.help if item.help else "_" + ) class FishComplete(ShellComplete): @@ -396,7 +406,9 @@ def get_completion_args(self) -> tuple[list[str], str]: def format_completion(self, item: CompletionItem) -> str: if item.help: - return f"{item.type},{item.value}\t{item.help}" + return "{type},{value}\t{help}".format( + type=item.type, value=item.value, help=item.help + ) # noqa: UP032 return f"{item.type},{item.value}" diff --git a/src/click/termui.py b/src/click/termui.py index dcbb22216..a2d9dff9e 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -608,13 +608,13 @@ def style( try: bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError(f"Unknown color {fg!r}") from None + raise TypeError(_("Unknown color {colour!r}").format(colour=fg)) from None # noqa: UP032 if bg: try: bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError(f"Unknown color {bg!r}") from None + raise TypeError(_("Unknown color {colour!r}").format(colour=bg)) from None # noqa: UP032 if bold is not None: bits.append(f"\033[{1 if bold else 22}m") diff --git a/src/click/types.py b/src/click/types.py index 23da68d39..07b7995ca 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -372,7 +380,7 @@ def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str: ).format(value=value, choice=choices_str, choices=choices_str) def __repr__(self) -> str: - return f"Choice({list(self.choices)})" + return _("Choice({choices})").format(choices=list(self.choices)) # noqa: UP032 def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -809,7 +817,11 @@ def convert( return f except OSError as e: - self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + self.fail( + f"'{format_filename(value)}': {e.strerror}", + param, + ctx, + ) def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -1114,7 +1126,9 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: try: if issubclass(ty, ParamType): raise AssertionError( - f"Attempted to use an uninstantiated parameter type ({ty})." + _( + "Attempted to use an uninstantiated parameter " "type ({type})." + ).format(type=ty) # noqa: UP032 ) except TypeError: # ty is an instance (correct), so issubclass fails. diff --git a/src/click/utils.py b/src/click/utils.py index ab2fe5889..356fb6a5e 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -1,3 +1,11 @@ +# Ask Ruff to accept the format method on strings, and not let pyupgrade +# always force f-strings. The latter are unfortunately not supported yet +# by Babel, a localisation library. +# +# Note: Using `# noqa: UP032` on lines has not worked, so a file +# setting. +# ruff: noqa: UP032 + from __future__ import annotations import collections.abc as cabc @@ -6,6 +14,7 @@ import sys import typing as t from functools import update_wrapper +from gettext import gettext as _ from types import ModuleType from types import TracebackType @@ -330,7 +339,7 @@ def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryI """ opener = binary_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) # noqa: UP032 return opener() @@ -351,7 +360,7 @@ def get_text_stream( """ opener = text_streams.get(name) if opener is None: - raise TypeError(f"Unknown standard stream '{name}'") + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) # noqa: UP032 return opener(encoding, errors) From 4768c980af04ff9f65e77ee8a68255597be0feca Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Fri, 2 May 2025 15:12:21 +0900 Subject: [PATCH 3/5] Add missing localisation wrappers Please note Windows may requires extra configuration (as it may not set variables expected by gettext). Click does not perform the extra for some reason, and deemed out of scope. --- src/click/_winconsole.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index 9b062f9dc..d38243379 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -28,6 +28,7 @@ from ctypes.wintypes import HANDLE from ctypes.wintypes import LPCWSTR from ctypes.wintypes import LPWSTR +from gettext import gettext as _ from ._compat import _NonClosingTextIOWrapper @@ -151,7 +152,7 @@ def readinto(self, b: Buffer) -> int: # wait for KeyboardInterrupt time.sleep(0.1) if not rv: - raise OSError("Windows error: {error}".format(error=GetLastError())) # noqa: UP032 + raise OSError(_("Windows error: {error}").format(error=GetLastError())) # noqa: UP032 if buffer[0] == EOF: return 0 @@ -168,7 +169,7 @@ def _get_error_message(errno: int) -> str: return "ERROR_SUCCESS" elif errno == ERROR_NOT_ENOUGH_MEMORY: return "ERROR_NOT_ENOUGH_MEMORY" - return "Windows error {errno}".format(errno=errno) # noqa: UP032 + return _("Windows error {errno}").format(errno=errno) # noqa: UP032 def write(self, b: Buffer) -> int: bytes_to_be_written = len(b) From 74d779597fe7ed45b135a4bd099d6c944461b263 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Fri, 2 May 2025 16:40:02 +0900 Subject: [PATCH 4/5] Restore more f-strings that do not need localisation Some f-strings changed to the format method in earlier commits do not need localisation. This commit restores them to reduce the amount of changes. --- src/click/core.py | 24 ++++++++++-------------- src/click/decorators.py | 24 ++++++++++++++---------- src/click/formatting.py | 24 ++++-------------------- src/click/shell_completion.py | 16 ++-------------- src/click/types.py | 6 +++--- 5 files changed, 33 insertions(+), 61 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index b5a7ca6c9..5f9552e25 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -84,19 +84,15 @@ def _check_nested_chain( return if register: - message = ( + message = _( "It is not possible to add the group {cmd_name!r} to another" - " group {base_cmd_name!r} that is in chain mode.".format( - cmd_name=cmd_name, base_cmd_name=base_command.name - ) # noqa: UP032 - ) + " group {base_cmd_name!r} that is in chain mode." + ).format(cmd_name=cmd_name, base_cmd_name=base_command.name) # noqa: UP032 else: - message = ( + message = _( "Found the group {cmd_name!r} as subcommand to another group " - " {base_cmd_name!r} that is in chain mode. This is not supported.".format( - cmd_name=cmd_name, base_cmd_name=base_command.name - ) # noqa: UP032 - ) + " {base_cmd_name!r} that is in chain mode. This is not supported." + ).format(cmd_name=cmd_name, base_cmd_name=base_command.name) # noqa: UP032 raise RuntimeError(message) @@ -2137,10 +2133,10 @@ def __init__( if __debug__: if self.type.is_composite and nargs != self.type.arity: raise ValueError( - "'nargs' must be {arity} (or None) for" - " type {type!r}, but it was {nargs}.".format( - arity=self.type.arity, type=self.type, nargs=nargs - ) + _( + "'nargs' must be {arity} (or None) for" + " type {type!r}, but it was {nargs}." + ).format(arity=self.type.arity, type=self.type, nargs=nargs) ) # Skip no default or callable default. diff --git a/src/click/decorators.py b/src/click/decorators.py index d6d646593..a258d4a4f 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -93,9 +93,11 @@ def new_func(*args: P.args, **kwargs: P.kwargs) -> R: if obj is None: raise RuntimeError( - "Managed to invoke callback without a context" - " object of type {type!r}" - " existing.".format(type=object_type.__name__) # noqa: UP032 + _( + "Managed to invoke callback without a context" + " object of type {type!r}" + " existing." + ).format(type=object_type.__name__) # noqa: UP032 ) # noqa: UP032 return ctx.invoke(f, obj, *args, **kwargs) @@ -129,14 +131,14 @@ def new_func(*args: P.args, **kwargs: P.kwargs) -> R: return update_wrapper(new_func, f) if doc_description is None: - doc_description = "the {key!r} key from :attr:`click.Context.meta`".format( + doc_description = _("the {key!r} key from :attr:`click.Context.meta`").format( key=key ) # noqa: UP032 - decorator.__doc__ = ( + decorator.__doc__ = _( "Decorator that passes {description} as the first argument" - " to the decorated function.".format(description=doc_description) # noqa: UP032 - ) + " to the decorated function." + ).format(description=doc_description) # noqa: UP032 return decorator @@ -508,13 +510,15 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: version = importlib.metadata.version(package_name) except importlib.metadata.PackageNotFoundError: raise RuntimeError( - "{name!r} is not installed. Try passing" - " 'package_name' instead.".format(name=package_name) # noqa: UP032 + _( + "{name!r} is not installed. Try passing" + " 'package_name' instead." + ).format(name=package_name) # noqa: UP032 ) from None if version is None: raise RuntimeError( - "Could not determine the version for {name!r} automatically.".format( + _("Could not determine the version for {name!r} automatically.").format( name=package_name ) # noqa: UP032 ) diff --git a/src/click/formatting.py b/src/click/formatting.py index f9b09c180..4307d8dfd 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -155,9 +155,7 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N if prefix is None: prefix = "{usage} ".format(usage=_("Usage:")) - usage_prefix = "{prefix:>{indent}}{prog} ".format( - prefix=prefix, indent=self.current_indent, prog=prog - ) + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " text_width = self.width - self.current_indent if text_width >= (term_len(usage_prefix) + 20): @@ -186,11 +184,7 @@ def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> N def write_heading(self, heading: str) -> None: """Writes a heading into the buffer.""" - self.write( - "{prefix:>{indent}}{message}:\n".format( - prefix="", indent=self.current_indent, message=heading - ) - ) + self.write(f"{'':>{self.current_indent}}{heading}:\n") def write_paragraph(self) -> None: """Writes a paragraph into the buffer.""" @@ -235,11 +229,7 @@ def write_dl( first_col = min(widths[0], col_max) + col_spacing for first, second in iter_rows(rows, len(widths)): - self.write( - "{prefix:>{indent}}{message}".format( - prefix="", indent=self.current_indent, message=first - ) - ) + self.write(f"{'':>{self.current_indent}}{first}") if not second: self.write("\n") continue @@ -257,13 +247,7 @@ def write_dl( self.write(f"{lines[0]}\n") for line in lines[1:]: - self.write( - "{prefix:>{indent}}{message}\n".format( - prefix="", - indent=first_col + self.current_indent, - message=line, - ) - ) + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") else: self.write("\n") diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index 890bef29e..cdb58222c 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -1,11 +1,3 @@ -# Ask Ruff to accept the format method on strings, and not let pyupgrade -# always force f-strings. The latter are unfortunately not supported yet -# by Babel, a localisation library. -# -# Note: Using `# noqa: UP032` on lines has not worked, so a file -# setting. -# ruff: noqa: UP032 - from __future__ import annotations import collections.abc as cabc @@ -381,9 +373,7 @@ def get_completion_args(self) -> tuple[list[str], str]: return args, incomplete def format_completion(self, item: CompletionItem) -> str: - return "{type}\n{value}\n{help}".format( - type=item.type, value=item.value, help=item.help if item.help else "_" - ) + return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" class FishComplete(ShellComplete): @@ -406,9 +396,7 @@ def get_completion_args(self) -> tuple[list[str], str]: def format_completion(self, item: CompletionItem) -> str: if item.help: - return "{type},{value}\t{help}".format( - type=item.type, value=item.value, help=item.help - ) # noqa: UP032 + return f"{item.type},{item.value}\t{item.help}" return f"{item.type},{item.value}" diff --git a/src/click/types.py b/src/click/types.py index 50ba1bd83..07b7995ca 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -326,10 +326,10 @@ def get_metavar(self, param: Parameter, ctx: Context) -> str | None: # Use curly braces to indicate a required argument. if param.required and param.param_type_name == "argument": - return "{{{choices}}}".format(choices=choices_str) # noqa: UP032 + return f"{{{choices_str}}}" # Use square braces to indicate an option or optional argument. - return "[{choices}]".format(choices=choices_str) # noqa: UP032 + return f"[{choices_str}]" def get_missing_message(self, param: Parameter, ctx: Context | None) -> str: """ @@ -442,7 +442,7 @@ def to_info_dict(self) -> dict[str, t.Any]: return info_dict def get_metavar(self, param: Parameter, ctx: Context) -> str | None: - return "[{formats}]".format(formats="|".join(self.formats)) # noqa: UP032 + return f"[{'|'.join(self.formats)}]" def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None: try: From caf571b9db2bfd885471ee4fd5bdedadefc212bd Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Mon, 12 May 2025 11:29:25 +0900 Subject: [PATCH 5/5] Fix formatting based on palllets/click main --- src/click/decorators.py | 3 +-- src/click/types.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/click/decorators.py b/src/click/decorators.py index a258d4a4f..3b00e6300 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -511,8 +511,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: except importlib.metadata.PackageNotFoundError: raise RuntimeError( _( - "{name!r} is not installed. Try passing" - " 'package_name' instead." + "{name!r} is not installed. Try passing 'package_name' instead." ).format(name=package_name) # noqa: UP032 ) from None diff --git a/src/click/types.py b/src/click/types.py index b223a5342..a31729548 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -1129,7 +1129,7 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: if issubclass(ty, ParamType): raise AssertionError( _( - "Attempted to use an uninstantiated parameter " "type ({type})." + "Attempted to use an uninstantiated parameter type ({type})." ).format(type=ty) # noqa: UP032 ) except TypeError: