pipx: All virtualenvs break when Python version is upgraded

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.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 3
  • Comments: 36 (17 by maintainers)

Commits related to this issue

Most upvoted comments

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

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.

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.

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.

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).

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.

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 

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.

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?

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

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.

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()

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?

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… https://github.com/pipxproject/pipx/issues/113 is still open and refers to https://github.com/pipxproject/pipx/issues/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.

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