Reformat codebase to satisfy CI linter check #306
Labels
No labels
Closed As
Duplicate
Closed As
Fixed
Closed As
Invalid
discussions
easyfix
enhancement
task
ai-review-please
Backlog Status
Needs Review
Backlog Status
Ready
chore
documentation
points
01
points
02
points
03
points
05
points
08
points
13
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Sprint Status
Blocked
Sprint Status
Done
Sprint Status
In Progress
Sprint Status
Review
Sprint Status
To Do
Technical Debt
Work Item
Bug
Work Item
Epic
Work Item
Spike
Work Item
Task
Work Item
User Story
No milestone
No project
3 participants
Notifications
Due date
No due date set.
Depends on
#311 Add ruff docs and configuration
quality/blockerbugs
#312 Reformat using ruff
quality/blockerbugs
#317 Fix linter problems
quality/blockerbugs
Reference
quality/blockerbugs#306
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
./run lint: #317I'm interested to compare the options as well.
I'm all for linters and blocking on them (at least when they look for actual coding issues and high-profile warnings, and not cosmetic stuff). We could check ty, which intends to replace mypy.
I don't have much experience with formatters. The idea that you don't need to think about formatting and it does a great job automatically sounds great on paper. But when I sometimes see black output, I don't like it very much. It seem more fit for machine reading than human reading, it's breaks lines very often and creates very long source code. The fact that it is "opinionated" and doesn't allow almost any configuration options doesn't help much. I'd like to have the benefits without feeling abused. I'll play with it more, and compare the output.
There's a blue fork which makes a few changes, especially with quotes.
ruff seems to produce almost exactly the same output as black, but allows more configuration options. For example you can decide whether to use single or double quotes (I find those harder to read), or even preserve the existing style, without reformatting everything. So this might be a bit better, even though will still probably produce too long code. It also has its own integrated version of flake8 and isort, so you don't need those.
Another option seems to be YAPF, which seems to produce more human-readable code, and is very configurable.
Then there is autopep8, which doesn't change the overall layout of the code, just fix PEP 8 violations.
Let's make this a research ticket ("spike") to compare and discuss the options, wdyt? Then we can have a separate ticket for actually implementing the changes. I'd start with linting, that seems less controversial (OTOH, it kinda depends whether we go with all-in-one ruff, or a collection of other tools).
I've been fine with black, but don't have any particular objections to ruff.
I had the same instinctive reaction to black at first - it wraps things too much! - but after a week or two I realized it does actually usually make things more readable for me that way. In the rare cases when I don't like how it wraps things, I just do it a different way manually; black usually accepts this so long as your alternative is 'clean'.
One thing I learned with any kind of formatter is commit your code changes locally first, then run the formatter. Then its changes are separated from your work. If you run it before committing and you don't like what it does, you've got a bit of a mess to unpick because you can't easily distinguish its changes from your work.
I would say that the two most discussed areas are probably these:
Other than that, it would be probably useful to use a tool which allows us to import/export its configuration. We could then create a standardized config containing our agreed values to be included with app source code and enforced by CI.
I admit I am partial to
ruff. I have used it before and never had any significant problem with it. It is very fast. The only area where it might be slightly lacking is maybe type checking and deeper code inspection. So for the CI job we could probably pair it with with a tool likepyrightor maybe rathermypy.Found this page comparing pylint vs ruff speed: https://pythonspeed.com/articles/pylint-flake8-ruff/
@adamwill @kparal What do you think? Would it be OK to continue with
ruff?Sure, as I said, I don't really have any objection. The speed argument has always made very little sense to me since all these things run so fast - 0.1 seconds versus 2 seconds is not a meaningful difference unless you're linting multiple times per minute, which why would you? - but it's not a reason not to pick ruff either.
I still didn't have time to evaluate the output from different formatters 🙁️ I'll try to get to it. But honestly, I would really like if we focused on linting issues only, and discussed (auto-)formatting separately afterwards. The fact that CI can catch linting errors is very useful. But cosmetic stuff like wrapping lines really is not. Some people might enjoy being bossed around by CI for formatting and whitespace "issues", but I guess I'm not one of them. And if the whole codebase is to be reformatted (and therefore making any git blame more difficult), I'd rather be completely sure that I want that before we do it.
Can Ruff be configured to just care about linting and avoid reformatting lines (or complaining about those)?
Yeah, we'll need to decide this before any reformatting takes place. I think somewhere between 100-120 would be reasonable.
Ruff seems to be pretty intelligent with this:
https://docs.astral.sh/ruff/settings/#format_quote-style
I also like it supports
preserveoption.But backticks are no longer used in Python, so there's little confusion to be had?
I personally use single quotes when possible, I can then read it faster. Double quotes are a duplicated character, it distracts me 😀️ Anyway, with ruff this can be non-enforced.
For blockerbugs I fought a lot with figuring out exclusion rules for mypy, because if often didn't work correctly with SQLAlchemy or Flask or whatever. Or maybe I didn't know how to configure it. Have you tried looking at
ty, which is supposed to be better?AI claims "While Ruff has replaced Flake8 for most linting, Pylint remains popular for "deep" analysis. Ruff is fast because it looks for patterns; Pylint is slower because it actually tries to understand the logic."
So it might still be useful to execute
pylintor similar. But again as with the other tools, it seems to be mostly PITA about formatting, drowning actual problems, so configuration would be necessary.In general, I'd like to hear your opinions on whether you consider cosmetic issues (line length, wrapping, indentation, whitespace, etc) important enough to be blocked by CI, and why. I personally care about real issues (uninitialized variable, comparing a string with
is, etc) or soft real issues (unused import, mutable function arguments as defaults, etc). But whether a line is one character over limit, or whether there should be one extra or fewer line between methods, that feels like a waste of time and tool bullying, to me. I would like to understand why it is important to you.What if we configured the current lint tools to ignore cosmetic problems? With
flake8, that can be done extremely quickly, raising signal-to-noise ratio.mypydoesn't produce cosmetic errors, it seems. I'm not sure if somebody can resolve it all quickly, I struggled with it back in the day. But maybe AI can, nowadays. Or consider whether to drop it, if it doesn't work sufficiently well for us (and perhaps replace it with something else in the future). Then we could make the lint step in CI mandatory, and we would be happy (or least happier)? And we can discuss the other stuff (formatting) with lower priority.Personally I like having formatting in CI just because it enforces consistency and removes any need to bikeshed about formatting manually. I don't really find it a waste of time because it takes ten seconds to run the formatter, and I guess I don't feel like it's bullying because I'm not usually particularly attached to however I formatted it manually; the cases where I actually did want it formatted in a 'weird' way, or disagree with how black reformats it, are rare enough that I'm fine just flagging them or reformatting manually.
I second Adam's opnion regarding formatting being mandatory. What one gets used to having all the code formatted the same way, it is much faster to skim through such code because brain already recognizes some patterns subconsciously. With enough training you will even learn to detect some pain points or bugs just by looking at the code without actually "reading" it.
I grew up with C and Java (and ZX Basic before that), so double-quotes as string delimiters are an obvious choice for me, but I understand where Kamil is coming from. Maybe we could make the formatter ambivalent in this regard.
What does this mean?
I've played with Ruff today.
ruff checkis very reasonable, that can be enabled right away. I'm not that fond ofruff format, as could be expected. I used this config:and tried to reformat
blockerbugs/controllers/main.py. It converted original 550 lines into 657 lines.The first thing that caught my eye is that it loves using parentheses everywhere. When there's a long command, it wraps it in them. The downside is that everything starts to look like a tuple. It requires careful reading to determine whether the resulting value is a function/statement output, or a tuple (all it takes is one command hidden somewhere in the middle, and it complete changes what it is).
For example, original:
Ruff output:
Original (the indentation could be better here, but that's not the point):
Ruff output:
The second irking behavior is that for function calls that are longer than one line, it forces a new line for each argument, making the calls outrageously long. When code includes frequent function calls like this one, it requires constant scrolling up and down, you can't fit the important parts on your screen because of this. Furthermore, it promotes bad coding habits. That's because if you omit arg names (
valueinstead offoo=value), the line is shorter, might fit on a single line, and Ruff won't explode it then. But arg names are really nice and useful, they make the code much more readable. So with Ruff, you're motivated to make the code less readable, in order to prevent Ruff to make it also less readable. Ugh... not great?Original:
Ruff output:
Original:
Ruff output:
This also happens for lists that are longer than one line, which is even more terrible, because the original lists are perfectly readable as they are.
Original:
Ruff output:
It affects perfectly readable dicts as well.
Original:
Ruff output:
I also think that the way it handles function definitions makes things less readable, because it keeps the args at the same indent as the main func body. From
blockerbugs/models/update.py, compare this:Original:
Ruff output:
Even this would be an improvement, but still doesn't satisfy Ruff:
None of this is a gamestopper, I could live with it (the exploded function calls probably annoy me the most, especially with the reinforcement of not using arg names). It's also not black or white, in some cases the reformatting makes the code a bit more legible indeed. I'm just not convinced this is an overall improvement. If I understand your responses, the biggest benefit is in unification of everything. Ok, but could we unify into something better? 😬️
I've also tested autopep8. With its default invocation, or with aggressiveness level 1, it mostly just touches whitespace, and behaves very civilized in general. I could imagine we use it by default. With aggressiveness level 2, it explodes lists into one item per line as Ruff does, but it at least doesn't touch function calls. Overall I think that autopep8 with aggressiveness level 0 or 1 could be a good contender.
@kparal All those
ruffexamples you consider "terrible", I actually see as "great" :) OK, maybe with the exception of the parentheses thing, which is just a sneaky way of breaking long expressions. Those multi-line lists or function parameters are actually very useful for review diffing or reordering. It is more difficult to visually compare two lists when they are compressed on one line. You can fold those multi-line lists or arguments in your editor and have a neat one-liners regardless.Again, I am not trying to promote my view as the only solution, just mentioning possible usefulness.
I use a per-word diff (meld, pycharm) which makes the changes easy to see. But yes, in a classic line diff it's harder.
In Pycharm, I only seem to be able to compress the whole function, or a code block, but not a function call arguments. Might work in VS Code. But even if it worked, if I fold it, then I don't see the contents, right? I'd just see e.g.
render_template(...). I wouldn't want that.I played with YAPF as well (I spent the whole day on it today 🙁️ ), I'll post my observations next.
Thanks for spending time on that! Maybe we could also have this discussion at Flock instead of here and now.
With diffing I mean reviews on Forge where it dumps the whole line to diff anyway and then you need to scan the line to find the changed thing (it is highlighted in some way but still). When you have only one thing at the line - well, there is your change.
Guess we could also ask our robot friends what solution would they recommend...
So, YAPF seems to stand somewhere in the middle between Black/Ruff and Autopep8. It touches the code much more than autopep8, but less than ruff. It's also highly configurable. Unfortunately it still seems to follow the paradigm of "reformat everything and spit out the result", instead of considering "is this an acceptable chunk of code?" and reformatting only when needed (that way, you'd have some leeway in your formatting).
For the record, I only tested the default
pep8style in YAPF. It contains several other styles to use. But I think they just configure the defaults differently, so I should be able to influence everything even when starting frompep8.Generally I find YAPF output more readable and closer to the original than Ruff output. For example it doesn't use parentheses to split long lines, like shown above. You can still write your statements directly, add newline after an opening bracket, etc.
I noticed some very weird behavior when dict values contained ternary operators:
Original:
YAPF:
Fortunately this can be fixed by setting
allow_split_before_dict_value=False, then it looks more reasonable:What I'm struggling with is once again function calls. As long as the line is short enough, it keeps the arguments on one line. But one it's longer, it splits then one arg per line.
Original:
YAPF:
This can be remediated by setting
split_before_named_assigns=False. Then it looks like Original. However, you can't then decide somewhere else that you want per-line arguments. It munches everything into the condensed form. So this:Original:
with
split_before_named_assigns=FalseYAPF becomes:That's terrible. I haven't found a way to be able to use both formatting styles in different places, except using the big hammer of
# yapf: disablein some of the places.Another, more minor pain point is with logical expressions. It reformats them into a condensed form, even if for readability purposes you'd want it otherwise.
Original:
YAPF:
There seem to be no configuration options for dealing with logical expressions (there are options for lists, dicts, etc, but not this), so again the only solution seems to be
# yapf: disable.Overall I feel unhappy with YAPF as well, just in a different way than with Ruff. Both of them produce the code alternative of factory-processed meat. The human element is lost in favor of the machine...
Yeah, I usually do my diffs locally, because the web diffs are not great (no idea why they don't use word-based diffs instead). For example if there's just indentation change, you can see it easily in proper tools, but the Forge diff is completely unhelpful.
I'd really prefer not to. I don't care whether a codebase uses single or double quotes but I hate when one uses both. Pick one!
@kparal wrote in #306 (comment):
You can usually tell formatters to ignore specific lines or blocks. I don't know about ruff, but for black see the docs.
FWIW I mostly agree with @jgroman , I am fine with almost all the ruff examples you posted. When I started using black I had the same instinctive reaction as you - it spreads things across too many lines! - but over time I came to the same position as @jgroman , it is usually actually better and more readable that way. I think black's rule is more or less that this is OK if the middle line is short enough:
otherwise it's one line per argument, and I like that. I like trailing braces always being required to be on a separate line.
The indentation thing for function definitions I think is fine. The trailing brace being back one indent acts as a sufficient separator, for me. I prefer that to making the indents arbitrarily different and having the trailing brace on the same line, I just find that style very hard to read since black nudged me to consistently having braces on a new line.
Your ternary case:
is for me a textbook case where I'd take the formatter output as a cue to tweak it a bit manually:
For me, the message I'm getting from the formatter there is "you're doing too much in one place here, slow down, split it out".
I also took some time to get used to it but nowadays I usually just tweak the line length and use whatever defaults are provided by the formatter. They are usually sane enough for me.
Well, there's only logical conclusion here. The AI is already taking over the world, and both of you have been replaced by terminators, who are trying but failing to completely pretend humanity. Help!
For me, the clear winner is autopep8. But since I'm outnumbered and one does not try to negotiate with terminators, I'll try to pretend to be one of you. Glory to the Machine God!
The remaining question is what should the max line length be. What are your
orderssuggestions? If we're going to reformat the whole codebase, I want to do it just once.In my local environment (my monitor, my font size, my IDE), line length of 120 chars is too much. I can't comfortably fit side-by-side diff unless I collapse IDE side bars (which is annoying). Also, docstrings with 120 chars seem quite wide to read. So, my suggestion is to pick either 100 or 110 chars. The docs feel a bit better at 100 chars, the code (particularly when heavily nested) at 110, which also avoids line explosion from ruff. But the differences are minor, most of the code I saw didn't exceed 100 chars anyway, even when re-flown to 110. I don't have a strong opinion between the two.
I would use the same limit for both code and comments/docstrings. While it is possible to have a different limit, it's just a complication with setting multiple gutters in editors, etc.
If we re-flow the whole codebase, I'd like comments and docstrings to be re-flown as well. Some of them are currently wrapped at 80 chars, and this would reduce some of the vertical sprawl, improving the readability. But Ruff doesn't support reformatting those (and some of them are tricky, e.g. including lists, where you want to keep newlines). There's a docformatter which might be able to help (as a one-time operation), but manual tweaking will be necessary. Or, just ask an AI agent, I guess, that should be able to do the just as well. Just make sure to instruct it that there must be no wording changes performed.
I use 100 on all my projects. I have some primordial directive deep in my programming that makes me want to wrap docstrings and comments at 72 or 80 chars, though. I don't remember why.
I am also fine with 100.
Regarding docstrings the thing is some IDEs can pick those up and use them for semantic help. For example VSCode does this - when you hover mouse pointer over some method, IDE would pop up the method docstring. It is quite helpful at times. Also maybe some docbuilders reuse docstrings in auto-built docs. I feel in both cases it might be preferable to keep docstrings shorter but I do not have any hard data to back this up.
Let's go with 100, then. For everything, because it just makes things simpler. If we can demonstrate some problem with comments/docstrings over 80 (there are already cases over 80, it just varies in different places), we can adjust (reflowing comments is not such a PITA as the code, we don't blame comments too much).
Jaroslav, can you prepare a PR? Ideally separate commits/PRs for:
Once we see no errors reported, we can even improve (simplify) the current CI workflow.
There will be one more PR dealing with code linting after existing two PRs are processed and merged.
Adjusted the ticket description to include a checklist
#311 and #312 merged. Now we just need the last PR to fix all errors reported by
./run lint.Fully fixed in #317