Skip to content

Commit

Permalink
Fix issues in windows when pipx manages itself (#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmess1221 authored Sep 26, 2021
1 parent 9601753 commit 6e1907b
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 9 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dev

- Fix to `pipx ensurepath` to fix behavior in user locales other than UTF-8, to fix #644. The internal change is to use userpath v1.6.0 or greater. (#700)
- Fix virtual environment inspection for Python releases that uses an int for its release serial number. (#706)
- Fix PermissionError in windows when pipx manages itself. (#718)

0.16.3

Expand Down
4 changes: 2 additions & 2 deletions src/pipx/commands/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pipx.emojis import hazard, stars
from pipx.package_specifier import parse_specifier_for_install, valid_pypi_name
from pipx.pipx_metadata_file import PackageInfo
from pipx.util import PipxError, mkdir, pipx_wrap, rmdir
from pipx.util import PipxError, mkdir, pipx_wrap, rmdir, safe_unlink
from pipx.venv import Venv

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -93,7 +93,7 @@ def _copy_package_apps(
mkdir(dest.parent)
if dest.exists():
logger.warning(f"{hazard} Overwriting file {str(dest)} with {str(src)}")
dest.unlink()
safe_unlink(dest)
if src.exists():
shutil.copy(src, dest)

Expand Down
16 changes: 14 additions & 2 deletions src/pipx/commands/reinstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
from pipx.commands.inject import inject_dep
from pipx.commands.install import install
from pipx.commands.uninstall import uninstall
from pipx.constants import EXIT_CODE_OK, EXIT_CODE_REINSTALL_VENV_NONEXISTENT, ExitCode
from pipx.emojis import sleep
from pipx.constants import (
EXIT_CODE_OK,
EXIT_CODE_REINSTALL_INVALID_PYTHON,
EXIT_CODE_REINSTALL_VENV_NONEXISTENT,
ExitCode,
)
from pipx.emojis import error, hazard, sleep
from pipx.util import PipxError
from pipx.venv import Venv, VenvContainer

Expand All @@ -22,6 +27,13 @@ def reinstall(
print(f"Nothing to reinstall for {venv_dir.name} {sleep}")
return EXIT_CODE_REINSTALL_VENV_NONEXISTENT

if Path(python).is_relative_to(venv_dir):
print(
f"{error} Error, the python executable would be deleted!",
"Change it using the --python option or PIPX_DEFAULT_PYTHON environment variable.",
)
return EXIT_CODE_REINSTALL_INVALID_PYTHON

venv = Venv(venv_dir, verbose=verbose)

if venv.pipx_metadata.main_package.package_or_url is not None:
Expand Down
4 changes: 2 additions & 2 deletions src/pipx/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
from pipx.emojis import hazard, sleep, stars
from pipx.pipx_metadata_file import PackageInfo
from pipx.util import rmdir
from pipx.util import rmdir, safe_unlink
from pipx.venv import Venv, VenvContainer
from pipx.venv_inspect import VenvMetadata

Expand Down Expand Up @@ -125,7 +125,7 @@ def uninstall(venv_dir: Path, local_bin_dir: Path, verbose: bool) -> ExitCode:

for bin_dir_app_path in bin_dir_app_paths:
try:
bin_dir_app_path.unlink()
safe_unlink(bin_dir_app_path)
except FileNotFoundError:
logger.info(f"tried to remove but couldn't find {bin_dir_app_path}")
else:
Expand Down
2 changes: 2 additions & 0 deletions src/pipx/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
PIPX_LOCAL_VENVS = PIPX_HOME / "venvs"
PIPX_LOG_DIR = PIPX_HOME / "logs"
DEFAULT_PIPX_SHARED_LIBS = PIPX_HOME / "shared"
PIPX_TRASH_DIR = PIPX_HOME / ".trash"
PIPX_SHARED_LIBS = Path(
os.environ.get("PIPX_SHARED_LIBS", DEFAULT_PIPX_SHARED_LIBS)
).resolve()
Expand All @@ -26,6 +27,7 @@
EXIT_CODE_UNINSTALL_VENV_NONEXISTENT = ExitCode(1)
EXIT_CODE_UNINSTALL_ERROR = ExitCode(1)
EXIT_CODE_REINSTALL_VENV_NONEXISTENT = ExitCode(1)
EXIT_CODE_REINSTALL_INVALID_PYTHON = ExitCode(1)

pipx_log_file: Optional[Path] = None

Expand Down
2 changes: 2 additions & 0 deletions src/pipx/emojis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ def use_emojis() -> bool:
if EMOJI_SUPPORT:
stars = "✨ 🌟 ✨"
hazard = "⚠️"
error = "⛔"
sleep = "😴"
else:
stars = ""
hazard = ""
error = ""
sleep = ""
4 changes: 3 additions & 1 deletion src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from pipx.constants import ExitCode
from pipx.emojis import hazard
from pipx.interpreter import DEFAULT_PYTHON
from pipx.util import PipxError, mkdir, pipx_wrap
from pipx.util import PipxError, mkdir, pipx_wrap, rmdir
from pipx.venv import VenvContainer
from pipx.version import __version__

Expand Down Expand Up @@ -721,6 +721,8 @@ def setup(args: argparse.Namespace) -> None:
mkdir(constants.LOCAL_BIN_DIR)
mkdir(constants.PIPX_VENV_CACHEDIR)

rmdir(constants.PIPX_TRASH_DIR, False)

old_pipx_venv_location = constants.PIPX_LOCAL_VENVS / "pipx-app"
if old_pipx_venv_location.exists():
logger.warning(
Expand Down
42 changes: 40 additions & 2 deletions src/pipx/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
import os
import random
import re
import shutil
import string
import subprocess
import sys
import textwrap
Expand All @@ -21,7 +23,7 @@

import pipx.constants
from pipx.animate import show_cursor
from pipx.constants import WINDOWS
from pipx.constants import PIPX_TRASH_DIR, WINDOWS

logger = logging.getLogger(__name__)

Expand All @@ -39,7 +41,17 @@ class RelevantSearch(NamedTuple):
category: str


def rmdir(path: Path) -> None:
def _get_trash_file(path: Path) -> Path:
if not PIPX_TRASH_DIR.is_dir():
PIPX_TRASH_DIR.mkdir()
prefix = "".join(random.choices(string.ascii_lowercase, k=8))
return PIPX_TRASH_DIR / f"{prefix}.{path.name}"


def rmdir(path: Path, safe_rm: bool = True) -> None:
if not path.is_dir():
return

logger.info(f"removing directory {path}")
try:
if WINDOWS:
Expand All @@ -49,6 +61,19 @@ def rmdir(path: Path) -> None:
except FileNotFoundError:
pass

# move it to be deleted later if it still exists
if path.is_dir():
if safe_rm:
logger.warning(
f"Failed to delete {path}. Will move it to a temp folder to delete later."
)

path.rename(_get_trash_file(path))
else:
logger.warning(
f"Failed to delete {path}. You may need to delete it manually."
)


def mkdir(path: Path) -> None:
if path.is_dir():
Expand All @@ -57,6 +82,19 @@ def mkdir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)


def safe_unlink(file: Path) -> None:
# Windows doesn't let us delete or overwrite files that are being run
# But it does let us rename/move it. To get around this issue, we can move
# the file to a temporary folder (to be deleted at a later time)

if not file.is_file():
return
try:
file.unlink()
except PermissionError:
file.rename(_get_trash_file(file))


def get_pypackage_bin_path(binary_name: str) -> Path:
return (
Path("__pypackages__")
Expand Down

0 comments on commit 6e1907b

Please sign in to comment.