Skip to content

Commit 0793175

Browse files
author
ever3001
committed
feat(commands): add clean command.
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.
1 parent 255a83f commit 0793175

File tree

7 files changed

+1025
-0
lines changed

7 files changed

+1025
-0
lines changed

changelog.d/1606.feature.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Add `pipx clean` command to remove pipx data.
2+
3+
The new command supports selective cleanup with `--cache`, `--logs`,
4+
`--trash`, and `--venvs` flags to remove specific components. Running
5+
`pipx clean` without flags performs a full cleanup, removing all pipx
6+
data and resetting to a fresh installation state. All cleanup operations
7+
require user confirmation unless the `--force` flag is used. Use
8+
`--verbose` for detailed output including paths and progress information.

docs/examples.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,51 @@ This example pins `pip` (temporarily, until the next automatic upgrade, if that
187187
```shell
188188
> pipx upgrade-shared --pip-args=pip==24.0
189189
```
190+
191+
## `pipx clean` examples
192+
193+
The `clean` command helps you free up disk space by removing pipx data. You can selectively clean specific components or perform a complete cleanup.
194+
195+
### Clean specific components
196+
197+
Remove only the cache (temporary virtual environments from `pipx run`):
198+
199+
```
200+
pipx clean --cache
201+
```
202+
203+
Remove only log files:
204+
205+
```
206+
pipx clean --logs
207+
```
208+
209+
Empty the trash directory:
210+
211+
```
212+
pipx clean --trash
213+
```
214+
215+
Remove all installed packages and their virtual environments:
216+
217+
```
218+
pipx clean --venvs
219+
```
220+
221+
### Clean multiple components
222+
223+
You can combine flags to clean multiple components at once:
224+
225+
```
226+
pipx clean --cache --logs --trash
227+
```
228+
229+
### Full cleanup
230+
231+
Remove **all** pipx data (this will uninstall all packages and reset pipx to a fresh state):
232+
233+
```
234+
pipx clean
235+
```
236+
237+
> ⚠️ **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.

src/pipx/commands/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pipx.commands.clean_cmd import clean
12
from pipx.commands.ensure_path import ensure_pipx_paths
23
from pipx.commands.environment import environment
34
from pipx.commands.inject import inject
@@ -13,6 +14,7 @@
1314
from pipx.commands.upgrade import upgrade, upgrade_all, upgrade_shared
1415

1516
__all__ = [
17+
"clean",
1618
"ensure_pipx_paths",
1719
"environment",
1820
"inject",

src/pipx/commands/clean_cmd.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""Clean command for pipx CLI."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from pipx.colors import bold, red
8+
from pipx.constants import (
9+
EXIT_CODE_CACHE_CLEANUP_FAIL,
10+
EXIT_CODE_FULL_CLEANUP_FAIL,
11+
EXIT_CODE_LOGS_CLEANUP_FAIL,
12+
EXIT_CODE_OK,
13+
EXIT_CODE_TRASH_CLEANUP_FAIL,
14+
EXIT_CODE_VENVS_CLEANUP_FAIL,
15+
ExitCode,
16+
)
17+
from pipx.emojis import hazard, stars
18+
from pipx.paths import ctx
19+
from pipx.util import rmdir
20+
from pipx.venv import VenvContainer
21+
22+
23+
def _cleanup_directory(
24+
path: Path,
25+
description: str,
26+
error_code: ExitCode,
27+
verbose: bool,
28+
) -> ExitCode:
29+
"""
30+
Remove a directory with standardized error handling and output.
31+
32+
Args:
33+
path: Directory path to remove
34+
description: User-friendly description of what's being removed
35+
error_code: Exit code to return on failure
36+
verbose: Whether to print detailed output
37+
38+
Returns:
39+
EXIT_CODE_OK on success, error_code on failure
40+
"""
41+
if not path.exists():
42+
if verbose:
43+
print(f"Skipping {description} (directory doesn't exist)")
44+
return EXIT_CODE_OK
45+
46+
action = f"Removing {description}..."
47+
if verbose:
48+
print(f"{action}")
49+
print(f" Path: {path}")
50+
else:
51+
print(bold(action))
52+
53+
try:
54+
rmdir(path, safe_rm=False)
55+
except Exception as e:
56+
print(f"{red(f'Error removing {description}:')} {e}")
57+
return error_code
58+
59+
print(f"{stars} {description.capitalize()} removed.")
60+
return EXIT_CODE_OK
61+
62+
63+
def _confirm_action(message: str) -> bool:
64+
"""
65+
Prompt user for confirmation.
66+
67+
Args:
68+
message: Question to ask the user
69+
70+
Returns:
71+
True if user confirms, False otherwise
72+
"""
73+
while True:
74+
response = input(f"{message} [y/N]: ").lower().strip()
75+
if response in ("y", "yes"):
76+
return True
77+
if response in ("n", "no", ""):
78+
return False
79+
print("Please answer 'y' or 'n'")
80+
81+
82+
def _full_cleanup(verbose: bool) -> ExitCode:
83+
"""
84+
Remove all pipx data, resetting to first installation state.
85+
86+
This is the nuclear option - removes everything including:
87+
- All installed packages (venvs)
88+
- Cache and temporary data
89+
- Logs
90+
- Trash
91+
- Shared libraries
92+
93+
Args:
94+
verbose: Print detailed output
95+
force: Skip confirmation prompt
96+
"""
97+
print(f"{hazard} {red('WARNING')}: This will remove ALL pipx data!")
98+
print(red("All installed packages will be lost."))
99+
print()
100+
101+
return _cleanup_directory(
102+
path=ctx.home,
103+
description="all pipx data",
104+
error_code=EXIT_CODE_FULL_CLEANUP_FAIL,
105+
verbose=verbose,
106+
)
107+
108+
109+
def _cache_cleanup(verbose: bool) -> ExitCode:
110+
"""Remove cached virtual environments from 'pipx run' commands."""
111+
return _cleanup_directory(
112+
path=ctx.venv_cache,
113+
description="cache and temporary data",
114+
error_code=EXIT_CODE_CACHE_CLEANUP_FAIL,
115+
verbose=verbose,
116+
)
117+
118+
119+
def _logs_cleanup(verbose: bool) -> ExitCode:
120+
"""Remove pipx log files."""
121+
return _cleanup_directory(
122+
path=ctx.logs,
123+
description="logs",
124+
error_code=EXIT_CODE_LOGS_CLEANUP_FAIL,
125+
verbose=verbose,
126+
)
127+
128+
129+
def _trash_cleanup(verbose: bool) -> ExitCode:
130+
"""Remove files in the trash directory."""
131+
return _cleanup_directory(
132+
path=ctx.trash,
133+
description="trash",
134+
error_code=EXIT_CODE_TRASH_CLEANUP_FAIL,
135+
verbose=verbose,
136+
)
137+
138+
139+
def _venvs_cleanup(verbose: bool) -> ExitCode:
140+
"""Remove all installed packages and their virtual environments."""
141+
venv_container = VenvContainer(ctx.venvs)
142+
venv_dirs = list(venv_container.iter_venv_dirs())
143+
144+
if not venv_dirs:
145+
if verbose:
146+
print("No installed packages to remove.")
147+
return EXIT_CODE_OK
148+
149+
print(bold(f"Removing {len(venv_dirs)} installed package(s)..."))
150+
151+
failed = []
152+
for venv_dir in venv_dirs:
153+
package_name = venv_dir.name
154+
try:
155+
if verbose:
156+
print(f" Removing {package_name}...")
157+
rmdir(venv_dir, safe_rm=False)
158+
except Exception as e:
159+
failed.append((package_name, e))
160+
if verbose:
161+
print(f" {red('Failed')}: {e}")
162+
163+
if failed:
164+
print(f"{red(f'Failed to remove {len(failed)} package(s):')}")
165+
for package_name, error in failed:
166+
print(f" - {package_name}: {error}")
167+
return EXIT_CODE_VENVS_CLEANUP_FAIL
168+
169+
print(f"{stars} All installed packages removed.")
170+
return EXIT_CODE_OK
171+
172+
173+
def clean(
174+
cache: bool = False,
175+
logs: bool = False,
176+
trash: bool = False,
177+
venvs: bool = False,
178+
verbose: bool = False,
179+
force: bool = False,
180+
) -> ExitCode:
181+
"""
182+
Clean pipx data directories.
183+
184+
If no specific options are provided, performs a full cleanup removing
185+
all pipx data. Otherwise, removes only the specified components.
186+
187+
Args:
188+
cache: Remove cache and temporary virtual environments
189+
logs: Remove log files
190+
trash: Empty trash directory
191+
venvs: Remove all installed packages
192+
verbose: Print detailed output including paths and progress
193+
194+
Returns:
195+
Combined exit code (0 if all succeeded, non-zero if any failed)
196+
"""
197+
if not force:
198+
if not _confirm_action("Are you sure you want to continue?"):
199+
print("Operation cancelled.")
200+
return EXIT_CODE_OK
201+
# Determine what to clean
202+
any_selected = cache or logs or trash or venvs
203+
204+
if not any_selected:
205+
# No specific options: full cleanup
206+
return _full_cleanup(verbose)
207+
208+
# Selective cleanup: run requested operations
209+
cleanup_operations = []
210+
if cache:
211+
cleanup_operations.append(_cache_cleanup)
212+
if logs:
213+
cleanup_operations.append(_logs_cleanup)
214+
if trash:
215+
cleanup_operations.append(_trash_cleanup)
216+
if venvs:
217+
cleanup_operations.append(_venvs_cleanup)
218+
219+
# Execute all operations and combine exit codes
220+
exit_code = EXIT_CODE_OK
221+
for operation in cleanup_operations:
222+
result = operation(verbose)
223+
exit_code |= result
224+
225+
return exit_code

src/pipx/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
EXIT_CODE_REINSTALL_VENV_NONEXISTENT = ExitCode(1)
2424
EXIT_CODE_REINSTALL_INVALID_PYTHON = ExitCode(1)
2525
EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND = ExitCode(1)
26+
EXIT_CODE_FULL_CLEANUP_FAIL = ExitCode(1)
27+
EXIT_CODE_CACHE_CLEANUP_FAIL = ExitCode(1)
28+
EXIT_CODE_LOGS_CLEANUP_FAIL = ExitCode(1)
29+
EXIT_CODE_TRASH_CLEANUP_FAIL = ExitCode(1)
30+
EXIT_CODE_VENVS_CLEANUP_FAIL = ExitCode(1)
2631

2732

2833
def is_windows() -> bool:

src/pipx/main.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,15 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar
408408
skip=skip_list,
409409
python_flag_passed=python_flag_passed,
410410
)
411+
elif args.command == "clean":
412+
return commands.clean(
413+
cache=args.cache,
414+
logs=args.logs,
415+
trash=args.trash,
416+
venvs=args.venvs,
417+
verbose=verbose,
418+
force=args.force,
419+
)
411420
elif args.command == "runpip":
412421
if not venv_dir:
413422
raise PipxError("Developer error: venv_dir is not defined.")
@@ -733,6 +742,56 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter, shared_parser: arg
733742
p.add_argument("package").completer = venv_completer
734743
add_python_options(p)
735744

745+
def _add_clean(
746+
subparsers: argparse._SubParsersAction,
747+
shared_parser: argparse.ArgumentParser
748+
) -> None:
749+
"""Add the clean command to the parser."""
750+
p = subparsers.add_parser(
751+
"clean",
752+
help="Completely reset pipx to first installation state",
753+
description=textwrap.dedent(
754+
f"""
755+
Removes all installed packages, temporary virtual environments,
756+
and cached data, effectively resetting pipx to its initial state.
757+
The granularity of the cleanup can be controlled using the following options:
758+
--venvs : Remove all installed packages and their virtual environments.
759+
--logs : Remove all pipx log files.
760+
--trash : Empty the pipx trash directory.
761+
--cache : Remove cached data and temporary virtual environments, leaving installed packages intact.
762+
"""
763+
),
764+
parents=[shared_parser],
765+
)
766+
p.add_argument(
767+
"--cache",
768+
action="store_true",
769+
help="Remove cached data and temporary virtual environments, leaving installed packages intact",
770+
)
771+
p.add_argument(
772+
"--logs",
773+
action="store_true",
774+
help="Remove all pipx log files",
775+
)
776+
p.add_argument(
777+
"--trash",
778+
action="store_true",
779+
help="Empty the pipx trash directory",
780+
)
781+
p.add_argument(
782+
"--venvs",
783+
action="store_true",
784+
help="Remove all installed packages and their virtual environments",
785+
)
786+
p.add_argument(
787+
"--force",
788+
"-f",
789+
action="store_true",
790+
help="Skip confirmation prompts (use with caution!)",
791+
)
792+
p.set_defaults(subparser=p)
793+
794+
736795

737796
def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None:
738797
p = subparsers.add_parser(
@@ -994,6 +1053,7 @@ def get_command_parser() -> Tuple[argparse.ArgumentParser, Dict[str, argparse.Ar
9941053
_add_uninstall_all(subparsers, shared_parser)
9951054
_add_reinstall(subparsers, completer_venvs.use, shared_parser)
9961055
_add_reinstall_all(subparsers, shared_parser)
1056+
_add_clean(subparsers, shared_parser)
9971057
_add_list(subparsers, shared_parser)
9981058
subparsers_with_subcommands["interpreter"] = _add_interpreter(subparsers, shared_parser)
9991059
_add_run(subparsers, shared_parser)

0 commit comments

Comments
 (0)