Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions src/check_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ def check_local(path: str, report: bool) -> None:

messages = []

# production dependencies
# main dependencies
dependencies = sorted(toml.dependencies.keys())
table = dependency_table_header(title="Production Dependencies")
table = dependency_table_header(title="Main Dependencies")

production_packages = get_packages(dependencies, messages)

Expand Down
4 changes: 2 additions & 2 deletions src/check_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ def check_remote(repo_url, report):

messages = []

# production dependencies
# Main dependencies
dependencies = sorted(toml.dependencies.keys())
table = dependency_table_header(title="Production Dependencies")
table = dependency_table_header(title="Main Dependencies")

production_packages = get_packages(dependencies, messages)

Expand Down
60 changes: 53 additions & 7 deletions src/helpers.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,77 @@
from packaging.version import InvalidVersion, parse
from rich import box
from rich.table import Table
from src.client import PyPiClient
from src.managers.package import Package


def calculate_update_type(current_version, latest_version):
"""
Calculate if the latest version is a major, minor, or patch update compared to the current version.
Returns one of: 'major', 'minor', 'patch', or None.
"""
try:
current = parse(current_version)
latest = parse(latest_version)
except InvalidVersion:
return None
if current == latest:
return None
if hasattr(current, "major") and hasattr(latest, "major"):
if latest.major > current.major:
return "major"
elif latest.minor > current.minor:
return "minor"
elif latest.micro > current.micro:
return "patch"
return None
Comment thread
nickmoreton marked this conversation as resolved.


def package_table_row(frozen, package):
name = package.name
latest_version = package.latest_version
frozen_version = frozen.dependencies.get(name.lower())

# Calculate update_type based on frozen version vs latest version
update_type = None
if frozen_version and frozen_version != latest_version and "git+https://" not in frozen_version:
update_type = calculate_update_type(frozen_version, latest_version)

if frozen_version and frozen_version != latest_version:
if "git+https://" in frozen_version:
status = "Check"
style = "red1"
style = "white"
frozen_version = "Forked package"
else:
status = "Outdated"
style = "yellow1"
# Set color based on update type
if update_type == "major":
style = "red1"
elif update_type == "minor":
style = "yellow1"
elif update_type == "patch":
style = "orange1"
else:
style = "yellow1" # fallback for when update_type is None
elif not frozen_version:
status = "Check"
style = "cyan1"
frozen_version = "Unable to determine version"
style = "white"
frozen_version = "Unknown version"
else:
status = "OK"
style = "green3"

if "git+https://" in frozen_version:
frozen_version = frozen_version.replace("git+https://", "")
frozen_version = f"{frozen_version.split('@')[0]} TAG {frozen_version.split('@')[1]}"
# Always reflect update_type if present
if update_type:
status = f"{status} ({update_type})"

# If it's a major update, add the repository URL if available
if update_type == "major":
repo_url = package.repository_url
if repo_url:
status = f"{status}\n{repo_url}"

# No need to process git URLs anymore since we set frozen_version to "Forked package"
return name, latest_version, frozen_version, status, style


Expand Down
14 changes: 14 additions & 0 deletions src/managers/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ def latest_version(self):
version_str = self.parse_versions_for_latest().__str__()
return version_str

@property
def repository_url(self):
"""
Returns the repository URL from project_urls in order of preference:
Changelog, Changes, Release log, Repository, then Source. Returns the first one found or None.
"""
project_urls = self.json["info"].get("project_urls", {})
if isinstance(project_urls, dict):
# Check in order of preference
for key in ["Changelog", "Changes", "Release log", "Repository", "Source"]:
if key in project_urls:
return project_urls[key]
return None

def parse_versions_for_latest(self):
"""
Parse the versions and return the latest version
Expand Down
63 changes: 52 additions & 11 deletions src/reporters/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,69 @@ def _table_header(self, headers: list) -> str:
return header

def _table_dependency_row(self, data: dict, status_class: str) -> str:
# Handle status that might contain newlines (repository URLs)
status_content = data["Status"]
ai_chat_request = ""

# Check if there's a URL on a new line (for major updates)
if "\n" in status_content:
status_parts = status_content.split("\n")
status_text = status_parts[0]
url = status_parts[1]
# Make the URL a clickable link
status_content = f"{status_text}<br><a href='{url}' target='_blank'>{url}</a>"
ai_chat_request += (
f"explain if there are breaking changes for this major version upgrade "
f"for this package {data['Package']} at {url} "
f"from version {data['Installed Version']} to version {data['Latest Version']} "
)

if ai_chat_request:
ai_chat_request = f"""<br><br>
<details name="example">
<summary>Suggested AI Chat Request</summary>
<p><textarea>{ai_chat_request}</textarea></p>
</details>"""

return f"""
<tr>
<td><span class='{status_class}'>{data['Package']}</span></td>
<td>{data['Installed Version']}</td>
<td>{data['Latest Version']}</td>
<td><span class='{status_class}'>{data['Status']}</span></td>
<td><span class='{status_class}'>{data['Installed Version']}</span></td>
Comment thread
nickmoreton marked this conversation as resolved.
<td><span class='{status_class}'>{data['Latest Version']}</span></td>
<td>
<span class='{status_class}'>{status_content}</span>
{ai_chat_request}
</td>
</tr>"""

def _gather_status_class(self, data: list) -> str:
status_class = "pico-color-red-500"
if data["Status"] == "Check":
status_class = "pico-color-cyan-500"
elif data["Status"] == "Outdated":
status_class = "pico-color-orange-500"
elif data["Status"] == "OK":
status_class = "pico-color-green-500"
status = data["Status"]

# Extract the base status (before any parentheses or newlines)
base_status = status.split("(")[0].split("\n")[0].strip()

# Check for update types in the status
if "(major)" in status:
status_class = "pico-color-red-500" # Red for major updates
elif "(minor)" in status:
status_class = "pico-color-amber-500" # Yellow for minor updates
elif "(patch)" in status:
status_class = "pico-color-pumpkin-500" # Orange for patch updates
elif base_status == "Check":
status_class = "pico-color-gray-500" # White/gray for check
elif base_status == "Outdated":
status_class = "pico-color-amber-500" # Yellow fallback for outdated
elif base_status == "OK":
status_class = "pico-color-green-500" # Green for up to date
else:
status_class = "pico-color-gray-500" # Default fallback

return status_class

def write_report(self):
self.report += self.report_header

production_section = self._heading("Production Dependencies")
production_section = self._heading("Main Dependencies")
production_section += self._open_table("striped")
production_section += self._table_header(["Package", "Installed Version", "Latest Version", "Status"])
for data in self.production_data:
Expand Down
54 changes: 41 additions & 13 deletions tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,13 @@ def test_table_dependency_row():
"Latest Version": "Latest Version",
"Status": "Status",
}
assert (
reporter._table_dependency_row(data, status_class)
== """
<tr>
<td><span class='status'>Package</span></td>
<td>Installed Version</td>
<td>Latest Version</td>
<td><span class='status'>Status</span></td>
</tr>"""
)
result = reporter._table_dependency_row(data, status_class)
assert result.startswith("\n <tr>")
assert "<td><span class='status'>Package</span></td>" in result
assert "<td><span class='status'>Installed Version</span></td>" in result
assert "<td><span class='status'>Latest Version</span></td>" in result
assert "<span class='status'>Status</span>" in result
assert result.endswith("</tr>")


def test_gather_status_class():
Expand All @@ -84,7 +81,7 @@ def test_gather_status_class():
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-red-500"
assert reporter._gather_status_class(data) == "pico-color-gray-500"

data = {
"Package": "Package",
Expand All @@ -94,7 +91,7 @@ def test_gather_status_class():
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-cyan-500"
assert reporter._gather_status_class(data) == "pico-color-gray-500"

data = {
"Package": "Package",
Expand All @@ -104,7 +101,7 @@ def test_gather_status_class():
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-orange-500"
assert reporter._gather_status_class(data) == "pico-color-amber-500"

data = {
"Package": "Package",
Expand All @@ -116,6 +113,37 @@ def test_gather_status_class():
reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-green-500"

# Test update type specific colors
data = {
"Package": "Package",
"Installed Version": "Installed Version",
"Latest Version": "Latest Version",
"Status": "Outdated (major)",
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-red-500"

data = {
"Package": "Package",
"Installed Version": "Installed Version",
"Latest Version": "Latest Version",
"Status": "Outdated (minor)",
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-amber-500"

data = {
"Package": "Package",
"Installed Version": "Installed Version",
"Latest Version": "Latest Version",
"Status": "Outdated (patch)",
}

reporter = HTMLReporter()
assert reporter._gather_status_class(data) == "pico-color-pumpkin-500"


def test_write_report():
reporter = HTMLReporter()
Expand Down