Skip to content
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

chore: move to Ruff and add rules #4483

Merged
merged 5 commits into from
Feb 22, 2023
Merged

Conversation

henryiii
Copy link
Collaborator

@henryiii henryiii commented Feb 1, 2023

Description

Moves the linters over to a single tool - Ruff. This combines a lot of other tools, is 10-100x faster (Rust rewrite), has auto-fix support for some rules, is a single no-dependency binary, and is configured in pyproject.toml. Build, scikit-build-core, Pandas, SciPy, cibuildwheel, FastAPI, and others have switched now too.

Adding a lot more rules since plugins are "free", basically, they don't complicate the environment setup, they are all compiled in.

Ruff technically only supports Python 3.7+, but I think it's fine and we are about to drop 3.6 support too anyway. (Flake8 only supports running on 3.8+ now anyway, too)

Suggested changelog entry:

* Moved the linting framework over to Ruff.

@henryiii henryiii force-pushed the henryiii/chore/ruff branch 2 times, most recently from 7948ee8 to f98562b Compare February 1, 2023 19:37
Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a review, really ... Looks like a big step forward in almost automatic source code cleanup!

Did you already look at the many CI failures? Is that something we should tackle as a team, divvying up the work?

else:
self.explanation = _make_explanation(a.splitlines(), b.splitlines())
return False
self.explanation = _make_explanation(a.splitlines(), b.splitlines())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@@ -54,7 +54,8 @@ def test_to_python():
mat2 = np.array(mat, copy=False)
assert mat2.shape == (5, 4)
assert abs(mat2).sum() == 11
assert mat2[2, 3] == 4 and mat2[3, 2] == 7
assert mat2[2, 3] == 4
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing!

@henryiii henryiii force-pushed the henryiii/chore/ruff branch 2 times, most recently from 6438372 to e660fb2 Compare February 9, 2023 13:40
@henryiii henryiii force-pushed the henryiii/chore/ruff branch 2 times, most recently from 5621b34 to 54e491b Compare February 16, 2023 15:55
@@ -72,13 +72,13 @@ def test_iterator_referencing():
vec = m.VectorNonCopyableIntPair()
vec.append([3, 4])
vec.append([5, 7])
assert [int(x) for x in vec.keys()] == [3, 5]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is kind of often wrong. I've had it trigger incorrectly in another package too. Not all objects play nice and have iter(x) == iter(x.keys()). I wish it was type-aware.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii
Copy link
Collaborator Author

Passing now.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing now.

AWESOME!

I could only glance through right now. I need to find a block of time to comb through more carefully. Asap.

"unused-argument", # covered by Ruff ARG
]

[tool.ruff]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a ruff doc we could link to from here?
To jump-start finding the answers to: what does this do and why do we need it?
(Only if there is an easy direct link and it makes sense to you.)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need to link to everything everywhere, and that URL might change in the future. I added a note in .pre-commit-config.yaml. Ruff is easy to find via Googling.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked more carefully now. Overall I'm still amazed, but I'm a worried by the changes ruff made to test_enum.py. Are there more like that elsewhere that I overlooked? I'll try to make another pass through later. I'll also try to get a coverage analysis before and after these changes.

tests/conftest.py Show resolved Hide resolved
def msg():
"""Sanitize messages and add custom failure explanation"""
return SanitizedString(_sanitize_message)


# noinspection PyUnusedLocal
def pytest_assertrepr_compare(op, left, right):
def pytest_assertrepr_compare(op, left, right): # noqa: ARG001
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to use

del op
del right

as a way to document what is (obviously) intentionally unused, rather than using suppressions.

Copy link
Collaborator Author

@henryiii henryiii Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the suppression. It's in the definition, rather than in the implementation, which IMO is better. I shouldn't have to look in the body of the function to be warned that some arguments are ignored. It's also possible these are never called by name in the hook, in which case _op, _right would be better.

@@ -66,20 +66,20 @@ def test_unscoped_enum():
# Unscoped enums will accept ==/!= int comparisons
y = m.UnscopedEnum.ETwo
assert y == 2
assert 2 == y
assert y == 2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All, or maybe almost all, changes in this file undermine the original intent in a critical way.

Is there a way to tell ruff not to touch any of the operator tests in this file?

Generally: the requirements for production code are subtly and critically different from requirements for unit test code. Does ruff have a concept of the two classes (that we could apply here)?

Copy link
Collaborator Author

@henryiii henryiii Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can disable checks per file. This is yoda-conditions from SIM.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful if I jump in and try? — Asking to make sure we're not both working on it at the same time.

Copy link
Collaborator Author

@henryiii henryiii Feb 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do it, since I'm looking at it now. It's just a simple series of commands:

$ git checkout master -- tests/test_enum.py
$ pre-commit run -a
...
Fixed 19 errors:
- tests/test_enum.py:
    10 × SIM201 (negate-equal-op)
     7 × SIM300 (yoda-conditions)
     2 × SIM202 (negate-not-equal-op)
$ git restore .

Then I add those codes to pyproject.toml.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation!

In pyproject.toml:

"tests/test_enum.py" = ["SIM201", "SIM300", "SIM202"]

For someone externally this will be very difficult to discover. Preferred alternative to make this more obvious (tested):

test_embed.py (e.g. top):

# It is critical that ruff does not rewrite the tests for overloaded operators:
# ruff: noqa SIM201 SIM202 SIM300

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A) that was just added a release or two ago :)

B) I'm not sure that's the right syntax. Did you verify that this is not a global ignore of all codes + some extra comment letters? The docs say ruff: noqa: SIM201 SIM202 SIM300 (note the second colon).

Sure, happy to do it that way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer to B: yes, that's exactly what it is. If the colon is missing, that's just a comment after a total noqa. Hmm, wait a minute, even with the colon, it still seems to be ignoring too much. Will investigate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answer to B: yes, that's exactly what it is.

Oh ... sorry, I picked up the syntax from some documentation page and apparently was too happy too quickly. Thanks for looking into this!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does show a warning, but you need a Ruff new enough to support it, which I didn't have. It's (as of writing) the latest version now.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
"RET", # flake8-return
"RUF100", # Ruff-specific
"SIM", # flake8-simplify
"UP", # pyupgrade
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we enable keep runtime typing for Pyupgrade?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can disable the pyupgrade code for those per file pattern, which is about the same (better in some ways, worse in others). Won't make a difference until py37 is the minimum, I think.

rev: "v2.1.3"
# Ruff, the Python auto-correcting linter written in Rust
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.247
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version is already out of date please autoupdate

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need another arg now to provide a non-zero exit code when fixits are applied.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think pre-commit cares - it can detect changed files.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not, this flag needs to be added to the pre-commit hook under entry.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran C++ coverage analysis 1. for master as-is, 2. with this PR as-is.

The results are exactly identical (line coverage & branch coverage).

I did that twice.
I also removed a couple tests, to convince myself that there are differences in that case.
I think we're good!

It would be great if we could have the test_enum.py suppressions in the file itself, but if that's not easily possible, I'm fine with this PR as-is.

]

[tool.ruff]
select = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newest version of ruff has PEP8-naming rules implemented. We should enable those too

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pybind11/setup_helpers.py:274:7: N801 Class name `build_ext` should use CapWords convention
tests/test_exceptions.py:292:7: N818 Exception name `FlakyException` should be named with an Error suffix
tests/test_pytypes.py:18:5: N802 Function name `test_handle_from_move_only_type_with_operator_PyObject` should be lowercase
tests/test_sequences_and_iterators.py:157:11: N818 Exception name `BadLen` should be named with an Error suffix
Found 4 errors.

Not sure it really matters in tests, and the one in the project is wrong. Guess I can just use --add-noqa and inject these four skips, though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, skipped naming rules for tests, and noqa'd the only non-test one.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
@henryiii
Copy link
Collaborator Author

We do have them in-file now.

@henryiii henryiii merged commit 438034c into pybind:master Feb 22, 2023
@henryiii henryiii deleted the henryiii/chore/ruff branch February 22, 2023 14:18
@github-actions github-actions bot added the needs changelog Possibly needs a changelog entry label Feb 22, 2023
@henryiii henryiii removed the needs changelog Possibly needs a changelog entry label Mar 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants