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