Skip to content

Commit

Permalink
Don't preimport threading early
Browse files Browse the repository at this point in the history
Gevent-based applications need to be able to import threading, socket,
ssl, etc... modules early to be able to monkey-patch them to use
lightweight greenlets instead of OS threads. This patching has to be
done as early as possible in the lifetime of the process

http://www.gevent.org/intro.html#monkey-patching

However starting from caae48b (use import hooks to patch
distutils/setuptools (pypa#1688)) threading became always preimported which,
for example, rendered gpython[1] unusable:

	(py38.virtualenv) kirr@deco:~/tmp/trashme$ gpython
	Traceback (most recent call last):
	  File "/home/kirr/tmp/trashme/py38.virtualenv/bin/gpython", line 8, in <module>
	    sys.exit(main())
	  File "/home/kirr/tmp/trashme/py38.virtualenv/lib/python3.8/site-packages/gpython/__init__.py", line 151, in main
	    raise RuntimeError('gpython: internal error: the following modules are pre-imported, but must be not:'
	RuntimeError: gpython: internal error: the following modules are pre-imported, but must be not:

	        ['threading']

	sys.modules:

	        ['__future__', '__main__', '_abc', '_bootlocale', '_codecs', '_collections', '_collections_abc', '_frozen_importlib', '_frozen_importlib_external', '_functools', '_heapq', '_imp', '_io', '_locale', '_operator', '_signal', '_sitebuiltins', '_sre', '_stat', '_thread', '_virtualenv', '_warnings', '_weakref', '_weakrefset', 'abc', 'builtins', 'codecs', 'collections', 'contextlib', 'copyreg', 'encodings', 'encodings.aliases', 'encodings.latin_1', 'encodings.utf_8', 'enum', 'functools', 'genericpath', 'gpython', 'heapq', 'importlib', 'importlib._bootstrap', 'importlib._bootstrap_external', 'importlib.abc', 'importlib.machinery', 'importlib.util', 'io', 'itertools', 'keyword', 'marshal', 'operator', 'os', 'os.path', 'posix', 'posixpath', 're', 'reprlib', 'site', 'sitecustomize', 'sre_compile', 'sre_constants', 'sre_parse', 'stat', 'sys', 'threading', 'time', 'types', 'warnings', 'zipimport', 'zope']

Fix it by importing threading lazily.

Fixes: pypa#1895

[1] https://pypi.org/project/pygolang/#gpython
  • Loading branch information
navytux committed Jul 14, 2020
1 parent 16f80ac commit 3c3eb2e
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 3 deletions.
23 changes: 20 additions & 3 deletions src/virtualenv/create/via_global_ref/_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,35 @@ def parse_config_files(self, *args, **kwargs):
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
from importlib.abc import MetaPathFinder
from importlib.util import find_spec
from threading import Lock
from functools import partial

class _Finder(MetaPathFinder):
"""A meta path finder that allows patching the imported distutils modules"""

fullname = None
lock = Lock()

# lock[0] is threading.Lock(), but initialized lazily to avoid importing
# threading very early at startup, because there are gevent-based
# applications that need to be first to import threading by themselves.
# See https://github.com/pypa/virtualenv/issues/1895 for details.
lock = []

def find_spec(self, fullname, path, target=None):
if fullname in _DISTUTILS_PATCH and self.fullname is None:
with self.lock:
# initialize lock[0] lazily
if len(self.lock) == 0:
import threading
lock = threading.Lock()
# there is possibility that two threads T1 and T2 are
# simultaneously running into find_spec, observing .lock as
# empty, and further going into hereby initialization.
# However due to the GIL, list.append() operation is atomic
# and this way only one of the threads will "win" to put
# the lock - that every thread will use - into .lock[0].
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
self.lock.append(lock)

with self.lock[0]:
self.fullname = fullname
try:
spec = find_spec(fullname, path)
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/create/test_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,16 @@ def test_zip_importer_can_import_setuptools(tmp_path):
env = os.environ.copy()
env[str("PYTHONPATH")] = str(zip_path)
subprocess.check_call([str(result.creator.exe), "-c", "from setuptools.dist import Distribution"], env=env)


# verify that python in created virtualenv does not preimport threading.
# https://github.com/pypa/virtualenv/issues/1895
def test_no_preimport_threading(tmp_path):
session = cli_run([ensure_text(str(tmp_path))])
out = subprocess.check_output(
[session.creator.exe, "-c", r"import sys; print('\n'.join(sorted(sys.modules)))"],
universal_newlines=True)
preimport = set()
for mod in out.splitlines():
preimport.add(mod)
assert "threading" not in preimport

0 comments on commit 3c3eb2e

Please sign in to comment.