Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,64 @@
# Ultimate Python Upgrader (`py-upgrade`)

[![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)
[![PyPI version](https://badge.fury.io/py/ultimate-python-upgrader.svg)](https://badge.fury.io/py/ultimate-python-upgrader)
[![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)

An intelligent, feature-rich CLI tool to manage and upgrade Python packages with a clean, modern interface and a powerful dependency safety-check.


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

## Key Features

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

## Installation

The tool is now available on PyPI. Install it with pip:
The tool is available on PyPI. Install it with pip:

```bash
pip install ultimate-python-upgrader
```
````

## Usage

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

**1. Check and upgrade all packages interactively**
The tool will first check for dependency conflicts before asking to proceed.

```bash
py-upgrade
```

**2. Upgrade all packages without confirmation**
**2. Upgrade with more parallel workers**

```bash
py-upgrade --yes
py-upgrade --yes --workers 20
```

**3. Perform a dry run to see what needs upgrading**

```bash
py-upgrade --dry-run
```

**4. Upgrade only specific packages**

```bash
py-upgrade numpy pandas
```

**5. Upgrade all packages EXCEPT certain ones**

```bash
py-upgrade --exclude black ruff
```

## Contributing

Contributions are welcome! Please feel free to submit a pull request.
Contributions are welcome\! Please feel free to submit a pull request.
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ build-backend = "hatchling.build"

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

# --- ADD THIS SECTION ---
[tool.hatch.build.targets.wheel]
packages = ["upgrade_tool"]
[project.optional-dependencies]
test = [
"pytest>=8.0.0",
"pytest-cov>=4.0.0"
]

[tool.hatch.build.targets.wheel]
packages = ["upgrade_tool"]
89 changes: 66 additions & 23 deletions upgrade_tool/main.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,68 @@
import subprocess
import sys
import re
from typing import List, Optional, Tuple

import typer
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn

# Import the concurrent futures module for threading
from concurrent.futures import ThreadPoolExecutor, as_completed

# Import the refactored utility functions
from .utils import get_outdated_packages, generate_packages_table

# Initialize Rich Console for beautiful printing
console = Console()

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

def upgrade_package(pkg: dict) -> Tuple[str, str, bool]:
def check_for_conflicts(packages_to_check: List[str]) -> Optional[str]:
"""
Worker function to upgrade a single package in a separate thread.
Performs a dry-run upgrade to detect dependency conflicts.

Args:
pkg: A dictionary containing package information ('name', 'latest_version').
packages_to_check: A list of package names to be upgraded.

Returns:
A tuple containing (package_name, latest_version, success_boolean).
A formatted string of conflict messages, or None if no conflicts are found.
"""
console.print("\n[bold cyan]Checking for potential dependency conflicts...[/bold cyan]")
command = [
sys.executable,
"-m",
"pip",
"install",
"--dry-run",
"--upgrade",
] + packages_to_check

process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8'
)
_, stderr = process.communicate()

# Find the specific dependency conflict block in pip's output
conflict_match = re.search(
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\.(.+)",
stderr,
re.DOTALL,
)

if conflict_match:
conflict_text = conflict_match.group(1).strip()
return conflict_text
return None

def upgrade_package(pkg: dict) -> Tuple[str, str, bool]:
"""
Worker function to upgrade a single package in a separate thread.
"""
pkg_name = pkg['name']
latest_version = pkg['latest_version']
try:
# Execute the pip upgrade command, suppressing output
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--upgrade", pkg_name],
stdout=subprocess.DEVNULL,
Expand All @@ -61,18 +88,18 @@ def upgrade(
),
workers: int = typer.Option(
10, "--workers", "-w", help="Number of concurrent workers for parallel upgrades."
)
),
):
"""
Checks for and concurrently upgrades outdated Python packages.
Checks for and concurrently upgrades outdated Python packages with dependency analysis.
"""
# --- Filtering Logic (Unchanged) ---
outdated_packages = get_outdated_packages()

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

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

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

if dry_run:
console.print(f"\n[bold yellow]--dry-run enabled. Would upgrade {len(target_packages)} packages with {workers} workers.[/bold yellow]")
console.print(f"\n[bold yellow]--dry-run enabled. Would simulate upgrade of {len(target_packages)} packages.[/bold yellow]")
raise typer.Exit()

# --- Intelligent Dependency Analysis ---
package_names = [pkg['name'] for pkg in target_packages]
conflicts = check_for_conflicts(package_names)

if conflicts:
console.print(
Panel.fit(
f"[bold]The following dependency conflicts were found:[/bold]\n\n{conflicts}",
title="[bold yellow]⚠️ Dependency Warning[/bold yellow]",
border_style="yellow",
padding=(1, 2),
)
)
else:
console.print("[bold green]✅ No dependency conflicts detected.[/bold green]")

# --- Confirmation ---
if not yes:
prompt_message = "\nProceed with the upgrade?"
if conflicts:
prompt_message = "\nConflicts were detected. Do you still wish to proceed with the upgrade?"

try:
confirmed = typer.confirm("\nProceed with the upgrade?")
confirmed = typer.confirm(prompt_message)
if not confirmed:
console.print("Upgrade cancelled by user.")
raise typer.Exit()
except typer.Abort:
console.print("\nUpgrade cancelled by user.")
raise typer.Exit()

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

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

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

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

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

# Advance the progress bar for each completed task
progress.advance(upgrade_task)

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