mirror of
https://github.com/astral-sh/uv
synced 2026-01-22 22:10:11 -05:00
`_virtualenv.py` doesn't need to import `__future__.annotations`, as it
has none.
Removing the import:
* Restores the action of the VIRTUALENV_PATCH on Python 3.6
* Eliminates 24 lines of error messages displayed by Python 3.6 when it
starts in an environment created by uv:
```plaintext
Error processing line 1 of /tmp/tmp.ENwqZ0oeyb/lib/python3.6/site-packages/_virtualenv.pth:
Traceback (most recent call last):
File "~/.pyenv/versions/3.6.15/lib/python3.6/site.py", line 168, in addpackage
exec(line)
File "<string>", line 1, in <module>
File "/tmp/tmp.ENwqZ0oeyb/lib/python3.6/site-packages/_virtualenv.py", line 3
from __future__ import annotations
^
SyntaxError: future feature annotations is not defined
Remainder of file ignored
```
(Python displays the errors above twice.)
I appreciate the Python team no longer support Python 3.6, but
RedHat-style Linux distributions will support Python 3.6 in their
`/usr/libexec/platform-python` until [releasever 8 expires in
2029](https://access.redhat.com/support/policy/updates/errata#RHEL8_Planning_Guide).
I'm happy for the community to move on, in general, but don't see the
harm in helping those who can't.
I'm not yet sure what in the “remainder of file ignored” is necessary
for my project's build, as I haven't yet finished digging that from
under Hatch. I'll follow up on #6426 when I do, so we can concentrate on
getting to the happy cow.
## Test Plan
```sh
( set -eu
export VIRTUAL_ENV="$(mktemp -d)"
./target/release/uv venv "$VIRTUAL_ENV" --python=python3.6
./target/release/uv pip install cowsay
$VIRTUAL_ENV/bin/python -m cowsay --text 'Look, a talking cow!' )
```
Happy output:
```plaintext
Using Python 3.6.15 interpreter at: ~/.local/bin/python3.6
Creating virtualenv at: /tmp/tmp.VHl4XNi3oI
Activate with: source /tmp//tmp.VHl4XNi3oI/bin/activate
Resolved 1 package in 929ms
Installed 1 package in 17ms
+ cowsay==6.0
____________________
| Look, a talking cow! |
====================
\
\
^__^
(oo)\_______
(__)\ )\/\
||----w |
|| ||
```
---------
Co-authored-by: Zanie Blue <contact@zanie.dev>
102 lines
4.2 KiB
Python
102 lines
4.2 KiB
Python
"""Patches that are applied at runtime to the virtual environment."""
|
|
|
|
import os
|
|
import sys
|
|
|
|
VIRTUALENV_PATCH_FILE = os.path.join(__file__)
|
|
|
|
|
|
def patch_dist(dist):
|
|
"""
|
|
Distutils allows user to configure some arguments via a configuration file:
|
|
https://docs.python.org/3.11/install/index.html#distutils-configuration-files.
|
|
|
|
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
|
|
""" # noqa: D205
|
|
# we cannot allow some install config as that would get packages installed outside of the virtual environment
|
|
old_parse_config_files = dist.Distribution.parse_config_files
|
|
|
|
def parse_config_files(self, *args, **kwargs):
|
|
result = old_parse_config_files(self, *args, **kwargs)
|
|
install = self.get_option_dict("install")
|
|
|
|
if "prefix" in install: # the prefix governs where to install the libraries
|
|
install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
|
|
for base in ("purelib", "platlib", "headers", "scripts", "data"):
|
|
key = f"install_{base}"
|
|
if key in install: # do not allow global configs to hijack venv paths
|
|
install.pop(key, None)
|
|
return result
|
|
|
|
dist.Distribution.parse_config_files = parse_config_files
|
|
|
|
|
|
# Import hook that patches some modules to ignore configuration values that break package installation in case
|
|
# of virtual environments.
|
|
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
|
|
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
|
|
|
|
|
class _Finder:
|
|
"""A meta path finder that allows patching the imported distutils modules."""
|
|
|
|
fullname = None
|
|
|
|
# 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 = [] # noqa: RUF012
|
|
|
|
def find_spec(self, fullname, path, target=None): # noqa: ARG002
|
|
if fullname in _DISTUTILS_PATCH and self.fullname is None:
|
|
# 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)
|
|
|
|
from functools import partial
|
|
from importlib.util import find_spec
|
|
|
|
with self.lock[0]:
|
|
self.fullname = fullname
|
|
try:
|
|
spec = find_spec(fullname, path)
|
|
if spec is not None:
|
|
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
|
|
is_new_api = hasattr(spec.loader, "exec_module")
|
|
func_name = "exec_module" if is_new_api else "load_module"
|
|
old = getattr(spec.loader, func_name)
|
|
func = self.exec_module if is_new_api else self.load_module
|
|
if old is not func:
|
|
try: # noqa: SIM105
|
|
setattr(spec.loader, func_name, partial(func, old))
|
|
except AttributeError:
|
|
pass # C-Extension loaders are r/o such as zipimporter with <3.7
|
|
return spec
|
|
finally:
|
|
self.fullname = None
|
|
return None
|
|
|
|
@staticmethod
|
|
def exec_module(old, module):
|
|
old(module)
|
|
if module.__name__ in _DISTUTILS_PATCH:
|
|
patch_dist(module)
|
|
|
|
@staticmethod
|
|
def load_module(old, name):
|
|
module = old(name)
|
|
if module.__name__ in _DISTUTILS_PATCH:
|
|
patch_dist(module)
|
|
return module
|
|
|
|
|
|
sys.meta_path.insert(0, _Finder())
|