Repository: pypa/pip
Pull request #11547 (2022-10-26 08:28:58)

Fix multi-digit version in entry point replacement
Previously, the special case to generate `pip` and `easy_install` entry points with the correct Python version (e.g. `pip3.9` on Python 3.9) only accounted for single-digit version segments, and did not work correctly on Python 3.10 and up. This was missed when Python 3.10 was released because we (accidentally) generated wheels that did not need any such replacements, but was exposed in CPython 3.11.0 since it bundled pip 22.3 generated against Python 3.10.

See python/cpython#98682

uranusjr: Urgh, test failures due to #11541.

Patch:
diff --git a/news/11547.bugfix.rst b/news/11547.bugfix.rst
new file mode 100644
index 000000000..29d566a23
--- /dev/null
+++ b/news/11547.bugfix.rst
@@ -0,0 +1,3 @@
+Fix entry point generation of ``pip.X``, ``pipX.Y``, and ``easy_install-X.Y``
+to correctly account for multi-digit Python version segments (e.g. the "11"
+part of 3.11).
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 1650d59a3..c79941398 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -325,7 +325,7 @@ def get_console_script_specs(console: Dict[str, str]) -> List[str]:
 
         scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
         # Delete any other versioned pip entry points
-        pip_ep = [k for k in console if re.match(r"pip(\d(\.\d)?)?$", k)]
+        pip_ep = [k for k in console if re.match(r"pip(\d+(\.\d+)?)?$", k)]
         for k in pip_ep:
             del console[k]
     easy_install_script = console.pop("easy_install", None)
@@ -340,7 +340,7 @@ def get_console_script_specs(console: Dict[str, str]) -> List[str]:
         )
         # Delete any other versioned easy_install entry points
         easy_install_ep = [
-            k for k in console if re.match(r"easy_install(-\d\.\d)?$", k)
+            k for k in console if re.match(r"easy_install(-\d+\.\d+)?$", k)
         ]
         for k in easy_install_ep:
             del console[k]


Test patch:
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index 6aec64702..c5a8f3be4 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -3,6 +3,7 @@ import csv
 import logging
 import os
 import pathlib
+import sys
 import textwrap
 from email import message_from_string
 from pathlib import Path
@@ -22,7 +23,11 @@ from pip._internal.models.direct_url import (
 from pip._internal.models.scheme import Scheme
 from pip._internal.operations.build.wheel_legacy import get_legacy_build_wheel_path
 from pip._internal.operations.install import wheel
-from pip._internal.operations.install.wheel import InstalledCSVRow, RecordPath
+from pip._internal.operations.install.wheel import (
+    InstalledCSVRow,
+    RecordPath,
+    get_console_script_specs,
+)
 from pip._internal.utils.compat import WINDOWS
 from pip._internal.utils.misc import hash_file
 from pip._internal.utils.unpacking import unpack_file
@@ -681,3 +686,31 @@ class TestWheelHashCalculators:
         h, length = wheel.rehash(os.fspath(self.test_file))
         assert length == str(self.test_file_len)
         assert h == self.test_file_hash_encoded
+
+
+def test_get_console_script_specs_replaces_python_version(
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    # Fake Python version.
+    monkeypatch.setattr(sys, "version_info", (10, 11))
+
+    entry_points = {
+        "pip": "real_pip",
+        "pip99": "whatever",
+        "pip99.88": "whatever",
+        "easy_install": "real_easy_install",
+        "easy_install-99.88": "whatever",
+        # The following shouldn't be replaced.
+        "not_pip_or_easy_install-99": "whatever",
+        "not_pip_or_easy_install-99.88": "whatever",
+    }
+    specs = get_console_script_specs(entry_points)
+    assert specs == [
+        "pip = real_pip",
+        "pip10 = real_pip",
+        "pip10.11 = real_pip",
+        "easy_install = real_easy_install",
+        "easy_install-10.11 = real_easy_install",
+        "not_pip_or_easy_install-99 = whatever",
+        "not_pip_or_easy_install-99.88 = whatever",
+    ]