From efb695e240f9bb3d5376e6dc9eb5dac58f2d62b8 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Wed, 30 Apr 2025 10:23:16 +0900 Subject: [PATCH 01/13] 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 b01035b299..9b062f9dca 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 fe77cb9851..4f1a38c2a1 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 a6e78fe04a..f9b09c180c 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 a8b7d2634a..20fa33eefd 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 cdb58222c5..890bef29e6 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 dcbb22216c..ece09f91bf 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 23da68d396..a996b082ae 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 ab2fe5889c..459f8eeeef 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 02/13] 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 bbf2386cc4..59567c6279 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 b01035b299..9b062f9dca 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 fe77cb9851..b5a7ca6c95 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 21f4c34224..d6d6465930 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 a6e78fe04a..4307d8dfd1 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 a8b7d2634a..8a02d78457 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 cdb58222c5..890bef29e6 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 dcbb22216c..a2d9dff9e9 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 23da68d396..07b7995ca7 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 ab2fe5889c..356fb6a5ee 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 03/13] 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 9b062f9dca..d382433796 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 04/13] 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 b5a7ca6c95..5f9552e258 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 d6d6465930..a258d4a4fd 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 f9b09c180c..4307d8dfd1 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 890bef29e6..cdb58222c5 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 50ba1bd836..07b7995ca7 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 05/13] 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 a258d4a4fd..3b00e6300b 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 b223a5342c..a317295487 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: From 381443141f2389cec292601e50de49b917996d2a Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Mon, 29 Dec 2025 15:46:35 +0900 Subject: [PATCH 06/13] Update src/click/_termui_impl.py Co-authored-by: Carmen Bianca BAKKER --- src/click/_termui_impl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 46915e579e..5fcb49f095 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -176,6 +176,7 @@ def format_eta(self) -> str: if t > 0: return "{d}{day_label} {h:02}:{m:02}:{s:02}".format( d=t, + # TRANSLATORS: The 'd' stands for 'day'. day_label=_("d"), h=hours, m=minutes, From 88437262d6a54a0ecf211aca398ce1fc4e6f1b30 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Mon, 29 Dec 2025 16:06:31 +0900 Subject: [PATCH 07/13] Remove translation on developer strings * After a round of code review from the team. * Possibly incomplete, as based on the author's evaluation of what is for UX and what is for developers. --- src/click/_winconsole.py | 4 ++-- src/click/core.py | 28 ++++++++++------------------ src/click/decorators.py | 35 ++++++++++------------------------- src/click/parser.py | 24 ++++++------------------ src/click/termui.py | 4 ++-- src/click/types.py | 14 ++------------ src/click/utils.py | 12 ++---------- 7 files changed, 34 insertions(+), 87 deletions(-) diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index 7e5a31fe80..d25178d66f 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -153,7 +153,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())) if buffer[0] == EOF: return 0 @@ -170,7 +170,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: {error}").format(error=errno) 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 032e26ad27..ea674abc81 100644 --- a/src/click/core.py +++ b/src/click/core.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 @@ -86,14 +78,14 @@ def _check_nested_chain( if register: 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 + 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." + ) else: 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 + 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." + ) raise RuntimeError(message) @@ -1117,7 +1109,7 @@ def get_short_help_str(self, limit: int = 45) -> str: if self.deprecated: deprecated_message = ( - _("(DEPRECATED: {target})".format(target=self.deprecated)) + _(f"(DEPRECATED: {self.deprecated})") if isinstance(self.deprecated, str) else _("(DEPRECATED)") ) @@ -1154,7 +1146,7 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: if self.deprecated: deprecated_message = ( - _("(DEPRECATED: {target})".format(target=self.deprecated)) + _(f"(DEPRECATED: {self.deprecated})") if isinstance(self.deprecated, str) else _("(DEPRECATED)") ) @@ -2763,7 +2755,7 @@ def __init__( if deprecated: deprecated_message = ( - _("(DEPRECATED: {target})".format(target=deprecated)) + _(f"(DEPRECATED: {deprecated})") if isinstance(deprecated, str) else _("(DEPRECATED)") ) @@ -2919,7 +2911,7 @@ def _parse_decls( for decl in decls: if decl.isidentifier(): if name is not None: - raise TypeError(_("Name '{name}' defined twice").format(name=name)) # noqa: UP032 + raise TypeError(_("Name '{name}' defined twice").format(name=name)) name = decl else: split_char = ";" if decl[:1] == "/" else "/" diff --git a/src/click/decorators.py b/src/click/decorators.py index 3b00e6300b..021ab38169 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.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 inspect @@ -93,12 +85,10 @@ 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 - ) # noqa: UP032 + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) return ctx.invoke(f, obj, *args, **kwargs) @@ -131,14 +121,12 @@ 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( - key=key - ) # noqa: UP032 + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" decorator.__doc__ = _( - "Decorator that passes {description} as the first argument" + f"Decorator that passes {doc_description} as the first argument" " to the decorated function." - ).format(description=doc_description) # noqa: UP032 + ) return decorator @@ -510,16 +498,13 @@ 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 + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." ) from None if version is None: raise RuntimeError( - _("Could not determine the version for {name!r} automatically.").format( - name=package_name - ) # noqa: UP032 + f"Could not determine the version for {package_name!r} automatically." ) echo( diff --git a/src/click/parser.py b/src/click/parser.py index 568219784b..d9a764950c 100644 --- a/src/click/parser.py +++ b/src/click/parser.py @@ -18,14 +18,6 @@ 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 @@ -153,7 +145,7 @@ def __init__( _("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) @@ -186,7 +178,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(_("unknown action '{action}'").format(action=self.action)) # noqa: UP032 + raise ValueError(f"unknown action '{self.action}'") state.order.append(self.obj) @@ -523,10 +515,8 @@ def __getattr__(name: str) -> object: "ParsingState", }: warnings.warn( - _( - "'parser.{name}' is deprecated and will be removed in Click 9.0." - " The old parser is available in 'optparse'." - ).format(name=name), # noqa: UP032 + f"'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'.", DeprecationWarning, stacklevel=2, ) @@ -536,10 +526,8 @@ 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/termui.py b/src/click/termui.py index 5120aedbe4..2cd1d31bab 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -614,13 +614,13 @@ def style( try: bits.append(f"\033[{_interpret_color(fg)}m") except KeyError: - raise TypeError(_("Unknown color {colour!r}").format(colour=fg)) from None # noqa: UP032 + raise TypeError(_("Unknown color {colour!r}").format(colour=fg)) from None if bg: try: bits.append(f"\033[{_interpret_color(bg, 10)}m") except KeyError: - raise TypeError(_("Unknown color {colour!r}").format(colour=bg)) from None # noqa: UP032 + raise TypeError(_("Unknown color {colour!r}").format(colour=bg)) from None 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 6c4ff808da..bac5982344 100644 --- a/src/click/types.py +++ b/src/click/types.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 @@ -380,7 +372,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 _("Choice({choices})").format(choices=list(self.choices)) # noqa: UP032 + return _("Choice({choices})").format(choices=list(self.choices)) def shell_complete( self, ctx: Context, param: Parameter, incomplete: str @@ -1172,9 +1164,7 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: try: if issubclass(ty, ParamType): raise AssertionError( - _( - "Attempted to use an uninstantiated parameter type ({type})." - ).format(type=ty) # noqa: UP032 + f"Attempted to use an uninstantiated parameter type ({ty})." ) except TypeError: # ty is an instance (correct), so issubclass fails. diff --git a/src/click/utils.py b/src/click/utils.py index 15eccbdad8..a8076a6635 100644 --- a/src/click/utils.py +++ b/src/click/utils.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 @@ -339,7 +331,7 @@ def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryI """ opener = binary_streams.get(name) if opener is None: - raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) # noqa: UP032 + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) return opener() @@ -360,7 +352,7 @@ def get_text_stream( """ opener = text_streams.get(name) if opener is None: - raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) # noqa: UP032 + raise TypeError(_("Unknown standard stream '{name}'").format(name=name)) return opener(encoding, errors) From 68a052f64a7050c0ffaf23184f045a36cf7b6bfa Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Mon, 29 Dec 2025 16:41:39 +0900 Subject: [PATCH 08/13] Fixes on self-CR --- src/click/core.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index ea674abc81..a21b801b59 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -77,12 +77,12 @@ def _check_nested_chain( return if register: - message = _( + 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." ) else: - message = _( + 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." ) @@ -1016,10 +1016,8 @@ def get_params(self, ctx: Context) -> list[Parameter]: for duplicate_opt in duplicate_opts: warnings.warn( ( - _( - "The parameter {param} is used more than once. " - "Remove its duplicate as parameters should be unique." - ).format(param=duplicate_opt) + f"The parameter {duplicate_opt} is used more than once. " + "Remove its duplicate as parameters should be unique." ), stacklevel=3, ) @@ -1109,9 +1107,9 @@ def get_short_help_str(self, limit: int = 45) -> str: if self.deprecated: deprecated_message = ( - _(f"(DEPRECATED: {self.deprecated})") + f"(DEPRECATED: {self.deprecated})" if isinstance(self.deprecated, str) - else _("(DEPRECATED)") + else "(DEPRECATED)" ) text = _("{text} {deprecated_message}").format( text=text, deprecated_message=deprecated_message @@ -1146,9 +1144,9 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: if self.deprecated: deprecated_message = ( - _(f"(DEPRECATED: {self.deprecated})") + f"(DEPRECATED: {self.deprecated})" if isinstance(self.deprecated, str) - else _("(DEPRECATED)") + else "(DEPRECATED)" ) text = _("{text} {deprecated_message}").format( text=text, deprecated_message=deprecated_message @@ -2179,22 +2177,15 @@ 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) + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." ) if required and deprecated: raise ValueError( - _( - "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, - ) + 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." ) def to_info_dict(self) -> dict[str, t.Any]: @@ -2755,9 +2746,9 @@ def __init__( if deprecated: deprecated_message = ( - _(f"(DEPRECATED: {deprecated})") + f"(DEPRECATED: {deprecated})" if isinstance(deprecated, str) - else _("(DEPRECATED)") + else "(DEPRECATED)" ) help = help + deprecated_message if help is not None else deprecated_message From 18b89189677b4f5fe42394235b817abdf5bfe727 Mon Sep 17 00:00:00 2001 From: Eric Platon Date: Mon, 29 Dec 2025 16:45:23 +0900 Subject: [PATCH 09/13] Fix after self-review --- src/click/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/click/decorators.py b/src/click/decorators.py index 021ab38169..21f4c34224 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -123,7 +123,7 @@ def new_func(*args: P.args, **kwargs: P.kwargs) -> R: if doc_description is None: doc_description = f"the {key!r} key from :attr:`click.Context.meta`" - decorator.__doc__ = _( + decorator.__doc__ = ( f"Decorator that passes {doc_description} as the first argument" " to the decorated function." ) From 7675de427840ce4db16e9059e219b295002cfd36 Mon Sep 17 00:00:00 2001 From: Eric Platon <__ic@tutamail.com> Date: Tue, 5 May 2026 17:27:26 +0900 Subject: [PATCH 10/13] Remove unnecessary comment The code looks clear from the context. --- src/click/_termui_impl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 8cdffaba57..219fbaa1f7 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -175,7 +175,6 @@ def format_eta(self) -> str: if t > 0: return "{d}{day_label} {h:02}:{m:02}:{s:02}".format( d=t, - # TRANSLATORS: The 'd' stands for 'day'. day_label=_("d"), h=hours, m=minutes, From 8274da6a2e6ff4871bf60aa71ac05782f1206c58 Mon Sep 17 00:00:00 2001 From: Eric Platon <__ic@tutamail.com> Date: Tue, 5 May 2026 17:27:45 +0900 Subject: [PATCH 11/13] Localise missing strings After review from core members. --- src/click/core.py | 18 ++++++++---------- src/click/decorators.py | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/click/core.py b/src/click/core.py index 06fcd47838..433bf8d44a 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1136,14 +1136,13 @@ def get_short_help_str(self, limit: int = 45) -> str: text = "" if self.deprecated: + localised_deprectated = _("deprecated").upper() deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" + f"({localised_deprectated}: {self.deprecated})" if isinstance(self.deprecated, str) - else "(DEPRECATED)" - ) - text = _("{text} {deprecated_message}").format( - text=text, deprecated_message=deprecated_message + else f"({localised_deprectated})" ) + text = f"{_(text)} {deprecated_message}" return text.strip() @@ -1173,14 +1172,13 @@ def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: text = "" if self.deprecated: + localised_deprectated = _("deprecated").upper() deprecated_message = ( - f"(DEPRECATED: {self.deprecated})" + f"({localised_deprectated}: {self.deprecated})" if isinstance(self.deprecated, str) - else "(DEPRECATED)" - ) - text = _("{text} {deprecated_message}").format( - text=text, deprecated_message=deprecated_message + else f"({localised_deprectated})" ) + text = f"{_(text)} {deprecated_message}" if text: formatter.write_paragraph() diff --git a/src/click/decorators.py b/src/click/decorators.py index 21f4c34224..14aee42ea4 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -396,8 +396,8 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: kwargs.setdefault("is_flag", True) kwargs.setdefault("callback", callback) kwargs.setdefault("expose_value", False) - kwargs.setdefault("prompt", "Do you want to continue?") - kwargs.setdefault("help", "Confirm the action without prompting.") + kwargs.setdefault("prompt", _("Do you want to continue?")) + kwargs.setdefault("help", _("Confirm the action without prompting.")) return option(*param_decls, **kwargs) From 0b75b01ec37ea591d76a51d50604589c69163a43 Mon Sep 17 00:00:00 2001 From: Eric Platon <__ic@tutamail.com> Date: Tue, 5 May 2026 17:28:00 +0900 Subject: [PATCH 12/13] Local yes/no labels Tentative approach, but looks fine. --- src/click/termui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/click/termui.py b/src/click/termui.py index 08d732895e..cbc0751dbf 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -255,7 +255,7 @@ def confirm( text, prompt_suffix, show_default, - "y/n" if default is None else ("Y/n" if default else "y/N"), + _("y/n") if default is None else (_("Y/n") if default else _("y/N")), ) while True: @@ -263,9 +263,9 @@ def confirm( value = _readline_prompt(visible_prompt_func, prompt, err).lower().strip() except (KeyboardInterrupt, EOFError): raise Abort() from None - if value in ("y", "yes"): + if value in (_("y"), _("yes")): rv = True - elif value in ("n", "no"): + elif value in (_("n"), _("no")): rv = False elif default is not None and value == "": rv = default From 2817d99a18d2d25c2f27b7191f673f0510983e1f Mon Sep 17 00:00:00 2001 From: Eric Platon <__ic@tutamail.com> Date: Fri, 8 May 2026 11:57:40 +0900 Subject: [PATCH 13/13] Revert "Local yes/no labels" This reverts commit 0b75b01ec37ea591d76a51d50604589c69163a43. --- src/click/termui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/click/termui.py b/src/click/termui.py index cbc0751dbf..08d732895e 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -255,7 +255,7 @@ def confirm( text, prompt_suffix, show_default, - _("y/n") if default is None else (_("Y/n") if default else _("y/N")), + "y/n" if default is None else ("Y/n" if default else "y/N"), ) while True: @@ -263,9 +263,9 @@ def confirm( value = _readline_prompt(visible_prompt_func, prompt, err).lower().strip() except (KeyboardInterrupt, EOFError): raise Abort() from None - if value in (_("y"), _("yes")): + if value in ("y", "yes"): rv = True - elif value in (_("n"), _("no")): + elif value in ("n", "no"): rv = False elif default is not None and value == "": rv = default