Skip to content
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

All virtualenvs break when Python version is upgraded #146

Closed
raxod502 opened this issue Apr 24, 2019 · 36 comments · Fixed by #246
Closed

All virtualenvs break when Python version is upgraded #146

raxod502 opened this issue Apr 24, 2019 · 36 comments · Fixed by #246

Comments

@raxod502
Copy link

I ran

$ pipx install poetry

just yesterday, and everything worked fine. Then I ran

$ brew upgrade

which upgraded my system Python from 3.7.2 to 3.7.3. Now pipx no longer works:

% poetry --version
zsh: /Users/raxod502/.local/bin/poetry: bad interpreter: /Users/raxod502/.local/pipx/venvs/poetry/bin/python: no such file or directory

% pipx run poetry --version
⚠️  poetry is already on your PATH and installed at /Users/raxod502/.local/bin/poetry. Downloading and running anyway.
Traceback (most recent call last):
  File "/Users/raxod502/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 525, in cli
    exit(run_pipx_command(parsed_pipx_args, binary_args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 141, in run_pipx_command
    use_cache,
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 101, in run
    retval = venv.run_binary(binary, binary_args)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 106, in run_binary
    return _run(cmd, check=False)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 127, in _run
    returncode = subprocess.run(cmd_str_list).returncode
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 472, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/.cache/d7fe01e92227b36/bin/poetry': '/Users/raxod502/.local/pipx/.cache/d7fe01e92227b36/bin/poetry'

% pipx upgrade poetry
Traceback (most recent call last):
  File "/Users/raxod502/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 525, in cli
    exit(run_pipx_command(parsed_pipx_args, binary_args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 184, in run_pipx_command
    include_deps=args.include_deps,
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 213, in upgrade
    old_version = venv.get_venv_metadata_for_package(package).package_version
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 74, in get_venv_metadata_for_package
    stdout=subprocess.PIPE,
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 472, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python'

% pipx list
venvs are in /Users/raxod502/.local/pipx/venvs
binaries are exposed on your $PATH at /Users/raxod502/.local/bin
multiprocessing.pool.RemoteTraceback:
"""
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py", line 121, in worker
    result = (True, func(*args, **kwds))
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py", line 44, in mapstar
    return list(map(*args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 538, in _get_package_summary
    metadata = venv.get_venv_metadata_for_package(package)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 74, in get_venv_metadata_for_package
    stdout=subprocess.PIPE,
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 472, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python'
"""

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/raxod502/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 525, in cli
    exit(run_pipx_command(parsed_pipx_args, binary_args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 187, in run_pipx_command
    commands.list_packages(PIPX_LOCAL_VENVS)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 599, in list_packages
    for package_summary in p.map(_get_package_summary, dirs):
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py", line 268, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py", line 657, in get
    raise self._value
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python'

% pipx install poetry
'poetry' already seems to be installed. Not modifying existing installation in '/Users/raxod502/.local/pipx/venvs/poetry'. Pass '--force' to force installation

% pipx install poetry --force
Installing to existing directory '/Users/raxod502/.local/pipx/venvs/poetry'
Error: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python3.7': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python3.7'
'/usr/local/opt/python/bin/python3.7 -m venv /Users/raxod502/.local/pipx/venvs/poetry' failed

% pipx reinstall-all python3
Traceback (most recent call last):
  File "/Users/raxod502/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 516, in cli
    exit(run_pipx_command(parsed_pipx_args, binary_args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 209, in run_pipx_command
    skip=args.skip,
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 467, in reinstall_all
    uninstall(venv_dir, package, local_bin_dir, verbose)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 426, in uninstall
    metadata = venv.get_venv_metadata_for_package(package)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 85, in get_venv_metadata_for_package
    stdout=subprocess.PIPE,
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 472, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python'

% pipx uninstall-all
Traceback (most recent call last):
  File "/Users/raxod502/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 516, in cli
    exit(run_pipx_command(parsed_pipx_args, binary_args))
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/main.py", line 191, in run_pipx_command
    commands.uninstall_all(PIPX_LOCAL_VENVS, LOCAL_BIN_DIR, verbose)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 449, in uninstall_all
    uninstall(venv_dir, package, local_bin_dir, verbose)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/commands.py", line 426, in uninstall
    metadata = venv.get_venv_metadata_for_package(package)
  File "/Users/raxod502/.local/lib/python/site-packages/pipx/Venv.py", line 85, in get_venv_metadata_for_package
    stdout=subprocess.PIPE,
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 472, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 775, in __init__
    restore_signals, start_new_session)
  File "/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/subprocess.py", line 1522, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/raxod502/.local/pipx/venvs/poetry/bin/python': '/Users/raxod502/.local/pipx/venvs/poetry/bin/python'

I assume the problem is that when pipx creates the virtual environments, it resolves /usr/local/bin/python3 as a symbolic link, which is problematic since the destination of that symlink changes every time Homebrew upgrades Python (thus making the old virtualenv's Python executable no longer work). I have experienced this problem with Pip as well, but presumably pipx could wrap this behavior somehow to make it work, or at least display a useful error message. At the least, it would be nice to have pipx reinstall-all python3 work properly.

Workaround is something like the following:

% mv ~/.local/pipx /tmp/pipx
% ls /tmp/pipx/venvs | xargs -L 1 pipx install

and manually prune any broken symlinks from ~/.local/bin.

@cs01
Copy link
Member

cs01 commented Apr 24, 2019

I wonder if pipx should use the --copies flag when creating venvs? (thoughts @jaraco?)

@jaraco
Copy link
Member

jaraco commented Apr 24, 2019

Personally, I like symlinks and would prefer to keep them. I wonder instead of pipx could have a command to repair venvs after its underlying Python changes. Actually, I guess I'd probably push this upstream and find out what is the best recommendation for any project using venvs to do after a Python upgrade... and then automate that recommendation in pipx.

@cs01
Copy link
Member

cs01 commented Apr 24, 2019

Another fix for the mean time would be to make a symlink where the old version of python was that points to the new one, then reinstall all.

The pipx run command fails because it's trying to reuse a cached virtual environment. Try

pipx run --no-cache poetry --version

@jaraco
Copy link
Member

jaraco commented Apr 24, 2019

I see venv has an --upgrade option, but it stipulates that it's only useful if Python was upgraded in place. I'm not sure what that means.

@cs01
Copy link
Member

cs01 commented Apr 24, 2019

Looks like --upgrade might be they way to go based on this answer. Want to give it a try, @raxod502?

python3 -m venv --upgrade ~/.local/pipx/venvs/poetry

@elgertam
Copy link

I've run into this a few times. My solution has been to manage a python-latest symlink that I point to from the pipx virtualenv. If pipx or any pipx-managed application ever gives me a stacktrace like above, I check the symlink and take an appropriate action.

One thing I've noticed (and even looked through the code a bit to try to resolve) is that either pipx or venv itself will dereference the path to the realpath. If I use my python-latest symlink to create a new tool using pipx, e.g. pipx install --python ~/.pyenv/versions/python-latest/bin/python pycowsay, the python symlink in the virtualenv's bin will point to /Users/user/.pyenv/versions/3.7.3/Python.framework/Versions/3.7/bin/python3.7, assuming ~/.pyenv/versions/python-latest` points to 3.7.3.

I've been meaning to look into exactly how pipx handles this realpath resolution (or if venv does this itself), since it would be nice to have a more stable pipx that doesn't break on random python upgrades. Admittedly, this is rare with pyenv (unlike homebrew now), but I've had it happen before.

@raxod502
Copy link
Author

Looks like --upgrade might be they way to go based on this answer. Want to give it a try, @raxod502?

Doesn't work, unfortunately:

% ~/.pyenv/versions/3.6.7/bin/python3.6 -m venv /tmp/pipx-test
% /tmp/pipx-test/bin/python --version
Python 3.6.7
% mv ~/.pyenv/versions/3.6.7 ~/.pyenv/versions/3.6.7-backup
% /tmp/pipx-test/bin/python --version
zsh: no such file or directory: /tmp/pipx-test/bin/python
% python3 -m venv --upgrade /tmp/pipx-test
Error: [Errno 2] No such file or directory: '/tmp/pipx-test/bin/python3': '/tmp/pipx-test/bin/python3'

I assume that's because the upgrade was not "in-place" (which is never going to be the case with Homebrew).

@ajkerrigan
Copy link

ajkerrigan commented Apr 29, 2019

I ran into this also, and used something like this to repair broken venvs. I don't think it's necessarily the right or best way to handle the problem, or at the very least it would need to be fleshed out a bit (there's no error handling or usage info, sloppily hardcoded to one python version, etc). But it's something that works in a pinch and could be the bones of something smarter.

VENV_DIR="${1:-${HOME}/.local/pipx/venvs}"

# Delete any symlinks that point nowhere
find -L "$VENV_DIR" -type l | xargs rm

# Assume that a directory is a broken virtual environment if
# after the symlink cleanup:
#
# 1. bin/activate exists
# 2. bin/python does not exist
#
# And for those cases, create a fresh virtual environment.
for dir in $(find "$VENV_DIR" -type d -depth 1); do
  if [[ -f "$dir/bin/activate" && -d "$dir/lib/python3.7" && ! -f "$dir/bin/python" ]]; then
    echo "Refreshing Python virtual environment for $dir..."
    python3 -m venv $dir
  fi
done‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

I think this is fundamentally similar to OP's workaround, just operates one level lower (as a venv repair rather than a pipx reinstall).

@AlJohri
Copy link
Contributor

AlJohri commented May 2, 2019

assuming this is specific to homebrew, an alternative solution is to suggest users set export HOMEBREW_NO_INSTALL_CLEANUP=1. Starting from homebrew 2.0.0 this auto cleanup feature was added

details: https://discourse.brew.sh/t/when-did-brew-upgrade-and-reinstall-start-removing-versions/4054

FWIW, all virtualenvs that directly depend on a particular version of a homebrew python will break if that version of python is deleted so this problem isn't specific to pipx

@chrish42
Copy link

chrish42 commented May 8, 2019

I also hit this problem. All of the solutions listed here feel more like workarounds to me. Users shouldn't have to think about this. Anyone understands why the symlink to python3.X is automatically getting expanded, and which bit of code is doing that?

@techdragon
Copy link

techdragon commented Jul 11, 2019

I've just uninstalled the homebrew version of pipx due to this error. I don't know if anyone else noticed the connection but by default pipx choses to create its virtualenvs using the python binary found by sys.executable of the python interpreter currently running pipx. Which combined with the default behaviour of homebrew means that this is going to break... quite often.

It actually surprised me that pipx wasnt using my default current shell's python binary (set by pyenv not homebrew).

If this is going to work reliably with homebrew, it has to use a stable python target, like the system python, a python installed using the python.com installer, or one provided by pyenv. You cant swap virtualenv symlinks around reliably, ( it works sometimes, but don't let that fool you 😉 ) and as @chrish42 pointed out, things like HOMEBREW_NO_INSTALL_CLEANUP=1 are just workarounds, (that in particular is a rather ugly one, that will leave junk around on users machines) so it would be good to have a proper fix.

Edit: Ive just done more digging and it looks like this came up before...
#113 is still open and refers to #17 as why pipx uses sys.executable.

From the comments on those issues, it looks like this issue might require some careful choices about just which python is chosen, and perhaps more explicit warnings about how an environment has to be setup, such as ~/.local/bin coming before ~/.pyenv/shims in a users path for things to work correctly.

@mambocab
Copy link

mambocab commented Aug 8, 2019

I just ran into this issue. I was probably using pipx off-label in that I bootstrapped it by:

  • creating a temporary virtualenv with pyenv-virtualenv
  • installing pipx into that virtualenv, and then running
  • pipx install pipx.

But, this lead to the unpleasant situation of all my pipx executables breaking when I deleted the underlying virtualenv. I'm aware that I did this to myself, but I expected pipx's virtualenvs to behave like those created by the virtualenv tool.

I think that the choice to symlink the interpreter is a misfeature, partly because of its fragility, and partly because it is different from the default behavior of virtualenv. virtualenv is widely-used enough to be a community standard, so I think that "pipx behaves like virtualenv" is a reasonable expectation, and the symlinking behavior breaks that expectation.

That said: if the team wants pipx to continue symlinking by default, I won't object; your opinions about this override mine. However, it would be nice to have an option to copy executables into the pipx-created virtualenvs, mimicking virtualenv's default behavior.

@mwarkentin
Copy link

mwarkentin commented Aug 26, 2019

I just ran into this as well, looks like my homebrew python got upgraded from 3.7.3 to 3.7.4 and broke things:

$ ll /usr/local/Cellar/python/3.7.3/bin/python3.7
ls: /usr/local/Cellar/python/3.7.3/bin/python3.7: No such file or directory
$ ll /usr/local/Cellar/python/3.7.4/
.brew/                 Frameworks/            IDLE 3.app/            INSTALL_RECEIPT.json   LICENSE                Python Launcher 3.app/ README.rst             bin/                   lib/                   libexec/               share/

Following for a solution.. Too bad though, pipx has been super easy to work with up until this point.

@techdragon
Copy link

techdragon commented Sep 18, 2019

I've just though of a particularly aggressive solution to the problem. It requires doing two things.

  • Build pipx with PyOxidizer (or any one of the other equivalent tools like PyInstaller )
  • Have pipx bring its own python. This doesn't need to be a full blown solution like pyenv but Python isnt huge, so why not just have pipx look after its own Python 3.x python "installs" so that it always has a reliable target, and then pipx can be in control of version updates. Just like our existing virtualenv folders, we would have a python version folder, and use these as the primary python versions. while pipx would be built so that it is no longer coupled to a python version and operates as a stand alone executable and doesn't need to look after the virtualenv its running from anymore, which is one of the biggest issues tools like this tend to face.

Thoughts?

@cs01
Copy link
Member

cs01 commented Sep 18, 2019

Using PyOxidizer sounds like it might work, and that would cover the second bullet point automatically.

Things it might make harder:

  • Building and distributing pipx because we would have to build pipx on each OS we want to distribute for. Maybe it's possible to get free CI tools that can do this now, one for mac, linux and windows. It's probably possible to build for linux and windows with Docker.
  • Using an external shared pip package. Right now pipx installs pip to a shared location and occasionally upgrades it. So either the PyOxidizer pipx would have a frozen, vendored pip that would go out of date, or we'd have to find a way to include other paths on our site-packages to manage an up-to-date version of pip somewhere on the user's system. (thoughts, @pfmoore?)

TBH I don't have the time to look into this, but am not opposed to it if someone makes it happen.

@techdragon
Copy link

@cs01 Building on each OS shouldn't be a huge issue, your are right about there being free CI tools to handle this.

As for the question of pip, I was picturing it as part of the normal virtualenv lifecycle management. pipx install some-program will make new virtualenv for "some-program" based on the latest installed "base" version of python that pipx has installed. Then we just use pip in the virtualenv... and updates to pip would be part of the normal pipx upgrade/pipx upgrade-all ... At least thats what I pictured. Am I missing something about how pipx works under the hood that requires the shared/common pip location ?

At the moment, I think the biggest complication/question is where/how we get the python versions. Borrowing the way that pyenv does it would require users to have a compiler toolchain, otherwise we would need to add python builds to the CI toolchain... I've built Python plenty of times over the years, but never for 're-distributing', so there might be some problems I'm not aware of here.

@pfmoore
Copy link
Member

pfmoore commented Sep 19, 2019

@techdragon pipx recently changed to use a single shared copy of pip and setuptools (look for PIPX_SHARED_LIBS in the source) as this speeds up environment creation significantly.

@itsayellow
Copy link
Contributor

itsayellow commented Sep 19, 2019

It seems that reinstall-all breaks when uninstall() uses the venv python to call venv.get_venv_metadata_for_package(package). What if we just cached that data inside of something like pipxrc (#220) in the venv? Then it seems that reinstall-all should be able to proceed without using the venv's python, and reinstall every pipx package after a system python upgrade.

To me this seems like an acceptable recovery of pipx's packages after a system python upgrade. Or at least much better than now.

@techdragon
Copy link

@pfmoore Thanks for filling me in there 😄

@itsayellow Your idea sounds less disruptive than mine would be, so its probably worth exploring that approach first to see if it can fix things.

itsayellow added a commit to itsayellow/pipx that referenced this issue Sep 20, 2019
Fixes pypa#146 by allowing reinstall-all() to call uninstall()
without breaking.
itsayellow added a commit to itsayellow/pipx that referenced this issue Sep 23, 2019
Fixes pypa#146 by allowing reinstall-all() to call uninstall()
without breaking.
itsayellow added a commit to itsayellow/pipx that referenced this issue Sep 24, 2019
This fixes a fatal error that would come with a invalid
python symlink in an existing shared library venv.
This is necessary for reinstall-all to be used successfully
to upgrade a system python (Issue pypa#146)

Since we are creating the shared libraries over again,
it is also just cleaner to allow things to start from
scratch.
@nixbytes
Copy link

nixbytes commented Oct 1, 2019

I use the following and this helps, I can gist this later into a shell script

basically

# add this bash_profile
export PATH="$PATH: $HOME/.local/bin"
export PIPX_HOME="$HOME/.local/pipx"
export PIPX_BIN_DIR="$HOME/.local/bin"

reinstall

ls ~/.local/pipx/venvs/ | xargs -L 1 pipx install 

cs01 pushed a commit that referenced this issue Oct 9, 2019
This fixes a fatal error that would come with a invalid
python symlink in an existing shared library venv.
This is necessary for reinstall-all to be used successfully
to upgrade a system python (Issue #146)

Since we are creating the shared libraries over again,
it is also just cleaner to allow things to start from
scratch.
@cs01
Copy link
Member

cs01 commented Oct 19, 2019

Thanks @Linuxbytes. I personally haven't run into this problem since I don't use pyenv, so I haven't tried your solution. Anyone else?

Another thing to look into is https://pypi.org/project/pipipxx/. It looks like it might help work around this issue. (see the text highlighted in bold below)

Rather than actually installing anything when you run “install”, pipipxx instead builds a temporary virtual environment, installs pipx there, and then uses that pipx to install pipx in your user local space, just like any other pipx-installed tool.

This has two notable side effects:

  1. If you uninstall your pipx-managed pipx, then all of the tools that you installed using that pipx will stop working because their Pythons suddenly point to nothing.
  2. If you want to change the Python used by all of your pipx-managed tools, you only need to reinstall one of them (pipx) rather than reinstalling all of them.

I'd be interested to hear if installing pipx with pipipxx makes upgrading python versions easier.

itsayellow added a commit to itsayellow/pipx that referenced this issue Nov 3, 2019
Fixes pypa#146 by allowing reinstall-all() to call uninstall()
without breaking.
itsayellow added a commit to itsayellow/pipx that referenced this issue Nov 4, 2019
Fixes pypa#146 by allowing reinstall-all() to call uninstall()
without breaking.
@cs01 cs01 closed this as completed in #246 Nov 5, 2019
@rpdelaney
Copy link

I seem to be having this problem after upgrading python from 3.7 to 3.8 on macos via homebrew 2.2.2. I don't have a lot of packages installed with pipx so reinstalling them (via uninstall then install) is not too cumbersome and seems to fix the problem. Still, I am reporting this since perhaps it means the problem is not totally solved. @cs01

@itsayellow
Copy link
Contributor

The current best way to fix a homebrew upgrade of the system python would be:

pipx reinstall-all

If you installed or reinstalled your packages after pipx version 0.15.0.0, then all options, package specs, and injected packages will be remembered and reinstall-all should recreate everything just like you installed them.

@AlJohri
Copy link
Contributor

AlJohri commented Jan 15, 2020

Even when homebrew upgrade's python, it can retain the old version as long as homebrew doesn't clean up.

I think it may be reasonable to request homebrew maintainers in the future to never cleanup certain formulae, such as a python. In the mean time, this environment variable prevents homebrew cleanup: export HOMEBREW_NO_INSTALL_CLEANUP=1

With this enabled, when upgrading to 3.7.6, the 3.7.5 is still available at this path:

/usr/local/Cellar/python/3.7.5/bin/python3
# or
/usr/local/Cellar/python/3.7.5/Frameworks/Python.framework/Versions/3.7/bin/python3.7

If pipx installed via homebrew uses the realpath of python, perhaps the breakage can be avoided?

$ realpath $(which python3)
/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/bin/python3.7
# or /usr/local/Cellar/python/3.7.6_1/bin/python3

@chrish42
Copy link

The solution to this would be to convince the Homebrew people in some way to maintain a symlink for the Python major.minor version (/usr/local/bin/python3.7), and have that exposed as the path, because that's the level at which things don't break if you upgrade Python. Then pipx would use that and not have its paths disappear underneath it upon a bugfix update (3.7.5 -> 3.7.6) of Python, where the later version is meant to be drop-in compatible with the earlier one. A bunch of other Python software would want that, too.

@gaborbernat
Copy link
Contributor

As virtualenv maintainer, I tend to disagree. I think the path forward here will be for pipx shim to automatically check if the home python changed (can just check the venvs pyenv.cfg home key), and if it did trigger a reinstall against the new Python (recreate venv + reinstall packages). @cs01?

@gaborbernat gaborbernat reopened this Jan 15, 2020
@AlJohri
Copy link
Contributor

AlJohri commented Jan 20, 2020

Here's an attempt at making this problem reproducible:

  1. Create venv with python 3.7.1, upgrade to python 3.7.6, verify venv still works ✅

    # uninstall all versions of python 3
    $ brew uninstall --ignore-dependencies python
    $ brew uninstall --force python
    
    # explicitly install python 3.7.1 (1d4f1d5602eac539f4c02b4a82f78b3a3ed5413f)
    $ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/1d4f1d5602eac539f4c02b4a82f78b3a3ed5413f/Formula/python.rb
    
    # ensure python 3.7.1 was installed successfully
    $ which python3 && python3 --version
    /usr/local/bin/python3
    Python 3.7.1
    
    # create venv using python 3.7.1
    $ python3 -m venv .venv
    
    $ .venv/bin/python3 --version && tree -L 3 .venv
    Python 3.7.1
    .venv
    ├── bin
    │   ├── activate
    │   ├── activate.csh
    │   ├── activate.fish
    │   ├── easy_install
    │   ├── easy_install-3.7
    │   ├── pip
    │   ├── pip3
    │   ├── pip3.7
    │   ├── python -> python3
    │   └── python3 -> /usr/local/bin/python3
    ├── include
    ├── lib
    │   └── python3.7
    │       └── site-packages
    └── pyvenv.cfg
    
    5 directories, 11 files
    
    # upgrade to latest python3
    $ brew upgrade python3
    ==> Upgrading 1 outdated package:
    python3 3.7.1 -> 3.7.6_1
    ==> Upgrading python3
    ...
    
    $ .venv/bin/python3 --version
    Python 3.7.6
  1. Install pipx with python 3.7.6, downgrade to python 3.7.1 and verify if pipx created venvs still work. ❌

    # start with the latest pipx and python
    $ brew update && brew upgrade python pipx
    
    # ensure pipx works and a sample tool (clokta) installed via pipx works
    $ pipx --version && pipx list && clokta --version
    
    # uninstall all versions of python 3
    $ brew uninstall --ignore-dependencies python
    $ brew uninstall --force python
    
    # explicitly install python 3.7.1 (1d4f1d5602eac539f4c02b4a82f78b3a3ed5413f)
    $ brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/1d4f1d5602eac539f4c02b4a82f78b3a3ed5413f/Formula/python.rb
    
    # ensure python 3.7.1 was installed successfully
    $ which python3 && python3 --version
    /usr/local/bin/python3
    Python 3.7.1
    
    # ----------------- BREAKS HERE --------------------
    # check if pipx still works
    $ pipx --version && pipx list && clokta --version
    0.15.1.1
    venvs are in /Users/johria/.local/pipx/venvs
    apps are exposed on your $PATH at /Users/johria/.local/bin
       package clokta has invalid interpreter /usr/local/Cellar/python/3.7.6_1/bin/python3.7
       package docx2pdf has invalid interpreter /usr/local/Cellar/python/3.7.6_1/bin/python3.7
       package jupyter has invalid interpreter /usr/local/Cellar/python/3.7.6_1/bin/python3.7
       package poetry has invalid interpreter /usr/local/Cellar/python/3.7.6_1/bin/python3.7
    zsh: /Users/johria/.local/bin/clokta: bad interpreter: /Users/johria/.local/pipx/venvs/clokta/bin/python: no such file or directory
    
    # upgrade to latest python3
    $ brew upgrade python3
    ==> Upgrading 1 outdated package:
    python3 3.7.1 -> 3.7.6_1
    ==> Upgrading python3
    ...
    
    # pipx works again
    $ pipx --version && pipx list && clokta --version

@gaborbernat are you sure its necessary to reinstall all packages for a minor python upgrade? At least in the homebrew context, I think we can make python minor upgrades more pleasant by linking to the correct place. I think your idea makes a lot of sense for major python upgrades though.

@kwrobert
Copy link

Here is a python script I hacked together that will fix all your virtual environments after an upgrade. It seems to work for me, but mileage may vary, some stuff is hardcoded, etc. Use at your own risk:

import os
from functools import lru_cache
from textwrap import dedent


def find_most_recent_homebrew_python(python_base):

    dirs = [
        item
        for item in os.listdir(python_base)
        if os.path.isdir(os.path.join(python_base, item))
    ]
    print("dirs = {}".format(dirs))
    path_to_versions = os.path.join(
        python_base, sorted(dirs)[-1], "Frameworks", "Python.framework", "Versions"
    )
    current_version_link = os.path.join(path_to_versions, "Current")
    current_version = os.path.realpath(current_version_link)
    return os.path.join(path_to_versions, current_version)


@lru_cache(maxsize=None)
def find_location_under_dir(base, location):
    print(f"Searching for location {location} under {base}")
    for (root, dirs, files) in os.walk(base):
        if location in files:
            result = os.path.join(root, location)
            print(f"Found file {location} under {base} at {result}")
            return result
        if location in dirs:
            result = os.path.join(root, location)
            print(f"Found directory {location} under {base} at {result}")
            return result
    print(f"Unable to find location {location} under {base}")
    return False


def main():

    venv_dir = os.path.expanduser("~/.virtualenvs")
    python_base = "/usr/local/Cellar/python"

    python_dir = find_most_recent_homebrew_python(python_base)

    print("python_dir = {}".format(python_dir))

    for (root, dirs, files) in os.walk(venv_dir):
        # print("root = {}".format(root))
        # print("dirs = {}".format(dirs))
        # print("files = {}".format(files))
        abs_files = [os.path.join(root, f) for f in files]
        broken_links = [
            f for f in abs_files if os.path.islink(f) and not os.path.exists(f)
        ]
        for link in broken_links:
            resolved_link = os.path.realpath(link)
            print(f"Found broken link {link} pointing to {resolved_link}")
            base, fname = os.path.split(link)
            new_target = find_location_under_dir(python_dir, fname) 
            # If we found a new target, repoint the link
            if new_target:
                print(f"Repointing {link} to {new_target}")
                os.remove(link)
                os.symlink(new_target, link)
                continue
            # If we didn't, see if we can find the filename of the old link
            missing_base, missing_fname = os.path.split(resolved_link)
            new_target = find_location_under_dir(python_dir, missing_fname) 
            if new_target:
                print(f"Repointing {link} to {new_target}")
                os.remove(link)
                os.symlink(new_target, link)
                continue
            # Require manual intervention here
            else:
                msg = dedent(
                    """
                    Unable to fix broken link {link} pointing to {resolved_link}.
                    You may want to manually intervene here.
                    Press enter to continue:
                    """
                    )
                input(msg)


if __name__ == "__main__":
    main()

@georgexsh
Copy link

georgexsh commented Mar 11, 2020

my simple and dirty workaround is force pipx to re-create its shared virtualenv by deleting it, and reinstall all packages:

rm -rf ~/.local/pipx/shared/
pipx reinstall-all

@polyrand
Copy link

Here is a python script I hacked together that will fix all your virtual environments after an upgrade. It seems to work for me, but mileage may vary, some stuff is hardcoded, etc. Use at your own risk:

import os
from functools import lru_cache
from textwrap import dedent


def find_most_recent_homebrew_python(python_base):

    dirs = [
        item
        for item in os.listdir(python_base)
        if os.path.isdir(os.path.join(python_base, item))
    ]
    print("dirs = {}".format(dirs))
    path_to_versions = os.path.join(
        python_base, sorted(dirs)[-1], "Frameworks", "Python.framework", "Versions"
    )
    current_version_link = os.path.join(path_to_versions, "Current")
    current_version = os.path.realpath(current_version_link)
    return os.path.join(path_to_versions, current_version)


@lru_cache(maxsize=None)
def find_location_under_dir(base, location):
    print(f"Searching for location {location} under {base}")
    for (root, dirs, files) in os.walk(base):
        if location in files:
            result = os.path.join(root, location)
            print(f"Found file {location} under {base} at {result}")
            return result
        if location in dirs:
            result = os.path.join(root, location)
            print(f"Found directory {location} under {base} at {result}")
            return result
    print(f"Unable to find location {location} under {base}")
    return False


def main():

    venv_dir = os.path.expanduser("~/.virtualenvs")
    python_base = "/usr/local/Cellar/python"

    python_dir = find_most_recent_homebrew_python(python_base)

    print("python_dir = {}".format(python_dir))

    for (root, dirs, files) in os.walk(venv_dir):
        # print("root = {}".format(root))
        # print("dirs = {}".format(dirs))
        # print("files = {}".format(files))
        abs_files = [os.path.join(root, f) for f in files]
        broken_links = [
            f for f in abs_files if os.path.islink(f) and not os.path.exists(f)
        ]
        for link in broken_links:
            resolved_link = os.path.realpath(link)
            print(f"Found broken link {link} pointing to {resolved_link}")
            base, fname = os.path.split(link)
            new_target = find_location_under_dir(python_dir, fname) 
            # If we found a new target, repoint the link
            if new_target:
                print(f"Repointing {link} to {new_target}")
                os.remove(link)
                os.symlink(new_target, link)
                continue
            # If we didn't, see if we can find the filename of the old link
            missing_base, missing_fname = os.path.split(resolved_link)
            new_target = find_location_under_dir(python_dir, missing_fname) 
            if new_target:
                print(f"Repointing {link} to {new_target}")
                os.remove(link)
                os.symlink(new_target, link)
                continue
            # Require manual intervention here
            else:
                msg = dedent(
                    """
                    Unable to fix broken link {link} pointing to {resolved_link}.
                    You may want to manually intervene here.
                    Press enter to continue:
                    """
                    )
                input(msg)


if __name__ == "__main__":
    main()

This worked perfectly, thank you! Just had to change my virtualenvs folder in:

venv_dir = os.path.expanduser("~/.virtualenvs")

From "~/.virtualenvs" to"~/.local/pipx/venvs"

@Integralist
Copy link

oh boy. a year after this issue is opened I see that there's been no progress to resolve it without hacky workarounds 😬 in some respects I'm glad because what I thought to be the problem has been confirmed here, but I'm also not gonna worry too much about trying to make this work. I think I don't use this tool enough to justify the effort to side step the problem every time I do a Python upgrade.

I know that sounds kinda snarky, I don't actually mean it to, I'm just honestly not a big enough user of this tool to want to have to mess around like this, to solve a problem such as this. It's good to see there are a few different workarounds at different levels the community of users has provided though (for future travellers to utilize, including myself if I do decide this tool is indepensible to my day-to-day workflow) ❤️

@itsayellow
Copy link
Contributor

oh boy. a year after this issue is opened I see that there's been no progress to resolve it without hacky workarounds 😬 in some respects I'm glad because what I thought to be the problem has been confirmed here, but I'm also not gonna worry too much about trying to make this work. I think I don't use this tool enough to justify the effort to side step the problem every time I do a Python upgrade.

I'm on mac, and homebrew upgrades my python all the time. All my pipx packages are easily fixed when this happens by:

pipx reinstall-all

This workaround doesn't seem too hacky to me.

@Integralist
Copy link

All my pipx packages are easily fixed when this happens by...

When I run pipx reinstall-all python3 I get...

(ins)$ pipx reinstall-all python3
uninstalled unimport! ✨ 🌟 ✨
WARNING: You are using pip version 19.3.1; however, version 20.1.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
  installed package unimport 0.2.62, Python 3.7.7
  These apps are now globally available
    - unimport
done! ✨ 🌟 ✨

Traceback (most recent call last):
  File "/Users/markmcdonnell/.local/bin/pipx", line 10, in <module>
    sys.exit(cli())
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/main.py", line 547, in cli
    exit(run_pipx_command(parsed_pipx_args))
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/main.py", line 213, in run_pipx_command
    commands.reinstall_all(
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/commands.py", line 491, in reinstall_all
    uninstall(venv_dir, package, local_bin_dir, verbose)
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/commands.py", line 450, in uninstall
    metadata = venv.get_venv_metadata_for_package(package)
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/Venv.py", line 160, in get_venv_metadata_for_package
    get_script_output(
  File "/Users/markmcdonnell/.local/lib/python3.8/site-packages/pipx/util.py", line 80, in get_script_output
    output = subprocess.run(
  File "/Users/markmcdonnell/.pyenv/versions/3.8-dev/lib/python3.8/subprocess.py", line 489, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/Users/markmcdonnell/.pyenv/versions/3.8-dev/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/Users/markmcdonnell/.pyenv/versions/3.8-dev/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: '/Users/markmcdonnell/.local/pipx/venvs/autopep8/bin/python'

@itsayellow
Copy link
Contributor

Thanks, @Integralist, this is a bug, but separate from the original issue here. Could you please start a new issue with the above comment?

In addition, could you show what version of pipx you are using? (i.e. the output from pipx --version) The syntax for reinstall-all you are using indicates your version of pipx may be very old. Currently to specify the python executable with reinstall-all the syntax is pipx reinstall-all --python python3.

It may be that your version of pipx is so old that you don't have many of the updates that made reinstall-all work successfully in situations like these. Also updates to pipx have made it fail gracefully to missing python executables.

@cs01
Copy link
Member

cs01 commented Jun 16, 2020

To add on to what @itsayellow said, could you re run the command with the --verbose flag when you create the new issue?

@tddschn
Copy link

tddschn commented Oct 2, 2023

I also need to
replace thon_base = "/usr/local/Cellar/python"
with thon_base = "/usr/local/Cellar/[email protected]"
(depending on the Homebrew Python version you're using and where you installed your Homebrew) for the script to run successfully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.