-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Automatic code formatting #5387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Hi Pekka, with this you are only looking to format Python Code ? What about the robot code that we use for atesting in the repo ? |
Yeah, this is only for Python code. |
My biggest annoyance with Black was that it removed the empty line after the class declaration in cases like this: class Example:
def __init__(self, arg):
... Black has recently stopped doing that, but apparently Ruff hasn't. That means we'll use Black and possibly migrate to Ruff once its behavior changes in this regard. |
I've been going through changes by Black and for most parts they are fine. There are, however, cases where I don't like the results too much. I'll go through the most important cases here as separate comments and also explain how I plan to handle them. In the end I plan to explain these in our contribution guidelines as well. Feel free to comment if you have other ideas how to handle these cases. You can also just use 👍 to indicate that you agree with my reasoning and 👎 if you think I should just go with what Black does by default. |
Problem 1: Three different ways to format signaturesHow Black formats function signatures depends on the signature length:
The first and the last formats look good to me. I like simple signatures using just a single line, and also like how all arguments of a long signature gets their own lines. I don't, however, understand the middle formatting at all. It's hard to read with all those argument names, types and default values, and the signature is also reformatted, creating a large diff, immediately if it gets a bit longer. Having three formatting approach, possibly even in subsequent functions, also feels pretty inconsistent to me. Unfortunately there isn't, AFAIK, a way to tell Black to automatically avoid the middle formatting. That can, however, be done manually by adding a comma after the last argument. The nice thing is that you can add a trailing comma to a single line signature and Black will automatically explode the signature when it is run again. Needing to add the comma manually is annoying nevertheless. |
Problem 2: Inline commend handlingI don't like Black formatting inline comments used like TypeHint = Union[
type, # Actual type.
str, # Type name or alias.
UnionType, # Union syntax (e.g. `int | float`).
'tuple[TypeHint, ...]' # Tuple of type hints. Behaves like a union.
] to TypeHint = Union[
type, # Actual type.
str, # Type name or alias.
UnionType, # Union syntax (e.g. `int | float`).
'tuple[TypeHint, ...]', # Tuple of type hints. Behaves like a union.
] I really think Black should be clever enough to see that comments have been aligned, figure out that there's probably a reason for that, and leave them alone. Anyway, it doesn't do that so we need alternatives. This is what I've done:
|
Problem 3: Exploding of importsWhen importing multiple items from a module, the import often gets so long that the line needs to be split. This is a slightly simplified example from src/robot/model/body.py: from typing import (Callable, cast, Generic, Iterable, Type, TYPE_CHECKING,
TypeVar, Union)
if TYPE_CHECKING:
from .control import (Break, Continue, Error, For, ForIteration, Group, If,
IfBranch, Return, Try, TryBranch, Var, While, WhileIteration) I really don't like how Black explodes these imports and produces this: from typing import (
Callable,
cast,
Generic,
Iterable,
Type,
TYPE_CHECKING,
TypeVar,
Union,
)
if TYPE_CHECKING:
from .control import (
Break,
Continue,
Error,
For,
ForIteration,
Group,
If,
IfBranch,
Return,
Try,
TryBranch,
Var,
While,
WhileIteration,
) I don't mind the above format too much in an There have been requests to change Black's behavior or to make it configurable (see e.g. this issue), but it's unlikely this changes. One alternative to handle this, as discussed in the aforementioned issue, is using isort that would also sort imports for us automatically. Unlike Black, isort is very configurable and we could configure it to produce, for example this output: from typing import (
Callable, Generic, Iterable, Type, TYPE_CHECKING, TypeVar, Union, cast
)
if TYPE_CHECKING:
from .control import (
Break, Continue, Error, For, ForIteration, Group, If,
IfBranch, Return, Try, TryBranch, Var, While, WhileIteration
) A problem with this is that there's at least one case where I actually prefer each imported items to be on their own lines. There's a convention to specify public module/package using redundant import aliases like in src/robot/api/parsing.py: from robot.parsing import (
get_tokens as get_tokens,
get_resource_tokens as get_resource_tokens,
get_init_tokens as get_init_tokens,
get_model as get_model,
get_resource_model as get_resource_model,
get_init_model as get_init_model,
Token as Token,
) In this case combining multiple items to the same row creates unreadable results: from robot.parsing import (
Token as Token, get_init_model as get_init_model,
get_init_tokens as get_init_tokens, get_model as get_model,
get_resource_model as get_resource_model,
get_resource_tokens as get_resource_tokens, get_tokens as get_tokens
) I guess what I really wanted was the one-item-per-line mode with import aliases using
UPDATE: It turned the desired import formatting can be accomplished like this:
Isort supports multiple ways how to organize multi line imports. In our case Ruff will use the Vertical Hanging Indent mode and isort is configured to use the Hanging Grid Grouped mode. See configuration in |
Problem 4: Handling generic classes with multiple parameters
class Branches(BaseBranches['Keyword', 'For', 'While', 'Group', 'If', 'Try', 'Var',
'Return', 'Continue', 'Break', 'Message', 'Error', IT]):
__slots__ = () When the above is formatted with Black, the result is this: class Branches(
BaseBranches[
'Keyword',
'For',
'While',
'Group',
'If',
'Try',
'Var',
'Return',
'Continue',
'Break',
'Message',
'Error',
IT,
]
):
__slots__ = () I understand why it's formatted like that, but the result is still rather odd. It doesn't even look like a class declaration and it also takes a lot of vertical space. My current thinking is keeping the current formatting and using class Branches(BaseBranches['Keyword', 'For', 'While', 'Group', 'If', 'Try',
'Var', 'Return', 'Continue', 'Break', 'Message',
'Error', IT]): # fmt: skip
__slots__ = () UPDATE: In the end decided to use this format: class Branches(BaseBranches[
"Keyword", "For", "While", "Group", "If", "Try", "Var", "Return", "Continue",
"Break", "Message", "Error", IT
]): # fmt: skip
__slots__ = () The motivation was to have no items after the final opening parentheses, because then items always start with consistent indentation regardless of the length of the class names. This is looks consistent with rest of the Black formatted code. I decided to use the same format also in other cases where lists were long but individual list items were simple. For example, instead of added_in_rf60 = {
"bg",
"bs",
"cs",
"de",
"en",
"es",
"fi",
"fr",
"hi",
"it",
"nl",
"pl",
"pt",
"pt-BR",
"ro",
"ru",
"sv",
"th",
"tr",
"uk",
"zh-CN",
"zh-TW",
} I decided to use added_in_rf60 = {
"bg", "bs", "cs", "de", "en", "es", "fi", "fr", "hi", "it", "nl", "pl",
"pt", "pt-BR", "ro", "ru", "sv", "th", "tr", "uk", "zh-CN", "zh-TW",
} # fmt: skip |
Problem 5: Handling Boolean expressionsBlack avoids parentheses around Boolean expressions and as the result formats code like ext = (
getattr(self.parser, 'EXTENSION', None)
or getattr(self.parser, 'extension', None)
) and return (
self._get_runner_from_resource_files(name)
or self._get_runner_from_libraries(name)
) to ext = getattr(self.parser, 'EXTENSION', None) or getattr(
self.parser, 'extension', None
) and return self._get_runner_from_resource_files(
name
) or self._get_runner_from_libraries(name) I consider the formatting in the last two examples horrible. There's an issue about changing this in Black's issue tracker, but no indication that the behavior would be changed. Luckily this doesn't happen too often and it's easy to use |
I've now locally gone through all formatting changes by Black and handled annoyances either by refactoring code or by using
|
Formatting changes committed in d2cdcfa. |
Things to do:
|
GitHub will ignore commits in this file by defaul and Git can be configured to ignore them locally as well. Initially contains the code formatting commit done as part of #5387. More commits, also past ones, can be added later if needed.
We should take automatic code formatting into use. I don't personally need it that much, but it would certainly make it easier for contributors to format their code and that saves my time in reviewing PRs.
I've experimented with Black already years ago, but back then it did some changes that I really disliked and that caused huge amount of noise in history. The situation is nowadays better in that regard, and the amount of unnecessary (in my opinion) and annoying (again, my opinion) changes is pretty small. The Ruff formatter is also so fast that formatting is instantaneous even with our relatively big project. Overall, auto-formatting benefits are nowadays bigger than problems even in my opinion.
Adding auto-formatting to an existing projects basically has two parts:
This issue concentrates fully on the first part. My plan is basically this:
ruff.toml
and commit it to the project root.ruff format
.ruff format
before creating a PR.Further automation can then be done later.
The text was updated successfully, but these errors were encountered: