Repository: pypa/pip

Issue:
`pip list` should not perform a self-update check
### Description

Running `pip` in env which is cu off from public network I found that on each execution it tries to check availability of its new version.
It causes long freezing of pip command and atthe end it prints message `WARNING: There was an error checking the latest version of pip.`


### Expected behavior

IMO `pip` should **only** do what is specified in commad lie parameters and nothing more.


### pip version

22.3.1

### Python version

3.8.16

### OS

Linux x86/64

### How to Reproduce

Run `pip list` in env which is cut off from access to the public network.

### Output

Example:
```
$ time pip list
Package                       Version
----------------------------- -----------------
alabaster                     0.7.12
appdirs                       1.4.4

[..]

urllib3                       1.26.12
wheel                         0.38.4
zipp                          3.11.0
WARNING: There was an error checking the latest version of pip.

real    2m0.771s
user    0m0.605s
sys     0m0.114s
```

### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).


Discussion:
@kloczek does the `--disable-pip-version-check` option help in your case?
Yes it helps however checking own version should not be part of the pip execution.
`pip` is python package management command and it suppose only do what is specified.
IMO whole version checking code can be removed because there are other commands which are dedicated to check version like `pip install -U --dry-run pip`.
BTW when `pip` checks its own version it is not possible to stop it using ctrl-c.
Othr observation: just tested `pip install -U --dry-run pip` and looks lik `pip` blindly several times is trying to access to pypi repo instead check pypi availability one time and than exit with error.
```console
[tkloczko@devel-g2v SPECS]$ time pip install -U --dry-run pip
Defaulting to user installation because normal site-packages is not writeable
Requirement already satisfied: pip in /usr/lib/python3.8/site-packages (22.3.1)
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f4667176880>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')': /simple/pip/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f4667176b80>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')': /simple/pip/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f4667176d30>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')': /simple/pip/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f4667176f10>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')': /simple/pip/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f4667123040>: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution')': /simple/pip/
WARNING: There was an error checking the latest version of pip.

real    13m1.157s
user    0m0.578s
sys     0m0.132s
```
Looks like even on `pip install -U --dry-run pip` at the end `pip` is trying to check ots own version one more time 😋 
> Yes it helps however checking own version should not be part of the pip execution.

This is a conscious design tradeoff, as we believe it is important to encourage users to upgrade to the latest pip version.
For situation like yours where this default behaviour is problematic we provide a way to disable it. This option can be enabled in a variety of ways, [including globally or per user](https://pip.pypa.io/en/stable/topics/configuration/).
Well, there is a legitimate workable thing here: `pip list` is primarily an (offline) introspection command and it shouldn't be invoking the self-check logic unless `--outdated` is passed.

If every possible software woud be encouraging users to upgrade something on every possible sotware execution it would be nightmare .. especially on the systems which has no access to public network.
Providing possibility to dissabla such functionality in configuration is not a solution as well 🤔 

Really such functionality does not make to much sense.
If someone cannot od don't wat to upgrade whatever will be implemented in `pip` it will have no effect.

I'm going to prepare patch which will completly remove that functionality.
Really `pip` should be doing **only** what that tools is asked .. and nothing more. KISS principle ..
> Really such functionality does not make to much sense.

Before making comments like this, please understand the background. Pip is core to Python packaging, and as such, if people are using outdated copies of pip, they will not be able to use new standards around such things as distrbution formats, index metadata delivery, etc. This will delay the adoption of such standards, and ultimately hold back the Python packaging ecosystem. So it is important that people use the latest copy of pip, which is why we chose the trade-off that we did.

While it's arguable that things have changed, and it is worthwhile to revisit the decision, negative comments like this don't really help the situation.

Personally, I think our current behaviour is still justified, and the options we provide for people who want to opt out of the version check are sufficient.
Even if it is arguable still it is design issue in `install -U` because that command blindly checks multiple times things over network without making sure first that access to the repository is possible.

Second issue is that it is not possible to stop checking version by pressing ctrl-c.
I suppose that hardcoding checking version of the pip on every `pip` execution must be hammering significanntly pypi cervers as well.
I would be not surprised in more than half af all pypi request woud be result of `pip` command executions.
OK, I've locked this as it is quickly becoming a "my opinion is better than the opinion of people who wrote and maintain the tool" type discussion. If you wish to have such a discussion, please have it on a different forum, this issue tracker is not the correct place.

---

> I'm going to prepare patch which will completly remove that functionality.

You're free to make choices that you like, but I will reiterate that `pip config set global.disable-pip-version-check true` is good-enough to disable the version check stuff. This has been mentioned already, indirectly.

Beyond that, please don't submit a PR with such a patch.

> Second issue is that it is not possible to stop checking version by pressing ctrl-c.

Nothing in pip is configured to suppress keyboard interrupts, especially within that logic. My network is a little too good to demonstrate this easily, so here's me doing a tiny patch to make it easier for me to demonstrate that:

```sh-session
❯ pip --version
pip 23.0.dev0 from /Users/pradyunsg/Developer/github/pip/src/pip (python 3.10)
❯ pwd
/Users/pradyunsg/Developer/github/pip
❯ git show HEAD --stat
commit c4566c6c828fa7b41f5656d30c6375a494d73ded (HEAD -> main, upstream/main, upstream/HEAD, origin/main, origin/HEAD)
Merge: 32634e589 dd70d4a3a
Author: Pradyun Gedam <pradyunsg@gmail.com>
Date:   Tue Dec 20 13:29:00 2022 +0000

    Merge pull request #11667 from edmorley/patch-1
    
    Remove duplicate news entry for #11547

 news/11547.bugfix.rst | 3 ---
 1 file changed, 3 deletions(-)
❯ git diff
diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py
index 9e2149c52..79f795c0f 100644
--- a/src/pip/_internal/self_outdated_check.py
+++ b/src/pip/_internal/self_outdated_check.py
@@ -232,6 +232,7 @@ def pip_self_version_check(session: PipSession, options: optparse.Values) -> Non
                 _get_current_remote_pip_version, session, options
             ),
         )
+        import time; time.sleep(100)
         if upgrade_prompt is not None:
             logger.warning("[present-rich] %s", upgrade_prompt)
     except Exception:
❯ pip list -vvv
[trimmed for posterity]
1 location(s) to search for versions of pip:
* https://pypi.org/simple/pip/
Fetching project page and analyzing links: https://pypi.org/simple/pip/
Getting page https://pypi.org/simple/pip/
Found index url https://pypi.org/simple
Looking up "https://pypi.org/simple/pip/" in the cache
Request header has "max_age" as 0, cache bypassed
Starting new HTTPS connection (1): pypi.org:443
https://pypi.org:443 "GET /simple/pip/ HTTP/1.1" 200 20678
Updating cache with response from "https://pypi.org/simple/pip/"
etag object cached for 1209600 seconds
Caching due to etag
Fetched page https://pypi.org/simple/pip/ as application/vnd.pypi.simple.v1+json
  Found link https://files.pythonhosted.org/packages/3d/9d/1e313763bdfb6a48977b65829c6ce2a43eaae29ea2f907c8bbef024a7219/pip-0.2.tar.gz (from https://pypi.org/simple/pip/), version: 0.2
  Found link https://files.pythonhosted.org/packages/18/ad/c0fe6cdfe1643a19ef027c7168572dac6283b80a384ddf21b75b921877da/pip-0.2.1.tar.gz (from https://pypi.org/simple/pip/), version: 0.2.1
  [manually trimmed a bunch of lines]
  Found link https://files.pythonhosted.org/packages/09/bd/2410905c76ee14c62baf69e3f4aa780226c1bbfc9485731ad018e35b0cb5/pip-22.3.1-py3-none-any.whl (from https://pypi.org/simple/pip/) (requires-python:>=3.7), version: 22.3.1
  Found link https://files.pythonhosted.org/packages/a3/50/c4d2727b99052780aad92c7297465af5fe6eec2dbae490aa9763273ffdc1/pip-22.3.1.tar.gz (from https://pypi.org/simple/pip/) (requires-python:>=3.7), version: 22.3.1
Skipping link: not a file: https://pypi.org/simple/pip/
Given no hashes to check 207 links for project 'pip': discarding no candidates
Remote version of pip: 22.3.1
Local version of pip:  23.0.dev0
Was pip installed by pip? True
^CTraceback (most recent call last):
  File "/Users/pradyunsg/Developer/github/pip/.venv/bin/pip", line 8, in <module>
    sys.exit(main())
  File "/Users/pradyunsg/Developer/github/pip/src/pip/_internal/cli/main.py", line 70, in main
    return command.main(cmd_args)
  File "/Users/pradyunsg/Developer/github/pip/src/pip/_internal/cli/base_command.py", line 101, in main
    return self._main(args)
  File "/Users/pradyunsg/Developer/github/pip/src/pip/_internal/cli/base_command.py", line 216, in _main
    self.handle_pip_version_check(options)
  File "/Users/pradyunsg/Developer/github/pip/src/pip/_internal/cli/req_command.py", line 190, in handle_pip_version_check
    pip_self_version_check(session, options)
  File "/Users/pradyunsg/Developer/github/pip/src/pip/_internal/self_outdated_check.py", line 235, in pip_self_version_check
    import time; time.sleep(100)
KeyboardInterrupt
```

---

For other maintainers, here's some relevant links:

- https://github.com/pypa/pip/issues/10420#issuecomment-911635434
- https://github.com/pypa/pip/issues/11427#issuecomment-1233917542
- https://github.com/pypa/pip/issues/11601#issuecomment-1317546885

---

I'll leave this open since there's a legit improvement to be made here, as noted in https://github.com/pypa/pip/issues/11677#issuecomment-1366664323. I suggest we keep this locked for at least a day or so.

Does that suggestion for a change seem reasonable?

Unlocking after a grace period.

Resolving patch:
diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py
--- a/src/pip/_internal/commands/list.py
+++ b/src/pip/_internal/commands/list.py
@@ -135,6 +135,10 @@ def add_options(self) -> None:
         self.parser.insert_option_group(0, index_opts)
         self.parser.insert_option_group(0, self.cmd_opts)
 
+    def handle_pip_version_check(self, options: Values) -> None:
+        if options.outdated or options.uptodate:
+            super().handle_pip_version_check(options)
+
     def _build_package_finder(
         self, options: Values, session: PipSession
     ) -> PackageFinder:


Test patch:
diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py
--- a/tests/unit/test_commands.py
+++ b/tests/unit/test_commands.py
@@ -1,3 +1,4 @@
+import os
 from typing import Callable, List
 from unittest import mock
 
@@ -104,6 +105,10 @@ def test_index_group_handle_pip_version_check(
     options.disable_pip_version_check = disable_pip_version_check
     options.no_index = no_index
 
+    # See test test_list_pip_version_check() below.
+    if command_name == "list":
+        expected_called = False
+
     command.handle_pip_version_check(options)
     if expected_called:
         mock_version_check.assert_called_once()
@@ -120,3 +125,20 @@ def is_requirement_command(command: Command) -> bool:
         return isinstance(command, RequirementCommand)
 
     check_commands(is_requirement_command, ["download", "install", "wheel"])
+
+
+@pytest.mark.parametrize("flag", ["", "--outdated", "--uptodate"])
+@mock.patch("pip._internal.cli.req_command.pip_self_version_check")
+@mock.patch.dict(os.environ, {"PIP_DISABLE_PIP_VERSION_CHECK": "no"})
+def test_list_pip_version_check(version_check_mock: mock.Mock, flag: str) -> None:
+    """
+    Ensure that pip list doesn't perform a version self-check unless given
+    --outdated or --uptodate (as they require hitting the network anyway).
+    """
+    command = create_command("list")
+    command.run = lambda *args, **kwargs: 0  # type: ignore[method-assign]
+    command.main([flag])
+    if flag != "":
+        version_check_mock.assert_called_once()
+    else:
+        version_check_mock.assert_not_called()