depkeeper 0.1.0.dev2__tar.gz → 0.1.0.dev3__tar.gz
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.
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/PKG-INFO +6 -4
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/README.md +1 -1
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/__init__.py +1 -1
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/__version__.py +1 -1
- depkeeper-0.1.0.dev3/depkeeper/commands/__init__.py +0 -0
- depkeeper-0.1.0.dev3/depkeeper/commands/check.py +608 -0
- depkeeper-0.1.0.dev3/depkeeper/commands/update.py +684 -0
- depkeeper-0.1.0.dev3/depkeeper/core/__init__.py +27 -0
- depkeeper-0.1.0.dev3/depkeeper/core/checker.py +458 -0
- depkeeper-0.1.0.dev3/depkeeper/core/data_store.py +645 -0
- depkeeper-0.1.0.dev3/depkeeper/core/dependency_analyzer.py +1011 -0
- depkeeper-0.1.0.dev3/depkeeper/core/parser.py +1138 -0
- depkeeper-0.1.0.dev3/depkeeper/models/__init__.py +23 -0
- depkeeper-0.1.0.dev3/depkeeper/models/conflict.py +151 -0
- depkeeper-0.1.0.dev3/depkeeper/models/package.py +371 -0
- depkeeper-0.1.0.dev3/depkeeper/models/requirement.py +148 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/__init__.py +100 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/console.py +238 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/filesystem.py +324 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/http.py +267 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/logger.py +146 -0
- depkeeper-0.1.0.dev3/depkeeper/utils/version_utils.py +92 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper.egg-info/PKG-INFO +6 -4
- depkeeper-0.1.0.dev3/depkeeper.egg-info/SOURCES.txt +34 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper.egg-info/requires.txt +2 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/pyproject.toml +8 -5
- depkeeper-0.1.0.dev2/depkeeper.egg-info/SOURCES.txt +0 -16
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/LICENSE +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/__main__.py +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/cli.py +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/constants.py +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/context.py +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper/exceptions.py +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper.egg-info/dependency_links.txt +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper.egg-info/entry_points.txt +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/depkeeper.egg-info/top_level.txt +0 -0
- {depkeeper-0.1.0.dev2 → depkeeper-0.1.0.dev3}/setup.cfg +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: depkeeper
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev3
|
|
4
4
|
Summary: Modern Python dependency management for requirements.txt files
|
|
5
|
-
Author:
|
|
5
|
+
Author: Rahul Kaushal
|
|
6
6
|
License: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/rahulkaushal04/depkeeper
|
|
8
|
-
Project-URL: Documentation, https://
|
|
8
|
+
Project-URL: Documentation, https://rahulkaushal04.github.io/depkeeper/
|
|
9
9
|
Project-URL: Repository, https://github.com/rahulkaushal04/depkeeper
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/rahulkaushal04/depkeeper/issues
|
|
11
11
|
Project-URL: Changelog, https://github.com/rahulkaushal04/depkeeper/blob/main/CHANGELOG.md
|
|
@@ -48,6 +48,8 @@ Provides-Extra: docs
|
|
|
48
48
|
Requires-Dist: mkdocs>=1.6.1; extra == "docs"
|
|
49
49
|
Requires-Dist: mkdocs-material>=9.7.1; extra == "docs"
|
|
50
50
|
Requires-Dist: mkdocstrings[python]>=0.26.1; extra == "docs"
|
|
51
|
+
Requires-Dist: pymdown-extensions>=10.15; extra == "docs"
|
|
52
|
+
Requires-Dist: mkdocs-minify-plugin>=0.8.0; extra == "docs"
|
|
51
53
|
|
|
52
54
|
# depkeeper
|
|
53
55
|
|
|
@@ -154,7 +156,7 @@ Options:
|
|
|
154
156
|
|
|
155
157
|
## Documentation
|
|
156
158
|
|
|
157
|
-
Full documentation will be available soon at **[https://
|
|
159
|
+
Full documentation will be available soon at **[https://rahulkaushal04.github.io/depkeeper/](https://rahulkaushal04.github.io/depkeeper/)**
|
|
158
160
|
|
|
159
161
|
---
|
|
160
162
|
|
|
@@ -103,7 +103,7 @@ Options:
|
|
|
103
103
|
|
|
104
104
|
## Documentation
|
|
105
105
|
|
|
106
|
-
Full documentation will be available soon at **[https://
|
|
106
|
+
Full documentation will be available soon at **[https://rahulkaushal04.github.io/depkeeper/](https://rahulkaushal04.github.io/depkeeper/)**
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
File without changes
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"""Check command implementation for depkeeper.
|
|
2
|
+
|
|
3
|
+
Analyzes a ``requirements.txt`` file to identify packages with available
|
|
4
|
+
updates, dependency conflicts, and Python version compatibility issues.
|
|
5
|
+
|
|
6
|
+
The command orchestrates three core components:
|
|
7
|
+
|
|
8
|
+
1. **RequirementsParser** — parses the requirements file into structured
|
|
9
|
+
:class:`Requirement` objects.
|
|
10
|
+
2. **VersionChecker** — queries PyPI concurrently to fetch latest versions
|
|
11
|
+
and compute recommendations.
|
|
12
|
+
3. **DependencyAnalyzer** — cross-validates all recommended versions and
|
|
13
|
+
resolves conflicts through iterative downgrading/constraining.
|
|
14
|
+
|
|
15
|
+
All components share a single :class:`PyPIDataStore` instance to guarantee
|
|
16
|
+
that each package's metadata is fetched at most once per invocation.
|
|
17
|
+
|
|
18
|
+
Typical usage::
|
|
19
|
+
|
|
20
|
+
# Show all packages with available updates
|
|
21
|
+
$ depkeeper check requirements.txt --outdated-only
|
|
22
|
+
|
|
23
|
+
# Machine-readable JSON output
|
|
24
|
+
$ depkeeper check --format json > report.json
|
|
25
|
+
|
|
26
|
+
# Enable conflict resolution (adjusts recommendations to be mutually compatible)
|
|
27
|
+
$ depkeeper check --check-conflicts
|
|
28
|
+
|
|
29
|
+
# Strict mode: only use pinned versions, don't infer from constraints
|
|
30
|
+
$ depkeeper check --strict-version-matching
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import sys
|
|
36
|
+
import json
|
|
37
|
+
import click
|
|
38
|
+
import asyncio
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Dict, List
|
|
41
|
+
|
|
42
|
+
from depkeeper.models import Package
|
|
43
|
+
from depkeeper.exceptions import DepKeeperError, ParseError
|
|
44
|
+
from depkeeper.context import pass_context, DepKeeperContext
|
|
45
|
+
from depkeeper.core import (
|
|
46
|
+
PyPIDataStore,
|
|
47
|
+
VersionChecker,
|
|
48
|
+
RequirementsParser,
|
|
49
|
+
DependencyAnalyzer,
|
|
50
|
+
ResolutionResult,
|
|
51
|
+
)
|
|
52
|
+
from depkeeper.utils import (
|
|
53
|
+
HTTPClient,
|
|
54
|
+
get_logger,
|
|
55
|
+
print_success,
|
|
56
|
+
print_error,
|
|
57
|
+
print_warning,
|
|
58
|
+
print_table,
|
|
59
|
+
get_raw_console,
|
|
60
|
+
colorize_update_type,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger = get_logger("commands.check")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.command()
|
|
67
|
+
@click.argument(
|
|
68
|
+
"file",
|
|
69
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
70
|
+
default="requirements.txt",
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--outdated-only",
|
|
74
|
+
is_flag=True,
|
|
75
|
+
help="Show only packages with available updates.",
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--format",
|
|
79
|
+
"-f",
|
|
80
|
+
type=click.Choice(["table", "simple", "json"], case_sensitive=False),
|
|
81
|
+
default="table",
|
|
82
|
+
help="Output format.",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--strict-version-matching",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
help="Only use exact version pins, don't infer from constraints.",
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--check-conflicts",
|
|
91
|
+
is_flag=True,
|
|
92
|
+
default=True,
|
|
93
|
+
help="Check for dependency conflicts between packages.",
|
|
94
|
+
)
|
|
95
|
+
@pass_context
|
|
96
|
+
def check(
|
|
97
|
+
ctx: DepKeeperContext,
|
|
98
|
+
file: Path,
|
|
99
|
+
outdated_only: bool,
|
|
100
|
+
format: str,
|
|
101
|
+
strict_version_matching: bool,
|
|
102
|
+
check_conflicts: bool,
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Check requirements file for available updates.
|
|
105
|
+
|
|
106
|
+
Parses the specified requirements file, queries PyPI for the latest
|
|
107
|
+
versions, and displays a report of packages that can be updated.
|
|
108
|
+
Optionally performs dependency conflict analysis to ensure recommended
|
|
109
|
+
updates are compatible with each other.
|
|
110
|
+
|
|
111
|
+
When ``--check-conflicts`` is enabled, the command:
|
|
112
|
+
|
|
113
|
+
1. Fetches initial recommendations for every package.
|
|
114
|
+
2. Cross-validates all recommendations to detect conflicts (package A's
|
|
115
|
+
dependency on B is incompatible with B's recommended version).
|
|
116
|
+
3. Iteratively adjusts versions (downgrading sources or constraining
|
|
117
|
+
targets) until a conflict-free set is found or the iteration limit
|
|
118
|
+
is reached.
|
|
119
|
+
4. Displays the final resolved versions along with any unresolved
|
|
120
|
+
conflicts.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
ctx: Depkeeper context with configuration and verbosity settings.
|
|
124
|
+
file: Path to the requirements file (default: ``requirements.txt``).
|
|
125
|
+
outdated_only: Only display packages with updates or conflicts.
|
|
126
|
+
format: Output format (``table``, ``simple``, or ``json``).
|
|
127
|
+
strict_version_matching: Don't infer current versions from
|
|
128
|
+
constraints like ``>=2.0``; only use exact pins (``==``).
|
|
129
|
+
check_conflicts: Enable cross-package conflict resolution.
|
|
130
|
+
|
|
131
|
+
Exits:
|
|
132
|
+
0 if all packages are up-to-date, 1 if updates are available or an
|
|
133
|
+
error occurred.
|
|
134
|
+
|
|
135
|
+
Example::
|
|
136
|
+
|
|
137
|
+
>>> # CLI invocation
|
|
138
|
+
$ depkeeper check requirements.txt --check-conflicts --format table
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
has_updates = asyncio.run(
|
|
142
|
+
_check_async(
|
|
143
|
+
ctx,
|
|
144
|
+
file,
|
|
145
|
+
outdated_only,
|
|
146
|
+
format,
|
|
147
|
+
infer_version_from_constraints=not strict_version_matching,
|
|
148
|
+
check_conflicts=check_conflicts,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
sys.exit(1 if has_updates else 0)
|
|
152
|
+
|
|
153
|
+
except DepKeeperError as e:
|
|
154
|
+
print_error(f"{e}")
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print_error(f"Unexpected error: {e}")
|
|
158
|
+
logger.exception("Error in check command")
|
|
159
|
+
sys.exit(1)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
# Async orchestration
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def _check_async(
|
|
168
|
+
ctx: DepKeeperContext,
|
|
169
|
+
file: Path,
|
|
170
|
+
outdated_only: bool,
|
|
171
|
+
format: str,
|
|
172
|
+
infer_version_from_constraints: bool,
|
|
173
|
+
check_conflicts: bool,
|
|
174
|
+
) -> bool:
|
|
175
|
+
"""Async implementation of the check command.
|
|
176
|
+
|
|
177
|
+
Core logic:
|
|
178
|
+
|
|
179
|
+
1. Parse the requirements file.
|
|
180
|
+
2. Create a shared :class:`PyPIDataStore` (guarantees each package is
|
|
181
|
+
fetched once).
|
|
182
|
+
3. Run :class:`VersionChecker` to compute initial recommendations.
|
|
183
|
+
4. Optionally run :class:`DependencyAnalyzer` to resolve conflicts.
|
|
184
|
+
5. Filter packages if ``--outdated-only`` is set.
|
|
185
|
+
6. Display results in the requested format.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
ctx: Depkeeper context.
|
|
189
|
+
file: Path to the requirements file.
|
|
190
|
+
outdated_only: Filter to show only packages needing updates.
|
|
191
|
+
format: Output format (``table``, ``simple``, ``json``).
|
|
192
|
+
infer_version_from_constraints: Infer current version from
|
|
193
|
+
constraints (e.g., ``>=2.0`` → current is ``2.0``).
|
|
194
|
+
check_conflicts: Enable dependency conflict resolution.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
``True`` if any package has updates or unresolved conflicts,
|
|
198
|
+
``False`` if everything is up-to-date.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
DepKeeperError: Requirements file cannot be parsed or is malformed.
|
|
202
|
+
"""
|
|
203
|
+
# Only show progress/status for human-readable formats
|
|
204
|
+
show_progress: bool = format == "table" or ctx.verbose > 0
|
|
205
|
+
|
|
206
|
+
logger.info("Checking %s...", file)
|
|
207
|
+
|
|
208
|
+
# ── Step 1: Parse requirements ────────────────────────────────────
|
|
209
|
+
parser = RequirementsParser()
|
|
210
|
+
try:
|
|
211
|
+
requirements = parser.parse_file(file)
|
|
212
|
+
except ParseError as e:
|
|
213
|
+
raise DepKeeperError(f"Failed to parse {file}: {e}") from e
|
|
214
|
+
|
|
215
|
+
if not requirements:
|
|
216
|
+
if show_progress:
|
|
217
|
+
print_warning("No packages found in requirements file")
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
logger.info("Found %d package(s)", len(requirements))
|
|
221
|
+
|
|
222
|
+
# ── Step 2: Fetch versions from PyPI (shared data store) ──────────
|
|
223
|
+
async with HTTPClient() as http:
|
|
224
|
+
data_store = PyPIDataStore(http)
|
|
225
|
+
|
|
226
|
+
# Warm the cache with all packages in one concurrent burst
|
|
227
|
+
await data_store.prefetch_packages([req.name for req in requirements])
|
|
228
|
+
|
|
229
|
+
# Use the same data store for version checking and conflict analysis
|
|
230
|
+
checker = VersionChecker(
|
|
231
|
+
data_store=data_store,
|
|
232
|
+
infer_version_from_constraints=infer_version_from_constraints,
|
|
233
|
+
)
|
|
234
|
+
packages = await checker.check_packages(requirements)
|
|
235
|
+
|
|
236
|
+
# ── Step 3: Resolve conflicts (optional) ──────────────────────
|
|
237
|
+
resolution_result = None
|
|
238
|
+
if check_conflicts:
|
|
239
|
+
logger.info("Cross-validating recommended versions...")
|
|
240
|
+
analyzer = DependencyAnalyzer(data_store=data_store)
|
|
241
|
+
resolution_result = await analyzer.resolve_and_annotate_conflicts(packages)
|
|
242
|
+
|
|
243
|
+
if show_progress and resolution_result:
|
|
244
|
+
# Display resolution summary (convergence status, version changes)
|
|
245
|
+
_display_resolution_summary(resolution_result)
|
|
246
|
+
|
|
247
|
+
# ── Step 4: Filter and display ────────────────────────────────────
|
|
248
|
+
if outdated_only:
|
|
249
|
+
packages = [p for p in packages if p.has_update() or p.has_conflicts()]
|
|
250
|
+
|
|
251
|
+
if not packages:
|
|
252
|
+
if show_progress:
|
|
253
|
+
msg = (
|
|
254
|
+
"All packages are up to date!"
|
|
255
|
+
if outdated_only
|
|
256
|
+
else "No packages to display"
|
|
257
|
+
)
|
|
258
|
+
(print_success if outdated_only else print_warning)(msg)
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
packages_needing_action = sum(1 for p in packages if p.has_update())
|
|
262
|
+
|
|
263
|
+
# Dispatch to the appropriate renderer
|
|
264
|
+
if format == "table":
|
|
265
|
+
_display_table(packages)
|
|
266
|
+
elif format == "simple":
|
|
267
|
+
_display_simple(packages)
|
|
268
|
+
else: # json
|
|
269
|
+
_display_json(packages)
|
|
270
|
+
|
|
271
|
+
# ── Final summary ──────────────────────────────────────────────────
|
|
272
|
+
if show_progress:
|
|
273
|
+
if packages_needing_action > 0:
|
|
274
|
+
print_warning(
|
|
275
|
+
f"\n{packages_needing_action} package(s) have updates available"
|
|
276
|
+
)
|
|
277
|
+
if resolution_result and resolution_result.packages_with_conflicts > 0:
|
|
278
|
+
print_warning(
|
|
279
|
+
f"{resolution_result.packages_with_conflicts} package(s) have "
|
|
280
|
+
"unresolved conflicts — see 'Conflicts' column"
|
|
281
|
+
)
|
|
282
|
+
else:
|
|
283
|
+
print_success("\nAll packages are up to date!")
|
|
284
|
+
|
|
285
|
+
return packages_needing_action > 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _display_resolution_summary(result: ResolutionResult) -> None:
|
|
289
|
+
"""Print a human-readable summary of the conflict resolution process.
|
|
290
|
+
|
|
291
|
+
Displays:
|
|
292
|
+
|
|
293
|
+
- Total packages analyzed
|
|
294
|
+
- Number of packages with conflicts
|
|
295
|
+
- Number of version changes made during resolution
|
|
296
|
+
- Convergence status (did resolution finish or hit iteration limit?)
|
|
297
|
+
- Details of each version change (original → resolved)
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
result: The :class:`ResolutionResult` from the dependency analyzer.
|
|
301
|
+
|
|
302
|
+
Example output::
|
|
303
|
+
|
|
304
|
+
Resolution Summary:
|
|
305
|
+
==================================================
|
|
306
|
+
Total packages: 15
|
|
307
|
+
Packages with conflicts: 2
|
|
308
|
+
Packages changed: 3
|
|
309
|
+
Converged: Yes (5 iterations)
|
|
310
|
+
|
|
311
|
+
Version changes:
|
|
312
|
+
• flask: 3.0.0 → 2.3.3 (downgraded)
|
|
313
|
+
• werkzeug: 3.0.1 → 2.3.7 (constrained)
|
|
314
|
+
"""
|
|
315
|
+
console = get_raw_console()
|
|
316
|
+
console.print("\n[bold]Resolution Summary:[/bold]")
|
|
317
|
+
console.print("=" * 50)
|
|
318
|
+
console.print(f"Total packages: {result.total_packages}")
|
|
319
|
+
console.print(f"Packages with conflicts: {result.packages_with_conflicts}")
|
|
320
|
+
|
|
321
|
+
changed = result.get_changed_packages()
|
|
322
|
+
console.print(f"Packages changed: {len(changed)}")
|
|
323
|
+
|
|
324
|
+
convergence_status = (
|
|
325
|
+
f"Yes ({result.iterations_used} iterations)"
|
|
326
|
+
if result.converged
|
|
327
|
+
else f"No (stopped after {result.iterations_used} iterations)"
|
|
328
|
+
)
|
|
329
|
+
console.print(f"Converged: {convergence_status}")
|
|
330
|
+
|
|
331
|
+
if changed:
|
|
332
|
+
console.print("\n[bold]Version changes:[/bold]")
|
|
333
|
+
for pkg_resolution in changed:
|
|
334
|
+
console.print(
|
|
335
|
+
f" • {pkg_resolution.name}: {pkg_resolution.original} → "
|
|
336
|
+
f"{pkg_resolution.resolved} ({pkg_resolution.status.value})"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
console.print("") # blank line separator
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Display renderers
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _display_table(packages: List[Package]) -> None:
|
|
348
|
+
"""Render packages as a Rich-formatted table.
|
|
349
|
+
|
|
350
|
+
Creates a visually appealing table with color-coded status indicators,
|
|
351
|
+
version columns, update types, conflict details, and Python
|
|
352
|
+
compatibility information.
|
|
353
|
+
|
|
354
|
+
Status indicators:
|
|
355
|
+
|
|
356
|
+
- ``✓ OK`` (green): Package is up-to-date.
|
|
357
|
+
- ``⬆ OUTDATED`` (yellow): Updates are available.
|
|
358
|
+
- ``⚠ CONFLICT`` (red): Dependency conflicts exist; no safe upgrade.
|
|
359
|
+
- ``⚠ INCOMP`` (red): Recommended version is lower than current
|
|
360
|
+
(would require a downgrade).
|
|
361
|
+
- ``✗ ERROR`` (red): PyPI query failed or package not found.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
packages: List of :class:`Package` objects to display.
|
|
365
|
+
|
|
366
|
+
Example::
|
|
367
|
+
|
|
368
|
+
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
|
|
369
|
+
┃ Status ┃ Package ┃ Current ┃ Latest ┃ Update Type ┃
|
|
370
|
+
┡━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
|
|
371
|
+
│ ✓ OK │ requests │ 2.31.0 │ 2.31.0 │ - │
|
|
372
|
+
│ ⬆ OUTDATED │ flask │ 2.0.0 │ 3.0.0 │ major │
|
|
373
|
+
└────────────┴─────────────┴───────────┴───────────┴──────────────┘
|
|
374
|
+
"""
|
|
375
|
+
data = [_create_table_row(pkg) for pkg in packages]
|
|
376
|
+
|
|
377
|
+
column_styles: Dict[str, Dict[str, Any]] = {
|
|
378
|
+
"Status": {"justify": "center", "no_wrap": True, "width": 10},
|
|
379
|
+
"Package": {"style": "bold cyan", "no_wrap": True},
|
|
380
|
+
"Current": {"justify": "center", "style": "dim"},
|
|
381
|
+
"Latest": {"justify": "center", "style": "bold green"},
|
|
382
|
+
"Recommended": {"justify": "center", "style": "bright_cyan"},
|
|
383
|
+
"Update Type": {"justify": "center"},
|
|
384
|
+
"Conflicts": {"justify": "left", "no_wrap": False},
|
|
385
|
+
"Python Support": {"justify": "left", "no_wrap": False},
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
print_table(
|
|
389
|
+
data,
|
|
390
|
+
title="Dependency Status",
|
|
391
|
+
column_styles=column_styles,
|
|
392
|
+
show_row_lines=True,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_table_row(pkg: Package) -> Dict[str, str]:
|
|
397
|
+
"""Build a Rich-formatted table row dictionary for a single package.
|
|
398
|
+
|
|
399
|
+
Determines the appropriate status indicator, version displays, and
|
|
400
|
+
conflict information based on the package's state. All logic for
|
|
401
|
+
status determination is delegated to :meth:`Package.get_display_data`.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
pkg: The :class:`Package` to render.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
A dictionary mapping column names to Rich markup strings.
|
|
408
|
+
|
|
409
|
+
Example::
|
|
410
|
+
|
|
411
|
+
{
|
|
412
|
+
"Status": "[yellow]⬆ OUTDATED[/yellow]",
|
|
413
|
+
"Package": "flask",
|
|
414
|
+
"Current": "2.0.0",
|
|
415
|
+
"Latest": "3.0.0",
|
|
416
|
+
"Recommended": "[bright_cyan]2.3.3[/bright_cyan]",
|
|
417
|
+
"Update Type": "[bold yellow]minor[/bold yellow]",
|
|
418
|
+
"Conflicts": "[dim]-[/dim]",
|
|
419
|
+
"Python Support": ">=3.8",
|
|
420
|
+
}
|
|
421
|
+
"""
|
|
422
|
+
python_support = pkg.render_python_compatibility()
|
|
423
|
+
|
|
424
|
+
# ── Error case (PyPI query failed) ────────────────────────────────
|
|
425
|
+
if not pkg.latest_version:
|
|
426
|
+
return {
|
|
427
|
+
"Status": "[red]✗ ERROR[/red]",
|
|
428
|
+
"Package": pkg.name,
|
|
429
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
430
|
+
"Latest": "[red]error[/red]",
|
|
431
|
+
"Recommended": "[dim]-[/dim]",
|
|
432
|
+
"Update Type": "[dim]-[/dim]",
|
|
433
|
+
"Conflicts": "[dim]-[/dim]",
|
|
434
|
+
"Python Support": "[dim]-[/dim]",
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# ── Fetch pre-computed display metadata ───────────────────────────
|
|
438
|
+
display = pkg.get_display_data()
|
|
439
|
+
|
|
440
|
+
# Recommended version: only show if it differs from current
|
|
441
|
+
recommended_display = "[dim]-[/dim]"
|
|
442
|
+
if pkg.recommended_version:
|
|
443
|
+
if pkg.current_version and pkg.recommended_version != pkg.current_version:
|
|
444
|
+
recommended_display = (
|
|
445
|
+
f"[bright_cyan]{pkg.recommended_version}[/bright_cyan]"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Conflicts: format as multi-line list of requirements
|
|
449
|
+
conflicts_display = "[dim]-[/dim]"
|
|
450
|
+
if display["has_conflicts"]:
|
|
451
|
+
conflict_lines = [
|
|
452
|
+
f"[red]⚠[/red] {c.source_package} needs {c.required_spec}"
|
|
453
|
+
for c in pkg.conflicts
|
|
454
|
+
]
|
|
455
|
+
conflicts_display = "\n".join(conflict_lines)
|
|
456
|
+
|
|
457
|
+
# ── Downgrade/incompatible case ───────────────────────────────────
|
|
458
|
+
if display["requires_downgrade"]:
|
|
459
|
+
return {
|
|
460
|
+
"Status": "[red]⚠ INCOMP[/red]",
|
|
461
|
+
"Package": pkg.name,
|
|
462
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
463
|
+
"Latest": pkg.latest_version,
|
|
464
|
+
"Recommended": recommended_display,
|
|
465
|
+
"Update Type": "[red]downgrade[/red]",
|
|
466
|
+
"Conflicts": conflicts_display,
|
|
467
|
+
"Python Support": python_support,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
# ── Conflict case ──────────────────────────────────────────────────
|
|
471
|
+
if display["has_conflicts"]:
|
|
472
|
+
if not pkg.has_update():
|
|
473
|
+
# True conflict: no safe upgrade exists
|
|
474
|
+
return {
|
|
475
|
+
"Status": "[red]⚠ CONFLICT[/red]",
|
|
476
|
+
"Package": pkg.name,
|
|
477
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
478
|
+
"Latest": pkg.latest_version,
|
|
479
|
+
"Recommended": recommended_display,
|
|
480
|
+
"Update Type": "[red]blocked[/red]",
|
|
481
|
+
"Conflicts": conflicts_display,
|
|
482
|
+
"Python Support": python_support,
|
|
483
|
+
}
|
|
484
|
+
else:
|
|
485
|
+
# Conflicts exist but a safe upgrade is available
|
|
486
|
+
colored_type = colorize_update_type(display["update_type"] or "update")
|
|
487
|
+
return {
|
|
488
|
+
"Status": "[yellow]⬆ OUTDATED[/yellow]",
|
|
489
|
+
"Package": pkg.name,
|
|
490
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
491
|
+
"Latest": pkg.latest_version,
|
|
492
|
+
"Recommended": recommended_display,
|
|
493
|
+
"Update Type": colored_type,
|
|
494
|
+
"Conflicts": conflicts_display,
|
|
495
|
+
"Python Support": python_support,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# ── Update available case ──────────────────────────────────────────
|
|
499
|
+
if display["update_available"]:
|
|
500
|
+
colored_type = colorize_update_type(display["update_type"] or "update")
|
|
501
|
+
return {
|
|
502
|
+
"Status": "[yellow]⬆ OUTDATED[/yellow]",
|
|
503
|
+
"Package": pkg.name,
|
|
504
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
505
|
+
"Latest": pkg.latest_version,
|
|
506
|
+
"Recommended": recommended_display,
|
|
507
|
+
"Update Type": colored_type,
|
|
508
|
+
"Conflicts": conflicts_display,
|
|
509
|
+
"Python Support": python_support,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# ── Up-to-date case ────────────────────────────────────────────────
|
|
513
|
+
return {
|
|
514
|
+
"Status": "[green]✓ OK[/green]",
|
|
515
|
+
"Package": pkg.name,
|
|
516
|
+
"Current": pkg.current_version or "[dim]-[/dim]",
|
|
517
|
+
"Latest": pkg.latest_version or "[dim]-[/dim]",
|
|
518
|
+
"Recommended": recommended_display,
|
|
519
|
+
"Update Type": "[dim]-[/dim]",
|
|
520
|
+
"Conflicts": conflicts_display,
|
|
521
|
+
"Python Support": python_support,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _display_simple(packages: List[Package]) -> None:
|
|
526
|
+
"""Render packages in simple, line-based text format.
|
|
527
|
+
|
|
528
|
+
Outputs one line per package showing status, name, current version,
|
|
529
|
+
and latest version. Conflict and Python compatibility details are
|
|
530
|
+
shown on indented lines below each package.
|
|
531
|
+
|
|
532
|
+
Suitable for piping to other tools or for minimal terminal output.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
packages: List of :class:`Package` objects to display.
|
|
536
|
+
|
|
537
|
+
Example::
|
|
538
|
+
|
|
539
|
+
[OUTDATED] flask 2.0.0 → 3.0.0
|
|
540
|
+
⚠ Conflict: werkzeug requires >=2.0,<3
|
|
541
|
+
Python: installed: >=3.7, latest: >=3.8
|
|
542
|
+
[OK] requests 2.31.0 → 2.31.0
|
|
543
|
+
"""
|
|
544
|
+
console = get_raw_console()
|
|
545
|
+
|
|
546
|
+
for pkg in packages:
|
|
547
|
+
# Main line: [STATUS] package_name current → latest
|
|
548
|
+
status, installed, latest, recommended = pkg.get_status_summary()
|
|
549
|
+
|
|
550
|
+
if recommended and recommended != latest:
|
|
551
|
+
# Show recommended version when it differs (conflict resolution)
|
|
552
|
+
console.print(
|
|
553
|
+
f"[{status}] {pkg.name:20} {installed:10} → {latest:10} "
|
|
554
|
+
f"(recommended: {recommended})"
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
console.print(f"[{status}] {pkg.name:20} {installed:10} → {latest:10}")
|
|
558
|
+
|
|
559
|
+
# Indented conflict details
|
|
560
|
+
if pkg.has_conflicts():
|
|
561
|
+
for conflict in pkg.conflicts:
|
|
562
|
+
console.print(
|
|
563
|
+
f" [red]⚠ Conflict:[/red] {conflict.to_display_string()}"
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
# Indented Python version requirements
|
|
567
|
+
current_req = pkg.get_version_python_req("current")
|
|
568
|
+
latest_req = pkg.get_version_python_req("latest")
|
|
569
|
+
if current_req or latest_req:
|
|
570
|
+
req_parts = []
|
|
571
|
+
if current_req:
|
|
572
|
+
req_parts.append(f"installed: {current_req}")
|
|
573
|
+
if latest_req:
|
|
574
|
+
req_parts.append(f"latest: {latest_req}")
|
|
575
|
+
if pkg.has_update():
|
|
576
|
+
recommended_req = pkg.get_version_python_req("recommended")
|
|
577
|
+
if recommended_req:
|
|
578
|
+
req_parts.append(f"recommended: {recommended_req}")
|
|
579
|
+
if req_parts:
|
|
580
|
+
console.print(f" Python: {', '.join(req_parts)}")
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _display_json(packages: List[Package]) -> None:
|
|
584
|
+
"""Render packages as formatted JSON for machine consumption.
|
|
585
|
+
|
|
586
|
+
Outputs a JSON array where each element contains complete package
|
|
587
|
+
information: versions, conflicts, metadata, and Python compatibility.
|
|
588
|
+
Suitable for piping to ``jq`` or parsing in scripts.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
packages: List of :class:`Package` objects to serialize.
|
|
592
|
+
|
|
593
|
+
Example::
|
|
594
|
+
|
|
595
|
+
[
|
|
596
|
+
{
|
|
597
|
+
"name": "flask",
|
|
598
|
+
"current_version": "2.0.0",
|
|
599
|
+
"latest_version": "3.0.0",
|
|
600
|
+
"recommended_version": "2.3.3",
|
|
601
|
+
"conflicts": [...],
|
|
602
|
+
"metadata": {...}
|
|
603
|
+
},
|
|
604
|
+
...
|
|
605
|
+
]
|
|
606
|
+
"""
|
|
607
|
+
data = [pkg.to_json() for pkg in packages]
|
|
608
|
+
print(json.dumps(data, indent=2))
|