pypa / pip

The Python package installer
https://pip.pypa.io/
MIT License
9.48k stars 3.01k forks source link

Misleading error message for broken setuptools #12969

Open tlandschoff-scale opened 2 days ago

tlandschoff-scale commented 2 days ago

Description

tl;dr: pip hides the actual ImportError when importing setuptools, hampering diagnosis. Suggested patch below.

A trivial problem in our setup lead into a lengthy search because of missing information. This can happen for legacy packages without build isolation configured via pyproject.toml. Consider this simple setup.py:

from distutils.core import setup
setup(name="hello_pip", version="0.0", py_modules="hello_pip")

Trying to pip install a package like that caused this error with pip:

$ pip install internal-package
error: subprocess-exited-with-error

× python setup.py egg_info did not run successfully.
│ exit code: 1
╰─> [1 lines of output]
    ERROR: Can not execute `setup.py` since setuptools is not available in the build environment.
    [end of output]

Turns out that in the end, somehow setuptools and jaraco.functools got incompatible in the environment, causing a lengthy error hunt. See below for how to reproduce.

Suggested patch:

diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py
index 96d1b2460..908315593 100644
--- a/src/pip/_internal/utils/setuptools_build.py
+++ b/src/pip/_internal/utils/setuptools_build.py
@@ -21,7 +21,10 @@ _SETUPTOOLS_SHIM = textwrap.dedent(

     try:
         import setuptools
-    except ImportError as error:
+    except ModuleNotFoundError as error:
+        if error.name != "setuptools":
+            raise  # some other module failed to load when importing setuptools
+
         print(
             "ERROR: Can not execute `setup.py` since setuptools is not available in "
             "the build environment.",

Other observations

Installing the same package in a fresh Python environment (which does not have setuptools at call) interestingly worked. To reproduce:

FROM python:3.12
RUN pip uninstall -y setuptools
COPY . /src/
RUN pip install /src/

The output misled me to think that our internal package is actually using build isolation and has a pyproject.toml:

#8 [4/4] RUN pip install /src/
#8 0.823 Processing /src
#8 0.826   Installing build dependencies: started
#8 3.963   Installing build dependencies: finished with status 'done'
#8 3.964   Getting requirements to build wheel: started
#8 4.233   Getting requirements to build wheel: finished with status 'done'
#8 4.234   Preparing metadata (pyproject.toml): started
#8 4.491   Preparing metadata (pyproject.toml): finished with status 'done'
#8 4.495 Building wheels for collected packages: hello_pip
#8 4.497   Building wheel for hello_pip (pyproject.toml): started
#8 4.787   Building wheel for hello_pip (pyproject.toml): finished with status 'done'

Expected behavior

pip should expose the actual error that occurred when trying to import setuptools.

pip version

24.2

Python version

3.12.4

OS

Linux

How to Reproduce

$ cat > setup.py <<EOF
from distutils.core import setup
setup(name="hello_pip", version="0.0", py_modules="hello_pip")
EOF

$ cat > hello_pip.py <<EOF
def hello():
    return "Hello, pip"
EOF

$ cat > Dockerfile <<EOF
FROM python:3.12
RUN pip install pip==24.2 jaraco.functools==3.7 setuptools==75.1.0
COPY . /src/
RUN pip install /src/
EOF

$ docker build --no-cache --progress=plain .
...
ERROR: failed to solve: process "/bin/sh -c pip install /src/" did not complete successfully: exit code: 1

Output

#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 157B done
#1 DONE 0.0s

#2 [internal] load metadata for docker.io/library/python:3.12
#2 DONE 0.0s

#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s

#4 [1/4] FROM docker.io/library/python:3.12
#4 CACHED

#5 [internal] load build context
#5 transferring context: 195B done
#5 DONE 0.0s

#6 [2/4] RUN pip install pip==24.2 jaraco.functools==3.7 setuptools==75.1.0
#6 2.550 Collecting pip==24.2
#6 2.736   Downloading pip-24.2-py3-none-any.whl.metadata (3.6 kB)
#6 2.829 Collecting jaraco.functools==3.7
#6 2.850   Downloading jaraco.functools-3.7.0-py3-none-any.whl.metadata (3.1 kB)
#6 3.322 Collecting setuptools==75.1.0
#6 3.342   Downloading setuptools-75.1.0-py3-none-any.whl.metadata (6.9 kB)
#6 3.450 Collecting more-itertools (from jaraco.functools==3.7)
#6 3.471   Downloading more_itertools-10.5.0-py3-none-any.whl.metadata (36 kB)
#6 3.601 Downloading pip-24.2-py3-none-any.whl (1.8 MB)
#6 5.200    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.8/1.8 MB 1.1 MB/s eta 0:00:00
#6 5.224 Downloading jaraco.functools-3.7.0-py3-none-any.whl (8.1 kB)
#6 5.256 Downloading setuptools-75.1.0-py3-none-any.whl (1.2 MB)
#6 6.355    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 1.2 MB/s eta 0:00:00
#6 6.378 Downloading more_itertools-10.5.0-py3-none-any.whl (60 kB)
#6 6.485    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.0/61.0 kB 603.2 kB/s eta 0:00:00
#6 6.694 Installing collected packages: setuptools, pip, more-itertools, jaraco.functools
#6 6.695   Attempting uninstall: setuptools
#6 6.701     Found existing installation: setuptools 70.0.0
#6 6.734     Uninstalling setuptools-70.0.0:
#6 6.814       Successfully uninstalled setuptools-70.0.0
#6 7.782   Attempting uninstall: pip
#6 7.788     Found existing installation: pip 24.0
#6 7.850     Uninstalling pip-24.0:
#6 8.056       Successfully uninstalled pip-24.0
#6 9.248 Successfully installed jaraco.functools-3.7.0 more-itertools-10.5.0 pip-24.2 setuptools-75.1.0
#6 9.248 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
#6 DONE 9.8s

#7 [3/4] COPY . /src/
#7 DONE 0.0s

#8 [4/4] RUN pip install /src/
#8 0.744 Processing /src
#8 0.746   Preparing metadata (setup.py): started
#8 0.825   Preparing metadata (setup.py): finished with status 'error'
#8 0.832   error: subprocess-exited-with-error
#8 0.832   
#8 0.832   × python setup.py egg_info did not run successfully.
#8 0.832   │ exit code: 1
#8 0.832   ╰─> [1 lines of output]
#8 0.832       ERROR: Can not execute `setup.py` since setuptools is not available in the build environment.
#8 0.832       [end of output]
#8 0.832   
#8 0.832   note: This error originates from a subprocess, and is likely not a problem with pip.
#8 0.877 error: metadata-generation-failed
#8 0.877 
#8 0.877 × Encountered error while generating package metadata.
#8 0.877 ╰─> See above for output.
#8 0.877 
#8 0.877 note: This is an issue with the package mentioned above, not pip.
#8 0.877 hint: See above for details.
#8 ERROR: process "/bin/sh -c pip install /src/" did not complete successfully: exit code: 1
------
 > [4/4] RUN pip install /src/:
0.832       [end of output]
0.832   
0.832   note: This error originates from a subprocess, and is likely not a problem with pip.
0.877 error: metadata-generation-failed
0.877 
0.877 × Encountered error while generating package metadata.
0.877 ╰─> See above for output.
0.877 
0.877 note: This is an issue with the package mentioned above, not pip.
0.877 hint: See above for details.
------
Dockerfile:4
--------------------
   2 |     RUN pip install pip==24.2 jaraco.functools==3.7 setuptools==75.1.0
   3 |     COPY . /src/
   4 | >>> RUN pip install /src/
   5 |     
   6 |     
--------------------
ERROR: failed to solve: process "/bin/sh -c pip install /src/" did not complete successfully: exit code: 1

Code of Conduct

pfmoore commented 2 days ago

Did adding -v (or maybe -vvv) not provide the necessary information?

tlandschoff-scale commented 2 days ago

@pfmoore Obviously it can't because the information was already eaten in the _SETUPTOOLS_SHIM. I also tried using the verbose output of pip when initially digging into this, to no avail.

Here is the output with -vvv:

#8 0.791   Running command python setup.py egg_info
#8 0.863   ERROR: Can not execute `setup.py` since setuptools is not available in the build environment.
#8 0.882   error: subprocess-exited-with-error
#8 0.882   
#8 0.882   × python setup.py egg_info did not run successfully.
#8 0.882   │ exit code: 1
#8 0.882   ╰─> See above for output.
#8 0.882   
#8 0.882   note: This error originates from a subprocess, and is likely not a problem with pip.
#8 0.884   full command: /usr/local/bin/python -c '
#8 0.884   exec(compile('"'"''"'"''"'"'
#8 0.884   # This is <pip-setuptools-caller> -- a caller that pip uses to run setup.py
#8 0.884   #
#8 0.884   # - It imports setuptools before invoking setup.py, to enable projects that directly
#8 0.884   #   import from `distutils.core` to work with newer packaging standards.
#8 0.884   # - It provides a clear error message when setuptools is not installed.
#8 0.884   # - It sets `sys.argv[0]` to the underlying `setup.py`, when invoking `setup.py` so
#8 0.884   #   setuptools doesn'"'"'t think the script is `-c`. This avoids the following warning:
#8 0.884   #     manifest_maker: standard file '"'"'-c'"'"' not found".
#8 0.884   # - It generates a shim setup.py, for handling setup.cfg-only projects.
#8 0.884   import os, sys, tokenize
#8 0.884   
#8 0.884   try:
#8 0.884       import setuptools
#8 0.884   except ImportError as error:
#8 0.884       print(
#8 0.884           "ERROR: Can not execute `setup.py` since setuptools is not available in "
#8 0.884           "the build environment.",
#8 0.884           file=sys.stderr,
#8 0.884       )
#8 0.884       sys.exit(1)
#8 0.884   
#8 0.884   __file__ = %r
#8 0.884   sys.argv[0] = __file__
#8 0.884   
#8 0.884   if os.path.exists(__file__):
#8 0.884       filename = __file__
#8 0.884       with tokenize.open(__file__) as f:
#8 0.884           setup_py_code = f.read()
#8 0.884   else:
#8 0.884       filename = "<auto-generated setuptools caller>"
#8 0.884       setup_py_code = "from setuptools import setup; setup()"
#8 0.884   
#8 0.884   exec(compile(setup_py_code, filename, "exec"))
#8 0.884   '"'"''"'"''"'"' % ('"'"'/src/setup.py'"'"',), "<pip-setuptools-caller>", "exec"))' egg_info --egg-base /tmp/pip-pip-egg-info-9743iszq
#8 0.884   cwd: /src/
#8 0.885   Preparing metadata (setup.py): finished with status 'error'
#8 0.929 Remote version of pip: 24.2
#8 0.930 Local version of pip:  24.2
#8 0.934 Was pip installed by pip? True
#8 0.938 error: metadata-generation-failed
#8 0.938 
#8 0.938 × Encountered error while generating package metadata.
#8 0.938 ╰─> See above for output.
#8 0.938 
#8 0.938 note: This is an issue with the package mentioned above, not pip.
#8 0.938 hint: See above for details.
#8 0.945 Exception information:
#8 0.945 Traceback (most recent call last):
#8 0.945   File "/usr/local/lib/python3.12/site-packages/pip/_internal/operations/build/metadata_legacy.py", line 64, in generate_metadata
#8 0.945     call_subprocess(
#8 0.945   File "/usr/local/lib/python3.12/site-packages/pip/_internal/utils/subprocess.py", line 209, in call_subprocess
#8 0.945     raise error
#8 0.945 pip._internal.exceptions.InstallationSubprocessError: python setup.py egg_info exited with 1
#8 0.945 
#8 0.945 The above exception was the direct cause of the following exception:
...
pfmoore commented 2 days ago

Ah, wait. This is in the legacy build path that uses setuptools when there's no pyproject.toml. I'd suggest you add a pyproject.toml to your project (specifying setuptools as the build backend). The handling (and reporting) of errors should be better in that case.

We're unlikely to do much with the legacy code path at this point, as it's been deprecated for some time.

tlandschoff-scale commented 2 days ago

Actually that is what we did: Add a pyproject.toml. This fixed it for one package, but we have like 30 that are old enough to break in this way. :cry:

Still trying to find a way out of this mess, especially for allowing point releases on old release branches.

Close this if you like, my main goal was to document what might be going on here. Anyway, I think applying the trivial patch can not hurt. YMMV

pfmoore commented 2 days ago

Thanks. I guess the answer here is that if you don't have the resources to update your packages, you should probably be cautious about upgrading pip, as you'll find that newer versions of pip will gradually stop working with out of date packaging practices.

One thing you could try is --use-pep517. That forces the use of the newer code path without requiring you to add a pyproject.toml. It's something of a stopgap, as it was only ever intended as a transitional option, but it might get you out of trouble for now, at least.

tlandschoff-scale commented 2 days ago

Thanks for the pointer. Actually, not pip was the problem here but an updated setuptools, that actually requires a newer version of jaraco.functools.

I only created the issue here because I ran in the wrong direction due to the incomplete error information.

Kudos, Torsten