-
Notifications
You must be signed in to change notification settings - Fork 861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Preserve symlinks when creating virtual environments #8433
Conversation
For posterity (and testing), I used this janky script to test various behaviors: import sys
import subprocess
import os
import tempfile
def create_venv_and_print_info(python_path):
virtualenv = '20.25.0'
uv = True
cargo = False
# Create a temporary directory for the first venv
with tempfile.TemporaryDirectory() as temp_dir:
# Create first venv using subprocess
if cargo:
subprocess.run(['../uv/target/debug/uv', 'venv', '-p', python_path, temp_dir], check=True)
elif uv:
subprocess.run(['uv', 'venv', '-p', python_path, temp_dir], check=True)
elif virtualenv is None:
subprocess.run([python_path, '-m', 'venv', temp_dir], check=True)
else:
subprocess.run(['uvx', f'virtualenv@{virtualenv}', '-p', python_path, temp_dir], check=True)
# Path to the Python binary in the first venv
venv_python = os.path.join(temp_dir, 'bin', 'python')
# Run Python in the nested venv to get the required information
command = f'{venv_python} -c "import sys; print(sys._base_executable); print(sys.base_prefix)"'
result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
# Print nested venv information
with open(os.path.join(temp_dir, 'pyvenv.cfg'), 'r') as f:
for line in f:
if line.startswith('home ='):
print(line.split('=')[1].strip())
break
print(result.stdout.strip().split('\n')[0])
print(result.stdout.strip().split('\n')[1])
# Create a nested temporary directory for the second venv
with tempfile.TemporaryDirectory() as nested_temp_dir:
# Create nested venv using subprocess
if cargo:
subprocess.run(['../uv/target/debug/uv', 'venv', '-p', venv_python, nested_temp_dir], check=True)
elif uv:
subprocess.run(['uv', 'venv', '-p', venv_python, nested_temp_dir], check=True)
elif virtualenv is None:
subprocess.run([venv_python, '-m', 'venv', nested_temp_dir], check=True)
else:
subprocess.run(['uvx', f'virtualenv@{virtualenv}', '-p', venv_python, nested_temp_dir], check=True)
# Path to the Python binary in the nested venv
nested_venv_python = os.path.join(nested_temp_dir, 'bin', 'python')
# Run Python in the nested venv to get the required information
command = f'{nested_venv_python} -c "import sys; print(sys._base_executable); print(sys.base_prefix)"'
result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
# Print nested venv information
with open(os.path.join(nested_temp_dir, 'pyvenv.cfg'), 'r') as f:
for line in f:
if line.startswith('home ='):
print(line.split('=')[1].strip())
break
print(result.stdout.strip().split('\n')[0])
print(result.stdout.strip().split('\n')[1])
if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage: python script.py <path_to_python_interpreter>")
sys.exit(1)
python_interpreter_path = sys.argv[1]
create_venv_and_print_info(python_interpreter_path) |
ee4fd37
to
552870d
Compare
Would the proposed solution break this person's situation? pypa/virtualenv#2682 |
Probably yeah. |
But I think it's the right tradeoff to have the behavior here and break that unusual setup. Per pypa/virtualenv#2770 (comment) though we could also just keep resolving while it's not a "Python environment". |
I was actually just about to link you the same recommendation 🙂 Can we please consider doing that instead? |
I'm not really sure how to detect that. |
cc @pfmoore for visibility |
Ah it seems that's also what someone recommended recently on the corresponding CPython issue: |
I don't fully understand the motivation for that CPython PR. That seems to be recursively resolving symlinks regardless of whether it's a virtual environment or not. Why would that not break the Homebrew case? |
Perhaps it would, I haven't looked at the code so much as I wanted to just bring it to your attention in case it helps in the implementation of Paul's idea. |
I think it still would because it's recursively resolving symlinks for |
(We seem to have this same issue in our test suite, we have symlinks to Pythons that aren't a full "install tree", just an executable.) |
This suggests we should take on some long needed improvements to our mock Python installations in tests. |
I'm not sure... I'm starting to wonder if this is really the right solution. |
Like, I need to figure out how we can know that |
E.g., we could check if |
Other options include...
|
FYI not sure if UV has the same concept but Hatch won't even consider certain paths as eligible for creating virtual environments if they are not considered stable: https://github.com/pypa/hatch/blob/hatch-v1.13.0/src/hatch/env/virtual.py#L411-L430 |
Ah yeah, we don't have such a concept. |
This breaks the goals of creating a stable Python versions directory as described in this internal design. Of course, we can do whatever we want for executables we control. But I was hoping that requirement would help inform the generalized solution. |
If we just want to resolve the linked issues, I think using |
closes #6640 Could you suggest how I should test it? (already tested locally) --------- Co-authored-by: konstin <konstin@mailbox.org> Co-authored-by: Charles Tapley Hoyt <cthoyt@gmail.com> Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
552870d
to
0706998
Compare
This is really complicated and may need to be resolved in the standard library (if at all). I'm tempted to just emulate the standard library behavior for now (use |
Wouldn't maintaining the current behavior cause the least amount of breakage? Not in the sense of UV but overall use. |
Do you mean, least disruption for existing users ? Or does the current behavior do something that you prefer as compared to the standard library? |
I'm just trying to understand, since there is not a great complete solution, which implementation breaks the fewest number of people based on your understanding of use cases in the wild. |
I've not really been following the variations on this, so I'm struggling to keep clear in my mind what's being proposed and what the various behaviours are. But my basic view is that no-one is served by having
The use cases I am aware of here are described in pypa/virtualenv#2682 and pypa/virtualenv#2770. Virtualenv "fixed" 2682, breaking compatibility with
As noted, I only know of 2 cases, and I would also be interested if
To be honest, that seems to me to be by far the safest option. If you do this, the only thing I'd suggest is that you keep an eye on python/cpython#106045 - I see @konstin is active on that issue, though, so you have that covered. One other thing I'll note. The original post here said:
I don't know if that's still the behaviour, but it seems like it would be broken on Windows. Windows virtual environments (at least ones created by |
I think it's very common for people to have their Homebrew virtual environments broken during patch version upgrades, e.g., there are a lot of complaints about this in pipx which I presume uses one of those two libraries for virtual environment creation. From what I understand, if it did not completely resolve the path, it'd use the minor-version directory that Homebrew constructs to avoid this problem and the user experience would be improved. I'm surprised you haven't seen complaints about this, it's a frequent pain point people highlight with Homebrew Python. (Besides this nit, I think I'm pretty well aligned with your summary) |
Thanks for chiming in @pfmoore -- I really appreciate it. I'm in agreement with your summary:
For uv users, using I only have a few other misc. comments to offer:
I think venv actually has the "right" behavior for Homebrew (see sheet), in that it resolves Regarding pypa/virtualenv#2682 and python/cpython#106045: it's true that we'll now suffer from these issues, but I have some suspicion that there's more going on there than just the
We actually already used |
Ok, I'm pursuing those changes in #8481 rather than repurposing this PR. |
From personal experience with Python users, these problems are underreported. I'm for example affected by #1795, but would have never considered reporting it. I also want to reraise python/cpython#114476: To have consensus between tools, we need a documented API with the correct value. |
In #8484, I also found that I needed to use a heuristic to workaround a limitation in the The heuristic I have there, for now, is: if |
f5c86ab
to
a5bce0a
Compare
Closing in favor of #8481. |
## Summary See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
## Summary See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
See extensive discussion in #8433 (comment). This PR brings us into alignment with the standard library by using `sys._base_executable` rather than canonicalizing the executable path. The benefits are primarily for Homebrew, where we'll now resolve to paths like `/opt/homebrew/opt/python@3.12/bin` instead of the undesirable `/opt/homebrew/Cellar/python@3.9/3.9.19_1/Frameworks/Python.framework/Versions/3.9/bin`. Most other users should see no change, though in some cases, nested virtual environments now have slightly different behavior -- namely, they _sometimes_ resolve to the virtual environment Python (at least for Homebrew; not for rtx or uv Pythons though). See [here](https://docs.google.com/spreadsheets/d/1Vw5ClYEjgrBJJhQiwa3cCenIA1GbcRyudYN9NwQaEcM/edit?gid=0#gid=0) for a breakdown. Closes #1640. Closes #1795.
Summary
Historically, when creating a virtual environments on Unix, we used
canonicalize
to resolve all symlinks to find the "base" interpreter. This has some undesirable affects, especially for Homebrew-installed Pythons, because it means that we use the patch version of the interpreter as a base -- so if you then upgrade your Python version with Homebrew, all of your virtual environments break.This PR modifies the behavior as follows:
sys.executable
without resolving anything.This leads to desired behavior for a variety of Pythons. The behaviors of various tools are collated here, with the proposed behavior on the far right. To summarize the results (AFAICT):
venv
uses thesys.executable
when outside a virtual environment, andsys._base_executable
when inside it. (This leads to non-ideal results for nested Homebrew virtual environments, as in the first column on the spreadsheet.)virtualenv
looks mostly like current-uv (it fully resolves the executable).virtualenv
did something else that looks more like this PR.Closes #1640.
Closes #1795.