Skip to content

Commit

Permalink
Merge pull request #12669 from pfmoore/fix_12666
Browse files Browse the repository at this point in the history
Pre-load script wrapper code to avoid errors when pip is being upgraded
  • Loading branch information
pradyunsg committed May 5, 2024
2 parents fa7566d + 8fe5215 commit 5545a15
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 6 deletions.
1 change: 1 addition & 0 deletions news/12666.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix intermittent "cannot locate t64.exe" errors when upgrading pip.
26 changes: 20 additions & 6 deletions src/pip/_vendor/distlib/scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@
sys.exit(%(func)s())
'''

# Pre-fetch the contents of all executable wrapper stubs.
# This is to address https://github.com/pypa/pip/issues/12666.
# When updating pip, we rename the old pip in place before installing the
# new version. If we try to fetch a wrapper *after* that rename, the finder
# machinery will be confused as the package is no longer available at the
# location where it was imported from. So we load everything into memory in
# advance.

# Issue 31: don't hardcode an absolute package name, but
# determine it relative to the current package
distlib_package = __name__.rsplit('.', 1)[0]

WRAPPERS = {
r.name: r.bytes
for r in finder(distlib_package).iterator("")
if r.name.endswith(".exe")
}


def enquote_executable(executable):
if ' ' in executable:
Expand Down Expand Up @@ -409,15 +427,11 @@ def _get_launcher(self, kind):
bits = '32'
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
name = '%s%s%s.exe' % (kind, bits, platform_suffix)
# Issue 31: don't hardcode an absolute package name, but
# determine it relative to the current package
distlib_package = __name__.rsplit('.', 1)[0]
resource = finder(distlib_package).find(name)
if not resource:
if name not in WRAPPERS:
msg = ('Unable to find resource %s in package %s' %
(name, distlib_package))
raise ValueError(msg)
return resource.bytes
return WRAPPERS[name]

# Public API follows

Expand Down
22 changes: 22 additions & 0 deletions tests/functional/test_self_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Check that pip can update itself correctly

from typing import Any


def test_self_update_editable(script: Any, pip_src: Any) -> None:
# Test that if we have an environment with pip installed in non-editable
# mode, that pip can safely update itself to an editable install.
# See https://github.com/pypa/pip/issues/12666 for details.

# Step 1. Install pip as non-editable. This is expected to succeed as
# the existing pip in the environment is installed in editable mode, so
# it only places a .pth file in the environment.
proc = script.pip("install", pip_src)
assert proc.returncode == 0
# Step 2. Using the pip we just installed, install pip *again*, but
# in editable mode. This could fail, as we'll need to uninstall the running
# pip in order to install the new copy, and uninstalling pip while it's
# running could fail. This test is specifically to ensure that doesn't
# happen...
proc = script.pip("install", "-e", pip_src)
assert proc.returncode == 0
47 changes: 47 additions & 0 deletions tools/vendoring/patches/distlib.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py
index cfa45d2af..e16292b83 100644
--- a/src/pip/_vendor/distlib/scripts.py
+++ b/src/pip/_vendor/distlib/scripts.py
@@ -49,6 +49,24 @@ if __name__ == '__main__':
sys.exit(%(func)s())
'''

+# Pre-fetch the contents of all executable wrapper stubs.
+# This is to address https://github.com/pypa/pip/issues/12666.
+# When updating pip, we rename the old pip in place before installing the
+# new version. If we try to fetch a wrapper *after* that rename, the finder
+# machinery will be confused as the package is no longer available at the
+# location where it was imported from. So we load everything into memory in
+# advance.
+
+# Issue 31: don't hardcode an absolute package name, but
+# determine it relative to the current package
+distlib_package = __name__.rsplit('.', 1)[0]
+
+WRAPPERS = {
+ r.name: r.bytes
+ for r in finder(distlib_package).iterator("")
+ if r.name.endswith(".exe")
+}
+

def enquote_executable(executable):
if ' ' in executable:
@@ -409,15 +427,11 @@ class ScriptMaker(object):
bits = '32'
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
name = '%s%s%s.exe' % (kind, bits, platform_suffix)
- # Issue 31: don't hardcode an absolute package name, but
- # determine it relative to the current package
- distlib_package = __name__.rsplit('.', 1)[0]
- resource = finder(distlib_package).find(name)
- if not resource:
+ if name not in WRAPPERS:
msg = ('Unable to find resource %s in package %s' %
(name, distlib_package))
raise ValueError(msg)
- return resource.bytes
+ return WRAPPERS[name]

# Public API follows

0 comments on commit 5545a15

Please sign in to comment.