pipu-cli 0.1.dev7__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 +863 -813
- 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 -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.0.dist-info}/WHEEL +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/top_level.txt +0 -0
pipu_cli/cli.py
CHANGED
|
@@ -1,905 +1,955 @@
|
|
|
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
|
+
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,
|
|
11
43
|
)
|
|
12
|
-
from .common import console
|
|
13
|
-
from pip._internal.commands.install import InstallCommand
|
|
14
44
|
|
|
15
45
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
19
50
|
|
|
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
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)
|
|
57
|
+
"""
|
|
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())
|
|
94
63
|
|
|
64
|
+
return (spec.strip(), None)
|
|
95
65
|
|
|
96
66
|
|
|
97
67
|
@click.group(invoke_without_command=True)
|
|
98
68
|
@click.pass_context
|
|
99
|
-
def cli(ctx):
|
|
69
|
+
def cli(ctx: click.Context) -> None:
|
|
100
70
|
"""
|
|
101
|
-
pipu - Python package updater
|
|
71
|
+
[bold cyan]pipu[/bold cyan] - A cute Python package updater
|
|
102
72
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if ctx.invoked_subcommand is None:
|
|
106
|
-
# No subcommand was invoked, launch the TUI
|
|
107
|
-
launch_tui()
|
|
73
|
+
Automatically checks for package updates and upgrades them with proper
|
|
74
|
+
constraint resolution.
|
|
108
75
|
|
|
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
|
|
109
80
|
|
|
110
|
-
|
|
111
|
-
@click.option('--pre', is_flag=True, help='Include pre-release versions (alpha, beta, rc, dev)')
|
|
112
|
-
@click.option('--debug', is_flag=True, help='Print debug information as packages are checked')
|
|
113
|
-
def list_packages(pre, debug):
|
|
114
|
-
"""
|
|
115
|
-
List outdated packages
|
|
116
|
-
|
|
117
|
-
Displays a formatted table of installed packages that have newer versions
|
|
118
|
-
available on the configured package indexes. By default, pre-release versions
|
|
119
|
-
are excluded unless --pre is specified.
|
|
81
|
+
Run [cyan]pipu <command> --help[/cyan] for command-specific help.
|
|
120
82
|
"""
|
|
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)
|
|
83
|
+
# If no subcommand provided, default to upgrade
|
|
84
|
+
if ctx.invoked_subcommand is None:
|
|
85
|
+
ctx.invoke(upgrade)
|
|
170
86
|
|
|
171
87
|
|
|
172
88
|
@cli.command()
|
|
173
|
-
@click.option(
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
176
118
|
"""
|
|
177
|
-
|
|
119
|
+
Refresh the package version cache.
|
|
178
120
|
|
|
179
|
-
|
|
180
|
-
|
|
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.
|
|
181
124
|
|
|
182
|
-
|
|
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
|
|
183
129
|
"""
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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)]
|
|
199
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")
|
|
200
153
|
|
|
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]")
|
|
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"}')
|
|
250
183
|
else:
|
|
251
|
-
console.print(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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]")
|
|
265
261
|
|
|
266
|
-
|
|
267
|
-
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.")
|
|
268
263
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
total_time = step1_time + step2_time + step3_time
|
|
265
|
+
if debug:
|
|
266
|
+
console.print(f"[dim]Total time: {total_time:.2f}s[/dim]")
|
|
272
267
|
|
|
273
|
-
|
|
274
|
-
console.print("[bold red]✗ Some packages failed to update.[/bold red]")
|
|
275
|
-
sys.exit(exit_code)
|
|
268
|
+
sys.exit(0)
|
|
276
269
|
|
|
270
|
+
except KeyboardInterrupt:
|
|
271
|
+
console.print("\n[yellow]Interrupted by user.[/yellow]")
|
|
272
|
+
sys.exit(130)
|
|
277
273
|
except Exception as e:
|
|
278
|
-
|
|
274
|
+
if output == "json":
|
|
275
|
+
print(json.dumps({"error": str(e)}))
|
|
276
|
+
else:
|
|
277
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
279
278
|
sys.exit(1)
|
|
280
279
|
|
|
281
280
|
|
|
282
281
|
@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
|
-
|
|
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:
|
|
322
357
|
"""
|
|
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)
|
|
358
|
+
Upgrade installed packages.
|
|
331
359
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
console.print("[red]Error: --invalidates-when cannot be used with --list, --remove, or --remove-all.[/red]")
|
|
335
|
-
sys.exit(1)
|
|
360
|
+
By default, upgrades all packages that have newer versions available.
|
|
361
|
+
Optionally specify PACKAGES to upgrade only those packages.
|
|
336
362
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
sys.exit(1)
|
|
363
|
+
Uses cached version data if available and fresh. Run [cyan]pipu update[/cyan]
|
|
364
|
+
to refresh the cache manually.
|
|
340
365
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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")
|
|
346
421
|
|
|
347
|
-
|
|
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)
|
|
348
462
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
)
|
|
352
503
|
else:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
)
|
|
362
545
|
else:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
# Handle --remove option
|
|
368
|
-
if remove_constraints:
|
|
369
|
-
if not constraint_specs:
|
|
370
|
-
console.print("[red]Error: At least one package name must be specified for removal.[/red]")
|
|
371
|
-
sys.exit(1)
|
|
372
|
-
|
|
373
|
-
from .package_constraints import remove_constraints_from_config, parse_invalidation_triggers_storage, get_current_environment_name, parse_inline_constraints
|
|
374
|
-
|
|
375
|
-
package_names = list(constraint_specs)
|
|
376
|
-
console.print(f"[bold blue]Removing constraints for {len(package_names)} package(s)...[/bold blue]")
|
|
546
|
+
latest_versions = get_latest_versions(
|
|
547
|
+
installed_packages, timeout=timeout, include_prereleases=pre
|
|
548
|
+
)
|
|
377
549
|
|
|
378
|
-
|
|
379
|
-
config_path, removed_constraints, removed_triggers = remove_constraints_from_config(package_names, env)
|
|
550
|
+
step2_time = time.time() - step2_start
|
|
380
551
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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]")
|
|
384
557
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
console.print(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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)
|
|
395
647
|
|
|
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}")
|
|
648
|
+
if show_blocked and blocked_packages:
|
|
649
|
+
console.print()
|
|
650
|
+
print_blocked_packages_table(blocked_packages, console=console)
|
|
402
651
|
|
|
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))}")
|
|
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)
|
|
505
657
|
|
|
506
|
-
|
|
658
|
+
if dry_run:
|
|
659
|
+
console.print("\n[bold cyan]Dry run complete.[/bold cyan] No packages were modified.")
|
|
660
|
+
sys.exit(0)
|
|
507
661
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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)
|
|
514
712
|
|
|
515
|
-
|
|
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]")
|
|
516
717
|
|
|
517
|
-
|
|
518
|
-
|
|
718
|
+
# Exit with appropriate code
|
|
719
|
+
failed = [pkg for pkg in results if not pkg.upgraded]
|
|
720
|
+
if failed:
|
|
519
721
|
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
722
|
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)
|
|
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)
|
|
654
731
|
except Exception as e:
|
|
655
|
-
console.print(f"[red]
|
|
732
|
+
console.print(f"\n[bold red]Error:[/bold red] {e}")
|
|
656
733
|
sys.exit(1)
|
|
657
734
|
|
|
658
735
|
|
|
659
736
|
@cli.command()
|
|
660
|
-
@click.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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:
|
|
667
760
|
"""
|
|
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
|
|
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
|
|
692
771
|
"""
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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]")
|
|
698
817
|
sys.exit(1)
|
|
699
818
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
819
|
+
with open(state_path, 'r') as f:
|
|
820
|
+
state_data = json.load(f)
|
|
821
|
+
else:
|
|
822
|
+
state_data = get_latest_state()
|
|
703
823
|
|
|
704
|
-
|
|
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)
|
|
705
828
|
|
|
706
|
-
|
|
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", "")
|
|
707
833
|
|
|
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]")
|
|
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
|
|
723
838
|
|
|
724
|
-
|
|
839
|
+
console.print(f"\n[bold]Rollback State:[/bold] {formatted_ts}")
|
|
840
|
+
if description:
|
|
841
|
+
console.print(f"[dim]{description}[/dim]")
|
|
842
|
+
console.print()
|
|
725
843
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
console.print("[red]Error: At least one package name must be specified for removal.[/red]")
|
|
730
|
-
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")
|
|
731
847
|
|
|
732
|
-
|
|
848
|
+
for pkg in packages:
|
|
849
|
+
table.add_row(pkg["name"], pkg["version"])
|
|
733
850
|
|
|
734
|
-
|
|
735
|
-
console.print(f"[bold blue]Removing ignores for {len(packages_list)} package(s)...[/bold blue]")
|
|
851
|
+
console.print(table)
|
|
736
852
|
|
|
737
|
-
|
|
738
|
-
|
|
853
|
+
if dry_run:
|
|
854
|
+
console.print("\n[bold cyan]Dry run complete.[/bold cyan] No packages were modified.")
|
|
855
|
+
sys.exit(0)
|
|
739
856
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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)
|
|
743
863
|
|
|
744
|
-
|
|
745
|
-
console.print("\n[bold green]✓ Ignores removed successfully![/bold green]")
|
|
746
|
-
console.print(f"[bold]File:[/bold] {config_path}")
|
|
864
|
+
console.print("\n[bold]Rolling back packages...[/bold]\n")
|
|
747
865
|
|
|
748
|
-
|
|
749
|
-
console.print("\n[bold]Ignores removed:[/bold]")
|
|
750
|
-
for package in removed_packages:
|
|
751
|
-
console.print(f" [red]Removed[/red]: {package}")
|
|
866
|
+
rolled_back = rollback_to_state(state_data, dry_run=False)
|
|
752
867
|
|
|
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))}")
|
|
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]")
|
|
841
874
|
|
|
842
|
-
|
|
875
|
+
sys.exit(0)
|
|
843
876
|
|
|
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
877
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
878
|
+
@cli.command()
|
|
879
|
+
def cache() -> None:
|
|
880
|
+
"""
|
|
881
|
+
Show cache information.
|
|
855
882
|
|
|
856
|
-
|
|
883
|
+
Displays details about the package version cache for the current
|
|
884
|
+
Python environment, including age and freshness status.
|
|
857
885
|
|
|
858
|
-
|
|
859
|
-
|
|
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()
|
|
860
892
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
config_path = get_recommended_pip_config_path()
|
|
893
|
+
# Show cache info
|
|
894
|
+
info = get_cache_info()
|
|
864
895
|
|
|
865
|
-
|
|
866
|
-
|
|
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]")
|
|
867
900
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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)}")
|
|
871
905
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
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")
|
|
875
914
|
|
|
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}")
|
|
915
|
+
sys.exit(0)
|
|
883
916
|
|
|
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
917
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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.
|
|
897
928
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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)
|
|
901
952
|
|
|
902
953
|
|
|
903
|
-
|
|
904
|
-
if __name__ == '__main__':
|
|
954
|
+
if __name__ == "__main__":
|
|
905
955
|
cli()
|