pipu-cli 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pipu_cli/__init__.py +1 -1
- pipu_cli/cache.py +41 -82
- pipu_cli/cli.py +81 -82
- 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.1.dist-info}/METADATA +1 -1
- pipu_cli-0.2.1.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.1.dist-info}/WHEEL +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.1.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.2.0.dist-info → pipu_cli-0.2.1.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
|
+
# 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)
|
|
231
234
|
|
|
232
|
-
|
|
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)
|
|
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
|
|
|
@@ -460,55 +449,39 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
460
449
|
console.print("[yellow]No packages found.[/yellow]")
|
|
461
450
|
sys.exit(0)
|
|
462
451
|
|
|
463
|
-
# Step 2:
|
|
452
|
+
# Step 2: Get latest versions (from cache or network)
|
|
464
453
|
if output != "json":
|
|
465
454
|
if use_cache:
|
|
466
455
|
console.print("\n[bold]Step 2/5:[/bold] Loading cached version data...")
|
|
467
456
|
else:
|
|
468
|
-
console.print("\n[bold]Step 2/5:[/bold]
|
|
457
|
+
console.print("\n[bold]Step 2/5:[/bold] Fetching latest versions from PyPI...")
|
|
469
458
|
|
|
470
459
|
step2_start = time.time()
|
|
471
460
|
latest_versions: dict = {}
|
|
461
|
+
cache_was_used = False
|
|
472
462
|
|
|
473
463
|
if use_cache:
|
|
474
|
-
# Load from cache
|
|
464
|
+
# Load latest versions from cache (skip PyPI queries entirely)
|
|
475
465
|
cache_data = load_cache()
|
|
476
|
-
if cache_data and cache_data.
|
|
477
|
-
# Reconstruct latest_versions from cache
|
|
478
|
-
#
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
)
|
|
466
|
+
if cache_data and cache_data.latest_versions:
|
|
467
|
+
# Reconstruct latest_versions dict from cache
|
|
468
|
+
# Maps InstalledPackage -> Package with latest version
|
|
469
|
+
for installed_pkg in installed_packages:
|
|
470
|
+
name_lower = installed_pkg.name.lower()
|
|
471
|
+
if name_lower in cache_data.latest_versions:
|
|
472
|
+
cached_version = cache_data.latest_versions[name_lower]
|
|
473
|
+
try:
|
|
474
|
+
latest_ver = Version(cached_version)
|
|
475
|
+
# Only include if it's actually newer
|
|
476
|
+
if latest_ver > installed_pkg.version:
|
|
477
|
+
latest_pkg = Package(
|
|
478
|
+
name=installed_pkg.name,
|
|
479
|
+
version=latest_ver
|
|
480
|
+
)
|
|
481
|
+
latest_versions[installed_pkg] = latest_pkg
|
|
482
|
+
except Exception:
|
|
483
|
+
pass # Skip invalid versions
|
|
484
|
+
cache_was_used = True
|
|
512
485
|
else:
|
|
513
486
|
use_cache = False
|
|
514
487
|
|
|
@@ -524,7 +497,7 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
524
497
|
) as progress:
|
|
525
498
|
task = progress.add_task("Checking packages...", total=len(installed_packages))
|
|
526
499
|
|
|
527
|
-
def update_progress(current, total):
|
|
500
|
+
def update_progress(current: int, total: int) -> None:
|
|
528
501
|
progress.update(task, completed=current)
|
|
529
502
|
|
|
530
503
|
if parallel > 1:
|
|
@@ -547,11 +520,18 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
547
520
|
installed_packages, timeout=timeout, include_prereleases=pre
|
|
548
521
|
)
|
|
549
522
|
|
|
523
|
+
# Update cache with fresh data
|
|
524
|
+
if cache_enabled:
|
|
525
|
+
version_cache = build_version_cache(latest_versions)
|
|
526
|
+
save_cache(version_cache, include_prereleases=pre)
|
|
527
|
+
|
|
550
528
|
step2_time = time.time() - step2_start
|
|
551
529
|
|
|
552
530
|
num_updates = len(latest_versions)
|
|
553
531
|
if output != "json":
|
|
554
532
|
console.print(f" Found {num_updates} packages with newer versions available")
|
|
533
|
+
if cache_was_used:
|
|
534
|
+
console.print(" [dim](from cache)[/dim]")
|
|
555
535
|
if debug:
|
|
556
536
|
console.print(f" [dim]Time: {step2_time:.2f}s[/dim]")
|
|
557
537
|
|
|
@@ -578,11 +558,6 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
578
558
|
|
|
579
559
|
step3_time = time.time() - step3_start
|
|
580
560
|
|
|
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
561
|
# Apply exclusions
|
|
587
562
|
excluded_names = set()
|
|
588
563
|
if exclude:
|
|
@@ -667,9 +642,14 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
667
642
|
console.print("[yellow]Upgrade cancelled.[/yellow]")
|
|
668
643
|
sys.exit(0)
|
|
669
644
|
|
|
645
|
+
# Separate editable and non-editable packages
|
|
646
|
+
editable_packages = [pkg for pkg in can_upgrade if pkg.is_editable]
|
|
647
|
+
non_editable_packages = [pkg for pkg in can_upgrade if not pkg.is_editable]
|
|
648
|
+
|
|
670
649
|
# Step 5: Install packages
|
|
671
650
|
if output != "json":
|
|
672
|
-
|
|
651
|
+
total_to_upgrade = len(non_editable_packages) + len(editable_packages)
|
|
652
|
+
console.print(f"[bold]Step 5/5:[/bold] Upgrading {total_to_upgrade} package(s)...\n")
|
|
673
653
|
step5_start = time.time()
|
|
674
654
|
|
|
675
655
|
# Save state for potential rollback
|
|
@@ -681,12 +661,31 @@ def upgrade(packages: tuple[str, ...], timeout: int, pre: bool, yes: bool, debug
|
|
|
681
661
|
save_state(pre_upgrade_packages, "Pre-upgrade state")
|
|
682
662
|
|
|
683
663
|
stream = ConsoleStream(console) if output != "json" else None
|
|
684
|
-
results =
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
664
|
+
results = []
|
|
665
|
+
|
|
666
|
+
# First, upgrade non-editable packages via pip install --upgrade
|
|
667
|
+
if non_editable_packages:
|
|
668
|
+
if output != "json":
|
|
669
|
+
console.print(f"Upgrading {len(non_editable_packages)} regular package(s)...\n")
|
|
670
|
+
regular_results = install_packages(
|
|
671
|
+
non_editable_packages,
|
|
672
|
+
output_stream=stream,
|
|
673
|
+
timeout=300,
|
|
674
|
+
version_constraints=package_constraints if package_constraints else None
|
|
675
|
+
)
|
|
676
|
+
results.extend(regular_results)
|
|
677
|
+
|
|
678
|
+
# Then, reinstall editable packages to update their versions
|
|
679
|
+
if editable_packages:
|
|
680
|
+
if output != "json":
|
|
681
|
+
console.print(f"\nReinstalling {len(editable_packages)} editable package(s)...\n")
|
|
682
|
+
editable_results = reinstall_editable_packages(
|
|
683
|
+
editable_packages,
|
|
684
|
+
output_stream=stream,
|
|
685
|
+
timeout=300
|
|
686
|
+
)
|
|
687
|
+
results.extend(editable_results)
|
|
688
|
+
|
|
690
689
|
step5_time = time.time() - step5_start
|
|
691
690
|
|
|
692
691
|
# Update requirements file if requested
|
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=DYhOEmnqarSXQjteuoviNNHqmkBb1xyBh7E49IDBJzs,1191
|
|
2
|
+
pipu_cli/cache.py,sha256=d5BOItcJSlNfPEnYb4tbMmntzIDjWIl1ZUb5Xo5bJiQ,8243
|
|
3
|
+
pipu_cli/cli.py,sha256=kj1ANOl_s-Ac02ZR-NLLREnM2_Av__KqMMBYr3cS2sY,34761
|
|
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.1.dist-info/licenses/LICENSE,sha256=q6TxVbSI0WMB9ulF2V0FWQfeA5om3d-T9X7QwuhdiYE,1075
|
|
12
|
+
pipu_cli-0.2.1.dist-info/METADATA,sha256=G39M3WDHd-oYkPEm4-pBTF6rlPNYIjUrKr717DyByVY,11919
|
|
13
|
+
pipu_cli-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
pipu_cli-0.2.1.dist-info/entry_points.txt,sha256=VSv6od00zOPblnFPflNLaci4jBtQIgLYJjL1BKxLz_o,42
|
|
15
|
+
pipu_cli-0.2.1.dist-info/top_level.txt,sha256=z3Yce93-jGQjGRpsGZUZvbS8osh3OyS7MVpzG0uBE5M,9
|
|
16
|
+
pipu_cli-0.2.1.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
|