Repository: pypa/pip

Issue:
`ensurepip` fails with `importlib.metadata` backend
### Description

In Debian, we use the pip from our pip package with `ensurepip`. We've noticed with `3.11`, and pip `22.1.1`, ensurepip isn't doing anything any more.

The relevant change is that the `importlib.metadata` is the default backend with Python 3.11. If we force it back to the `pkg_resources` backend, it works.

### Expected behavior

`ensurepip` is expected to install pip. It doesn't do anything, any more.

### pip version

22.1.2+ dc7abcada7e263e80af6c8d4ea2147c067c78d01

### Python version

3.11+ f9d0240db809fbb4443dc8f96a18e4c49af3fb7f

### OS

Linux

### How to Reproduce

1. Install Python 3.11.
2. Build / download a current (>= 22.1) pip wheel. Install it into `ensurepip/_bundled/` in your Python installation.
3. Update `_PIP_VERSION` in `ensurepip/__init__.py` to refer to the new version.
4. `python3.11 -m venv testve`
5. `ls testve/lib/python3.11/site-packages/`

This should not be empty, but will be.

For extra debugging:
1. `testve/bin/python -Im ensurepip --upgrade --default-pip -v`

### Output

```sh-session
Using pip 22.2.dev0 from /tmp/tmp3dhtsvpz/pip-22.2.dev0-py3-none-any.whl/pip (python 3.11)
Looking in links: /tmp/tmp3dhtsvpz
Requirement already satisfied: setuptools in ./tmp3dhtsvpz/setuptools-58.1.0-py3-none-any.whl (58.1.0)
Requirement already satisfied: pip in ./tmp3dhtsvpz/pip-22.2.dev0-py3-none-any.whl (22.2.dev0)
```


### Code of Conduct

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


Discussion:
Huh, `pkg_resources` explicitly ignores `.dist-info` in a wheel, but `importlib.metadata` does not:

https://github.com/pypa/pip/blob/dc7abcada7e263e80af6c8d4ea2147c067c78d01/src/pip/_vendor/pkg_resources/__init__.py#L1978-L1981

As you can tell from comment, the logic is pretty outdated and is arguably a bug, but this is relied by pip and “works”. I’ll work on a fix to do this properly.
Should ensurepip use `--force-reinstall`? But that would probably break the current way of fast repeated calls to ensurepip.
ensurepip probably should use `--force-reinstall`, but pip also should ignore the dist-info found in a wheel.
> ensurepip probably should use `--force-reinstall`

I took a look at the code. It assumes that repeated calls don't reinstall. So I guess this would need to check if pip and setuptools is installed first and if at least one of them is not, pass `--force-reinstall`. Ideally, make this decision separately for pip and setuptools. Not sure if worth it, considering we assume pip should ignore the dist-info found in a wheel.


I'd lean toward not "fixing" this via ignoring the dist-info, and I think we should instead update ensurepip to work the way get-pip.py does.

Is there any reason to not do it this way?
get-pip always uses `--upgrade` and `--force-reinstall`. ensurepip does not reinstall on every occasion and only passes `--upgrade` when the user invoked it as such.
I didn't mean in terms of the flags passed, but rather in terms of how we're putting a copy of pip on sys.path.

I don't think we should be trying to use pip while it's packaged in a wheel -- we know that's fragile in multiple ways and something that we don't support users doing either.
There’s good justification to ignore `.dist-info` in a wheel though (not for `.whl` in general, but an actual wheel)—since a “distribution” in a wheel is not actually installed and might not work, it should not be considered “installed”. I think we should do it regardless.
Indeed, let's do both then. :)

Resolving patch:
diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py
--- a/src/pip/_internal/metadata/importlib/_envs.py
+++ b/src/pip/_internal/metadata/importlib/_envs.py
@@ -10,12 +10,24 @@
 from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
 
 from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
+from pip._internal.models.wheel import Wheel
 from pip._internal.utils.deprecation import deprecated
+from pip._internal.utils.filetypes import WHEEL_EXTENSION
 
 from ._compat import BasePath, get_dist_name, get_info_location
 from ._dists import Distribution
 
 
+def _looks_like_wheel(location: str) -> bool:
+    if not location.endswith(WHEEL_EXTENSION):
+        return False
+    if not os.path.isfile(location):
+        return False
+    if not Wheel.wheel_file_re.match(os.path.basename(location)):
+        return False
+    return zipfile.is_zipfile(location)
+
+
 class _DistributionFinder:
     """Finder to locate distributions.
 
@@ -36,6 +48,11 @@ def __init__(self) -> None:
 
     def _find_impl(self, location: str) -> Iterator[FoundResult]:
         """Find distributions in a location."""
+        # Skip looking inside a wheel. Since a package inside a wheel is not
+        # always valid (due to .data directories etc.), its .dist-info entry
+        # should not be considered an installed distribution.
+        if _looks_like_wheel(location):
+            return
         # To know exactly where we find a distribution, we have to feed in the
         # paths one by one, instead of dumping the list to importlib.metadata.
         for dist in importlib.metadata.distributions(path=[location]):


Test patch:
diff --git a/tests/unit/metadata/test_metadata.py b/tests/unit/metadata/test_metadata.py
--- a/tests/unit/metadata/test_metadata.py
+++ b/tests/unit/metadata/test_metadata.py
@@ -1,4 +1,5 @@
 import logging
+import os
 from pathlib import Path
 from typing import cast
 from unittest import mock
@@ -9,6 +10,7 @@
 from pip._internal.metadata import (
     BaseDistribution,
     get_directory_distribution,
+    get_environment,
     get_wheel_distribution,
 )
 from pip._internal.metadata.base import FilesystemWheel
@@ -102,3 +104,28 @@ def test_metadata_dict(tmp_path: Path) -> None:
     metadata_dict = dist.metadata_dict
     assert metadata_dict["name"] == "pkga"
     assert metadata_dict["version"] == "1.0.1"
+
+
+def test_no_dist_found_in_wheel(tmp_path: Path) -> None:
+    location = os.fspath(tmp_path.joinpath("pkg-1-py3-none-any.whl"))
+    make_wheel(name="pkg", version="1").save_to(location)
+    assert get_environment([location]).get_distribution("pkg") is None
+
+
+def test_dist_found_in_directory_named_whl(tmp_path: Path) -> None:
+    dir_path = tmp_path.joinpath("pkg-1-py3-none-any.whl")
+    info_path = dir_path.joinpath("pkg-1.dist-info")
+    info_path.mkdir(parents=True)
+    info_path.joinpath("METADATA").write_text("Name: pkg")
+    location = os.fspath(dir_path)
+    dist = get_environment([location]).get_distribution("pkg")
+    assert dist is not None and dist.location is not None
+    assert Path(dist.location) == Path(location)
+
+
+def test_dist_found_in_zip(tmp_path: Path) -> None:
+    location = os.fspath(tmp_path.joinpath("pkg.zip"))
+    make_wheel(name="pkg", version="1").save_to(location)
+    dist = get_environment([location]).get_distribution("pkg")
+    assert dist is not None and dist.location is not None
+    assert Path(dist.location) == Path(location)