pipu-cli 0.2.0__py3-none-any.whl → 0.2.2__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 +1 -1
- pipu_cli/cache.py +41 -82
- pipu_cli/cli.py +249 -219
- pipu_cli/config_file.py +0 -1
- pipu_cli/package_management.py +154 -9
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.2.dist-info}/METADATA +1 -1
- pipu_cli-0.2.2.dist-info/RECORD +16 -0
- pipu_cli-0.2.0.dist-info/RECORD +0 -16
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.2.dist-info}/WHEEL +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.2.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.2.dist-info}/top_level.txt +0 -0
pipu_cli/__init__.py
CHANGED
pipu_cli/cache.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
"""Package
|
|
1
|
+
"""Package version caching for pipu.
|
|
2
2
|
|
|
3
|
-
This module provides caching of package
|
|
3
|
+
This module provides caching of latest package versions from PyPI to speed up
|
|
4
4
|
repeated runs of pipu. The cache is per-environment, identified by the
|
|
5
5
|
Python executable path, making it compatible with venv, conda, mise, etc.
|
|
6
|
+
|
|
7
|
+
The cache stores only the latest available versions - constraint resolution
|
|
8
|
+
is performed at upgrade time with the current installed package state.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
import hashlib
|
|
9
12
|
import json
|
|
10
13
|
import logging
|
|
11
14
|
import sys
|
|
12
|
-
from dataclasses import dataclass,
|
|
15
|
+
from dataclasses import dataclass, asdict
|
|
13
16
|
from datetime import datetime, timezone
|
|
14
17
|
from pathlib import Path
|
|
15
18
|
from typing import Dict, List, Optional, Any
|
|
@@ -22,24 +25,15 @@ from pipu_cli.config import DEFAULT_CACHE_TTL, CACHE_BASE_DIR
|
|
|
22
25
|
logger = logging.getLogger(__name__)
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
@dataclass
|
|
26
|
-
class CachedPackage:
|
|
27
|
-
"""Cached information about a single package."""
|
|
28
|
-
name: str
|
|
29
|
-
installed_version: str
|
|
30
|
-
latest_version: str
|
|
31
|
-
is_upgradable: bool
|
|
32
|
-
is_editable: bool
|
|
33
|
-
checked_at: str # ISO format timestamp
|
|
34
|
-
|
|
35
|
-
|
|
36
28
|
@dataclass
|
|
37
29
|
class CacheData:
|
|
38
|
-
"""
|
|
30
|
+
"""Cache data structure - stores latest versions from PyPI."""
|
|
39
31
|
environment_id: str
|
|
40
32
|
python_executable: str
|
|
41
33
|
updated_at: str # ISO format timestamp
|
|
42
|
-
|
|
34
|
+
include_prereleases: bool
|
|
35
|
+
# Maps package name (lowercase) to latest version string
|
|
36
|
+
latest_versions: Dict[str, str]
|
|
43
37
|
|
|
44
38
|
|
|
45
39
|
def get_environment_id() -> str:
|
|
@@ -52,7 +46,6 @@ def get_environment_id() -> str:
|
|
|
52
46
|
:returns: Short hash identifying the environment
|
|
53
47
|
"""
|
|
54
48
|
executable = sys.executable
|
|
55
|
-
# Create a short hash of the executable path
|
|
56
49
|
hash_obj = hashlib.sha256(executable.encode())
|
|
57
50
|
return hash_obj.hexdigest()[:12]
|
|
58
51
|
|
|
@@ -99,17 +92,19 @@ def load_cache() -> Optional[CacheData]:
|
|
|
99
92
|
environment_id=data["environment_id"],
|
|
100
93
|
python_executable=data["python_executable"],
|
|
101
94
|
updated_at=data["updated_at"],
|
|
102
|
-
|
|
95
|
+
include_prereleases=data.get("include_prereleases", False),
|
|
96
|
+
latest_versions=data.get("latest_versions", {})
|
|
103
97
|
)
|
|
104
98
|
except (json.JSONDecodeError, KeyError, TypeError) as e:
|
|
105
99
|
logger.debug(f"Failed to load cache: {e}")
|
|
106
100
|
return None
|
|
107
101
|
|
|
108
102
|
|
|
109
|
-
def save_cache(
|
|
110
|
-
"""Save
|
|
103
|
+
def save_cache(latest_versions: Dict[str, str], include_prereleases: bool = False) -> Path:
|
|
104
|
+
"""Save latest version data to the cache.
|
|
111
105
|
|
|
112
|
-
:param
|
|
106
|
+
:param latest_versions: Dictionary mapping package names (lowercase) to latest version strings
|
|
107
|
+
:param include_prereleases: Whether prereleases were included in version check
|
|
113
108
|
:returns: Path to the saved cache file
|
|
114
109
|
"""
|
|
115
110
|
cache_dir = get_cache_dir()
|
|
@@ -121,7 +116,8 @@ def save_cache(packages: Dict[str, Dict[str, Any]]) -> Path:
|
|
|
121
116
|
environment_id=get_environment_id(),
|
|
122
117
|
python_executable=sys.executable,
|
|
123
118
|
updated_at=datetime.now(timezone.utc).isoformat(),
|
|
124
|
-
|
|
119
|
+
include_prereleases=include_prereleases,
|
|
120
|
+
latest_versions=latest_versions
|
|
125
121
|
)
|
|
126
122
|
|
|
127
123
|
with open(cache_path, 'w') as f:
|
|
@@ -143,7 +139,6 @@ def is_cache_fresh(ttl_seconds: int = DEFAULT_CACHE_TTL) -> bool:
|
|
|
143
139
|
|
|
144
140
|
try:
|
|
145
141
|
updated_at = datetime.fromisoformat(cache.updated_at)
|
|
146
|
-
# Ensure updated_at is timezone-aware
|
|
147
142
|
if updated_at.tzinfo is None:
|
|
148
143
|
updated_at = updated_at.replace(tzinfo=timezone.utc)
|
|
149
144
|
|
|
@@ -237,61 +232,6 @@ def clear_all_caches() -> int:
|
|
|
237
232
|
return count
|
|
238
233
|
|
|
239
234
|
|
|
240
|
-
def get_cached_package(name: str) -> Optional[Dict[str, Any]]:
|
|
241
|
-
"""Get cached data for a specific package.
|
|
242
|
-
|
|
243
|
-
:param name: Package name (case-insensitive)
|
|
244
|
-
:returns: Package cache data or None
|
|
245
|
-
"""
|
|
246
|
-
cache = load_cache()
|
|
247
|
-
if cache is None:
|
|
248
|
-
return None
|
|
249
|
-
|
|
250
|
-
# Normalize name for lookup
|
|
251
|
-
name_lower = name.lower()
|
|
252
|
-
return cache.packages.get(name_lower)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def build_cache_from_results(
|
|
256
|
-
installed_packages: List[Any],
|
|
257
|
-
latest_versions: Dict[Any, Any],
|
|
258
|
-
upgradable_packages: List[Any]
|
|
259
|
-
) -> Dict[str, Dict[str, Any]]:
|
|
260
|
-
"""Build cache data from pipu's package analysis results.
|
|
261
|
-
|
|
262
|
-
:param installed_packages: List of InstalledPackage objects
|
|
263
|
-
:param latest_versions: Dict mapping InstalledPackage to LatestVersionInfo
|
|
264
|
-
:param upgradable_packages: List of UpgradePackageInfo objects
|
|
265
|
-
:returns: Dictionary suitable for save_cache()
|
|
266
|
-
"""
|
|
267
|
-
# Create lookup for upgradable packages
|
|
268
|
-
upgradable_names = {pkg.name.lower() for pkg in upgradable_packages}
|
|
269
|
-
|
|
270
|
-
packages = {}
|
|
271
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
272
|
-
|
|
273
|
-
for installed in installed_packages:
|
|
274
|
-
name_lower = installed.name.lower()
|
|
275
|
-
|
|
276
|
-
# Find latest version if available
|
|
277
|
-
latest_version = None
|
|
278
|
-
for inst_pkg, latest_info in latest_versions.items():
|
|
279
|
-
if inst_pkg.name.lower() == name_lower:
|
|
280
|
-
latest_version = str(latest_info.version)
|
|
281
|
-
break
|
|
282
|
-
|
|
283
|
-
packages[name_lower] = {
|
|
284
|
-
"name": installed.name,
|
|
285
|
-
"installed_version": str(installed.version),
|
|
286
|
-
"latest_version": latest_version or str(installed.version),
|
|
287
|
-
"is_upgradable": name_lower in upgradable_names,
|
|
288
|
-
"is_editable": installed.is_editable,
|
|
289
|
-
"checked_at": now
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return packages
|
|
293
|
-
|
|
294
|
-
|
|
295
235
|
def get_cache_info() -> Dict[str, Any]:
|
|
296
236
|
"""Get information about the current cache.
|
|
297
237
|
|
|
@@ -300,7 +240,7 @@ def get_cache_info() -> Dict[str, Any]:
|
|
|
300
240
|
cache = load_cache()
|
|
301
241
|
cache_path = get_cache_path()
|
|
302
242
|
|
|
303
|
-
info = {
|
|
243
|
+
info: Dict[str, Any] = {
|
|
304
244
|
"exists": cache is not None,
|
|
305
245
|
"path": str(cache_path),
|
|
306
246
|
"environment_id": get_environment_id(),
|
|
@@ -309,8 +249,27 @@ def get_cache_info() -> Dict[str, Any]:
|
|
|
309
249
|
|
|
310
250
|
if cache:
|
|
311
251
|
info["updated_at"] = cache.updated_at
|
|
312
|
-
info["package_count"] = len(cache.
|
|
313
|
-
info["
|
|
314
|
-
|
|
252
|
+
info["package_count"] = len(cache.latest_versions)
|
|
253
|
+
info["include_prereleases"] = cache.include_prereleases
|
|
254
|
+
age_seconds = get_cache_age_seconds()
|
|
255
|
+
info["age_seconds"] = age_seconds
|
|
256
|
+
info["age_human"] = format_cache_age(age_seconds)
|
|
315
257
|
|
|
316
258
|
return info
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def build_version_cache(
|
|
262
|
+
latest_versions: Dict[Any, Any]
|
|
263
|
+
) -> Dict[str, str]:
|
|
264
|
+
"""Build cache data from pipu's version check results.
|
|
265
|
+
|
|
266
|
+
:param latest_versions: Dict mapping InstalledPackage to Package with latest version
|
|
267
|
+
:returns: Dictionary mapping package names (lowercase) to latest version strings
|
|
268
|
+
"""
|
|
269
|
+
result: Dict[str, str] = {}
|
|
270
|
+
|
|
271
|
+
for installed_pkg, latest_pkg in latest_versions.items():
|
|
272
|
+
name_lower = installed_pkg.name.lower()
|
|
273
|
+
result[name_lower] = str(latest_pkg.version)
|
|
274
|
+
|
|
275
|
+
return result
|
pipu_cli/cli.py
CHANGED
|
@@ -13,13 +13,16 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskPr
|
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
15
|
from pipu_cli.package_management import (
|
|
16
|
+
Package,
|
|
16
17
|
inspect_installed_packages,
|
|
17
18
|
get_latest_versions,
|
|
18
19
|
get_latest_versions_parallel,
|
|
19
20
|
resolve_upgradable_packages,
|
|
20
21
|
resolve_upgradable_packages_with_reasons,
|
|
21
22
|
install_packages,
|
|
23
|
+
reinstall_editable_packages,
|
|
22
24
|
)
|
|
25
|
+
from packaging.version import Version
|
|
23
26
|
from pipu_cli.pretty import (
|
|
24
27
|
print_upgradable_packages_table,
|
|
25
28
|
print_upgrade_results,
|
|
@@ -34,7 +37,7 @@ from pipu_cli.cache import (
|
|
|
34
37
|
is_cache_fresh,
|
|
35
38
|
load_cache,
|
|
36
39
|
save_cache,
|
|
37
|
-
|
|
40
|
+
build_version_cache,
|
|
38
41
|
get_cache_info,
|
|
39
42
|
format_cache_age,
|
|
40
43
|
get_cache_age_seconds,
|
|
@@ -122,6 +125,8 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
122
125
|
packages and stores it locally. This speeds up subsequent upgrade
|
|
123
126
|
commands by avoiding repeated network requests.
|
|
124
127
|
|
|
128
|
+
Constraint resolution is performed at upgrade time, not during update.
|
|
129
|
+
|
|
125
130
|
[bold]Examples:[/bold]
|
|
126
131
|
pipu update Update cache with defaults
|
|
127
132
|
pipu update --parallel 4 Update with parallel requests
|
|
@@ -154,7 +159,7 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
154
159
|
try:
|
|
155
160
|
# Step 1: Inspect installed packages
|
|
156
161
|
if output != "json":
|
|
157
|
-
console.print("[bold]Step 1/
|
|
162
|
+
console.print("[bold]Step 1/2:[/bold] Inspecting installed packages...")
|
|
158
163
|
|
|
159
164
|
step1_start = time.time()
|
|
160
165
|
if output != "json":
|
|
@@ -184,9 +189,9 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
184
189
|
console.print("[yellow]No packages found.[/yellow]")
|
|
185
190
|
sys.exit(0)
|
|
186
191
|
|
|
187
|
-
# Step 2:
|
|
192
|
+
# Step 2: Fetch latest versions from PyPI and save to cache
|
|
188
193
|
if output != "json":
|
|
189
|
-
console.print("\n[bold]Step 2/
|
|
194
|
+
console.print("\n[bold]Step 2/2:[/bold] Fetching latest versions from PyPI...")
|
|
190
195
|
|
|
191
196
|
step2_start = time.time()
|
|
192
197
|
if output != "json":
|
|
@@ -199,7 +204,7 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
199
204
|
) as progress:
|
|
200
205
|
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
201
206
|
|
|
202
|
-
def update_progress(current, total):
|
|
207
|
+
def update_progress(current: int, total: int) -> None:
|
|
203
208
|
progress.update(task, completed=current)
|
|
204
209
|
|
|
205
210
|
if parallel > 1:
|
|
@@ -223,45 +228,29 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
223
228
|
)
|
|
224
229
|
step2_time = time.time() - step2_start
|
|
225
230
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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)
|
|
240
234
|
|
|
241
|
-
|
|
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)
|
|
235
|
+
num_with_updates = len(latest_versions)
|
|
246
236
|
|
|
247
237
|
if output == "json":
|
|
248
238
|
result = {
|
|
249
239
|
"status": "success",
|
|
250
240
|
"packages_checked": num_installed,
|
|
251
|
-
"packages_with_updates":
|
|
252
|
-
"packages_upgradable": num_upgradable,
|
|
241
|
+
"packages_with_updates": num_with_updates,
|
|
253
242
|
"cache_path": str(cache_path)
|
|
254
243
|
}
|
|
255
244
|
print(json.dumps(result, indent=2))
|
|
256
245
|
else:
|
|
257
|
-
console.print(f" {
|
|
246
|
+
console.print(f" Cached {num_with_updates} packages with updates available")
|
|
258
247
|
if debug:
|
|
259
|
-
console.print(f" [dim]Time: {
|
|
248
|
+
console.print(f" [dim]Time: {step2_time:.2f}s[/dim]")
|
|
260
249
|
console.print(f" [dim]Cache saved to: {cache_path}[/dim]")
|
|
261
250
|
|
|
262
|
-
console.print("\n[bold green]
|
|
251
|
+
console.print("\n[bold green]Cache updated![/bold green] Run [cyan]pipu upgrade[/cyan] to upgrade your packages.")
|
|
263
252
|
|
|
264
|
-
total_time = step1_time + step2_time
|
|
253
|
+
total_time = step1_time + step2_time
|
|
265
254
|
if debug:
|
|
266
255
|
console.print(f"[dim]Total time: {total_time:.2f}s[/dim]")
|
|
267
256
|
|
|
@@ -278,6 +267,225 @@ def update(timeout: int, pre: bool, parallel: int, debug: bool, output: str) ->
|
|
|
278
267
|
sys.exit(1)
|
|
279
268
|
|
|
280
269
|
|
|
270
|
+
# --- Helper functions for upgrade command ---
|
|
271
|
+
|
|
272
|
+
def _step1_inspect_packages(
|
|
273
|
+
console: Console, output: str, timeout: int, debug: bool
|
|
274
|
+
) -> tuple[list, float]:
|
|
275
|
+
"""Step 1: Inspect installed packages."""
|
|
276
|
+
if output != "json":
|
|
277
|
+
console.print("[bold]Step 1/5:[/bold] Inspecting installed packages...")
|
|
278
|
+
|
|
279
|
+
step_start = time.time()
|
|
280
|
+
if output != "json":
|
|
281
|
+
with Progress(
|
|
282
|
+
SpinnerColumn(),
|
|
283
|
+
TextColumn("[progress.description]{task.description}"),
|
|
284
|
+
console=console,
|
|
285
|
+
transient=True
|
|
286
|
+
) as progress:
|
|
287
|
+
progress.add_task("Loading packages...", total=None)
|
|
288
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
289
|
+
else:
|
|
290
|
+
installed_packages = inspect_installed_packages(timeout=timeout)
|
|
291
|
+
step_time = time.time() - step_start
|
|
292
|
+
|
|
293
|
+
if output != "json":
|
|
294
|
+
console.print(f" Found {len(installed_packages)} installed packages")
|
|
295
|
+
if debug:
|
|
296
|
+
console.print(f" [dim]Time: {step_time:.2f}s[/dim]")
|
|
297
|
+
|
|
298
|
+
return installed_packages, step_time
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _step2_get_latest_versions(
|
|
302
|
+
console: Console, output: str, debug: bool,
|
|
303
|
+
installed_packages: list, use_cache: bool, cache_enabled: bool,
|
|
304
|
+
timeout: int, pre: bool, parallel: int
|
|
305
|
+
) -> tuple[dict, float, bool]:
|
|
306
|
+
"""Step 2: Get latest versions from cache or network."""
|
|
307
|
+
if output != "json":
|
|
308
|
+
if use_cache:
|
|
309
|
+
console.print("\n[bold]Step 2/5:[/bold] Loading cached version data...")
|
|
310
|
+
else:
|
|
311
|
+
console.print("\n[bold]Step 2/5:[/bold] Fetching latest versions from PyPI...")
|
|
312
|
+
|
|
313
|
+
step_start = time.time()
|
|
314
|
+
latest_versions: dict = {}
|
|
315
|
+
cache_was_used = False
|
|
316
|
+
|
|
317
|
+
if use_cache:
|
|
318
|
+
cache_data = load_cache()
|
|
319
|
+
if cache_data and cache_data.latest_versions:
|
|
320
|
+
for installed_pkg in installed_packages:
|
|
321
|
+
name_lower = installed_pkg.name.lower()
|
|
322
|
+
if name_lower in cache_data.latest_versions:
|
|
323
|
+
cached_version = cache_data.latest_versions[name_lower]
|
|
324
|
+
try:
|
|
325
|
+
latest_ver = Version(cached_version)
|
|
326
|
+
if latest_ver > installed_pkg.version:
|
|
327
|
+
latest_pkg = Package(name=installed_pkg.name, version=latest_ver)
|
|
328
|
+
latest_versions[installed_pkg] = latest_pkg
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
cache_was_used = True
|
|
332
|
+
else:
|
|
333
|
+
use_cache = False
|
|
334
|
+
|
|
335
|
+
if not use_cache:
|
|
336
|
+
if output != "json":
|
|
337
|
+
with Progress(
|
|
338
|
+
TextColumn("[progress.description]{task.description}"),
|
|
339
|
+
BarColumn(),
|
|
340
|
+
TaskProgressColumn(),
|
|
341
|
+
console=console,
|
|
342
|
+
transient=True
|
|
343
|
+
) as progress:
|
|
344
|
+
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
345
|
+
|
|
346
|
+
def update_progress(current: int, total: int) -> None:
|
|
347
|
+
progress.update(task, completed=current)
|
|
348
|
+
|
|
349
|
+
if parallel > 1:
|
|
350
|
+
latest_versions = get_latest_versions_parallel(
|
|
351
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
352
|
+
max_workers=parallel, progress_callback=update_progress
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
latest_versions = get_latest_versions(
|
|
356
|
+
installed_packages, timeout=timeout, include_prereleases=pre,
|
|
357
|
+
progress_callback=update_progress
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
if parallel > 1:
|
|
361
|
+
latest_versions = get_latest_versions_parallel(
|
|
362
|
+
installed_packages, timeout=timeout, include_prereleases=pre, max_workers=parallel
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
latest_versions = get_latest_versions(
|
|
366
|
+
installed_packages, timeout=timeout, include_prereleases=pre
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if cache_enabled:
|
|
370
|
+
version_cache = build_version_cache(latest_versions)
|
|
371
|
+
save_cache(version_cache, include_prereleases=pre)
|
|
372
|
+
|
|
373
|
+
step_time = time.time() - step_start
|
|
374
|
+
|
|
375
|
+
if output != "json":
|
|
376
|
+
console.print(f" Found {len(latest_versions)} packages with newer versions available")
|
|
377
|
+
if cache_was_used:
|
|
378
|
+
console.print(" [dim](from cache)[/dim]")
|
|
379
|
+
if debug:
|
|
380
|
+
console.print(f" [dim]Time: {step_time:.2f}s[/dim]")
|
|
381
|
+
|
|
382
|
+
return latest_versions, step_time, cache_was_used
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _step3_resolve_packages(
|
|
386
|
+
console: Console, output: str, debug: bool,
|
|
387
|
+
latest_versions: dict, installed_packages: list, show_blocked: bool,
|
|
388
|
+
exclude: str, packages: tuple
|
|
389
|
+
) -> tuple[list, list, dict, float]:
|
|
390
|
+
"""Step 3: Resolve upgradable packages and apply filters."""
|
|
391
|
+
if output != "json":
|
|
392
|
+
console.print("\n[bold]Step 3/5:[/bold] Resolving dependency constraints...")
|
|
393
|
+
step_start = time.time()
|
|
394
|
+
|
|
395
|
+
if show_blocked:
|
|
396
|
+
upgradable_packages, blocked_packages = resolve_upgradable_packages_with_reasons(
|
|
397
|
+
latest_versions, installed_packages
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
all_upgradable = resolve_upgradable_packages(latest_versions, installed_packages)
|
|
401
|
+
upgradable_packages = [pkg for pkg in all_upgradable if pkg.upgradable]
|
|
402
|
+
blocked_packages = []
|
|
403
|
+
|
|
404
|
+
step_time = time.time() - step_start
|
|
405
|
+
|
|
406
|
+
# Apply exclusions
|
|
407
|
+
excluded_names = set()
|
|
408
|
+
if exclude:
|
|
409
|
+
excluded_names = {name.strip().lower() for name in exclude.split(',')}
|
|
410
|
+
if debug and excluded_names:
|
|
411
|
+
console.print(f" [dim]Excluding: {', '.join(sorted(excluded_names))}[/dim]")
|
|
412
|
+
|
|
413
|
+
can_upgrade = [pkg for pkg in upgradable_packages if pkg.name.lower() not in excluded_names]
|
|
414
|
+
|
|
415
|
+
# Parse package specifications and filter
|
|
416
|
+
package_constraints: dict = {}
|
|
417
|
+
if packages:
|
|
418
|
+
requested_packages = set()
|
|
419
|
+
for spec in packages:
|
|
420
|
+
name, constraint = parse_package_spec(spec)
|
|
421
|
+
requested_packages.add(name.lower())
|
|
422
|
+
if constraint:
|
|
423
|
+
package_constraints[name.lower()] = constraint
|
|
424
|
+
|
|
425
|
+
can_upgrade = [pkg for pkg in can_upgrade if pkg.name.lower() in requested_packages]
|
|
426
|
+
|
|
427
|
+
if debug:
|
|
428
|
+
console.print(f" [dim]Filtering to: {', '.join(packages)}[/dim]")
|
|
429
|
+
if package_constraints:
|
|
430
|
+
console.print(f" [dim]Version constraints: {package_constraints}[/dim]")
|
|
431
|
+
|
|
432
|
+
if output != "json":
|
|
433
|
+
console.print(f" {len(can_upgrade)} packages can be safely upgraded")
|
|
434
|
+
if debug:
|
|
435
|
+
console.print(f" [dim]Time: {step_time:.2f}s[/dim]")
|
|
436
|
+
|
|
437
|
+
return can_upgrade, blocked_packages, package_constraints, step_time
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _step5_install_packages(
|
|
441
|
+
console: Console, output: str,
|
|
442
|
+
can_upgrade: list, package_constraints: dict
|
|
443
|
+
) -> tuple[list, float]:
|
|
444
|
+
"""Step 5: Install/upgrade packages."""
|
|
445
|
+
editable_packages = [pkg for pkg in can_upgrade if pkg.is_editable]
|
|
446
|
+
non_editable_packages = [pkg for pkg in can_upgrade if not pkg.is_editable]
|
|
447
|
+
|
|
448
|
+
if output != "json":
|
|
449
|
+
total_to_upgrade = len(non_editable_packages) + len(editable_packages)
|
|
450
|
+
console.print(f"[bold]Step 5/5:[/bold] Upgrading {total_to_upgrade} package(s)...\n")
|
|
451
|
+
step_start = time.time()
|
|
452
|
+
|
|
453
|
+
# Save state for potential rollback
|
|
454
|
+
from pipu_cli.rollback import save_state
|
|
455
|
+
pre_upgrade_packages = [
|
|
456
|
+
{"name": pkg.name, "version": str(pkg.version)}
|
|
457
|
+
for pkg in can_upgrade
|
|
458
|
+
]
|
|
459
|
+
save_state(pre_upgrade_packages, "Pre-upgrade state")
|
|
460
|
+
|
|
461
|
+
stream = ConsoleStream(console) if output != "json" else None
|
|
462
|
+
results = []
|
|
463
|
+
|
|
464
|
+
if non_editable_packages:
|
|
465
|
+
if output != "json":
|
|
466
|
+
console.print(f"Upgrading {len(non_editable_packages)} regular package(s)...\n")
|
|
467
|
+
regular_results = install_packages(
|
|
468
|
+
non_editable_packages,
|
|
469
|
+
output_stream=stream,
|
|
470
|
+
timeout=300,
|
|
471
|
+
version_constraints=package_constraints if package_constraints else None
|
|
472
|
+
)
|
|
473
|
+
results.extend(regular_results)
|
|
474
|
+
|
|
475
|
+
if editable_packages:
|
|
476
|
+
if output != "json":
|
|
477
|
+
console.print(f"\nReinstalling {len(editable_packages)} editable package(s)...\n")
|
|
478
|
+
editable_results = reinstall_editable_packages(
|
|
479
|
+
editable_packages,
|
|
480
|
+
output_stream=stream,
|
|
481
|
+
timeout=300
|
|
482
|
+
)
|
|
483
|
+
results.extend(editable_results)
|
|
484
|
+
|
|
485
|
+
step_time = time.time() - step_start
|
|
486
|
+
return results, step_time
|
|
487
|
+
|
|
488
|
+
|
|
281
489
|
@cli.command()
|
|
282
490
|
@click.argument('packages', nargs=-1)
|
|
283
491
|
@click.option(
|
|
@@ -429,29 +637,7 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
429
637
|
console.print(f"[dim]Using cached data ({format_cache_age(cache_age)})[/dim]\n")
|
|
430
638
|
|
|
431
639
|
# Step 1: Inspect installed packages
|
|
432
|
-
|
|
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]")
|
|
640
|
+
installed_packages, step1_time = _step1_inspect_packages(console, output, timeout, debug)
|
|
455
641
|
|
|
456
642
|
if not installed_packages:
|
|
457
643
|
if output == "json":
|
|
@@ -460,100 +646,11 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
460
646
|
console.print("[yellow]No packages found.[/yellow]")
|
|
461
647
|
sys.exit(0)
|
|
462
648
|
|
|
463
|
-
# Step 2:
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
)
|
|
503
|
-
else:
|
|
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
|
-
)
|
|
545
|
-
else:
|
|
546
|
-
latest_versions = get_latest_versions(
|
|
547
|
-
installed_packages, timeout=timeout, include_prereleases=pre
|
|
548
|
-
)
|
|
549
|
-
|
|
550
|
-
step2_time = time.time() - step2_start
|
|
551
|
-
|
|
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]")
|
|
649
|
+
# Step 2: Get latest versions (from cache or network)
|
|
650
|
+
latest_versions, step2_time, _ = _step2_get_latest_versions(
|
|
651
|
+
console, output, debug, installed_packages, use_cache, cache_enabled,
|
|
652
|
+
timeout, pre, parallel
|
|
653
|
+
)
|
|
557
654
|
|
|
558
655
|
if not latest_versions:
|
|
559
656
|
if output == "json":
|
|
@@ -563,52 +660,10 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
563
660
|
sys.exit(0)
|
|
564
661
|
|
|
565
662
|
# Step 3: Resolve upgradable packages
|
|
566
|
-
|
|
567
|
-
console
|
|
568
|
-
|
|
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]")
|
|
663
|
+
can_upgrade, blocked_packages, package_constraints, step3_time = _step3_resolve_packages(
|
|
664
|
+
console, output, debug, latest_versions, installed_packages, show_blocked,
|
|
665
|
+
exclude, packages
|
|
666
|
+
)
|
|
612
667
|
|
|
613
668
|
if not can_upgrade:
|
|
614
669
|
if output == "json":
|
|
@@ -625,12 +680,6 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
625
680
|
print_blocked_packages_table(blocked_packages, console=console)
|
|
626
681
|
sys.exit(0)
|
|
627
682
|
|
|
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
683
|
# Step 4: Display table and ask for confirmation
|
|
635
684
|
if output == "json":
|
|
636
685
|
assert json_formatter is not None
|
|
@@ -668,26 +717,7 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
668
717
|
sys.exit(0)
|
|
669
718
|
|
|
670
719
|
# Step 5: Install packages
|
|
671
|
-
|
|
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
|
|
720
|
+
results, step5_time = _step5_install_packages(console, output, can_upgrade, package_constraints)
|
|
691
721
|
|
|
692
722
|
# Update requirements file if requested
|
|
693
723
|
if update_requirements:
|
pipu_cli/config_file.py
CHANGED
pipu_cli/package_management.py
CHANGED
|
@@ -54,6 +54,7 @@ class InstalledPackage(Package):
|
|
|
54
54
|
"""Information about an installed package."""
|
|
55
55
|
constrained_dependencies: Dict[str, str] = field(default_factory=dict, hash=False, compare=False)
|
|
56
56
|
is_editable: bool = False
|
|
57
|
+
editable_location: Optional[str] = None
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
@dataclass(frozen=True)
|
|
@@ -62,6 +63,7 @@ class UpgradePackageInfo(Package):
|
|
|
62
63
|
upgradable: bool
|
|
63
64
|
latest_version: Version
|
|
64
65
|
is_editable: bool = False
|
|
66
|
+
editable_location: Optional[str] = None
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
@dataclass(frozen=True)
|
|
@@ -70,6 +72,7 @@ class UpgradedPackage(Package):
|
|
|
70
72
|
upgraded: bool
|
|
71
73
|
previous_version: Version
|
|
72
74
|
is_editable: bool = False
|
|
75
|
+
editable_location: Optional[str] = None
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
@dataclass(frozen=True)
|
|
@@ -78,6 +81,7 @@ class BlockedPackageInfo(Package):
|
|
|
78
81
|
latest_version: Version
|
|
79
82
|
blocked_by: List[str] # List of "package_name (constraint)" strings
|
|
80
83
|
is_editable: bool = False
|
|
84
|
+
editable_location: Optional[str] = None
|
|
81
85
|
|
|
82
86
|
|
|
83
87
|
def inspect_installed_packages(timeout: int = 10) -> List[InstalledPackage]:
|
|
@@ -115,8 +119,9 @@ def inspect_installed_packages(timeout: int = 10) -> List[InstalledPackage]:
|
|
|
115
119
|
logger.warning(f"Invalid version for {package_name}: {dist.version}. Skipping.")
|
|
116
120
|
continue
|
|
117
121
|
|
|
118
|
-
# Check if package is editable
|
|
122
|
+
# Check if package is editable and get its location
|
|
119
123
|
is_editable = canonical_name in editable_packages
|
|
124
|
+
editable_location = editable_packages.get(canonical_name) if is_editable else None
|
|
120
125
|
|
|
121
126
|
# Extract constrained dependencies
|
|
122
127
|
constrained_dependencies = _extract_constrained_dependencies(dist)
|
|
@@ -126,6 +131,7 @@ def inspect_installed_packages(timeout: int = 10) -> List[InstalledPackage]:
|
|
|
126
131
|
name=package_name,
|
|
127
132
|
version=package_version,
|
|
128
133
|
is_editable=is_editable,
|
|
134
|
+
editable_location=editable_location,
|
|
129
135
|
constrained_dependencies=constrained_dependencies
|
|
130
136
|
)
|
|
131
137
|
|
|
@@ -788,7 +794,8 @@ def resolve_upgradable_packages(
|
|
|
788
794
|
version=installed_pkg.version,
|
|
789
795
|
upgradable=can_upgrade,
|
|
790
796
|
latest_version=latest_version,
|
|
791
|
-
is_editable=installed_pkg.is_editable
|
|
797
|
+
is_editable=installed_pkg.is_editable,
|
|
798
|
+
editable_location=installed_pkg.editable_location
|
|
792
799
|
))
|
|
793
800
|
|
|
794
801
|
return result
|
|
@@ -891,7 +898,8 @@ def resolve_upgradable_packages_with_reasons(
|
|
|
891
898
|
version=installed_pkg.version,
|
|
892
899
|
upgradable=True,
|
|
893
900
|
latest_version=latest_version,
|
|
894
|
-
is_editable=installed_pkg.is_editable
|
|
901
|
+
is_editable=installed_pkg.is_editable,
|
|
902
|
+
editable_location=installed_pkg.editable_location
|
|
895
903
|
))
|
|
896
904
|
elif is_actual_upgrade:
|
|
897
905
|
# Blocked package
|
|
@@ -901,7 +909,8 @@ def resolve_upgradable_packages_with_reasons(
|
|
|
901
909
|
version=installed_pkg.version,
|
|
902
910
|
latest_version=latest_version,
|
|
903
911
|
blocked_by=reasons,
|
|
904
|
-
is_editable=installed_pkg.is_editable
|
|
912
|
+
is_editable=installed_pkg.is_editable,
|
|
913
|
+
editable_location=installed_pkg.editable_location
|
|
905
914
|
))
|
|
906
915
|
|
|
907
916
|
return upgradable, blocked
|
|
@@ -1035,7 +1044,8 @@ def install_packages(
|
|
|
1035
1044
|
version=pkg.version,
|
|
1036
1045
|
upgraded=False,
|
|
1037
1046
|
previous_version=pkg.version,
|
|
1038
|
-
is_editable=pkg.is_editable
|
|
1047
|
+
is_editable=pkg.is_editable,
|
|
1048
|
+
editable_location=pkg.editable_location
|
|
1039
1049
|
)
|
|
1040
1050
|
for pkg in packages_to_upgrade
|
|
1041
1051
|
]
|
|
@@ -1078,7 +1088,8 @@ def install_packages(
|
|
|
1078
1088
|
version=current_version,
|
|
1079
1089
|
upgraded=True,
|
|
1080
1090
|
previous_version=previous_version,
|
|
1081
|
-
is_editable=pkg_info.is_editable
|
|
1091
|
+
is_editable=pkg_info.is_editable,
|
|
1092
|
+
editable_location=pkg_info.editable_location
|
|
1082
1093
|
)
|
|
1083
1094
|
results.append(upgraded_pkg)
|
|
1084
1095
|
logger.info(f"Successfully upgraded {pkg_info.name} from {previous_version} to {current_version}")
|
|
@@ -1090,7 +1101,8 @@ def install_packages(
|
|
|
1090
1101
|
version=actual_version,
|
|
1091
1102
|
upgraded=False,
|
|
1092
1103
|
previous_version=previous_version,
|
|
1093
|
-
is_editable=pkg_info.is_editable
|
|
1104
|
+
is_editable=pkg_info.is_editable,
|
|
1105
|
+
editable_location=pkg_info.editable_location
|
|
1094
1106
|
)
|
|
1095
1107
|
results.append(upgraded_pkg)
|
|
1096
1108
|
logger.info(f"Package {pkg_info.name} was not upgraded (still at {actual_version})")
|
|
@@ -1119,7 +1131,8 @@ def install_packages(
|
|
|
1119
1131
|
version=pkg.version,
|
|
1120
1132
|
upgraded=False,
|
|
1121
1133
|
previous_version=pkg.version,
|
|
1122
|
-
is_editable=pkg.is_editable
|
|
1134
|
+
is_editable=pkg.is_editable,
|
|
1135
|
+
editable_location=pkg.editable_location
|
|
1123
1136
|
)
|
|
1124
1137
|
for pkg in packages_to_upgrade
|
|
1125
1138
|
]
|
|
@@ -1139,7 +1152,139 @@ def install_packages(
|
|
|
1139
1152
|
version=pkg.version,
|
|
1140
1153
|
upgraded=False,
|
|
1141
1154
|
previous_version=pkg.version,
|
|
1142
|
-
is_editable=pkg.is_editable
|
|
1155
|
+
is_editable=pkg.is_editable,
|
|
1156
|
+
editable_location=pkg.editable_location
|
|
1143
1157
|
)
|
|
1144
1158
|
for pkg in packages_to_upgrade
|
|
1145
1159
|
]
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def reinstall_editable_packages(
|
|
1163
|
+
editable_packages: List[UpgradePackageInfo],
|
|
1164
|
+
output_stream: Optional[OutputStream] = None,
|
|
1165
|
+
timeout: int = 300,
|
|
1166
|
+
) -> List[UpgradedPackage]:
|
|
1167
|
+
"""
|
|
1168
|
+
Reinstall editable packages to update their version metadata.
|
|
1169
|
+
|
|
1170
|
+
Uses `pip install --config-settings editable_mode=compat -e <path>` to reinstall
|
|
1171
|
+
each editable package. This updates the package version in the environment
|
|
1172
|
+
while maintaining the editable install.
|
|
1173
|
+
|
|
1174
|
+
:param editable_packages: List of UpgradePackageInfo objects for editable packages
|
|
1175
|
+
:param output_stream: Optional stream implementing write() and flush() for live progress updates
|
|
1176
|
+
:param timeout: Timeout in seconds for each installation (default: 300)
|
|
1177
|
+
:returns: List of UpgradedPackage objects with upgrade status
|
|
1178
|
+
"""
|
|
1179
|
+
if not editable_packages:
|
|
1180
|
+
return []
|
|
1181
|
+
|
|
1182
|
+
results = []
|
|
1183
|
+
|
|
1184
|
+
for pkg in editable_packages:
|
|
1185
|
+
if not pkg.editable_location:
|
|
1186
|
+
logger.warning(f"Editable package {pkg.name} has no location, skipping")
|
|
1187
|
+
results.append(UpgradedPackage(
|
|
1188
|
+
name=pkg.name,
|
|
1189
|
+
version=pkg.version,
|
|
1190
|
+
upgraded=False,
|
|
1191
|
+
previous_version=pkg.version,
|
|
1192
|
+
is_editable=True,
|
|
1193
|
+
editable_location=pkg.editable_location
|
|
1194
|
+
))
|
|
1195
|
+
continue
|
|
1196
|
+
|
|
1197
|
+
if output_stream:
|
|
1198
|
+
output_stream.write(f"Reinstalling editable package: {pkg.name} from {pkg.editable_location}\n")
|
|
1199
|
+
output_stream.flush()
|
|
1200
|
+
|
|
1201
|
+
cmd = [
|
|
1202
|
+
sys.executable, '-m', 'pip', 'install',
|
|
1203
|
+
'--config-settings', 'editable_mode=compat',
|
|
1204
|
+
'-e', pkg.editable_location
|
|
1205
|
+
]
|
|
1206
|
+
|
|
1207
|
+
try:
|
|
1208
|
+
process = subprocess.Popen(
|
|
1209
|
+
cmd,
|
|
1210
|
+
stdout=subprocess.PIPE,
|
|
1211
|
+
stderr=subprocess.PIPE,
|
|
1212
|
+
text=True,
|
|
1213
|
+
bufsize=1
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
# Read output
|
|
1217
|
+
stdout, stderr = process.communicate(timeout=timeout)
|
|
1218
|
+
returncode = process.returncode
|
|
1219
|
+
|
|
1220
|
+
if output_stream and stdout:
|
|
1221
|
+
output_stream.write(stdout)
|
|
1222
|
+
if output_stream and stderr:
|
|
1223
|
+
output_stream.write(stderr)
|
|
1224
|
+
if output_stream:
|
|
1225
|
+
output_stream.flush()
|
|
1226
|
+
|
|
1227
|
+
if returncode == 0:
|
|
1228
|
+
# Get the new version after reinstall
|
|
1229
|
+
env = get_default_environment()
|
|
1230
|
+
canonical_name = canonicalize_name(pkg.name)
|
|
1231
|
+
new_version = pkg.version # Default to old version
|
|
1232
|
+
|
|
1233
|
+
for dist in env.iter_all_distributions():
|
|
1234
|
+
dist_name = dist.metadata.get("name", "")
|
|
1235
|
+
if canonicalize_name(dist_name) == canonical_name:
|
|
1236
|
+
try:
|
|
1237
|
+
new_version = Version(str(dist.version))
|
|
1238
|
+
except InvalidVersion:
|
|
1239
|
+
pass
|
|
1240
|
+
break
|
|
1241
|
+
|
|
1242
|
+
results.append(UpgradedPackage(
|
|
1243
|
+
name=pkg.name,
|
|
1244
|
+
version=new_version,
|
|
1245
|
+
upgraded=new_version > pkg.version,
|
|
1246
|
+
previous_version=pkg.version,
|
|
1247
|
+
is_editable=True,
|
|
1248
|
+
editable_location=pkg.editable_location
|
|
1249
|
+
))
|
|
1250
|
+
logger.info(f"Reinstalled editable package {pkg.name}: {pkg.version} -> {new_version}")
|
|
1251
|
+
else:
|
|
1252
|
+
results.append(UpgradedPackage(
|
|
1253
|
+
name=pkg.name,
|
|
1254
|
+
version=pkg.version,
|
|
1255
|
+
upgraded=False,
|
|
1256
|
+
previous_version=pkg.version,
|
|
1257
|
+
is_editable=True,
|
|
1258
|
+
editable_location=pkg.editable_location
|
|
1259
|
+
))
|
|
1260
|
+
logger.warning(f"Failed to reinstall editable package {pkg.name}")
|
|
1261
|
+
|
|
1262
|
+
except subprocess.TimeoutExpired:
|
|
1263
|
+
if output_stream:
|
|
1264
|
+
output_stream.write(f"ERROR: Timeout reinstalling {pkg.name}\n")
|
|
1265
|
+
output_stream.flush()
|
|
1266
|
+
results.append(UpgradedPackage(
|
|
1267
|
+
name=pkg.name,
|
|
1268
|
+
version=pkg.version,
|
|
1269
|
+
upgraded=False,
|
|
1270
|
+
previous_version=pkg.version,
|
|
1271
|
+
is_editable=True,
|
|
1272
|
+
editable_location=pkg.editable_location
|
|
1273
|
+
))
|
|
1274
|
+
logger.error(f"Timeout reinstalling editable package {pkg.name}")
|
|
1275
|
+
|
|
1276
|
+
except Exception as e:
|
|
1277
|
+
if output_stream:
|
|
1278
|
+
output_stream.write(f"ERROR: Failed to reinstall {pkg.name}: {e}\n")
|
|
1279
|
+
output_stream.flush()
|
|
1280
|
+
results.append(UpgradedPackage(
|
|
1281
|
+
name=pkg.name,
|
|
1282
|
+
version=pkg.version,
|
|
1283
|
+
upgraded=False,
|
|
1284
|
+
previous_version=pkg.version,
|
|
1285
|
+
is_editable=True,
|
|
1286
|
+
editable_location=pkg.editable_location
|
|
1287
|
+
))
|
|
1288
|
+
logger.error(f"Error reinstalling editable package {pkg.name}: {e}")
|
|
1289
|
+
|
|
1290
|
+
return results
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pipu_cli/__init__.py,sha256=sGvHQxiNJy0We55W1Z7bjPQfsyJDbeFPhNxH4Qa60Io,1191
|
|
2
|
+
pipu_cli/cache.py,sha256=d5BOItcJSlNfPEnYb4tbMmntzIDjWIl1ZUb5Xo5bJiQ,8243
|
|
3
|
+
pipu_cli/cli.py,sha256=Sl3kHM5-yYVsomXYRy7KCL0e6dC_mSS5fFIdMgf6pVg,34963
|
|
4
|
+
pipu_cli/config.py,sha256=lixyWhJBz5GqdyRIygc4g5Wzc94HPC4sVFCgeINtQtw,1542
|
|
5
|
+
pipu_cli/config_file.py,sha256=0vJbaDS4WDR4RRYA8gKLDtQRi-Stzm9a85qjmnNCqys,2186
|
|
6
|
+
pipu_cli/output.py,sha256=9g64hxHIXxJlq0mmhRwZnbPMMGPpTfSRkH90rc-QPjA,3510
|
|
7
|
+
pipu_cli/package_management.py,sha256=TXATyeu0RmoRbN1nsDKhZCwlnrZGgj-lK5GnL7opoh8,50661
|
|
8
|
+
pipu_cli/pretty.py,sha256=6qBohKDtocm6vJc2rtH9RLgvvHYJiaGMnmhs6QyC0kE,9293
|
|
9
|
+
pipu_cli/requirements.py,sha256=zbh7XwxD9he_5csJitEGT0NfiE4qbXPw_-JSDuHv4G8,2665
|
|
10
|
+
pipu_cli/rollback.py,sha256=gL9ueYtAKDoALqRfJE2S5gnw1Id5QBLaPC1UL_WjzzY,2980
|
|
11
|
+
pipu_cli-0.2.2.dist-info/licenses/LICENSE,sha256=q6TxVbSI0WMB9ulF2V0FWQfeA5om3d-T9X7QwuhdiYE,1075
|
|
12
|
+
pipu_cli-0.2.2.dist-info/METADATA,sha256=HuJ0G_iQoFXXySEQVv9v0ikeFGOA2ixZNprthTx8iVA,11919
|
|
13
|
+
pipu_cli-0.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pipu_cli-0.2.2.dist-info/entry_points.txt,sha256=VSv6od00zOPblnFPflNLaci4jBtQIgLYJjL1BKxLz_o,42
|
|
15
|
+
pipu_cli-0.2.2.dist-info/top_level.txt,sha256=z3Yce93-jGQjGRpsGZUZvbS8osh3OyS7MVpzG0uBE5M,9
|
|
16
|
+
pipu_cli-0.2.2.dist-info/RECORD,,
|
pipu_cli-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
pipu_cli/__init__.py,sha256=oN3HVmBGVKNTLG_Ut4GZrH1W3EfUrK_MJqZQ1NIwOnc,1191
|
|
2
|
-
pipu_cli/cache.py,sha256=kQa1GrrDcCT_CcJuMPX79BLBrcB55NArtxQTzzwyNOE,9199
|
|
3
|
-
pipu_cli/cli.py,sha256=W1LNlo3zbO11x-AiFJHJY1Z2eFSNIVcnECLANNgPnwY,34807
|
|
4
|
-
pipu_cli/config.py,sha256=lixyWhJBz5GqdyRIygc4g5Wzc94HPC4sVFCgeINtQtw,1542
|
|
5
|
-
pipu_cli/config_file.py,sha256=2X2UqtJQV7M-czNTx_Hqu-rWLKkK7cn8l8kZ4HNbBkM,2196
|
|
6
|
-
pipu_cli/output.py,sha256=9g64hxHIXxJlq0mmhRwZnbPMMGPpTfSRkH90rc-QPjA,3510
|
|
7
|
-
pipu_cli/package_management.py,sha256=p4m7o5W-RZu4wOZMiU5Sp6WvvnZ2SbUoCIXqYIT3yQU,44822
|
|
8
|
-
pipu_cli/pretty.py,sha256=6qBohKDtocm6vJc2rtH9RLgvvHYJiaGMnmhs6QyC0kE,9293
|
|
9
|
-
pipu_cli/requirements.py,sha256=zbh7XwxD9he_5csJitEGT0NfiE4qbXPw_-JSDuHv4G8,2665
|
|
10
|
-
pipu_cli/rollback.py,sha256=gL9ueYtAKDoALqRfJE2S5gnw1Id5QBLaPC1UL_WjzzY,2980
|
|
11
|
-
pipu_cli-0.2.0.dist-info/licenses/LICENSE,sha256=q6TxVbSI0WMB9ulF2V0FWQfeA5om3d-T9X7QwuhdiYE,1075
|
|
12
|
-
pipu_cli-0.2.0.dist-info/METADATA,sha256=ZArCxjCSlgfz4uTSYb0Szpa3_fOjpOJqaNYxjLHF_jU,11919
|
|
13
|
-
pipu_cli-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
pipu_cli-0.2.0.dist-info/entry_points.txt,sha256=VSv6od00zOPblnFPflNLaci4jBtQIgLYJjL1BKxLz_o,42
|
|
15
|
-
pipu_cli-0.2.0.dist-info/top_level.txt,sha256=z3Yce93-jGQjGRpsGZUZvbS8osh3OyS7MVpzG0uBE5M,9
|
|
16
|
-
pipu_cli-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|