Skip to content

Commit 526b244

Browse files
feat: Add Intelligent Dependency Conflict Analysis (#4)
* updated TOML file * docs: Update project files for v1.2.0 release * feat: Add a description of the new feature
1 parent b07e08b commit 526b244

3 files changed

Lines changed: 90 additions & 38 deletions

File tree

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,64 @@
11
# Ultimate Python Upgrader (`py-upgrade`)
22

3-
[![CI](https://github.com/psywarrior1998/upgrade_all_python/actions/workflows/ci.yml/badge.svg)](https://github.com/psywarrior1998/upgrade_all_python/actions/workflows/ci.yml)
43
[![PyPI version](https://badge.fury.io/py/ultimate-python-upgrader.svg)](https://badge.fury.io/py/ultimate-python-upgrader)
4+
[![CI](https://github.com/psywarrior1998/upgrade_all_python/actions/workflows/ci.yml/badge.svg)](https://github.com/psywarrior1998/upgrade_all_python/actions/workflows/ci.yml)
5+
6+
An intelligent, feature-rich CLI tool to manage and upgrade Python packages with a clean, modern interface and a powerful dependency safety-check.
7+
58

6-
An intelligent, feature-rich CLI tool to manage and upgrade Python packages with a clean, modern interface.
79

810
## Key Features
911

10-
- **Interactive & Beautiful UI**: Uses Rich to display outdated packages in a clean, readable table.
11-
- **Blazing Fast**: Upgrades packages with a clear progress bar.
12+
- **Intelligent Dependency Analysis**: Automatically performs a pre-flight check to detect and warn you about potential dependency conflicts *before* you upgrade, preventing broken environments.
13+
- **Concurrent & Fast**: Upgrades packages in parallel using multiple workers, dramatically reducing the time you spend waiting.
14+
- **Rich & Interactive UI**: Uses `rich` to display outdated packages in a clean, readable table with clear progress bars.
1215
- **Selective Upgrades**: Upgrade all packages, or specify exactly which ones to include or exclude.
13-
- **Safety First**: Includes a `--dry-run` mode to see what would be upgraded without making changes.
16+
- **Safety First**: Includes a `--dry-run` mode to see what would be upgraded without making any changes.
1417
- **Automation Friendly**: A `--yes` flag allows for use in automated scripts.
1518

1619
## Installation
1720

18-
The tool is now available on PyPI. Install it with pip:
21+
The tool is available on PyPI. Install it with pip:
1922

2023
```bash
2124
pip install ultimate-python-upgrader
22-
```
25+
````
2326

2427
## Usage
2528

2629
Once installed, the `py-upgrade` command will be available.
2730

2831
**1. Check and upgrade all packages interactively**
32+
The tool will first check for dependency conflicts before asking to proceed.
33+
2934
```bash
3035
py-upgrade
3136
```
3237

33-
**2. Upgrade all packages without confirmation**
38+
**2. Upgrade with more parallel workers**
39+
3440
```bash
35-
py-upgrade --yes
41+
py-upgrade --yes --workers 20
3642
```
3743

3844
**3. Perform a dry run to see what needs upgrading**
45+
3946
```bash
4047
py-upgrade --dry-run
4148
```
4249

4350
**4. Upgrade only specific packages**
51+
4452
```bash
4553
py-upgrade numpy pandas
4654
```
4755

4856
**5. Upgrade all packages EXCEPT certain ones**
57+
4958
```bash
5059
py-upgrade --exclude black ruff
5160
```
5261

5362
## Contributing
5463

55-
Contributions are welcome! Please feel free to submit a pull request.
64+
Contributions are welcome\! Please feel free to submit a pull request.

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "ultimate-python-upgrader"
7-
version = "1.1.0"
7+
version = "1.2.0"
88
authors = [
99
{ name="Your Name", email="your@email.com" },
1010
]
11-
description = "An intelligent, feature-rich CLI tool to manage and upgrade Python packages."
11+
description = "An intelligent, feature-rich CLI tool to manage and upgrade Python packages with dependency analysis."
1212
readme = "README.md"
1313
requires-python = ">=3.8"
1414
license = { file="LICENSE" }
@@ -32,11 +32,11 @@ dependencies = [
3232
[project.scripts]
3333
py-upgrade = "upgrade_tool.main:app"
3434

35-
# --- ADD THIS SECTION ---
36-
[tool.hatch.build.targets.wheel]
37-
packages = ["upgrade_tool"]
3835
[project.optional-dependencies]
3936
test = [
4037
"pytest>=8.0.0",
4138
"pytest-cov>=4.0.0"
4239
]
40+
41+
[tool.hatch.build.targets.wheel]
42+
packages = ["upgrade_tool"]

upgrade_tool/main.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,68 @@
11
import subprocess
22
import sys
3+
import re
34
from typing import List, Optional, Tuple
45

56
import typer
67
from rich.console import Console
8+
from rich.panel import Panel
79
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
8-
9-
# Import the concurrent futures module for threading
1010
from concurrent.futures import ThreadPoolExecutor, as_completed
1111

12-
# Import the refactored utility functions
1312
from .utils import get_outdated_packages, generate_packages_table
1413

15-
# Initialize Rich Console for beautiful printing
1614
console = Console()
1715

18-
# Create a Typer app for our CLI
1916
app = typer.Typer(
2017
name="py-upgrade",
2118
help="An intelligent, feature-rich CLI tool to manage and upgrade Python packages.",
2219
add_completion=False,
2320
)
2421

25-
def upgrade_package(pkg: dict) -> Tuple[str, str, bool]:
22+
def check_for_conflicts(packages_to_check: List[str]) -> Optional[str]:
2623
"""
27-
Worker function to upgrade a single package in a separate thread.
28-
24+
Performs a dry-run upgrade to detect dependency conflicts.
25+
2926
Args:
30-
pkg: A dictionary containing package information ('name', 'latest_version').
27+
packages_to_check: A list of package names to be upgraded.
3128
3229
Returns:
33-
A tuple containing (package_name, latest_version, success_boolean).
30+
A formatted string of conflict messages, or None if no conflicts are found.
31+
"""
32+
console.print("\n[bold cyan]Checking for potential dependency conflicts...[/bold cyan]")
33+
command = [
34+
sys.executable,
35+
"-m",
36+
"pip",
37+
"install",
38+
"--dry-run",
39+
"--upgrade",
40+
] + packages_to_check
41+
42+
process = subprocess.Popen(
43+
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8'
44+
)
45+
_, stderr = process.communicate()
46+
47+
# Find the specific dependency conflict block in pip's output
48+
conflict_match = re.search(
49+
r"ERROR: pip's dependency resolver does not currently take into account all the packages that are installed\. This behaviour is the source of the following dependency conflicts\.(.+)",
50+
stderr,
51+
re.DOTALL,
52+
)
53+
54+
if conflict_match:
55+
conflict_text = conflict_match.group(1).strip()
56+
return conflict_text
57+
return None
58+
59+
def upgrade_package(pkg: dict) -> Tuple[str, str, bool]:
60+
"""
61+
Worker function to upgrade a single package in a separate thread.
3462
"""
3563
pkg_name = pkg['name']
3664
latest_version = pkg['latest_version']
3765
try:
38-
# Execute the pip upgrade command, suppressing output
3966
subprocess.check_call(
4067
[sys.executable, "-m", "pip", "install", "--upgrade", pkg_name],
4168
stdout=subprocess.DEVNULL,
@@ -61,18 +88,18 @@ def upgrade(
6188
),
6289
workers: int = typer.Option(
6390
10, "--workers", "-w", help="Number of concurrent workers for parallel upgrades."
64-
)
91+
),
6592
):
6693
"""
67-
Checks for and concurrently upgrades outdated Python packages.
94+
Checks for and concurrently upgrades outdated Python packages with dependency analysis.
6895
"""
69-
# --- Filtering Logic (Unchanged) ---
7096
outdated_packages = get_outdated_packages()
7197

7298
if not outdated_packages:
7399
console.print("[bold green]✨ All packages are up to date! ✨[/bold green]")
74100
raise typer.Exit()
75101

102+
# --- Filtering Logic ---
76103
if packages_to_upgrade:
77104
name_to_pkg = {pkg['name'].lower(): pkg for pkg in outdated_packages}
78105
target_packages = [name_to_pkg[name.lower()] for name in packages_to_upgrade if name.lower() in name_to_pkg]
@@ -87,25 +114,45 @@ def upgrade(
87114
console.print("[bold yellow]No packages match the specified criteria for upgrade.[/bold yellow]")
88115
raise typer.Exit()
89116

90-
# --- Display and Confirmation (Unchanged) ---
91117
table = generate_packages_table(target_packages, title="Outdated Python Packages")
92118
console.print(table)
93119

94120
if dry_run:
95-
console.print(f"\n[bold yellow]--dry-run enabled. Would upgrade {len(target_packages)} packages with {workers} workers.[/bold yellow]")
121+
console.print(f"\n[bold yellow]--dry-run enabled. Would simulate upgrade of {len(target_packages)} packages.[/bold yellow]")
96122
raise typer.Exit()
123+
124+
# --- Intelligent Dependency Analysis ---
125+
package_names = [pkg['name'] for pkg in target_packages]
126+
conflicts = check_for_conflicts(package_names)
127+
128+
if conflicts:
129+
console.print(
130+
Panel.fit(
131+
f"[bold]The following dependency conflicts were found:[/bold]\n\n{conflicts}",
132+
title="[bold yellow]⚠️ Dependency Warning[/bold yellow]",
133+
border_style="yellow",
134+
padding=(1, 2),
135+
)
136+
)
137+
else:
138+
console.print("[bold green]✅ No dependency conflicts detected.[/bold green]")
97139

140+
# --- Confirmation ---
98141
if not yes:
142+
prompt_message = "\nProceed with the upgrade?"
143+
if conflicts:
144+
prompt_message = "\nConflicts were detected. Do you still wish to proceed with the upgrade?"
145+
99146
try:
100-
confirmed = typer.confirm("\nProceed with the upgrade?")
147+
confirmed = typer.confirm(prompt_message)
101148
if not confirmed:
102149
console.print("Upgrade cancelled by user.")
103150
raise typer.Exit()
104151
except typer.Abort:
105152
console.print("\nUpgrade cancelled by user.")
106153
raise typer.Exit()
107154

108-
# --- Concurrent Execution Logic (The New Engine) ---
155+
# --- Concurrent Execution Logic ---
109156
console.print(f"\n[bold blue]Starting parallel upgrade with {workers} workers...[/bold blue]")
110157

111158
progress = Progress(
@@ -122,12 +169,9 @@ def upgrade(
122169
with progress:
123170
upgrade_task = progress.add_task("[green]Upgrading...", total=len(target_packages))
124171

125-
# Create a thread pool with the specified number of workers
126172
with ThreadPoolExecutor(max_workers=workers) as executor:
127-
# Submit an upgrade task for each package
128173
future_to_pkg = {executor.submit(upgrade_package, pkg): pkg for pkg in target_packages}
129174

130-
# Process results as they complete
131175
for future in as_completed(future_to_pkg):
132176
pkg_name, latest_version, success = future.result()
133177

@@ -138,10 +182,9 @@ def upgrade(
138182
progress.console.print(f" ❌ [red]Failed to upgrade {pkg_name}[/red]")
139183
fail_count += 1
140184

141-
# Advance the progress bar for each completed task
142185
progress.advance(upgrade_task)
143186

144-
# --- Summary Report (Unchanged) ---
187+
# --- Summary Report ---
145188
console.print("\n--- [bold]Upgrade Complete[/bold] ---")
146189
console.print(f"[green]Successfully upgraded:[/green] {success_count} packages")
147190
if fail_count > 0:

0 commit comments

Comments
 (0)