Repository: pypa/pip

Issue:
Revisit displaying setuptools in `pip freeze`?
[Given](https://github.com/pypa/setuptools/issues/945) [the](https://github.com/pypa/setuptools/issues/942) [frustration](https://github.com/pypa/setuptools/issues/940) [with](https://github.com/pypa/setuptools/issues/939) [setuptools](https://github.com/pypa/setuptools/issues/938) [34](https://github.com/pypa/setuptools/issues/937) is it perhaps a good time to reconsider the display of setuptools in pip freeze?  At least at $work, it is a strong suggestion to _pin everything_ in applications (you can see our full set of tooling [here](https://github.com/Yelp/requirements-tools)), even setuptools / pip / wheel for repeatable builds -- perhaps setuptools and its new dependencies should be included as well (without needing our [own implementation of pip freeze](https://github.com/Yelp/requirements-tools/blob/b68372e5fb56f11826ccdc1f66e9105985803610/requirements_tools/upgrade_requirements.py#L55-L77) which includes them).


Discussion:
Is [`pip freeze --all`](https://pip.pypa.io/en/stable/reference/pip_freeze/#cmdoption--all) what you're looking for?
I'm suggesting that setuptools be excluded from the non-`--all` output.
I don't think in a general case, setuptools deserves a special case over the other items there (pip, wheel, distribute), so I'd equivocate that to suggesting that the`--all` option be removed together and be made the default.
`pip list` already displays `setuptools` by default.
Given that post-PEP 517, we're gonna have no special casing for setuptools or even wheel, I'm a +1 to @ivoz's suggestion, if it's not implemented yet.
I'm gonna assign this to myself but if anyone wants to try their hand at this before I get too, you're more than welcome to do so.
I've been suggesting `pip freeze --all` for as long as I can remember now -- maybe we should make that the default?
I also agree that it would make sense for `pip freeze` to also output `setuptools` & `wheel` version (especially post-PEP 517 as Pradyun mentionned), but I'm more ambivalent with `pip` version.

The other question is how should we handle the transition ?
- simply add a `.removal` changelog stating those packages are not excluded from `freeze` anymore and suggest to `| grep -vE "^(pip|wheel|setuptools)="` to simulate the previous behavior ?
- make `--all` the default (if we want to also output `pip` version) and add a new `--legacy` option for a few releases ?

At least, removing the `--all` option would help for #7157 ^^
I see the problem entirely the other way around. All the issues linked in OP have one common trait: they are all using a very old version of pip (or some installation that’s even older). Recent versions of pip do not cause the frustration, and adding setuptools to `pip freeze` helps none of those affected *because they would never see that new behaviour*. So IMO

1. pip should include pip itself into `pip freeze`.
2. Setuptools can include some kind of (semi-permanent) safty net in `setup.py` to hint how people using legacy pip to resolve the problem when it inevitably breaks their obsolete setup.

Edit to clarify: I don’t object to including setuptools in `pip freeze` (because PEP 517), but only don’t think that would solve the majority of the problem linked above.
## New transition plan

### Phase 1:
- add an `--exclude` cumulative option to `pip freeze` and `pip list` (`pip freeze --exclude pip --exclude setuptools`): this will allow users to explicitly configure their pip to do what they want
  ```
  [freeze]
  exclude =
    pip
    setuptools
  ```
- add a deprecation warning when using `pip freeze` without `--all` or any `--exclude option` stating that `--all` will soon be the default

### Phase 2:
- make `--all` the default
- deprecate `--all` option that now does nothing.

All approaches here feel weird to me right now. That isn't to say that we shouldn't pick any of them, but is mostly just me stating my thoughts.

---

With the above transition plan, we'd create busy-work for users who want to constantly "do the right thing" and address warnings they get from the tool.

- User X uses `pip freeze` today.
- After updating to pip [v1], they see a deprecation warning that says "hey, --all is gonna be the default soon". They add `--all` to the invocation, and it silences that warning.
- After updating to pip [v2], they now see a different deprecation warning that says "hey, --all is useless now". They now have to remove `--all` from the invocation to silence the warning.

I don't imagine most folks care about this additional pinning -- they won't see see any direct benefits from this change and we eat into a bit of our "churn budget".

---

#9068 also feels weird, partly because I think it's a good idea. I guess the only thing it's missing is an escape hatch -- as proposed here to be called `--exclude` (which I really like BTW). It's also at odds with our deprecation policy -- I definitely think our policy is good since [Hyrum's Law](https://www.hyrumslaw.com/) applies.

It feels like the best approach might be "make a breaking change without much noise, and properly communicate/fix things", and I really don't want to say "let's do that". :(

As a middle ground, I've pushed https://github.com/pypa/pip/pull/9073/files which implement Phase 1 without the warning.
@xavfernandez I have mixed feeling with this idea of making `--all` the default. (sorry to be late to the party)

In most cases, applications that are installed in a virtualenv do not depend on pip, setuptools nor wheel, so it is not necessary to include these in pinned requirements.

When you do, and use `freeze --all > requirements.txt`, and commit requirements.txt to source control, what typically happens is

- a developer obtains the code, and creates a virtualenv: this usually installs a recent pip, wheel, and setuptools
- `pip install -r requirements`: this first run uses the recent pip to do the installation, which downgrades it to the version in requirements.txt
- subsequent use of pip in the virtualenv therefore uses a different version than the one that was used for the first install

So the pinning of pip, setuptools and wheel is actually misleading because on the initial install from `requirements.txt` in a fresh venv, it has no effect. 

OTOH when the application depends on setuptools or wheel (or pip), not freezing them by default is incorrect.

What would be more natural IMO, is that `pip freeze` includes setuptools, wheel, or pip only when they are dependencies of another installed distribution.

I remained vague in https://github.com/pypa/pip/pull/9073/files#diff-429186493476e02aaccfb73ba9d42328c0a6b04528054a8a42ca4de2b11e9b6a by mentioning a future version, this could easily be a year or more :)
Hopefully, this might leave time for users to explicitly switch to `--exclude` if that is what they want and/or for us to cleverly nudge them to do so.

With the generalization of PEP-517, I'm hopeful the inclusion of `setuptools` & `wheel` will be less and less of a problem (albeit slowly) but I agree that `pip` will always be a special snowflake and I don't really see a way around that (should pip warn the user when changing its own version with other packages ? `WARNING: pip is going to update to pip XX but the current install will still be running with pip YY. If the install needs to run with pip YY, please update pip first`. ?)

> What would be more natural IMO, is that `pip freeze` includes setuptools, wheel, or pip only when they are dependencies of another installed distribution.

I agree that this would be somewhat "cleaner" but it would also mean:
```
$ pip freeze
setuptools==50.3.2
setuptools-scm==4.1.2
$ pip uninstall setuptools-scm -yq
$ pip freeze
```
and I would not call this "natural" ^^
> I would not call this "natural" ^^

Hm... yes, maybe trying to be that smart is out of scope for pip freeze indeed, and that should be the role of higher level tools that know more about the user intent and context.
Isn't the big problem here that *virtualenv* installs setuptools and wheel? If it weren't for that, project environments would only ever contain the packages that the project actually depended on, and build dependencies would be in `pyproject.toml`.

So maybe we should do something like:

1. Use `--all` outside of a virtual environment.
2. Within a virtual environment, exclude whatever virtualenv (or venv, if the environment was created by venv¹) installs by default.

Yes, this is awkward, but with isolated builds, there should be no reason to install build tools in environments anyway. So we're working round other projects' (incorrect?) ideas of what "everyone needs" - which is *always* awkward...

¹ And maybe we need to consider conda environments as another case 🙁 
Perhaps now that ensurepip and virtualenv no longer install setuptools/wheel when used with Python 3.12+, pip can also stop excluding these projects from freeze output when used with 3.12+?

I was thinking the same when I saw the issue title in my notification (before reading the content)! Personally I’d be more than happy to approve such a pull request.

Resolving patch:
diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py
--- a/src/pip/_internal/commands/freeze.py
+++ b/src/pip/_internal/commands/freeze.py
@@ -1,6 +1,6 @@
 import sys
 from optparse import Values
-from typing import List
+from typing import AbstractSet, List
 
 from pip._internal.cli import cmdoptions
 from pip._internal.cli.base_command import Command
@@ -8,7 +8,18 @@
 from pip._internal.operations.freeze import freeze
 from pip._internal.utils.compat import stdlib_pkgs
 
-DEV_PKGS = {"pip", "setuptools", "distribute", "wheel"}
+
+def _should_suppress_build_backends() -> bool:
+    return sys.version_info < (3, 12)
+
+
+def _dev_pkgs() -> AbstractSet[str]:
+    pkgs = {"pip"}
+
+    if _should_suppress_build_backends():
+        pkgs |= {"setuptools", "distribute", "wheel"}
+
+    return pkgs
 
 
 class FreezeCommand(Command):
@@ -61,7 +72,7 @@ def add_options(self) -> None:
             action="store_true",
             help=(
                 "Do not skip these packages in the output:"
-                " {}".format(", ".join(DEV_PKGS))
+                " {}".format(", ".join(_dev_pkgs()))
             ),
         )
         self.cmd_opts.add_option(
@@ -77,7 +88,7 @@ def add_options(self) -> None:
     def run(self, options: Values, args: List[str]) -> int:
         skip = set(stdlib_pkgs)
         if not options.freeze_all:
-            skip.update(DEV_PKGS)
+            skip.update(_dev_pkgs())
 
         if options.excludes:
             skip.update(options.excludes)


Test patch:
diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py
--- a/tests/functional/test_freeze.py
+++ b/tests/functional/test_freeze.py
@@ -88,11 +88,49 @@ def test_basic_freeze(script: PipTestEnvironment) -> None:
 
 
 def test_freeze_with_pip(script: PipTestEnvironment) -> None:
-    """Test pip shows itself"""
+    """Test that pip shows itself only when --all is used"""
+    result = script.pip("freeze")
+    assert "pip==" not in result.stdout
     result = script.pip("freeze", "--all")
     assert "pip==" in result.stdout
 
 
+def test_freeze_with_setuptools(script: PipTestEnvironment) -> None:
+    """
+    Test that pip shows setuptools only when --all is used
+    or _should_suppress_build_backends() returns false
+    """
+
+    result = script.pip("freeze", "--all")
+    assert "setuptools==" in result.stdout
+
+    (script.site_packages_path / "mock.pth").write_text("import mock\n")
+
+    (script.site_packages_path / "mock.py").write_text(
+        textwrap.dedent(
+            """\
+                import pip._internal.commands.freeze as freeze
+                freeze._should_suppress_build_backends = lambda: False
+            """
+        )
+    )
+
+    result = script.pip("freeze")
+    assert "setuptools==" in result.stdout
+
+    (script.site_packages_path / "mock.py").write_text(
+        textwrap.dedent(
+            """\
+                import pip._internal.commands.freeze as freeze
+                freeze._should_suppress_build_backends = lambda: True
+            """
+        )
+    )
+
+    result = script.pip("freeze")
+    assert "setuptools==" not in result.stdout
+
+
 def test_exclude_and_normalization(script: PipTestEnvironment, tmpdir: Path) -> None:
     req_path = wheel.make_wheel(name="Normalizable_Name", version="1.0").save_to_dir(
         tmpdir