Repository: pypa/pip

Issue:
Consider switching to ruff for linting
### What's the problem this feature will solve?

We currently use flake8 for our linting. There's nothing wrong with that, but ruff is a lot faster, as well as being a single tool rather than one tool plus some (independently maintained) plugins.

The set of warnings is going to be slightly different (ruff doesn't claim to implement *everything* included in the various plugins it copies) but I'm not sure we care as long as the results on the current codebase are the same - I don't believe anyone ever audited the full list of checks currently applied[^1], our style rules are basically just "must pass the tools we use in our standard linting config". On which basis, changing the linter seems perfectly acceptable.

[^1]: Much less review each new linter release to decide if we like whatever gets changed 🙂

### Describe the solution you'd like

Replace our existing flake8 configuration with ruff. We could also use ruff in place of isort, further simplifying our setup.

### Alternative Solutions

Stick with what we have. It's not as if there's a great need to change.

To be clear here, I'm not pushing for this, I'm raising this mostly just to get a feel for what others think of the idea.

### Additional context

I did some experimenting, and the following addition to `pyproject.toml` seems to replicate what we have for flake8.

```
[tool.ruff]
exclude = [
    "./build",
    ".nox",
    ".tox",
    ".scratch",
    "_vendor",
    "data",
]
ignore = [
    "B019",
    "B020",
    "B904", # Ruff enables opinionated warnings by default
    "B905", # Ruff enables opinionated warnings by default
    "G202",
]
line-length = 88
select = [
    "B",
    "E",
    "F",
    "W",
    "G",
    "ISC",
]

[tool.ruff.per-file-ignores]
"noxfile.py" = ["G"]
"tests/*" = ["B011"]
```

There are two warnings remaining:

```
src\pip\_internal\network\auth.py:517:9: B018 Found useless expression. Either assign it to a variable or remove it.
tests\lib\__init__.py:687:39: B026 Star-arg unpacking after a keyword argument is strongly discouraged
```

To be honest, both seem legitimate (although it's a matter of opinion whether we should fix or ignore them, I don't really have a preference) and I'm not clear why flake8-bugbear doesn't flag them - I couldn't see any indication that they are disabled by default.

### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).


Discussion:
Also, as a result of https://github.com/globality-corp/flake8-logging-format/issues/68, I expect our current lint setup to start failing on Python 3.12 (I first started thinking about using ruff because I hit that problem locally, as I configure virtualenv to omit setuptools and wheel by default).
Let's do it. :) 
I'm a big fan of Ruff, I will point out a couple of things to be aware of:

Last I checked parsing stack is based on RustPython which uses an LL(1)  parser rather than a PEG parser, this can lead to them to implementing new syntax rather slowly. E.g. match statement syntax has only been supported since February 2023 even though it had been in Python since October 2021.

Also Ruff is relatively new and may be missing edge cases. E.g. I discovered recently it's scoping rules inside comprehensions inside class scope was wrong (I found this as I was testing PEP 709 discussion), I reported and the author was was very quick to fix.

Neither of these things I think should significantly affect Pip, but something to be aware of.

Resolving patch:
diff --git a/noxfile.py b/noxfile.py
--- a/noxfile.py
+++ b/noxfile.py
@@ -219,7 +219,7 @@ def pinned_requirements(path: Path) -> Iterator[Tuple[str, str]]:
         new_version = old_version
         for inner_name, inner_version in pinned_requirements(vendor_txt):
             if inner_name == name:
-                # this is a dedicated assignment, to make flake8 happy
+                # this is a dedicated assignment, to make lint happy
                 new_version = inner_version
                 break
         else:
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -514,7 +514,9 @@ def handle_401(self, resp: Response, **kwargs: Any) -> Response:
 
         # Consume content and release the original connection to allow our new
         #   request to reuse the same one.
-        resp.content
+        # The result of the assignment isn't used, it's just needed to consume
+        # the content.
+        _ = resp.content
         resp.raw.release_conn()
 
         # Add our new username and password to the request


Test patch:
diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py
--- a/tests/functional/test_install_compat.py
+++ b/tests/functional/test_install_compat.py
@@ -7,8 +7,12 @@
 
 import pytest
 
-from tests.lib import pyversion  # noqa: F401
-from tests.lib import PipTestEnvironment, TestData, assert_all_changes
+from tests.lib import (
+    PipTestEnvironment,
+    TestData,
+    assert_all_changes,
+    pyversion,  # noqa: F401
+)
 
 
 @pytest.mark.network
diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py
--- a/tests/functional/test_install_upgrade.py
+++ b/tests/functional/test_install_upgrade.py
@@ -6,8 +6,13 @@
 
 import pytest
 
-from tests.lib import pyversion  # noqa: F401
-from tests.lib import PipTestEnvironment, ResolverVariant, TestData, assert_all_changes
+from tests.lib import (
+    PipTestEnvironment,
+    ResolverVariant,
+    TestData,
+    assert_all_changes,
+    pyversion,  # noqa: F401
+)
 from tests.lib.local_repos import local_checkout
 from tests.lib.wheel import make_wheel
 
diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py
--- a/tests/functional/test_install_user.py
+++ b/tests/functional/test_install_user.py
@@ -8,12 +8,12 @@
 
 import pytest
 
-from tests.lib import pyversion  # noqa: F401
 from tests.lib import (
     PipTestEnvironment,
     TestData,
     create_basic_wheel_for_package,
     need_svn,
+    pyversion,  # noqa: F401
 )
 from tests.lib.local_repos import local_checkout
 from tests.lib.venv import VirtualEnvironment
diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py
--- a/tests/functional/test_install_vcs_git.py
+++ b/tests/functional/test_install_vcs_git.py
@@ -3,11 +3,11 @@
 
 import pytest
 
-from tests.lib import pyversion  # noqa: F401
 from tests.lib import (
     PipTestEnvironment,
     _change_test_package_version,
     _create_test_package,
+    pyversion,  # noqa: F401
 )
 from tests.lib.git_submodule_helpers import (
     _change_test_package_submodule,
diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py
--- a/tests/functional/test_wheel.py
+++ b/tests/functional/test_wheel.py
@@ -7,8 +7,11 @@
 import pytest
 
 from pip._internal.cli.status_codes import ERROR
-from tests.lib import pyversion  # noqa: F401
-from tests.lib import PipTestEnvironment, TestData
+from tests.lib import (
+    PipTestEnvironment,
+    TestData,
+    pyversion,  # noqa: F401
+)
 
 
 def add_files_to_dist_directory(folder: Path) -> None:
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -684,7 +684,9 @@ def run(
         # Pass expect_stderr=True to allow any stderr.  We do this because
         # we do our checking of stderr further on in check_stderr().
         kw["expect_stderr"] = True
-        result = super().run(cwd=cwd, *args, **kw)
+        # Ignore linter check
+        # B026 Star-arg unpacking after a keyword argument is strongly discouraged
+        result = super().run(cwd=cwd, *args, **kw)  # noqa: B026
 
         if expect_error and not allow_error:
             if result.returncode == 0: