pipu-cli 0.1.dev6__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipu_cli/__init__.py +2 -2
- pipu_cli/cache.py +316 -0
- pipu_cli/cli.py +867 -812
- pipu_cli/config.py +7 -58
- pipu_cli/config_file.py +80 -0
- pipu_cli/output.py +99 -0
- pipu_cli/package_management.py +1145 -0
- pipu_cli/pretty.py +286 -0
- pipu_cli/requirements.py +100 -0
- pipu_cli/rollback.py +110 -0
- pipu_cli-0.2.0.dist-info/METADATA +422 -0
- pipu_cli-0.2.0.dist-info/RECORD +16 -0
- pipu_cli/common.py +0 -4
- pipu_cli/internals.py +0 -819
- pipu_cli/package_constraints.py +0 -2286
- pipu_cli/thread_safe.py +0 -243
- pipu_cli/ui/__init__.py +0 -51
- pipu_cli/ui/apps.py +0 -1460
- pipu_cli/ui/constants.py +0 -19
- pipu_cli/ui/modal_dialogs.py +0 -1375
- pipu_cli/ui/table_widgets.py +0 -345
- pipu_cli/utils.py +0 -169
- pipu_cli-0.1.dev6.dist-info/METADATA +0 -517
- pipu_cli-0.1.dev6.dist-info/RECORD +0 -19
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/WHEEL +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.1.dev6.dist-info → pipu_cli-0.2.0.dist-info}/top_level.txt +0 -0
pipu_cli/cli.py
CHANGED
|
@@ -1,900 +1,955 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import List
|
|
3
|
-
import rich_click as click
|
|
4
|
-
from .internals import list_outdated
|
|
5
|
-
from .package_constraints import read_constraints, read_ignores, read_invalidation_triggers
|
|
6
|
-
from .common import console
|
|
7
|
-
from pip._internal.commands.install import InstallCommand
|
|
1
|
+
"""CLI interface for pipu using rich_click."""
|
|
8
2
|
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Optional
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Clean up: remove the constraint file and unset environment variable
|
|
59
|
-
if constraint_file_path:
|
|
60
|
-
if 'PIP_CONSTRAINT' in os.environ:
|
|
61
|
-
del os.environ['PIP_CONSTRAINT']
|
|
62
|
-
if os.path.exists(constraint_file_path):
|
|
63
|
-
try:
|
|
64
|
-
os.unlink(constraint_file_path)
|
|
65
|
-
except Exception:
|
|
66
|
-
pass # Best effort cleanup
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def launch_tui() -> None:
|
|
70
|
-
"""
|
|
71
|
-
Launch the main TUI interface.
|
|
9
|
+
import rich_click as click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.logging import RichHandler
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from pipu_cli.package_management import (
|
|
16
|
+
inspect_installed_packages,
|
|
17
|
+
get_latest_versions,
|
|
18
|
+
get_latest_versions_parallel,
|
|
19
|
+
resolve_upgradable_packages,
|
|
20
|
+
resolve_upgradable_packages_with_reasons,
|
|
21
|
+
install_packages,
|
|
22
|
+
)
|
|
23
|
+
from pipu_cli.pretty import (
|
|
24
|
+
print_upgradable_packages_table,
|
|
25
|
+
print_upgrade_results,
|
|
26
|
+
print_blocked_packages_table,
|
|
27
|
+
ConsoleStream,
|
|
28
|
+
select_packages_interactively,
|
|
29
|
+
)
|
|
30
|
+
from pipu_cli.output import JsonOutputFormatter
|
|
31
|
+
from pipu_cli.config_file import load_config, get_config_value
|
|
32
|
+
from pipu_cli.config import DEFAULT_CACHE_TTL
|
|
33
|
+
from pipu_cli.cache import (
|
|
34
|
+
is_cache_fresh,
|
|
35
|
+
load_cache,
|
|
36
|
+
save_cache,
|
|
37
|
+
build_cache_from_results,
|
|
38
|
+
get_cache_info,
|
|
39
|
+
format_cache_age,
|
|
40
|
+
get_cache_age_seconds,
|
|
41
|
+
clear_cache,
|
|
42
|
+
clear_all_caches,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Configure rich_click
|
|
47
|
+
click.rich_click.USE_RICH_MARKUP = True
|
|
48
|
+
click.rich_click.SHOW_ARGUMENTS = True
|
|
49
|
+
click.rich_click.GROUP_ARGUMENTS_OPTIONS = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_package_spec(spec: str) -> tuple[str, Optional[str]]:
|
|
53
|
+
"""Parse a package specification like 'requests==2.31.0' or 'requests>=2.30'.
|
|
54
|
+
|
|
55
|
+
:param spec: Package specification string
|
|
56
|
+
:returns: Tuple of (package_name, version_constraint or None)
|
|
72
57
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.print(f"[yellow]🧹 {cleanup_summary}[/yellow]")
|
|
79
|
-
console.print("[dim]Press any key to continue...[/dim]")
|
|
80
|
-
input() # Wait for user acknowledgment before launching TUI
|
|
81
|
-
|
|
82
|
-
from .ui import main_tui_app
|
|
83
|
-
main_tui_app()
|
|
84
|
-
except Exception as e:
|
|
85
|
-
console.print(f"[red]Error launching TUI: {e}[/red]")
|
|
86
|
-
sys.exit(1)
|
|
87
|
-
|
|
58
|
+
# Common specifier patterns (check longest operators first to avoid partial matches)
|
|
59
|
+
for op in ['==', '>=', '<=', '~=', '!=', '>', '<']:
|
|
60
|
+
if op in spec:
|
|
61
|
+
parts = spec.split(op, 1)
|
|
62
|
+
return (parts[0].strip(), op + parts[1].strip())
|
|
88
63
|
|
|
64
|
+
return (spec.strip(), None)
|
|
89
65
|
|
|
90
66
|
|
|
91
67
|
@click.group(invoke_without_command=True)
|
|
92
68
|
@click.pass_context
|
|
93
|
-
def cli(ctx):
|
|
94
|
-
"""
|
|
95
|
-
pipu - Python package updater with constraint management
|
|
96
|
-
|
|
97
|
-
If no command is specified, launches the interactive TUI.
|
|
69
|
+
def cli(ctx: click.Context) -> None:
|
|
98
70
|
"""
|
|
99
|
-
|
|
100
|
-
# No subcommand was invoked, launch the TUI
|
|
101
|
-
launch_tui()
|
|
71
|
+
[bold cyan]pipu[/bold cyan] - A cute Python package updater
|
|
102
72
|
|
|
73
|
+
Automatically checks for package updates and upgrades them with proper
|
|
74
|
+
constraint resolution.
|
|
103
75
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"""
|
|
109
|
-
List outdated packages
|
|
76
|
+
[bold]Commands:[/bold]
|
|
77
|
+
pipu update Refresh package version cache
|
|
78
|
+
pipu upgrade Upgrade packages (default command)
|
|
79
|
+
pipu rollback Restore packages to a previous state
|
|
110
80
|
|
|
111
|
-
|
|
112
|
-
available on the configured package indexes. By default, pre-release versions
|
|
113
|
-
are excluded unless --pre is specified.
|
|
81
|
+
Run [cyan]pipu <command> --help[/cyan] for command-specific help.
|
|
114
82
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
_, _, cleanup_summary = cleanup_invalid_constraints_and_triggers()
|
|
119
|
-
if cleanup_summary:
|
|
120
|
-
console.print(f"[yellow]🧹 {cleanup_summary}[/yellow]")
|
|
121
|
-
|
|
122
|
-
# Read constraints, ignores, and invalidation triggers from configuration
|
|
123
|
-
constraints = read_constraints()
|
|
124
|
-
ignores = read_ignores()
|
|
125
|
-
invalidation_triggers = read_invalidation_triggers()
|
|
126
|
-
|
|
127
|
-
# Set up debug callbacks if debug mode is enabled
|
|
128
|
-
progress_callback = None
|
|
129
|
-
result_callback = None
|
|
130
|
-
if debug:
|
|
131
|
-
def debug_progress(package_name):
|
|
132
|
-
console.print(f"[dim]DEBUG: Checking {package_name}...[/dim]")
|
|
133
|
-
|
|
134
|
-
def debug_callback(package_result):
|
|
135
|
-
pkg_name = package_result.get('name', 'unknown')
|
|
136
|
-
current_ver = package_result.get('version', 'unknown')
|
|
137
|
-
latest_ver = package_result.get('latest_version', 'unknown')
|
|
138
|
-
if current_ver != latest_ver:
|
|
139
|
-
console.print(f"[dim]DEBUG: {pkg_name}: {current_ver} -> {latest_ver}[/dim]")
|
|
140
|
-
else:
|
|
141
|
-
console.print(f"[dim]DEBUG: {pkg_name}: {current_ver} (up-to-date)[/dim]")
|
|
142
|
-
|
|
143
|
-
progress_callback = debug_progress
|
|
144
|
-
result_callback = debug_callback
|
|
145
|
-
|
|
146
|
-
# Use the internals function to get outdated packages and print the table
|
|
147
|
-
outdated_packages = list_outdated(
|
|
148
|
-
console=console,
|
|
149
|
-
print_table=True,
|
|
150
|
-
constraints=constraints,
|
|
151
|
-
ignores=ignores,
|
|
152
|
-
pre=pre,
|
|
153
|
-
progress_callback=progress_callback,
|
|
154
|
-
result_callback=result_callback,
|
|
155
|
-
invalidation_triggers=invalidation_triggers
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
# The function already prints the table, so we just return the data
|
|
159
|
-
return outdated_packages
|
|
160
|
-
|
|
161
|
-
except Exception as e:
|
|
162
|
-
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
163
|
-
sys.exit(1)
|
|
83
|
+
# If no subcommand provided, default to upgrade
|
|
84
|
+
if ctx.invoked_subcommand is None:
|
|
85
|
+
ctx.invoke(upgrade)
|
|
164
86
|
|
|
165
87
|
|
|
166
88
|
@cli.command()
|
|
167
|
-
@click.option(
|
|
168
|
-
|
|
169
|
-
|
|
89
|
+
@click.option(
|
|
90
|
+
"--timeout",
|
|
91
|
+
type=int,
|
|
92
|
+
default=10,
|
|
93
|
+
help="Network timeout in seconds for package queries"
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--pre",
|
|
97
|
+
is_flag=True,
|
|
98
|
+
help="Include pre-release versions"
|
|
99
|
+
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--parallel",
|
|
102
|
+
type=int,
|
|
103
|
+
default=1,
|
|
104
|
+
help="Number of parallel requests for version checking (default: 1)"
|
|
105
|
+
)
|
|
106
|
+
@click.option(
|
|
107
|
+
"--debug",
|
|
108
|
+
is_flag=True,
|
|
109
|
+
help="Enable debug logging"
|
|
110
|
+
)
|
|
111
|
+
@click.option(
|
|
112
|
+
"--output",
|
|
113
|
+
type=click.Choice(["human", "json"]),
|
|
114
|
+
default="human",
|
|
115
|
+
help="Output format (human-readable or json)"
|
|
116
|
+
)
|
|
117
|
+
def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) -> None:
|
|
170
118
|
"""
|
|
171
|
-
|
|
119
|
+
Refresh the package version cache.
|
|
172
120
|
|
|
173
|
-
|
|
174
|
-
|
|
121
|
+
Fetches the latest version information from PyPI for all installed
|
|
122
|
+
packages and stores it locally. This speeds up subsequent upgrade
|
|
123
|
+
commands by avoiding repeated network requests.
|
|
175
124
|
|
|
176
|
-
|
|
125
|
+
[bold]Examples:[/bold]
|
|
126
|
+
pipu update Update cache with defaults
|
|
127
|
+
pipu update --parallel 4 Update with parallel requests
|
|
128
|
+
pipu update --pre Include pre-release versions
|
|
177
129
|
"""
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
130
|
+
console = Console()
|
|
131
|
+
|
|
132
|
+
# Load configuration file
|
|
133
|
+
config = load_config()
|
|
134
|
+
if timeout == 10:
|
|
135
|
+
timeout = get_config_value(config, 'timeout', 10)
|
|
136
|
+
if not pre:
|
|
137
|
+
pre = get_config_value(config, 'pre', False)
|
|
138
|
+
if not debug:
|
|
139
|
+
debug = get_config_value(config, 'debug', False)
|
|
140
|
+
if parallel == 1:
|
|
141
|
+
parallel = get_config_value(config, 'parallel', 1)
|
|
142
|
+
|
|
143
|
+
# Configure logging
|
|
144
|
+
if debug and output != "json":
|
|
145
|
+
logging.basicConfig(
|
|
146
|
+
level=logging.DEBUG,
|
|
147
|
+
format='%(message)s',
|
|
148
|
+
handlers=[RichHandler(console=console, show_time=False, show_path=False, markup=True)]
|
|
193
149
|
)
|
|
150
|
+
logging.getLogger('pip._internal').setLevel(logging.WARNING)
|
|
151
|
+
logging.getLogger('pip._vendor').setLevel(logging.WARNING)
|
|
152
|
+
console.print("[dim]Debug mode enabled[/dim]\n")
|
|
194
153
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
console.print("[bold red]⚠ Constraint Violations Detected![/bold red]")
|
|
225
|
-
console.print(get_constraint_violation_summary(invalidated_constraints))
|
|
226
|
-
|
|
227
|
-
# Filter out packages that would violate constraints
|
|
228
|
-
violating_package_names = set()
|
|
229
|
-
for violators in invalidated_constraints.values():
|
|
230
|
-
violating_package_names.update(pkg.lower() for pkg in violators)
|
|
231
|
-
|
|
232
|
-
# Keep only safe packages
|
|
233
|
-
original_count = len(packages_to_update)
|
|
234
|
-
packages_to_update = [
|
|
235
|
-
pkg for pkg in packages_to_update
|
|
236
|
-
if pkg['name'].lower() not in violating_package_names
|
|
237
|
-
]
|
|
238
|
-
|
|
239
|
-
blocked_count = original_count - len(packages_to_update)
|
|
240
|
-
|
|
241
|
-
if packages_to_update:
|
|
242
|
-
console.print(f"\n[yellow]Proceeding with {len(packages_to_update)} packages that don't violate constraints.")
|
|
243
|
-
console.print(f"Blocked {blocked_count} packages due to constraint violations.[/yellow]")
|
|
154
|
+
try:
|
|
155
|
+
# Step 1: Inspect installed packages
|
|
156
|
+
if output != "json":
|
|
157
|
+
console.print("[bold]Step 1/3:[/bold] Inspecting installed packages...")
|
|
158
|
+
|
|
159
|
+
step1_start = time.time()
|
|
160
|
+
if output != "json":
|
|
161
|
+
with Progress(
|
|
162
|
+
SpinnerColumn(),
|
|
163
|
+
TextColumn("[progress.description]{task.description}"),
|
|
164
|
+
console=console,
|
|
165
|
+
transient=True
|
|
166
|
+
) as progress:
|
|
167
|
+
task = progress.add_task("Loading packages...", total=None)
|
|
168
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
169
|
+
progress.update(task, completed=True)
|
|
170
|
+
else:
|
|
171
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
172
|
+
step1_time = time.time() - step1_start
|
|
173
|
+
|
|
174
|
+
num_installed = len(installed_packages)
|
|
175
|
+
if output != "json":
|
|
176
|
+
console.print(f" Found {num_installed} installed packages")
|
|
177
|
+
if debug:
|
|
178
|
+
console.print(f" [dim]Time: {step1_time:.2f}s[/dim]")
|
|
179
|
+
|
|
180
|
+
if not installed_packages:
|
|
181
|
+
if output == "json":
|
|
182
|
+
print('{"error": "No packages found"}')
|
|
244
183
|
else:
|
|
245
|
-
console.print(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
184
|
+
console.print("[yellow]No packages found.[/yellow]")
|
|
185
|
+
sys.exit(0)
|
|
186
|
+
|
|
187
|
+
# Step 2: Check for updates
|
|
188
|
+
if output != "json":
|
|
189
|
+
console.print("\n[bold]Step 2/3:[/bold] Fetching latest versions...")
|
|
190
|
+
|
|
191
|
+
step2_start = time.time()
|
|
192
|
+
if output != "json":
|
|
193
|
+
with Progress(
|
|
194
|
+
TextColumn("[progress.description]{task.description}"),
|
|
195
|
+
BarColumn(),
|
|
196
|
+
TaskProgressColumn(),
|
|
197
|
+
console=console,
|
|
198
|
+
transient=True
|
|
199
|
+
) as progress:
|
|
200
|
+
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
201
|
+
|
|
202
|
+
def update_progress(current, total):
|
|
203
|
+
progress.update(task, completed=current)
|
|
204
|
+
|
|
205
|
+
if parallel > 1:
|
|
206
|
+
latest_versions = get_latest_versions_parallel(
|
|
207
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
208
|
+
max_workers=parallel, progress_callback=update_progress
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
latest_versions = get_latest_versions(
|
|
212
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
213
|
+
progress_callback=update_progress
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
if parallel > 1:
|
|
217
|
+
latest_versions = get_latest_versions_parallel(
|
|
218
|
+
installed_packages, timeout=timeout, include_prereleases=pre, max_workers=parallel
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
latest_versions = get_latest_versions(
|
|
222
|
+
installed_packages, timeout=timeout, include_prereleases=pre
|
|
223
|
+
)
|
|
224
|
+
step2_time = time.time() - step2_start
|
|
225
|
+
|
|
226
|
+
num_updates = len(latest_versions)
|
|
227
|
+
if output != "json":
|
|
228
|
+
console.print(f" Found {num_updates} packages with newer versions available")
|
|
229
|
+
if debug:
|
|
230
|
+
console.print(f" [dim]Time: {step2_time:.2f}s[/dim]")
|
|
231
|
+
|
|
232
|
+
# Step 3: Resolve and cache
|
|
233
|
+
if output != "json":
|
|
234
|
+
console.print("\n[bold]Step 3/3:[/bold] Resolving constraints and saving cache...")
|
|
235
|
+
|
|
236
|
+
step3_start = time.time()
|
|
237
|
+
all_upgradable = resolve_upgradable_packages(latest_versions, installed_packages)
|
|
238
|
+
upgradable_packages = [pkg for pkg in all_upgradable if pkg.upgradable]
|
|
239
|
+
step3_time = time.time() - step3_start
|
|
240
|
+
|
|
241
|
+
# Build and save cache
|
|
242
|
+
cache_data = build_cache_from_results(installed_packages, latest_versions, upgradable_packages)
|
|
243
|
+
cache_path = save_cache(cache_data)
|
|
244
|
+
|
|
245
|
+
num_upgradable = len(upgradable_packages)
|
|
246
|
+
|
|
247
|
+
if output == "json":
|
|
248
|
+
result = {
|
|
249
|
+
"status": "success",
|
|
250
|
+
"packages_checked": num_installed,
|
|
251
|
+
"packages_with_updates": num_updates,
|
|
252
|
+
"packages_upgradable": num_upgradable,
|
|
253
|
+
"cache_path": str(cache_path)
|
|
254
|
+
}
|
|
255
|
+
print(json.dumps(result, indent=2))
|
|
256
|
+
else:
|
|
257
|
+
console.print(f" {num_upgradable} packages can be safely upgraded")
|
|
258
|
+
if debug:
|
|
259
|
+
console.print(f" [dim]Time: {step3_time:.2f}s[/dim]")
|
|
260
|
+
console.print(f" [dim]Cache saved to: {cache_path}[/dim]")
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
console.print("[bold green]✓ All packages updated successfully![/bold green]")
|
|
262
|
+
console.print("\n[bold green]Package list updated![/bold green] Run [cyan]pipu upgrade[/cyan] to upgrade your packages.")
|
|
262
263
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
total_time = step1_time + step2_time + step3_time
|
|
265
|
+
if debug:
|
|
266
|
+
console.print(f"[dim]Total time: {total_time:.2f}s[/dim]")
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
console.print("[bold red]✗ Some packages failed to update.[/bold red]")
|
|
269
|
-
sys.exit(exit_code)
|
|
268
|
+
sys.exit(0)
|
|
270
269
|
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
console.print("\n[yellow]Interrupted by user.[/yellow]")
|
|
272
|
+
sys.exit(130)
|
|
271
273
|
except Exception as e:
|
|
272
|
-
|
|
274
|
+
if output == "json":
|
|
275
|
+
print(json.dumps({"error": str(e)}))
|
|
276
|
+
else:
|
|
277
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
273
278
|
sys.exit(1)
|
|
274
279
|
|
|
275
280
|
|
|
276
281
|
@cli.command()
|
|
277
|
-
@click.argument('
|
|
278
|
-
@click.option(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
@click.argument('packages', nargs=-1)
|
|
283
|
+
@click.option(
|
|
284
|
+
"--timeout",
|
|
285
|
+
type=int,
|
|
286
|
+
default=10,
|
|
287
|
+
help="Network timeout in seconds for package queries"
|
|
288
|
+
)
|
|
289
|
+
@click.option(
|
|
290
|
+
"--pre",
|
|
291
|
+
is_flag=True,
|
|
292
|
+
help="Include pre-release versions"
|
|
293
|
+
)
|
|
294
|
+
@click.option(
|
|
295
|
+
"--yes", "-y",
|
|
296
|
+
is_flag=True,
|
|
297
|
+
help="Automatically confirm upgrade without prompting"
|
|
298
|
+
)
|
|
299
|
+
@click.option(
|
|
300
|
+
"--debug",
|
|
301
|
+
is_flag=True,
|
|
302
|
+
help="Enable debug logging and show performance timing"
|
|
303
|
+
)
|
|
304
|
+
@click.option(
|
|
305
|
+
"--dry-run",
|
|
306
|
+
is_flag=True,
|
|
307
|
+
help="Show what would be upgraded without actually upgrading"
|
|
308
|
+
)
|
|
309
|
+
@click.option(
|
|
310
|
+
"--exclude",
|
|
311
|
+
type=str,
|
|
312
|
+
default="",
|
|
313
|
+
help="Comma-separated list of packages to exclude from upgrade"
|
|
314
|
+
)
|
|
315
|
+
@click.option(
|
|
316
|
+
"--show-blocked",
|
|
317
|
+
is_flag=True,
|
|
318
|
+
help="Show packages that cannot be upgraded and why"
|
|
319
|
+
)
|
|
320
|
+
@click.option(
|
|
321
|
+
"--output",
|
|
322
|
+
type=click.Choice(["human", "json"]),
|
|
323
|
+
default="human",
|
|
324
|
+
help="Output format (human-readable or json)"
|
|
325
|
+
)
|
|
326
|
+
@click.option(
|
|
327
|
+
"--update-requirements",
|
|
328
|
+
type=click.Path(exists=True),
|
|
329
|
+
default=None,
|
|
330
|
+
help="Update the specified requirements.txt file with new versions"
|
|
331
|
+
)
|
|
332
|
+
@click.option(
|
|
333
|
+
"--parallel",
|
|
334
|
+
type=int,
|
|
335
|
+
default=1,
|
|
336
|
+
help="Number of parallel requests for version checking (default: 1)"
|
|
337
|
+
)
|
|
338
|
+
@click.option(
|
|
339
|
+
"--interactive", "-i",
|
|
340
|
+
is_flag=True,
|
|
341
|
+
help="Interactively select packages to upgrade"
|
|
342
|
+
)
|
|
343
|
+
@click.option(
|
|
344
|
+
"--no-cache",
|
|
345
|
+
is_flag=True,
|
|
346
|
+
help="Skip cache and fetch fresh version data"
|
|
347
|
+
)
|
|
348
|
+
@click.option(
|
|
349
|
+
"--cache-ttl",
|
|
350
|
+
type=int,
|
|
351
|
+
default=None,
|
|
352
|
+
help=f"Cache freshness threshold in seconds (default: {DEFAULT_CACHE_TTL})"
|
|
353
|
+
)
|
|
354
|
+
def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug: bool, dry_run: bool,
|
|
355
|
+
exclude: str, show_blocked: bool, output: str, update_requirements: Optional[str],
|
|
356
|
+
parallel: int, interactive: bool, no_cache: bool, cache_ttl: Optional[int]) -> None:
|
|
285
357
|
"""
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
Sets version constraints for packages in the pip configuration file.
|
|
289
|
-
Constraints prevent packages from being updated beyond specified versions.
|
|
290
|
-
|
|
291
|
-
Note: Automatic constraints from installed packages are now discovered
|
|
292
|
-
on every pipu execution and do not need to be manually added. The constraints
|
|
293
|
-
you add here will override any automatic constraints.
|
|
294
|
-
|
|
295
|
-
\b
|
|
296
|
-
Examples:
|
|
297
|
-
pipu constrain "requests==2.31.0"
|
|
298
|
-
pipu constrain "numpy>=1.20.0" "pandas<2.0.0"
|
|
299
|
-
pipu constrain "django~=4.1.0" --env production
|
|
300
|
-
pipu constrain "flask<2" --invalidates-when "other_package>=1" --invalidates-when "another_package>1.5"
|
|
301
|
-
pipu constrain --list
|
|
302
|
-
pipu constrain --list --env production
|
|
303
|
-
pipu constrain --remove requests numpy
|
|
304
|
-
pipu constrain --remove django --env production
|
|
305
|
-
pipu constrain --remove-all --env production
|
|
306
|
-
pipu constrain --remove-all --yes
|
|
307
|
-
|
|
308
|
-
\f
|
|
309
|
-
:param constraint_specs: One or more constraint specifications or package names (for --remove)
|
|
310
|
-
:param env: Target environment section name
|
|
311
|
-
:param list_constraints: List existing constraints instead of adding new ones
|
|
312
|
-
:param remove_constraints: Remove constraints for specified packages
|
|
313
|
-
:param remove_all_constraints: Remove all constraints from environment(s)
|
|
314
|
-
:param skip_confirmation: Skip confirmation prompt for --remove-all
|
|
315
|
-
:raises SystemExit: Exits with code 1 if an error occurs
|
|
316
|
-
"""
|
|
317
|
-
try:
|
|
318
|
-
# Validate mutually exclusive options
|
|
319
|
-
# Note: constraint_specs with --remove are package names, not constraint specs
|
|
320
|
-
has_constraint_specs_for_adding = bool(constraint_specs) and not (remove_constraints or remove_all_constraints)
|
|
321
|
-
active_options = [list_constraints, remove_constraints, remove_all_constraints, has_constraint_specs_for_adding]
|
|
322
|
-
if sum(active_options) > 1:
|
|
323
|
-
console.print("[red]Error: Cannot use --list, --remove, --remove-all, and constraint specs together. Use only one at a time.[/red]")
|
|
324
|
-
sys.exit(1)
|
|
325
|
-
|
|
326
|
-
# Validate --invalidates-when can only be used when adding constraints
|
|
327
|
-
if invalidation_triggers and (list_constraints or remove_constraints or remove_all_constraints):
|
|
328
|
-
console.print("[red]Error: --invalidates-when cannot be used with --list, --remove, or --remove-all.[/red]")
|
|
329
|
-
sys.exit(1)
|
|
358
|
+
Upgrade installed packages.
|
|
330
359
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
sys.exit(1)
|
|
360
|
+
By default, upgrades all packages that have newer versions available.
|
|
361
|
+
Optionally specify PACKAGES to upgrade only those packages.
|
|
334
362
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
from .package_constraints import list_all_constraints
|
|
363
|
+
Uses cached version data if available and fresh. Run [cyan]pipu update[/cyan]
|
|
364
|
+
to refresh the cache manually.
|
|
338
365
|
|
|
339
|
-
|
|
366
|
+
[bold]Examples:[/bold]
|
|
367
|
+
pipu upgrade Upgrade all packages
|
|
368
|
+
pipu upgrade requests numpy Upgrade specific packages
|
|
369
|
+
pipu upgrade --dry-run Preview without installing
|
|
370
|
+
pipu upgrade -i Interactive package selection
|
|
371
|
+
pipu upgrade --no-cache Force fresh version check
|
|
372
|
+
"""
|
|
373
|
+
console = Console()
|
|
374
|
+
|
|
375
|
+
# Load configuration file
|
|
376
|
+
config = load_config()
|
|
377
|
+
|
|
378
|
+
# Apply config file values for options at defaults
|
|
379
|
+
if timeout == 10:
|
|
380
|
+
timeout = get_config_value(config, 'timeout', 10)
|
|
381
|
+
if not exclude:
|
|
382
|
+
exclude_list = get_config_value(config, 'exclude', [])
|
|
383
|
+
if exclude_list:
|
|
384
|
+
exclude = ','.join(exclude_list)
|
|
385
|
+
if not pre:
|
|
386
|
+
pre = get_config_value(config, 'pre', False)
|
|
387
|
+
if not yes:
|
|
388
|
+
yes = get_config_value(config, 'yes', False)
|
|
389
|
+
if not debug:
|
|
390
|
+
debug = get_config_value(config, 'debug', False)
|
|
391
|
+
if not dry_run:
|
|
392
|
+
dry_run = get_config_value(config, 'dry_run', False)
|
|
393
|
+
if not show_blocked:
|
|
394
|
+
show_blocked = get_config_value(config, 'show_blocked', False)
|
|
395
|
+
if output == "human":
|
|
396
|
+
output = get_config_value(config, 'output', 'human')
|
|
397
|
+
if cache_ttl is None:
|
|
398
|
+
cache_ttl = get_config_value(config, 'cache_ttl', DEFAULT_CACHE_TTL)
|
|
399
|
+
|
|
400
|
+
# Check if caching is enabled
|
|
401
|
+
cache_enabled = get_config_value(config, 'cache_enabled', True) and not no_cache
|
|
402
|
+
|
|
403
|
+
# Initialize JSON formatter if needed
|
|
404
|
+
json_formatter = JsonOutputFormatter() if output == "json" else None
|
|
405
|
+
|
|
406
|
+
# Interactive mode only works in human output mode
|
|
407
|
+
if interactive and output == "json":
|
|
408
|
+
console.print("[yellow]Warning: --interactive is not compatible with --output json. Ignoring --interactive.[/yellow]")
|
|
409
|
+
interactive = False
|
|
410
|
+
|
|
411
|
+
# Configure logging
|
|
412
|
+
if debug and output != "json":
|
|
413
|
+
logging.basicConfig(
|
|
414
|
+
level=logging.DEBUG,
|
|
415
|
+
format='%(message)s',
|
|
416
|
+
handlers=[RichHandler(console=console, show_time=False, show_path=False, markup=True)]
|
|
417
|
+
)
|
|
418
|
+
logging.getLogger('pip._internal').setLevel(logging.WARNING)
|
|
419
|
+
logging.getLogger('pip._vendor').setLevel(logging.WARNING)
|
|
420
|
+
console.print("[dim]Debug mode enabled[/dim]\n")
|
|
340
421
|
|
|
341
|
-
|
|
422
|
+
try:
|
|
423
|
+
# Check cache freshness
|
|
424
|
+
effective_cache_ttl = DEFAULT_CACHE_TTL if cache_ttl is None else cache_ttl
|
|
425
|
+
use_cache = cache_enabled and is_cache_fresh(effective_cache_ttl)
|
|
426
|
+
|
|
427
|
+
if use_cache and output != "json":
|
|
428
|
+
cache_age = get_cache_age_seconds()
|
|
429
|
+
console.print(f"[dim]Using cached data ({format_cache_age(cache_age)})[/dim]\n")
|
|
430
|
+
|
|
431
|
+
# Step 1: Inspect installed packages
|
|
432
|
+
if output != "json":
|
|
433
|
+
console.print("[bold]Step 1/5:[/bold] Inspecting installed packages...")
|
|
434
|
+
|
|
435
|
+
step1_start = time.time()
|
|
436
|
+
if output != "json":
|
|
437
|
+
with Progress(
|
|
438
|
+
SpinnerColumn(),
|
|
439
|
+
TextColumn("[progress.description]{task.description}"),
|
|
440
|
+
console=console,
|
|
441
|
+
transient=True
|
|
442
|
+
) as progress:
|
|
443
|
+
task = progress.add_task("Loading packages...", total=None)
|
|
444
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
445
|
+
progress.update(task, completed=True)
|
|
446
|
+
else:
|
|
447
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
448
|
+
step1_time = time.time() - step1_start
|
|
449
|
+
|
|
450
|
+
num_installed = len(installed_packages)
|
|
451
|
+
if output != "json":
|
|
452
|
+
console.print(f" Found {num_installed} installed packages")
|
|
453
|
+
if debug:
|
|
454
|
+
console.print(f" [dim]Time: {step1_time:.2f}s[/dim]")
|
|
455
|
+
|
|
456
|
+
if not installed_packages:
|
|
457
|
+
if output == "json":
|
|
458
|
+
print('{"error": "No packages found"}')
|
|
459
|
+
else:
|
|
460
|
+
console.print("[yellow]No packages found.[/yellow]")
|
|
461
|
+
sys.exit(0)
|
|
342
462
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
463
|
+
# Step 2: Check for updates (from cache or network)
|
|
464
|
+
if output != "json":
|
|
465
|
+
if use_cache:
|
|
466
|
+
console.print("\n[bold]Step 2/5:[/bold] Loading cached version data...")
|
|
467
|
+
else:
|
|
468
|
+
console.print("\n[bold]Step 2/5:[/bold] Checking for updates...")
|
|
469
|
+
|
|
470
|
+
step2_start = time.time()
|
|
471
|
+
latest_versions: dict = {}
|
|
472
|
+
|
|
473
|
+
if use_cache:
|
|
474
|
+
# Load from cache
|
|
475
|
+
cache_data = load_cache()
|
|
476
|
+
if cache_data and cache_data.packages:
|
|
477
|
+
# Reconstruct latest_versions from cache
|
|
478
|
+
# We need to fetch fresh for accurate constraint resolution
|
|
479
|
+
# Cache is mainly to avoid the slow PyPI queries
|
|
480
|
+
if output != "json":
|
|
481
|
+
with Progress(
|
|
482
|
+
TextColumn("[progress.description]{task.description}"),
|
|
483
|
+
BarColumn(),
|
|
484
|
+
TaskProgressColumn(),
|
|
485
|
+
console=console,
|
|
486
|
+
transient=True
|
|
487
|
+
) as progress:
|
|
488
|
+
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
489
|
+
|
|
490
|
+
def update_progress(current, total):
|
|
491
|
+
progress.update(task, completed=current)
|
|
492
|
+
|
|
493
|
+
if parallel > 1:
|
|
494
|
+
latest_versions = get_latest_versions_parallel(
|
|
495
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
496
|
+
max_workers=parallel, progress_callback=update_progress
|
|
497
|
+
)
|
|
498
|
+
else:
|
|
499
|
+
latest_versions = get_latest_versions(
|
|
500
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
501
|
+
progress_callback=update_progress
|
|
502
|
+
)
|
|
346
503
|
else:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
504
|
+
if parallel > 1:
|
|
505
|
+
latest_versions = get_latest_versions_parallel(
|
|
506
|
+
installed_packages, timeout=timeout, include_prereleases=pre, max_workers=parallel
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
latest_versions = get_latest_versions(
|
|
510
|
+
installed_packages, timeout=timeout, include_prereleases=pre
|
|
511
|
+
)
|
|
512
|
+
else:
|
|
513
|
+
use_cache = False
|
|
514
|
+
|
|
515
|
+
if not use_cache:
|
|
516
|
+
# Fetch from network
|
|
517
|
+
if output != "json":
|
|
518
|
+
with Progress(
|
|
519
|
+
TextColumn("[progress.description]{task.description}"),
|
|
520
|
+
BarColumn(),
|
|
521
|
+
TaskProgressColumn(),
|
|
522
|
+
console=console,
|
|
523
|
+
transient=True
|
|
524
|
+
) as progress:
|
|
525
|
+
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
526
|
+
|
|
527
|
+
def update_progress(current, total):
|
|
528
|
+
progress.update(task, completed=current)
|
|
529
|
+
|
|
530
|
+
if parallel > 1:
|
|
531
|
+
latest_versions = get_latest_versions_parallel(
|
|
532
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
533
|
+
max_workers=parallel, progress_callback=update_progress
|
|
534
|
+
)
|
|
535
|
+
else:
|
|
536
|
+
latest_versions = get_latest_versions(
|
|
537
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
538
|
+
progress_callback=update_progress
|
|
539
|
+
)
|
|
540
|
+
else:
|
|
541
|
+
if parallel > 1:
|
|
542
|
+
latest_versions = get_latest_versions_parallel(
|
|
543
|
+
installed_packages, timeout=timeout, include_prereleases=pre, max_workers=parallel
|
|
544
|
+
)
|
|
356
545
|
else:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
546
|
+
latest_versions = get_latest_versions(
|
|
547
|
+
installed_packages, timeout=timeout, include_prereleases=pre
|
|
548
|
+
)
|
|
360
549
|
|
|
361
|
-
|
|
362
|
-
if remove_constraints:
|
|
363
|
-
if not constraint_specs:
|
|
364
|
-
console.print("[red]Error: At least one package name must be specified for removal.[/red]")
|
|
365
|
-
sys.exit(1)
|
|
550
|
+
step2_time = time.time() - step2_start
|
|
366
551
|
|
|
367
|
-
|
|
552
|
+
num_updates = len(latest_versions)
|
|
553
|
+
if output != "json":
|
|
554
|
+
console.print(f" Found {num_updates} packages with newer versions available")
|
|
555
|
+
if debug:
|
|
556
|
+
console.print(f" [dim]Time: {step2_time:.2f}s[/dim]")
|
|
368
557
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
558
|
+
if not latest_versions:
|
|
559
|
+
if output == "json":
|
|
560
|
+
print('{"upgradable": [], "upgradable_count": 0, "message": "All packages are up to date"}')
|
|
561
|
+
else:
|
|
562
|
+
console.print("\n[bold green]All packages are up to date![/bold green]")
|
|
563
|
+
sys.exit(0)
|
|
564
|
+
|
|
565
|
+
# Step 3: Resolve upgradable packages
|
|
566
|
+
if output != "json":
|
|
567
|
+
console.print("\n[bold]Step 3/5:[/bold] Resolving dependency constraints...")
|
|
568
|
+
step3_start = time.time()
|
|
569
|
+
|
|
570
|
+
if show_blocked:
|
|
571
|
+
upgradable_packages, blocked_packages = resolve_upgradable_packages_with_reasons(
|
|
572
|
+
latest_versions, installed_packages
|
|
573
|
+
)
|
|
574
|
+
else:
|
|
575
|
+
all_upgradable = resolve_upgradable_packages(latest_versions, installed_packages)
|
|
576
|
+
upgradable_packages = [pkg for pkg in all_upgradable if pkg.upgradable]
|
|
577
|
+
blocked_packages = []
|
|
578
|
+
|
|
579
|
+
step3_time = time.time() - step3_start
|
|
580
|
+
|
|
581
|
+
# Update cache with fresh data (if we fetched from network)
|
|
582
|
+
if not use_cache and cache_enabled:
|
|
583
|
+
cache_data = build_cache_from_results(installed_packages, latest_versions, upgradable_packages)
|
|
584
|
+
save_cache(cache_data)
|
|
585
|
+
|
|
586
|
+
# Apply exclusions
|
|
587
|
+
excluded_names = set()
|
|
588
|
+
if exclude:
|
|
589
|
+
excluded_names = {name.strip().lower() for name in exclude.split(',')}
|
|
590
|
+
if debug and excluded_names:
|
|
591
|
+
console.print(f" [dim]Excluding: {', '.join(sorted(excluded_names))}[/dim]")
|
|
592
|
+
|
|
593
|
+
# Filter to only upgradable packages (excluding excluded ones)
|
|
594
|
+
can_upgrade = [pkg for pkg in upgradable_packages if pkg.name.lower() not in excluded_names]
|
|
595
|
+
|
|
596
|
+
# Parse package specifications and filter to specific packages if provided
|
|
597
|
+
package_constraints = {}
|
|
598
|
+
if packages:
|
|
599
|
+
requested_packages = set()
|
|
600
|
+
for spec in packages:
|
|
601
|
+
name, constraint = parse_package_spec(spec)
|
|
602
|
+
requested_packages.add(name.lower())
|
|
603
|
+
if constraint:
|
|
604
|
+
package_constraints[name.lower()] = constraint
|
|
605
|
+
|
|
606
|
+
can_upgrade = [pkg for pkg in can_upgrade if pkg.name.lower() in requested_packages]
|
|
607
|
+
|
|
608
|
+
if debug:
|
|
609
|
+
console.print(f" [dim]Filtering to: {', '.join(packages)}[/dim]")
|
|
610
|
+
if package_constraints:
|
|
611
|
+
console.print(f" [dim]Version constraints: {package_constraints}[/dim]")
|
|
612
|
+
|
|
613
|
+
if not can_upgrade:
|
|
614
|
+
if output == "json":
|
|
615
|
+
assert json_formatter is not None
|
|
616
|
+
json_data = json_formatter.format_all(
|
|
617
|
+
upgradable=[],
|
|
618
|
+
blocked=blocked_packages if show_blocked else None
|
|
619
|
+
)
|
|
620
|
+
print(json_data)
|
|
621
|
+
else:
|
|
622
|
+
console.print("\n[yellow]No packages can be upgraded (all blocked by constraints).[/yellow]")
|
|
623
|
+
if show_blocked and blocked_packages:
|
|
624
|
+
console.print()
|
|
625
|
+
print_blocked_packages_table(blocked_packages, console=console)
|
|
626
|
+
sys.exit(0)
|
|
627
|
+
|
|
628
|
+
num_upgradable = len(can_upgrade)
|
|
629
|
+
if output != "json":
|
|
630
|
+
console.print(f" {num_upgradable} packages can be safely upgraded")
|
|
631
|
+
if debug:
|
|
632
|
+
console.print(f" [dim]Time: {step3_time:.2f}s[/dim]")
|
|
633
|
+
|
|
634
|
+
# Step 4: Display table and ask for confirmation
|
|
635
|
+
if output == "json":
|
|
636
|
+
assert json_formatter is not None
|
|
637
|
+
if dry_run:
|
|
638
|
+
json_data = json_formatter.format_all(
|
|
639
|
+
upgradable=can_upgrade,
|
|
640
|
+
blocked=blocked_packages if show_blocked else None
|
|
641
|
+
)
|
|
642
|
+
print(json_data)
|
|
643
|
+
sys.exit(0)
|
|
644
|
+
else:
|
|
645
|
+
console.print("\n[bold]Step 4/5:[/bold] Packages ready for upgrade:\n")
|
|
646
|
+
print_upgradable_packages_table(can_upgrade, console=console)
|
|
389
647
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
for package, triggers in removed_triggers.items():
|
|
394
|
-
for trigger in triggers:
|
|
395
|
-
console.print(f" [red]Removed trigger[/red]: {trigger}")
|
|
648
|
+
if show_blocked and blocked_packages:
|
|
649
|
+
console.print()
|
|
650
|
+
print_blocked_packages_table(blocked_packages, console=console)
|
|
396
651
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
current_env = get_current_environment_name()
|
|
403
|
-
if current_env and current_env != "global":
|
|
404
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {current_env}")
|
|
405
|
-
else:
|
|
406
|
-
console.print("\n[bold cyan]Environment updated:[/bold cyan] global")
|
|
407
|
-
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
except ValueError as e:
|
|
411
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
412
|
-
sys.exit(1)
|
|
413
|
-
except IOError as e:
|
|
414
|
-
console.print(f"[red]Error writing configuration: {e}[/red]")
|
|
415
|
-
sys.exit(1)
|
|
416
|
-
|
|
417
|
-
# Handle --remove-all option
|
|
418
|
-
if remove_all_constraints:
|
|
419
|
-
from .package_constraints import remove_all_constraints_from_config, list_all_constraints, parse_invalidation_triggers_storage
|
|
420
|
-
|
|
421
|
-
# Get confirmation if not using --yes and removing from all environments
|
|
422
|
-
if not env and not skip_confirmation:
|
|
423
|
-
# Show what will be removed
|
|
424
|
-
try:
|
|
425
|
-
all_constraints = list_all_constraints()
|
|
426
|
-
if not all_constraints:
|
|
427
|
-
console.print("[yellow]No constraints found in any environment.[/yellow]")
|
|
428
|
-
return
|
|
429
|
-
|
|
430
|
-
console.print("[bold red]WARNING: This will remove ALL constraints from ALL environments![/bold red]")
|
|
431
|
-
console.print("\n[bold]Constraints that will be removed:[/bold]")
|
|
432
|
-
|
|
433
|
-
total_constraints = 0
|
|
434
|
-
for env_name, constraints in all_constraints.items():
|
|
435
|
-
console.print(f"\n[bold cyan]{env_name}:[/bold cyan]")
|
|
436
|
-
for package, constraint_spec in sorted(constraints.items()):
|
|
437
|
-
console.print(f" {package}{constraint_spec}")
|
|
438
|
-
total_constraints += 1
|
|
439
|
-
|
|
440
|
-
console.print(f"\n[bold]Total: {total_constraints} constraint(s) in {len(all_constraints)} environment(s)[/bold]")
|
|
441
|
-
|
|
442
|
-
if not click.confirm("\nAre you sure you want to remove all constraints?"):
|
|
443
|
-
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
except Exception:
|
|
447
|
-
# If we can't list constraints, ask for generic confirmation
|
|
448
|
-
if not click.confirm("Are you sure you want to remove all constraints from all environments?"):
|
|
449
|
-
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
450
|
-
return
|
|
451
|
-
|
|
452
|
-
console.print(f"[bold blue]Removing all constraints from {'all environments' if not env else env}...[/bold blue]")
|
|
453
|
-
|
|
454
|
-
try:
|
|
455
|
-
config_path, removed_constraints, removed_triggers_by_env = remove_all_constraints_from_config(env)
|
|
456
|
-
|
|
457
|
-
if not removed_constraints:
|
|
458
|
-
if env:
|
|
459
|
-
console.print(f"[yellow]No constraints found in environment '{env}'.[/yellow]")
|
|
460
|
-
else:
|
|
461
|
-
console.print("[yellow]No constraints found in any environment.[/yellow]")
|
|
462
|
-
return
|
|
463
|
-
|
|
464
|
-
# Triggers are already cleaned up by remove_all_constraints_from_config
|
|
465
|
-
|
|
466
|
-
# Display summary
|
|
467
|
-
console.print("\n[bold green]✓ All constraints removed successfully![/bold green]")
|
|
468
|
-
console.print(f"[bold]File:[/bold] {config_path}")
|
|
469
|
-
|
|
470
|
-
# Show removed constraints
|
|
471
|
-
console.print("\n[bold]Constraints removed:[/bold]")
|
|
472
|
-
total_removed = 0
|
|
473
|
-
for env_name, constraints in removed_constraints.items():
|
|
474
|
-
console.print(f"\n[bold cyan]{env_name}:[/bold cyan]")
|
|
475
|
-
for package, constraint_spec in sorted(constraints.items()):
|
|
476
|
-
console.print(f" [red]Removed[/red]: {package}{constraint_spec}")
|
|
477
|
-
total_removed += 1
|
|
478
|
-
|
|
479
|
-
console.print(f"\n[bold]Total removed: {total_removed} constraint(s) from {len(removed_constraints)} environment(s)[/bold]")
|
|
480
|
-
|
|
481
|
-
# Show removed invalidation triggers if any
|
|
482
|
-
if removed_triggers_by_env:
|
|
483
|
-
console.print("\n[bold]Invalidation triggers removed:[/bold]")
|
|
484
|
-
total_triggers_removed = 0
|
|
485
|
-
for env_name, env_triggers in removed_triggers_by_env.items():
|
|
486
|
-
console.print(f"\n[bold cyan]{env_name}:[/bold cyan]")
|
|
487
|
-
for package, triggers in env_triggers.items():
|
|
488
|
-
for trigger in triggers:
|
|
489
|
-
console.print(f" [red]Removed trigger[/red]: {trigger}")
|
|
490
|
-
total_triggers_removed += 1
|
|
491
|
-
console.print(f"\n[bold]Total triggers removed: {total_triggers_removed}[/bold]")
|
|
492
|
-
|
|
493
|
-
# Show which environment(s) were updated
|
|
494
|
-
if env:
|
|
495
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {env}")
|
|
496
|
-
else:
|
|
497
|
-
environments_updated = list(removed_constraints.keys())
|
|
498
|
-
console.print(f"\n[bold cyan]Environments updated:[/bold cyan] {', '.join(sorted(environments_updated))}")
|
|
652
|
+
if interactive:
|
|
653
|
+
can_upgrade = select_packages_interactively(can_upgrade, console)
|
|
654
|
+
if not can_upgrade:
|
|
655
|
+
console.print("[yellow]No packages selected for upgrade.[/yellow]")
|
|
656
|
+
sys.exit(0)
|
|
499
657
|
|
|
500
|
-
|
|
658
|
+
if dry_run:
|
|
659
|
+
console.print("\n[bold cyan]Dry run complete.[/bold cyan] No packages were modified.")
|
|
660
|
+
sys.exit(0)
|
|
501
661
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
662
|
+
# Skip confirmation if interactive mode (already confirmed) or --yes flag
|
|
663
|
+
if not yes and not interactive and output != "json":
|
|
664
|
+
console.print()
|
|
665
|
+
confirm = click.confirm("Do you want to proceed with the upgrade?", default=True)
|
|
666
|
+
if not confirm:
|
|
667
|
+
console.print("[yellow]Upgrade cancelled.[/yellow]")
|
|
668
|
+
sys.exit(0)
|
|
669
|
+
|
|
670
|
+
# Step 5: Install packages
|
|
671
|
+
if output != "json":
|
|
672
|
+
console.print("\n[bold]Step 5/5:[/bold] Upgrading packages...\n")
|
|
673
|
+
step5_start = time.time()
|
|
674
|
+
|
|
675
|
+
# Save state for potential rollback
|
|
676
|
+
from pipu_cli.rollback import save_state
|
|
677
|
+
pre_upgrade_packages = [
|
|
678
|
+
{"name": pkg.name, "version": str(pkg.version)}
|
|
679
|
+
for pkg in can_upgrade
|
|
680
|
+
]
|
|
681
|
+
save_state(pre_upgrade_packages, "Pre-upgrade state")
|
|
682
|
+
|
|
683
|
+
stream = ConsoleStream(console) if output != "json" else None
|
|
684
|
+
results = install_packages(
|
|
685
|
+
can_upgrade,
|
|
686
|
+
output_stream=stream,
|
|
687
|
+
timeout=300,
|
|
688
|
+
version_constraints=package_constraints if package_constraints else None
|
|
689
|
+
)
|
|
690
|
+
step5_time = time.time() - step5_start
|
|
691
|
+
|
|
692
|
+
# Update requirements file if requested
|
|
693
|
+
if update_requirements:
|
|
694
|
+
from pathlib import Path
|
|
695
|
+
from pipu_cli.requirements import update_requirements_file
|
|
696
|
+
req_path = Path(update_requirements)
|
|
697
|
+
updated = update_requirements_file(req_path, results)
|
|
698
|
+
if updated and output != "json":
|
|
699
|
+
console.print(f"\n[bold green]Updated {updated} package(s) in {update_requirements}[/bold green]")
|
|
700
|
+
|
|
701
|
+
# Print results summary
|
|
702
|
+
if output == "json":
|
|
703
|
+
assert json_formatter is not None
|
|
704
|
+
json_data = json_formatter.format_all(
|
|
705
|
+
upgradable=can_upgrade,
|
|
706
|
+
blocked=blocked_packages if show_blocked else None,
|
|
707
|
+
results=results
|
|
708
|
+
)
|
|
709
|
+
print(json_data)
|
|
710
|
+
else:
|
|
711
|
+
print_upgrade_results(results, console=console)
|
|
508
712
|
|
|
509
|
-
|
|
713
|
+
if debug:
|
|
714
|
+
console.print(f"\n[dim]Step 5 time: {step5_time:.2f}s[/dim]")
|
|
715
|
+
total_time = step1_time + step2_time + step3_time + step5_time
|
|
716
|
+
console.print(f"[dim]Total time: {total_time:.2f}s[/dim]")
|
|
510
717
|
|
|
511
|
-
|
|
512
|
-
|
|
718
|
+
# Exit with appropriate code
|
|
719
|
+
failed = [pkg for pkg in results if not pkg.upgraded]
|
|
720
|
+
if failed:
|
|
513
721
|
sys.exit(1)
|
|
514
|
-
|
|
515
|
-
console.print("[bold blue]Adding constraints to pip configuration...[/bold blue]")
|
|
516
|
-
|
|
517
|
-
# Use backend function for constraint addition
|
|
518
|
-
from .package_constraints import (
|
|
519
|
-
add_constraints_to_config,
|
|
520
|
-
validate_invalidation_triggers,
|
|
521
|
-
get_recommended_pip_config_path,
|
|
522
|
-
get_current_environment_name,
|
|
523
|
-
parse_invalidation_triggers_storage,
|
|
524
|
-
merge_invalidation_triggers,
|
|
525
|
-
format_invalidation_triggers,
|
|
526
|
-
parse_inline_constraints,
|
|
527
|
-
parse_requirement_line
|
|
528
|
-
)
|
|
529
|
-
import configparser
|
|
530
|
-
|
|
531
|
-
# Get the recommended config file path early for error handling
|
|
532
|
-
config_path = get_recommended_pip_config_path()
|
|
533
|
-
|
|
534
|
-
try:
|
|
535
|
-
# Validate invalidation triggers if provided
|
|
536
|
-
validated_triggers = []
|
|
537
|
-
if invalidation_triggers:
|
|
538
|
-
validated_triggers = validate_invalidation_triggers(list(invalidation_triggers))
|
|
539
|
-
|
|
540
|
-
# Add constraints using the backend function
|
|
541
|
-
config_path, changes = add_constraints_to_config(constraint_list, env)
|
|
542
|
-
|
|
543
|
-
# Handle invalidation triggers if provided
|
|
544
|
-
if validated_triggers:
|
|
545
|
-
# Load the config and add triggers for the constrained packages
|
|
546
|
-
config = configparser.ConfigParser()
|
|
547
|
-
config.read(config_path)
|
|
548
|
-
|
|
549
|
-
# Determine target environment section
|
|
550
|
-
if env is None:
|
|
551
|
-
env = get_current_environment_name()
|
|
552
|
-
section_name = env if env else 'global'
|
|
553
|
-
|
|
554
|
-
# Get existing invalidation triggers
|
|
555
|
-
existing_triggers_storage = {}
|
|
556
|
-
if config.has_option(section_name, 'constraint_invalid_when'):
|
|
557
|
-
existing_value = config.get(section_name, 'constraint_invalid_when')
|
|
558
|
-
existing_triggers_storage = parse_invalidation_triggers_storage(existing_value)
|
|
559
|
-
|
|
560
|
-
# Get current constraints to find the packages that were added/updated
|
|
561
|
-
current_constraints = {}
|
|
562
|
-
if config.has_option(section_name, 'constraints'):
|
|
563
|
-
constraints_value = config.get(section_name, 'constraints')
|
|
564
|
-
if any(op in constraints_value for op in ['>=', '<=', '==', '!=', '~=', '>', '<']):
|
|
565
|
-
current_constraints = parse_inline_constraints(constraints_value)
|
|
566
|
-
|
|
567
|
-
# Process triggers for each package that was specified (whether changed or not)
|
|
568
|
-
updated_triggers_storage = existing_triggers_storage.copy()
|
|
569
|
-
|
|
570
|
-
# Get all package names from the constraint list
|
|
571
|
-
all_package_names = set()
|
|
572
|
-
for spec in constraint_list:
|
|
573
|
-
parsed = parse_requirement_line(spec)
|
|
574
|
-
if parsed:
|
|
575
|
-
all_package_names.add(parsed['name'].lower())
|
|
576
|
-
|
|
577
|
-
for package_name in all_package_names:
|
|
578
|
-
# Get existing triggers for this package
|
|
579
|
-
existing_package_triggers = existing_triggers_storage.get(package_name, [])
|
|
580
|
-
|
|
581
|
-
# Merge with new triggers
|
|
582
|
-
merged_triggers = merge_invalidation_triggers(existing_package_triggers, validated_triggers)
|
|
583
|
-
|
|
584
|
-
if merged_triggers:
|
|
585
|
-
updated_triggers_storage[package_name] = merged_triggers
|
|
586
|
-
|
|
587
|
-
# Format and store the triggers
|
|
588
|
-
if updated_triggers_storage:
|
|
589
|
-
trigger_entries = []
|
|
590
|
-
for package_name, triggers in updated_triggers_storage.items():
|
|
591
|
-
# Get the constraint for this package to format properly
|
|
592
|
-
if package_name in current_constraints:
|
|
593
|
-
package_constraint = current_constraints[package_name]
|
|
594
|
-
formatted_entry = format_invalidation_triggers(f"{package_name}{package_constraint}", triggers)
|
|
595
|
-
if formatted_entry:
|
|
596
|
-
trigger_entries.append(formatted_entry)
|
|
597
|
-
|
|
598
|
-
if trigger_entries:
|
|
599
|
-
triggers_value = ','.join(trigger_entries)
|
|
600
|
-
config.set(section_name, 'constraint_invalid_when', triggers_value)
|
|
601
|
-
|
|
602
|
-
# Write the updated config file
|
|
603
|
-
with open(config_path, 'w', encoding='utf-8') as f:
|
|
604
|
-
config.write(f)
|
|
605
|
-
|
|
606
|
-
except Exception as e:
|
|
607
|
-
if isinstance(e, ValueError):
|
|
608
|
-
raise e
|
|
609
|
-
else:
|
|
610
|
-
raise IOError(f"Failed to write pip config file '{config_path}': {e}")
|
|
611
|
-
|
|
612
|
-
if not changes and not validated_triggers:
|
|
613
|
-
console.print("[yellow]No changes made - all constraints already exist with the same values.[/yellow]")
|
|
614
|
-
return
|
|
615
|
-
|
|
616
|
-
# Display summary
|
|
617
|
-
console.print("\n[bold green]✓ Configuration updated successfully![/bold green]")
|
|
618
|
-
console.print(f"[bold]File:[/bold] {config_path}")
|
|
619
|
-
|
|
620
|
-
# Show changes
|
|
621
|
-
if changes:
|
|
622
|
-
console.print("\n[bold]Constraints modified:[/bold]")
|
|
623
|
-
for package, (action, constraint) in changes.items():
|
|
624
|
-
action_color = "green" if action == "added" else "yellow"
|
|
625
|
-
console.print(f" [{action_color}]{action.title()}[/{action_color}]: {package}{constraint}")
|
|
626
|
-
|
|
627
|
-
# Show invalidation triggers if added
|
|
628
|
-
if validated_triggers:
|
|
629
|
-
console.print("\n[bold]Invalidation triggers added:[/bold]")
|
|
630
|
-
for trigger in validated_triggers:
|
|
631
|
-
console.print(f" [cyan]Trigger[/cyan]: {trigger}")
|
|
632
|
-
|
|
633
|
-
# Show which environment was updated
|
|
634
|
-
if env:
|
|
635
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {env}")
|
|
636
722
|
else:
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
sys.exit(1)
|
|
646
|
-
except IOError as e:
|
|
647
|
-
console.print(f"[red]Error writing configuration: {e}[/red]")
|
|
648
|
-
sys.exit(1)
|
|
723
|
+
sys.exit(0)
|
|
724
|
+
|
|
725
|
+
except KeyboardInterrupt:
|
|
726
|
+
console.print("\n[yellow]Interrupted by user.[/yellow]")
|
|
727
|
+
sys.exit(130)
|
|
728
|
+
except click.Abort:
|
|
729
|
+
console.print("\n[yellow]Update cancelled by user[/yellow]")
|
|
730
|
+
sys.exit(130)
|
|
649
731
|
except Exception as e:
|
|
650
|
-
console.print(f"[red]
|
|
732
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
651
733
|
sys.exit(1)
|
|
652
734
|
|
|
653
735
|
|
|
654
736
|
@cli.command()
|
|
655
|
-
@click.
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
737
|
+
@click.option(
|
|
738
|
+
"--list", "-l",
|
|
739
|
+
"list_states_flag",
|
|
740
|
+
is_flag=True,
|
|
741
|
+
help="List all saved rollback states"
|
|
742
|
+
)
|
|
743
|
+
@click.option(
|
|
744
|
+
"--dry-run",
|
|
745
|
+
is_flag=True,
|
|
746
|
+
help="Show what would be restored without actually restoring"
|
|
747
|
+
)
|
|
748
|
+
@click.option(
|
|
749
|
+
"--yes", "-y",
|
|
750
|
+
is_flag=True,
|
|
751
|
+
help="Automatically confirm rollback without prompting"
|
|
752
|
+
)
|
|
753
|
+
@click.option(
|
|
754
|
+
"--state",
|
|
755
|
+
type=str,
|
|
756
|
+
default=None,
|
|
757
|
+
help="Specific state file to rollback to (use --list to see available states)"
|
|
758
|
+
)
|
|
759
|
+
def rollback(list_states_flag: bool, dry_run: bool, yes: bool, state: Optional[str]) -> None:
|
|
662
760
|
"""
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
pipu ignore --list --env production
|
|
674
|
-
pipu ignore --remove requests numpy
|
|
675
|
-
pipu ignore --remove flask --env production
|
|
676
|
-
pipu ignore --remove-all --env production
|
|
677
|
-
pipu ignore --remove-all --yes
|
|
678
|
-
|
|
679
|
-
\f
|
|
680
|
-
:param package_names: One or more package names to ignore or remove
|
|
681
|
-
:param env: Target environment section name
|
|
682
|
-
:param list_ignores: List existing ignores instead of adding new ones
|
|
683
|
-
:param remove_ignores: Remove ignores for specified packages
|
|
684
|
-
:param remove_all_ignores: Remove all ignores from environment(s)
|
|
685
|
-
:param skip_confirmation: Skip confirmation prompt for --remove-all
|
|
686
|
-
:raises SystemExit: Exits with code 1 if an error occurs
|
|
761
|
+
Restore packages to a previous state.
|
|
762
|
+
|
|
763
|
+
Before each upgrade, pipu saves the current package versions. Use this
|
|
764
|
+
command to restore packages to their pre-upgrade state.
|
|
765
|
+
|
|
766
|
+
[bold]Examples:[/bold]
|
|
767
|
+
pipu rollback --list List all saved states
|
|
768
|
+
pipu rollback --dry-run Preview what would be restored
|
|
769
|
+
pipu rollback --yes Rollback without confirmation
|
|
770
|
+
pipu rollback --state FILE Rollback to a specific state
|
|
687
771
|
"""
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
772
|
+
from pipu_cli.rollback import get_latest_state, rollback_to_state, list_states as get_states, ROLLBACK_DIR
|
|
773
|
+
|
|
774
|
+
console = Console()
|
|
775
|
+
|
|
776
|
+
# List saved states if requested
|
|
777
|
+
if list_states_flag:
|
|
778
|
+
states = get_states()
|
|
779
|
+
if not states:
|
|
780
|
+
console.print("[yellow]No saved states found.[/yellow]")
|
|
781
|
+
console.print(f"[dim]States are saved in: {ROLLBACK_DIR}[/dim]")
|
|
782
|
+
sys.exit(0)
|
|
783
|
+
|
|
784
|
+
table = Table(title="[bold]Saved Rollback States[/bold]")
|
|
785
|
+
table.add_column("#", style="dim", width=3)
|
|
786
|
+
table.add_column("State File", style="cyan")
|
|
787
|
+
table.add_column("Timestamp", style="green")
|
|
788
|
+
table.add_column("Packages", style="magenta", justify="right")
|
|
789
|
+
table.add_column("Description", style="dim")
|
|
790
|
+
|
|
791
|
+
for idx, s in enumerate(states, 1):
|
|
792
|
+
ts = s["timestamp"]
|
|
793
|
+
if len(ts) == 15 and ts[8] == "_":
|
|
794
|
+
formatted_ts = f"{ts[:4]}-{ts[4:6]}-{ts[6:8]} {ts[9:11]}:{ts[11:13]}:{ts[13:15]}"
|
|
795
|
+
else:
|
|
796
|
+
formatted_ts = ts
|
|
797
|
+
|
|
798
|
+
table.add_row(
|
|
799
|
+
str(idx),
|
|
800
|
+
s["file"],
|
|
801
|
+
formatted_ts,
|
|
802
|
+
str(s["package_count"]),
|
|
803
|
+
s["description"] or "-"
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
console.print(table)
|
|
807
|
+
console.print(f"\n[dim]States saved in: {ROLLBACK_DIR}[/dim]")
|
|
808
|
+
console.print("[dim]Use --state <filename> to rollback to a specific state[/dim]")
|
|
809
|
+
sys.exit(0)
|
|
810
|
+
|
|
811
|
+
# Get the state to rollback to
|
|
812
|
+
if state:
|
|
813
|
+
state_path = ROLLBACK_DIR / state
|
|
814
|
+
if not state_path.exists():
|
|
815
|
+
console.print(f"[red]State file not found:[/red] {state}")
|
|
816
|
+
console.print("[dim]Use 'pipu rollback --list' to see available states[/dim]")
|
|
693
817
|
sys.exit(1)
|
|
694
818
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
819
|
+
with open(state_path, 'r') as f:
|
|
820
|
+
state_data = json.load(f)
|
|
821
|
+
else:
|
|
822
|
+
state_data = get_latest_state()
|
|
698
823
|
|
|
699
|
-
|
|
824
|
+
if state_data is None:
|
|
825
|
+
console.print("[yellow]No saved state found.[/yellow]")
|
|
826
|
+
console.print("[dim]A state is automatically saved before each upgrade.[/dim]")
|
|
827
|
+
sys.exit(0)
|
|
700
828
|
|
|
701
|
-
|
|
829
|
+
# Show what will be rolled back
|
|
830
|
+
packages = state_data.get("packages", [])
|
|
831
|
+
timestamp = state_data.get("timestamp", "unknown")
|
|
832
|
+
description = state_data.get("description", "")
|
|
702
833
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
console.print("[yellow]No ignores found in any environment.[/yellow]")
|
|
708
|
-
return
|
|
709
|
-
|
|
710
|
-
# Display ignores
|
|
711
|
-
for env_name, ignores in all_ignores.items():
|
|
712
|
-
console.print(f"\n[bold cyan]Environment: {env_name}[/bold cyan]")
|
|
713
|
-
if ignores:
|
|
714
|
-
for package in sorted(ignores):
|
|
715
|
-
console.print(f" {package}")
|
|
716
|
-
else:
|
|
717
|
-
console.print(" [dim]No ignores[/dim]")
|
|
834
|
+
if len(timestamp) == 15 and timestamp[8] == "_":
|
|
835
|
+
formatted_ts = f"{timestamp[:4]}-{timestamp[4:6]}-{timestamp[6:8]} {timestamp[9:11]}:{timestamp[11:13]}:{timestamp[13:15]}"
|
|
836
|
+
else:
|
|
837
|
+
formatted_ts = timestamp
|
|
718
838
|
|
|
719
|
-
|
|
839
|
+
console.print(f"\n[bold]Rollback State:[/bold] {formatted_ts}")
|
|
840
|
+
if description:
|
|
841
|
+
console.print(f"[dim]{description}[/dim]")
|
|
842
|
+
console.print()
|
|
720
843
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
console.print("[red]Error: At least one package name must be specified for removal.[/red]")
|
|
725
|
-
sys.exit(1)
|
|
844
|
+
table = Table(title=f"[bold]{len(packages)} Package(s) to Restore[/bold]")
|
|
845
|
+
table.add_column("Package", style="cyan")
|
|
846
|
+
table.add_column("Version", style="green")
|
|
726
847
|
|
|
727
|
-
|
|
848
|
+
for pkg in packages:
|
|
849
|
+
table.add_row(pkg["name"], pkg["version"])
|
|
728
850
|
|
|
729
|
-
|
|
730
|
-
console.print(f"[bold blue]Removing ignores for {len(packages_list)} package(s)...[/bold blue]")
|
|
851
|
+
console.print(table)
|
|
731
852
|
|
|
732
|
-
|
|
733
|
-
|
|
853
|
+
if dry_run:
|
|
854
|
+
console.print("\n[bold cyan]Dry run complete.[/bold cyan] No packages were modified.")
|
|
855
|
+
sys.exit(0)
|
|
734
856
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
857
|
+
if not yes:
|
|
858
|
+
console.print()
|
|
859
|
+
confirm = click.confirm("Do you want to proceed with the rollback?", default=True)
|
|
860
|
+
if not confirm:
|
|
861
|
+
console.print("[yellow]Rollback cancelled.[/yellow]")
|
|
862
|
+
sys.exit(0)
|
|
738
863
|
|
|
739
|
-
|
|
740
|
-
console.print("\n[bold green]✓ Ignores removed successfully![/bold green]")
|
|
741
|
-
console.print(f"[bold]File:[/bold] {config_path}")
|
|
864
|
+
console.print("\n[bold]Rolling back packages...[/bold]\n")
|
|
742
865
|
|
|
743
|
-
|
|
744
|
-
console.print("\n[bold]Ignores removed:[/bold]")
|
|
745
|
-
for package in removed_packages:
|
|
746
|
-
console.print(f" [red]Removed[/red]: {package}")
|
|
866
|
+
rolled_back = rollback_to_state(state_data, dry_run=False)
|
|
747
867
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if current_env and current_env != "global":
|
|
755
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {current_env}")
|
|
756
|
-
else:
|
|
757
|
-
console.print("\n[bold cyan]Environment updated:[/bold cyan] global")
|
|
758
|
-
|
|
759
|
-
return
|
|
760
|
-
|
|
761
|
-
except ValueError as e:
|
|
762
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
763
|
-
sys.exit(1)
|
|
764
|
-
except IOError as e:
|
|
765
|
-
console.print(f"[red]Error writing configuration: {e}[/red]")
|
|
766
|
-
sys.exit(1)
|
|
767
|
-
|
|
768
|
-
# Handle --remove-all option
|
|
769
|
-
if remove_all_ignores:
|
|
770
|
-
from .package_constraints import remove_all_ignores_from_config, list_all_ignores
|
|
771
|
-
|
|
772
|
-
# Get confirmation if not using --yes and removing from all environments
|
|
773
|
-
if not env and not skip_confirmation:
|
|
774
|
-
# Show what will be removed
|
|
775
|
-
try:
|
|
776
|
-
all_ignores = list_all_ignores()
|
|
777
|
-
if not all_ignores:
|
|
778
|
-
console.print("[yellow]No ignores found in any environment.[/yellow]")
|
|
779
|
-
return
|
|
780
|
-
|
|
781
|
-
console.print("[bold red]WARNING: This will remove ALL ignores from ALL environments![/bold red]")
|
|
782
|
-
console.print("\n[bold]Ignores that will be removed:[/bold]")
|
|
783
|
-
|
|
784
|
-
total_ignores = 0
|
|
785
|
-
for env_name, ignores in all_ignores.items():
|
|
786
|
-
console.print(f"\n[bold cyan]{env_name}:[/bold cyan]")
|
|
787
|
-
for package in sorted(ignores):
|
|
788
|
-
console.print(f" {package}")
|
|
789
|
-
total_ignores += 1
|
|
790
|
-
|
|
791
|
-
console.print(f"\n[bold]Total: {total_ignores} ignore(s) in {len(all_ignores)} environment(s)[/bold]")
|
|
792
|
-
|
|
793
|
-
if not click.confirm("\nAre you sure you want to remove all ignores?"):
|
|
794
|
-
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
795
|
-
return
|
|
796
|
-
|
|
797
|
-
except Exception:
|
|
798
|
-
# If we can't list ignores, ask for generic confirmation
|
|
799
|
-
if not click.confirm("Are you sure you want to remove all ignores from all environments?"):
|
|
800
|
-
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
801
|
-
return
|
|
802
|
-
|
|
803
|
-
console.print(f"[bold blue]Removing all ignores from {'all environments' if not env else env}...[/bold blue]")
|
|
804
|
-
|
|
805
|
-
try:
|
|
806
|
-
config_path, removed_ignores = remove_all_ignores_from_config(env)
|
|
807
|
-
|
|
808
|
-
if not removed_ignores:
|
|
809
|
-
if env:
|
|
810
|
-
console.print(f"[yellow]No ignores found in environment '{env}'.[/yellow]")
|
|
811
|
-
else:
|
|
812
|
-
console.print("[yellow]No ignores found in any environment.[/yellow]")
|
|
813
|
-
return
|
|
814
|
-
|
|
815
|
-
# Display summary
|
|
816
|
-
console.print("\n[bold green]✓ All ignores removed successfully![/bold green]")
|
|
817
|
-
console.print(f"[bold]File:[/bold] {config_path}")
|
|
818
|
-
|
|
819
|
-
# Show removed ignores
|
|
820
|
-
console.print("\n[bold]Ignores removed:[/bold]")
|
|
821
|
-
total_removed = 0
|
|
822
|
-
for env_name, ignores in removed_ignores.items():
|
|
823
|
-
console.print(f"\n[bold cyan]{env_name}:[/bold cyan]")
|
|
824
|
-
for package in sorted(ignores):
|
|
825
|
-
console.print(f" [red]Removed[/red]: {package}")
|
|
826
|
-
total_removed += 1
|
|
827
|
-
|
|
828
|
-
console.print(f"\n[bold]Total removed: {total_removed} ignore(s) from {len(removed_ignores)} environment(s)[/bold]")
|
|
829
|
-
|
|
830
|
-
# Show which environment(s) were updated
|
|
831
|
-
if env:
|
|
832
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {env}")
|
|
833
|
-
else:
|
|
834
|
-
environments_updated = list(removed_ignores.keys())
|
|
835
|
-
console.print(f"\n[bold cyan]Environments updated:[/bold cyan] {', '.join(sorted(environments_updated))}")
|
|
868
|
+
if rolled_back:
|
|
869
|
+
console.print(f"\n[bold green]Successfully rolled back {len(rolled_back)} package(s):[/bold green]")
|
|
870
|
+
for pkg in rolled_back:
|
|
871
|
+
console.print(f" - {pkg}")
|
|
872
|
+
else:
|
|
873
|
+
console.print("[yellow]No packages were rolled back.[/yellow]")
|
|
836
874
|
|
|
837
|
-
|
|
875
|
+
sys.exit(0)
|
|
838
876
|
|
|
839
|
-
except ValueError as e:
|
|
840
|
-
console.print(f"[red]Error: {e}[/red]")
|
|
841
|
-
sys.exit(1)
|
|
842
|
-
except IOError as e:
|
|
843
|
-
console.print(f"[red]Error writing configuration: {e}[/red]")
|
|
844
|
-
sys.exit(1)
|
|
845
877
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
878
|
+
@cli.command()
|
|
879
|
+
def cache() -> None:
|
|
880
|
+
"""
|
|
881
|
+
Show cache information.
|
|
850
882
|
|
|
851
|
-
|
|
883
|
+
Displays details about the package version cache for the current
|
|
884
|
+
Python environment, including age and freshness status.
|
|
852
885
|
|
|
853
|
-
|
|
854
|
-
|
|
886
|
+
[bold]Examples:[/bold]
|
|
887
|
+
pipu cache Show cache status
|
|
888
|
+
pipu clean Clear current environment cache
|
|
889
|
+
pipu clean --all Clear all environment caches
|
|
890
|
+
"""
|
|
891
|
+
console = Console()
|
|
855
892
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
config_path = get_recommended_pip_config_path()
|
|
893
|
+
# Show cache info
|
|
894
|
+
info = get_cache_info()
|
|
859
895
|
|
|
860
|
-
|
|
861
|
-
|
|
896
|
+
console.print("[bold]Cache Information[/bold]\n")
|
|
897
|
+
console.print(f" Environment ID: [cyan]{info['environment_id']}[/cyan]")
|
|
898
|
+
console.print(f" Python: [dim]{info['python_executable']}[/dim]")
|
|
899
|
+
console.print(f" Cache path: [dim]{info['path']}[/dim]")
|
|
862
900
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
901
|
+
if info['exists']:
|
|
902
|
+
console.print("\n [green]Cache exists[/green]")
|
|
903
|
+
console.print(f" Updated: {info.get('age_human', 'unknown')}")
|
|
904
|
+
console.print(f" Packages cached: {info.get('package_count', 0)}")
|
|
866
905
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
console.print(
|
|
906
|
+
# Check if fresh
|
|
907
|
+
if is_cache_fresh():
|
|
908
|
+
console.print(" Status: [green]Fresh[/green] (within TTL)")
|
|
909
|
+
else:
|
|
910
|
+
console.print(" Status: [yellow]Stale[/yellow] (will refresh on next upgrade)")
|
|
911
|
+
else:
|
|
912
|
+
console.print("\n [yellow]No cache[/yellow]")
|
|
913
|
+
console.print(" Run [cyan]pipu update[/cyan] to create cache")
|
|
870
914
|
|
|
871
|
-
|
|
872
|
-
console.print("\n[bold]Ignores modified:[/bold]")
|
|
873
|
-
for package, action in changes.items():
|
|
874
|
-
if action == 'added':
|
|
875
|
-
console.print(f" [green]Added[/green]: {package}")
|
|
876
|
-
elif action == 'already_exists':
|
|
877
|
-
console.print(f" [yellow]Already ignored[/yellow]: {package}")
|
|
915
|
+
sys.exit(0)
|
|
878
916
|
|
|
879
|
-
# Show which environment was updated
|
|
880
|
-
if env:
|
|
881
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {env}")
|
|
882
|
-
else:
|
|
883
|
-
current_env = get_current_environment_name()
|
|
884
|
-
if current_env and current_env != "global":
|
|
885
|
-
console.print(f"\n[bold cyan]Environment updated:[/bold cyan] {current_env}")
|
|
886
|
-
else:
|
|
887
|
-
console.print("\n[bold cyan]Environment updated:[/bold cyan] global")
|
|
888
917
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
918
|
+
@cli.command()
|
|
919
|
+
@click.option(
|
|
920
|
+
"--all", "-a",
|
|
921
|
+
"clean_all",
|
|
922
|
+
is_flag=True,
|
|
923
|
+
help="Clean up files for all environments"
|
|
924
|
+
)
|
|
925
|
+
def clean(clean_all: bool) -> None:
|
|
926
|
+
"""
|
|
927
|
+
Clean up pipu caches and temporary files.
|
|
892
928
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
929
|
+
Removes cached package version data and other temporary files
|
|
930
|
+
created by pipu. By default, cleans up files for the current
|
|
931
|
+
Python environment only.
|
|
932
|
+
|
|
933
|
+
[bold]Examples:[/bold]
|
|
934
|
+
pipu clean Clean current environment
|
|
935
|
+
pipu clean --all Clean all environments
|
|
936
|
+
"""
|
|
937
|
+
console = Console()
|
|
938
|
+
|
|
939
|
+
if clean_all:
|
|
940
|
+
count = clear_all_caches()
|
|
941
|
+
if count > 0:
|
|
942
|
+
console.print(f"[bold green]Cleared {count} cache(s).[/bold green]")
|
|
943
|
+
else:
|
|
944
|
+
console.print("[yellow]No caches to clear.[/yellow]")
|
|
945
|
+
else:
|
|
946
|
+
if clear_cache():
|
|
947
|
+
console.print("[bold green]Cache cleared for current environment.[/bold green]")
|
|
948
|
+
else:
|
|
949
|
+
console.print("[yellow]No cache to clear for current environment.[/yellow]")
|
|
950
|
+
|
|
951
|
+
sys.exit(0)
|
|
896
952
|
|
|
897
953
|
|
|
898
|
-
|
|
899
|
-
if __name__ == '__main__':
|
|
954
|
+
if __name__ == "__main__":
|
|
900
955
|
cli()
|