Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ repos:
- "packaging>=20"
- "platformdirs>=2.1"
- "tomli; python_version < '3.11'"
- "pytest"
# Configuration for codespell is in pyproject.toml
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
Expand Down
8 changes: 8 additions & 0 deletions changelog.d/1606.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Add `pipx clean` command to remove pipx data.

The new command supports selective cleanup with `--cache`, `--logs`,
`--trash`, and `--venvs` flags to remove specific components. Running
`pipx clean` without flags performs a full cleanup, removing all pipx
data and resetting to a fresh installation state. All cleanup operations
require user confirmation unless the `--force` flag is used. Use
`--verbose` for detailed output including paths and progress information.
48 changes: 48 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,51 @@ This example pins `pip` (temporarily, until the next automatic upgrade, if that
```shell
> pipx upgrade-shared --pip-args=pip==24.0
```

## `pipx clean` examples

The `clean` command helps you free up disk space by removing pipx data. You can selectively clean specific components or perform a complete cleanup.

### Clean specific components

Remove only the cache (temporary virtual environments from `pipx run`):

```
pipx clean --cache
```

Remove only log files:

```
pipx clean --logs
```

Empty the trash directory:

```
pipx clean --trash
```

Remove all installed packages and their virtual environments:

```
pipx clean --venvs
```

### Clean multiple components

You can combine flags to clean multiple components at once:

```
pipx clean --cache --logs --trash
```

### Full cleanup

Remove **all** pipx data (this will uninstall all packages and reset pipx to a fresh state):

```
pipx clean
```

> ⚠️ **Warning**: Running `pipx clean` without any flags will remove ALL pipx data, including all installed packages. This operation requires confirmation unless the `--force` flag is used.
2 changes: 2 additions & 0 deletions src/pipx/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pipx.commands.clean_cmd import clean
from pipx.commands.ensure_path import ensure_pipx_paths
from pipx.commands.environment import environment
from pipx.commands.inject import inject
Expand All @@ -13,6 +14,7 @@
from pipx.commands.upgrade import upgrade, upgrade_all, upgrade_shared

__all__ = [
"clean",
"ensure_pipx_paths",
"environment",
"inject",
Expand Down
228 changes: 228 additions & 0 deletions src/pipx/commands/clean_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""Clean command for pipx CLI."""

from __future__ import annotations

from typing import TYPE_CHECKING

from pipx.colors import bold, red
from pipx.constants import (
EXIT_CODE_CACHE_CLEANUP_FAIL,
EXIT_CODE_FULL_CLEANUP_FAIL,
EXIT_CODE_LOGS_CLEANUP_FAIL,
EXIT_CODE_OK,
EXIT_CODE_TRASH_CLEANUP_FAIL,
EXIT_CODE_VENVS_CLEANUP_FAIL,
ExitCode,
)
from pipx.emojis import hazard, stars
from pipx.paths import ctx
from pipx.util import rmdir
from pipx.venv import VenvContainer

if TYPE_CHECKING:
from pathlib import Path


def _cleanup_directory(
path: Path,
description: str,
error_code: ExitCode,
verbose: bool,
) -> ExitCode:
"""
Remove a directory with standardized error handling and output.

Args:
path: Directory path to remove
description: User-friendly description of what's being removed
error_code: Exit code to return on failure
verbose: Whether to print detailed output

Returns:
EXIT_CODE_OK on success, error_code on failure
"""
if not path.exists():
if verbose:
print(f"Skipping {description} (directory doesn't exist)")
return EXIT_CODE_OK

action = f"Removing {description}..."
if verbose:
print(f"{action}")
print(f" Path: {path}")
else:
print(bold(action))

try:
rmdir(path, safe_rm=False)
except Exception as e:
print(f"{red(f'Error removing {description}:')} {e}")
return error_code

print(f"{stars} {description.capitalize()} removed.")
return EXIT_CODE_OK


def _confirm_action(message: str) -> bool:
"""
Prompt user for confirmation.

Args:
message: Question to ask the user

Returns:
True if user confirms, False otherwise
"""
while True:
response = input(f"{message} [y/N]: ").lower().strip()
if response in ("y", "yes"):
return True
if response in ("n", "no", ""):
return False
print("Please answer 'y' or 'n'")


def _full_cleanup(verbose: bool) -> ExitCode:
"""
Remove all pipx data, resetting to first installation state.

This is the nuclear option - removes everything including:
- All installed packages (venvs)
- Cache and temporary data
- Logs
- Trash
- Shared libraries

Args:
verbose: Print detailed output
force: Skip confirmation prompt
"""
print(f"{hazard} {red('WARNING')}: This will remove ALL pipx data!")
print(red("All installed packages will be lost."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the perspective of user, I think these messages should be displayed before the y/N prompt?

print()

return _cleanup_directory(
path=ctx.home,
description="all pipx data",
error_code=EXIT_CODE_FULL_CLEANUP_FAIL,
verbose=verbose,
)


def _cache_cleanup(verbose: bool) -> ExitCode:
"""Remove cached virtual environments from 'pipx run' commands."""
return _cleanup_directory(
path=ctx.venv_cache,
description="cache and temporary data",
error_code=EXIT_CODE_CACHE_CLEANUP_FAIL,
verbose=verbose,
)


def _logs_cleanup(verbose: bool) -> ExitCode:
"""Remove pipx log files."""
return _cleanup_directory(
path=ctx.logs,
description="logs",
error_code=EXIT_CODE_LOGS_CLEANUP_FAIL,
verbose=verbose,
)


def _trash_cleanup(verbose: bool) -> ExitCode:
"""Remove files in the trash directory."""
return _cleanup_directory(
path=ctx.trash,
description="trash",
error_code=EXIT_CODE_TRASH_CLEANUP_FAIL,
verbose=verbose,
)


def _venvs_cleanup(verbose: bool) -> ExitCode:
"""Remove all installed packages and their virtual environments."""
venv_container = VenvContainer(ctx.venvs)
venv_dirs = list(venv_container.iter_venv_dirs())

if not venv_dirs:
if verbose:
print("No installed packages to remove.")
return EXIT_CODE_OK

print(bold(f"Removing {len(venv_dirs)} installed package(s)..."))

failed = []
for venv_dir in venv_dirs:
package_name = venv_dir.name
try:
if verbose:
print(f" Removing {package_name}...")
rmdir(venv_dir, safe_rm=False)
except Exception as e:
failed.append((package_name, e))
if verbose:
print(f" {red('Failed')}: {e}")

if failed:
print(f"{red(f'Failed to remove {len(failed)} package(s):')}")
for package_name, error in failed:
print(f" - {package_name}: {error}")
return EXIT_CODE_VENVS_CLEANUP_FAIL

print(f"{stars} All installed packages removed.")
return EXIT_CODE_OK


def clean(
cache: bool = False,
logs: bool = False,
trash: bool = False,
venvs: bool = False,
verbose: bool = False,
force: bool = False,
) -> ExitCode:
"""
Clean pipx data directories.

If no specific options are provided, performs a full cleanup removing
all pipx data. Otherwise, removes only the specified components.

Args:
cache: Remove cache and temporary virtual environments
logs: Remove log files
trash: Empty trash directory
venvs: Remove all installed packages
verbose: Print detailed output including paths and progress

Returns:
Combined exit code (0 if all succeeded, non-zero if any failed)
"""
if not force:
if not _confirm_action("Are you sure you want to continue?"):
print("Operation cancelled.")
return EXIT_CODE_OK
# Determine what to clean
any_selected = cache or logs or trash or venvs

if not any_selected:
# No specific options: full cleanup
return _full_cleanup(verbose)

# Selective cleanup: run requested operations
cleanup_operations = []
if cache:
cleanup_operations.append(_cache_cleanup)
if logs:
cleanup_operations.append(_logs_cleanup)
if trash:
cleanup_operations.append(_trash_cleanup)
if venvs:
cleanup_operations.append(_venvs_cleanup)

# Execute all operations and combine exit codes
for operation in cleanup_operations:
result: ExitCode = operation(verbose)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ExitCode required by linter?

if result != EXIT_CODE_OK:
return result

return EXIT_CODE_OK
5 changes: 5 additions & 0 deletions src/pipx/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
EXIT_CODE_REINSTALL_VENV_NONEXISTENT = ExitCode(1)
EXIT_CODE_REINSTALL_INVALID_PYTHON = ExitCode(1)
EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND = ExitCode(1)
EXIT_CODE_FULL_CLEANUP_FAIL = ExitCode(1)
EXIT_CODE_CACHE_CLEANUP_FAIL = ExitCode(1)
EXIT_CODE_LOGS_CLEANUP_FAIL = ExitCode(1)
EXIT_CODE_TRASH_CLEANUP_FAIL = ExitCode(1)
EXIT_CODE_VENVS_CLEANUP_FAIL = ExitCode(1)


def is_windows() -> bool:
Expand Down
Loading