-
- Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
setuptools version
setuptools>=60.7.0
Python version
Python 3.x (tested on 3.9 & 3.10)
OS
Any (tested on Linux and MacOS)
Additional environment information
No response
Description
Recent changes in Setuptools, introduced in version 60.7.0, break gevent concurrent networking library. When setuptools>=60.7.0 is installed, gevent is unable to monkey-patch the standard library and freezes in runtime.
There are more detail in gevent/gevent#1865 and the script used in reproduction steps comes from that issue.
Gevent monkey-patches networking and concurrency functions in standard library to replace them with versions that allow other green threads to run when the function is waiting for I/O. This monkey-patching needs to happen before any of the patched modules are loaded.
One of the changes in Setuptools 60.7.0 was using a vendored more_itertools library. This library, in turn, imports concurrent.futures.threading, which is one of the modules monkey-patched by gevent. Gevent code includes a from pkg_resources import iter_entry_points line, which triggers more_itertools import when gevent is imported, which creates a chicken-and-egg problem.
This is fixed by a following quick-and-dirty patch to setuptools, from which I will prepare a proper pull request. I'm sending a bug report first to provide context, because it will require a change in jaraco.text too. Note that this is a proof of concept, not a final proposed change.
git diff
diff --git i/pkg_resources/_vendor/jaraco/functools.py w/pkg_resources/_vendor/jaraco/functools.py index a3fea3a1..e8f47eb8 100644 --- i/pkg_resources/_vendor/jaraco/functools.py +++ w/pkg_resources/_vendor/jaraco/functools.py @@ -5,14 +5,18 @@ import collections import types import itertools -import pkg_resources.extern.more_itertools - from typing import Callable, TypeVar CallableT = TypeVar("CallableT", bound=Callable[..., object]) +def _consume(iterator): + """Consume iterator (itertools recipe)""" + # feed the entire iterator into a zero-length deque + collections.deque(iterator, maxlen=0) + + def compose(*funcs): """ Compose any number of unary functions into a single unary function. @@ -385,7 +389,7 @@ def print_yielded(func): None """ print_all = functools.partial(map, print) - print_results = compose(more_itertools.consume, print_all, func) + print_results = compose(_consume, print_all, func) return functools.wraps(func)(print_results) diff --git i/setuptools/_vendor/jaraco/functools.py w/setuptools/_vendor/jaraco/functools.py index bbd8b29f..e8f47eb8 100644 --- i/setuptools/_vendor/jaraco/functools.py +++ w/setuptools/_vendor/jaraco/functools.py @@ -5,14 +5,18 @@ import collections import types import itertools -import setuptools.extern.more_itertools - from typing import Callable, TypeVar CallableT = TypeVar("CallableT", bound=Callable[..., object]) +def _consume(iterator): + """Consume iterator (itertools recipe)""" + # feed the entire iterator into a zero-length deque + collections.deque(iterator, maxlen=0) + + def compose(*funcs): """ Compose any number of unary functions into a single unary function. @@ -385,7 +389,7 @@ def print_yielded(func): None """ print_all = functools.partial(map, print) - print_results = compose(more_itertools.consume, print_all, func) + print_results = compose(_consume, print_all, func) return functools.wraps(func)(print_results) diff --git i/setuptools/command/build_py.py w/setuptools/command/build_py.py index c3fdc092..a946d1fd 100644 --- i/setuptools/command/build_py.py +++ w/setuptools/command/build_py.py @@ -8,7 +8,7 @@ import io import distutils.errors import itertools import stat -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools.recipes import unique_everseen def make_writable(target): diff --git i/setuptools/command/test.py w/setuptools/command/test.py index 4a389e4d..17bc69ee 100644 --- i/setuptools/command/test.py +++ w/setuptools/command/test.py @@ -19,7 +19,7 @@ from pkg_resources import ( EntryPoint, ) from setuptools import Command -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools.recipes import unique_everseen class ScanningLoader(TestLoader): diff --git i/setuptools/dist.py w/setuptools/dist.py index 733ae14f..ef00e417 100644 --- i/setuptools/dist.py +++ w/setuptools/dist.py @@ -28,7 +28,7 @@ from distutils.util import rfc822_escape from setuptools.extern import packaging from setuptools.extern import ordered_set -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools.recipes import unique_everseen from . import SetuptoolsDeprecationWarning diff --git i/setuptools/msvc.py w/setuptools/msvc.py index 281ea1c2..5908a06e 100644 --- i/setuptools/msvc.py +++ w/setuptools/msvc.py @@ -30,7 +30,7 @@ import itertools import subprocess import distutils.errors from setuptools.extern.packaging.version import LegacyVersion -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools.recipes import unique_everseen from .monkey import get_unpatched diff --git i/setuptools/package_index.py w/setuptools/package_index.py index 051e523a..f85a4b5e 100644 --- i/setuptools/package_index.py +++ w/setuptools/package_index.py @@ -27,7 +27,7 @@ from distutils import log from distutils.errors import DistutilsError from fnmatch import translate from setuptools.wheel import Wheel -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools.recipes import unique_everseen EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')unique_everseenis imported directly from therecipessubmodule which doesn't importconcurrency. I'm not sure it's necessary or that it doesn't importmore_itertools.morevia parent module anyway. This function is 16 lines copied from stdlib documentation, so in final PR I'll probably consider vendoring just this one functionconsumeis one-liner from stdlib documentation (only this branch of upstream implementation'sifis used), so it was pasted directly into source- The
more_itertoolslibrary is the largest part of 60.7.0 diff. It's about 5KLOC, vendored twice (insetuptoolsand inpkg_resources). Out of this amount of code, only two functions are used; one is a one-linerconsumeand the other is 16 lines of code.
The freezes can and should be fixed in gevent (by using importlib.metadata / importlib_metadata on Python 3; if I'm right, this version of Setuptools is Python 3 only, so pkg_resources on Python 2.7 should be safe), but I believe that at the same time it requires a fix on Setuptools side, for the following reasons:
- Minimize side effects of simply importing a library as low-level and fundamental as
setuptools/pkg_resources - Reduce size of codebase by removing second largest vendored dependency
more_itertools(or postprocessing it intools/vendored.pyto remove the problematicmore_itertools.moreand keeping onlymore_itertools.recipes)
Expected behavior
I expected that the test code from "How to Reproduce" section will run.
How to Reproduce
- Install
geventandsetuptools>=60.7.0 - Run the following code
from gevent import monkey import gevent if __name__ == "__main__": monkey.patch_thread() monkey.patch_subprocess() import subprocess from concurrent.futures import ThreadPoolExecutor def func(): # gevent.sleep(0) # uncomment to fail return subprocess.run(["ls", "-l", "/dev/null"], capture_output=True) with ThreadPoolExecutor(max_workers=5) as executor: fut = executor.submit(func) print(fut.result())The script will freeze.
Output
% python3 -m venv ./venv % . ./venv/bin/activate (venv) % pip install -U pip gevent setuptools==60.6.0 Requirement already satisfied: pip in ./venv/lib/python3.9/site-packages (21.3.1) Collecting pip Using cached pip-22.0.3-py3-none-any.whl (2.1 MB) Collecting gevent Using cached gevent-21.12.0-cp39-cp39-macosx_12_0_arm64.whl Collecting setuptools==60.6.0 Using cached setuptools-60.6.0-py3-none-any.whl (953 kB) Collecting greenlet<2.0,>=1.1.0 Using cached greenlet-1.1.2-cp39-cp39-macosx_12_0_arm64.whl Collecting zope.interface Using cached zope.interface-5.4.0-cp39-cp39-macosx_12_0_arm64.whl Collecting zope.event Using cached zope.event-4.5.0-py2.py3-none-any.whl (6.8 kB) Installing collected packages: setuptools, zope.interface, zope.event, greenlet, pip, gevent Attempting uninstall: setuptools Found existing installation: setuptools 60.5.0 Uninstalling setuptools-60.5.0: Successfully uninstalled setuptools-60.5.0 Attempting uninstall: pip Found existing installation: pip 21.3.1 Uninstalling pip-21.3.1: Successfully uninstalled pip-21.3.1 Successfully installed gevent-21.12.0 greenlet-1.1.2 pip-22.0.3 setuptools-60.6.0 zope.event-4.5.0 zope.interface-5.4.0 (venv) % cat test.py from gevent import monkey import gevent if __name__ == "__main__": monkey.patch_thread() monkey.patch_subprocess() import subprocess from concurrent.futures import ThreadPoolExecutor def func(): # gevent.sleep(0) # uncomment to fail return subprocess.run(["ls", "-l", "/dev/null"], capture_output=True) with ThreadPoolExecutor(max_workers=5) as executor: fut = executor.submit(func) print(fut.result()) (venv) % python test.py CompletedProcess(args=['ls', '-l', '/dev/null'], returncode=0, stdout=b'crw-rw-rw- 1 root wheel 0x3000002 Feb 8 17:38 /dev/null\n', stderr=b'') (venv) % pip install -U setuptools Requirement already satisfied: setuptools in ./venv/lib/python3.9/site-packages (60.6.0) Collecting setuptools Using cached setuptools-60.8.1-py3-none-any.whl (1.1 MB) Installing collected packages: setuptools Attempting uninstall: setuptools Found existing installation: setuptools 60.6.0 Uninstalling setuptools-60.6.0: Successfully uninstalled setuptools-60.6.0 Successfully installed setuptools-60.8.1 (venv) % python test.py # Script freezes, interrupted with Ctrl+C ^CException ignored in: <built-in method acquire of _thread.lock object at 0x1037e26f0> Traceback (most recent call last): File "/Users/maciejp/Src/github.com/HealthByRo/tmp/testink/venv/lib/python3.9/site-packages/gevent/os.py", line 431, in fork_and_watch pid = fork() KeyboardInterrupt: Traceback (most recent call last): File "/Users/maciejp/Src/github.com/HealthByRo/tmp/testink/test.py", line 16, in <module> fut = executor.submit(func) File "/opt/homebrew/Cellar/python@3.9/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/thread.py", line 177, in submit return f RuntimeError: release unlocked lock (venv) %