Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
225 changes: 225 additions & 0 deletions src/pipx/commands/clean_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
"""Clean command for pipx CLI."""

from pathlib import Path

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


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

rmdir(ctx.venvs, safe_rm=True)

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
57 changes: 57 additions & 0 deletions src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,15 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar
skip=skip_list,
python_flag_passed=python_flag_passed,
)
elif args.command == "clean":
return commands.clean(
cache=args.cache,
logs=args.logs,
trash=args.trash,
venvs=args.venvs,
verbose=verbose,
force=args.force,
)
elif args.command == "runpip":
if not venv_dir:
raise PipxError("Developer error: venv_dir is not defined.")
Expand Down Expand Up @@ -734,6 +743,53 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter, shared_parser: arg
add_python_options(p)


def _add_clean(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None:
"""Add the clean command to the parser."""
p = subparsers.add_parser(
"clean",
help="Completely reset pipx to first installation state",
description=textwrap.dedent(
"""
Removes all installed packages, temporary virtual environments,
and cached data, effectively resetting pipx to its initial state.
The granularity of the cleanup can be controlled using the following options:
--venvs : Remove all installed packages and their virtual environments.
--logs : Remove all pipx log files.
--trash : Empty the pipx trash directory.
--cache : Remove cached data and temporary virtual environments, leaving installed packages intact.
"""
),
parents=[shared_parser],
)
p.add_argument(
"--cache",
action="store_true",
help="Remove cached data and temporary virtual environments, leaving installed packages intact",
)
p.add_argument(
"--logs",
action="store_true",
help="Remove all pipx log files",
)
p.add_argument(
"--trash",
action="store_true",
help="Empty the pipx trash directory",
)
p.add_argument(
"--venvs",
action="store_true",
help="Remove all installed packages and their virtual environments",
)
p.add_argument(
"--force",
"-f",
action="store_true",
help="Skip confirmation prompts (use with caution!)",
)
p.set_defaults(subparser=p)
Copy link
Member

Choose a reason for hiding this comment

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

Is this used anywhere else?



def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None:
p = subparsers.add_parser(
"reinstall-all",
Expand Down Expand Up @@ -994,6 +1050,7 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar
_add_uninstall_all(subparsers, shared_parser)
_add_reinstall(subparsers, completer_venvs.use, shared_parser)
_add_reinstall_all(subparsers, shared_parser)
_add_clean(subparsers, shared_parser)
_add_list(subparsers, shared_parser)
subparsers_with_subcommands["interpreter"] = _add_interpreter(subparsers, shared_parser)
_add_run(subparsers, shared_parser)
Expand Down
Loading