Repository: pypa/setuptools

Issue:
SystemError: Parent module 'setuptools' not loaded, cannot perform relative import with setuptools 50
After upgrading setuptools to 50.0 today, the environment fails to locate the entry points as it could not import distutils

```
$ python --version
Python 3.5.1
$ python -c "import distutils"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 969, in _find_and_load
  File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 666, in _load_unlocked
  File "<frozen importlib._bootstrap>", line 577, in module_from_spec
  File "/home/gchan/tmp/setuptools-python-3.5/lib/python3.5/site-packages/_distutils_hack/__init__.py", line 82, in create_module
    return importlib.import_module('._distutils', 'setuptools')
  File "/home/gchan/tmp/setuptools-python-3.5/lib64/python3.5/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 981, in _gcd_import
  File "<frozen importlib._bootstrap>", line 931, in _sanity_check
SystemError: Parent module 'setuptools' not loaded, cannot perform relative import
```

The issue could not be found in the python 3.8 environment.  


Discussion:
Found the same issue with python 3.6.1 (pyenv)
Same here, Ubuntu 16.04. Seems to be another symptom of #2350.

```
 SystemError: Parent module 'setuptools' not loaded, cannot perform relative import
   File "<frozen importlib._bootstrap>", line 931, in _sanity_check
   File "<frozen importlib._bootstrap>", line 981, in _gcd_import
     return _bootstrap._gcd_import(name[level:], package, level)
   File "/usr/lib/python3.5/importlib/__init__.py", line 126, in import_module
     return importlib.import_module('._distutils', 'setuptools')
   File "/usr/local/lib/python3.5/dist-packages/_distutils_hack/__init__.py", line 82, in create_module
   File "<frozen importlib._bootstrap>", line 577, in module_from_spec
   File "<frozen importlib._bootstrap>", line 666, in _load_unlocked
   File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
   File "<frozen importlib._bootstrap>", line 969, in _find_and_load
     from distutils.version import StrictVersion
   File "/usr/local/lib/python3.5/dist-packages/redis/connection.py", line 2, in <module>
     from redis.connection import (ConnectionPool, UnixDomainSocketConnection,
   File "/usr/local/lib/python3.5/dist-packages/redis/client.py", line 12, in <module>
     from redis.client import Redis, StrictRedis
   File "/usr/local/lib/python3.5/dist-packages/redis/__init__.py", line 1, in <module>
     from redis import Redis
   [...]
```
The same can be said for Matplotlib:
```
main.py:6: in <module>
    import matplotlib
../venv/lib/python3.5/site-packages/matplotlib/__init__.py:120: in <module>
    import distutils.version
<frozen importlib._bootstrap>:969: in _find_and_load
    ???
<frozen importlib._bootstrap>:958: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:666: in _load_unlocked
    ???
<frozen importlib._bootstrap>:577: in module_from_spec
    ???
../venv/lib/python3.5/site-packages/_distutils_hack/__init__.py:82: in create_module
    return importlib.import_module('._distutils', 'setuptools')
../venv/lib/python3.5/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:981: in _gcd_import
    ???
<frozen importlib._bootstrap>:931: in _sanity_check
    ???
E   SystemError: Parent module 'setuptools' not loaded, cannot perform relative import
```
Same issue.
Worked around with downgrading and blacklisting 50.0:
```python
pip install "setuptools!=50.0" --force-reinstall
```
and in `requirements.txt`:
```
setuptools!=50.0
```

The workaround described in the [changelog](https://github.com/pypa/setuptools/blob/17cb9d6bf249cefe653d3bdb712582409035a7db/CHANGES.rst#v5000) worked for me:
```
export SETUPTOOLS_USE_DISTUTILS=stdlib
```
Same issue here with python 3.5.2,

```
❯ pipenv
Traceback (most recent call last):
  File "/home/apinio/.local/bin/pipenv", line 5, in <module>
    from pipenv import cli
  File "/home/apinio/.local/lib/python3.5/site-packages/pipenv/__init__.py", line 55, in <module>
    from .cli import cli
  File "/home/apinio/.local/lib/python3.5/site-packages/pipenv/cli/__init__.py", line 4, in <module>
    from .command import cli    # noqa
  File "/home/apinio/.local/lib/python3.5/site-packages/pipenv/cli/command.py", line 15, in <module>
    from .options import (
  File "/home/apinio/.local/lib/python3.5/site-packages/pipenv/cli/options.py", line 14, in <module>
    from ..utils import is_valid_url
  File "/home/apinio/.local/lib/python3.5/site-packages/pipenv/utils.py", line 16, in <module>
    from distutils.spawn import find_executable
  File "<frozen importlib._bootstrap>", line 969, in _find_and_load
  File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 666, in _load_unlocked
  File "<frozen importlib._bootstrap>", line 577, in module_from_spec
  File "/home/apinio/.local/lib/python3.5/site-packages/_distutils_hack/__init__.py", line 82, in create_module
    return importlib.import_module('._distutils', 'setuptools')
  File "/usr/lib/python3.5/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 981, in _gcd_import
  File "<frozen importlib._bootstrap>", line 931, in _sanity_check
SystemError: Parent module 'setuptools' not loaded, cannot perform relative import
```

Downgrading to `setuptools-49.6.0` fixes the problem.
Can anyone clarify what `SETUPTOOLS_USE_DISTUTILS=stdlib` actually does? I realize it's a workaround, and it's definitely necessary right now for many of us, but how different is it from pre-50 behavior, if at all?

The environment variable is explained in the changelog, along with the change that caused the breakage: https://setuptools.readthedocs.io/en/latest/history.html#v50-0-0 .  But I think the number of "environments or invocations where this behavior is undesirable" was far larger than expected, being basically anybody on Debian, Ubuntu, or any other Debian-derived distribution.
For what it's worth:

1. I'm not terribly new to Python, but the impact of "Once again, Setuptools overrides the stdlib distutils on import." is completely lost on me, especially when it comes to discovering entrypoints.
2. Whatever this "number of environments" is, by far the biggest impact to me is that my organization heavily uses Python (and setuptools entrypoints) inside of Docker images... and _all_ of our images are based on Debian. I'd expect that many people/organizations are in this same situation (given the popularity of Debian as a base for Docker images) but may not have rebuilt images recently enough for the impact to be discovered.
On my side the impact is significant as every environments (by default installed by the latest setuptools) breaks. Many dependencies / libraries directly import distutils for the LooseVersion class. We cannot ask the downstream users to inject the environment variable always as well. I think eventually we should accept that setuptools monkeypatch distutils internally. But I just do not understand 

1. Why does it occur on older Python 3 versions, e.g. python 3.5 and 3.6?

2. Why does setuptools hack the stdlib distutils even when other packages import distutils directly? 

> The environment variable is explained in the changelog, along with the change that caused the breakage: https://setuptools.readthedocs.io/en/latest/history.html#v50-0-0 . But I think the number of "environments or invocations where this behavior is undesirable" was far larger than expected, being basically anybody on Debian, Ubuntu, or any other Debian-derived distribution.

@jmbowman I read it, but I am concerned about the phrase "temporary escape hatch".... this is turning out to be a significant change that'll have a broad negative impact (especially, as @jantman notes, it will impact many who don't regularly rebuild their images and will run into this in the coming days and weeks). The thought that this will soon become impossible to override is worrisome - some reassurance that this won't be so temporary an escape hatch would be appreciated.

I'd be happy just to pin down to < 50.0 in the requirements until this no longer breaks things, but that's **highly discouraged** when it comes to a requirements-based install. I'm not sure there's any other viable workaround *except* for the escape hatch above.

(FWIW I'm @MartyMacGyver above so this is part of the same thread)
> Why does setuptools hack the stdlib distutils even when other packages import distutils directly?

Mainly because of the use-case where a package imports distutils before importing setuptools described in #2259. Probably that should be the issue linked in the changelog. Perhaps the hack could be made more sophisticated to inspect the call stack and only enable the hack when a setup.py is present.

> Why does it occur on older Python 3 versions, e.g. python 3.5 and 3.6?

I'm yet unsure. The issue doesn't occur on all Python 3.6 or 3.5 environments. Can someone suggest a way to replicate the issue, preferably minimally?

>  But I think the number of "environments or invocations where this behavior is undesirable" was far larger than expected, being basically anybody on Debian, Ubuntu, or any other Debian-derived distribution.

Indeed, the impact is undesirably huge. I had gone down the route of attempting to maintain compatibility and incorporate the debian-specific changes, but [decided against that](https://github.com/pypa/setuptools/issues/2232#issuecomment-656850300). However, that was before #2259, which expanded the scope of the hack. Perhaps we should re-open #2232 to address Debian-specific needs.

I'll use this ticket to focus surgically on the reported error.
Using Ubuntu Bionic and Python 3.6, the error does not occur for me:

```
draft $ docker run -it jaraco/multipy-tox python3.6 -m pip-run -q setuptools==50 -- -c "import distutils; print('done')"
WARNING: You are using pip version 19.3.1; however, version 20.2.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
done
```
Similarly on Python 3.5 (unable to reproduce):

```
$ docker run -it jaraco/multipy-tox python3.5 -m pip-run --no-build-isolation -q setuptools==50 -- -c "import distutils; print(distutils
.__file__); import setuptools; print(setuptools.__version__)"
/usr/local/lib/python3.5/dist-packages/setuptools/_distutils/__init__.py
50.0.0
```
Still present in 50.0.1
```
18:55:48         Collecting setuptools!=50.0.0
18:55:48           Downloading setuptools-50.0.1-py3-none-any.whl (784 kB)
18:55:48         
18:55:48         Installing collected packages: setuptools
18:55:48           Attempting uninstall: setuptools
18:55:48             Found existing installation: setuptools 49.6.0
18:55:48             Uninstalling setuptools-49.6.0:
18:55:48               Successfully uninstalled setuptools-49.6.0
18:55:50         Successfully installed setuptools-50.0.1
18:55:50         nox > Command python -cimport sys; from distutils.sysconfig import get_python_lib; sys.stdout.write(get_python_lib()) failed with exit code 1:
18:55:50         Traceback (most recent call last):
18:55:50           File "<string>", line 1, in <module>
18:55:50           File "<frozen importlib._bootstrap>", line 969, in _find_and_load
18:55:50           File "<frozen importlib._bootstrap>", line 958, in _find_and_load_unlocked
18:55:50           File "<frozen importlib._bootstrap>", line 666, in _load_unlocked
18:55:50           File "<frozen importlib._bootstrap>", line 577, in module_from_spec
18:55:50           File "/tmp/kitchen/testing/.nox/runtests-parametrized-3-coverage-true-crypto-m2crypto-transport-zeromq/lib/python3.5/site-packages/_distutils_hack/__init__.py", line 83, in create_module
18:55:50             return importlib.import_module('._distutils', 'setuptools')
18:55:50           File "/usr/lib/python3.5/importlib/__init__.py", line 126, in import_module
18:55:50             return _bootstrap._gcd_import(name[level:], package, level)
18:55:50           File "<frozen importlib._bootstrap>", line 981, in _gcd_import
18:55:50           File "<frozen importlib._bootstrap>", line 931, in _sanity_check
18:55:50         SystemError: Parent module 'setuptools' not loaded, cannot perform relative import
18:55:50         nox > Session runtests-parametrized-3(coverage=True, crypto='m2crypto', transport='zeromq') failed.
```
I think the micro version of Python is key here and is why some folks can't reproduce this issue while others can. Older Python 3.5.x, 3.6.x and 3.7.x versions were affected by https://bugs.python.org/issue30876.

Using an absolute import instead of a relative import at [\_distutils_hack/\_\_init__.py#L83](https://github.com/pypa/setuptools/blob/db378e28cc70ec4ed3c21dfbd255c9ea54a861c8/_distutils_hack/__init__.py#L83) seems to do the trick for Python 3.6.2, i.e.

```python
return importlib.import_module('setuptools._distutils')
```
instead of
```python
return importlib.import_module('._distutils', 'setuptools')
```
I can reproduce this on Python 3.5.3 on a Debian stretch VM, but not on my local Ubuntu with Python 3.5.9 (from the deadsnakes PPA). Form those who've reported reproduces above and included a patch number, they're all 3.5.3 or lower.
Might there be a patch somewhere between Python 3.5.4-3.5.9 which mitigates the issue?
> I think the micro version of Python is key here and is why some folks can't reproduce this issue while others can. Older Python 3.5.x, 3.6.x and 3.7.x versions were affected by https://bugs.python.org/issue30876.
> 
> Using an absolute import instead of a relative import at [_distutils_hack/__init__.py#L83](https://github.com/pypa/setuptools/blob/db378e28cc70ec4ed3c21dfbd255c9ea54a861c8/_distutils_hack/__init__.py#L83) seems to do the trick for Python 3.6.2, i.e.
> 
> ```python
> return importlib.import_module('setuptools._distutils')
> ```
> 
> instead of
> 
> ```python
> return importlib.import_module('._distutils', 'setuptools')
> ```

Applying this fix to my local `_distutils_hack/__init__.py` fixes the issue for me (Python 3.5.3).

Good insight. Sounds like a good mitigation.

Resolving patch:
diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py
--- a/_distutils_hack/__init__.py
+++ b/_distutils_hack/__init__.py
@@ -80,7 +80,7 @@ def spec_for_distutils(self):
         class DistutilsLoader(importlib.abc.Loader):
 
             def create_module(self, spec):
-                return importlib.import_module('._distutils', 'setuptools')
+                return importlib.import_module('setuptools._distutils')
 
             def exec_module(self, module):
                 pass


Test patch: