fix: implement semantic version sorting

- Add sort_versions_semantically function using packaging library
- Fix issue where pre-release versions appeared before stable (5.2rc1 vs 5.2.5)
- Handle edge cases: dev, post, invalid versions with graceful fallback
- Add comprehensive test suite covering all scenarios
- Maintain backward compatibility with existing functionality
This commit is contained in:
Ryan Malloy 2025-08-15 11:53:40 -06:00
parent 146952f404
commit 251ceb4c2d
8 changed files with 885 additions and 3 deletions

182
VERSION_SORTING_FIX.md Normal file
View File

@ -0,0 +1,182 @@
# Semantic Version Sorting Fix
## Problem Description
The `get_package_versions` tool was using basic string sorting for package versions instead of semantic version sorting. This caused incorrect ordering where pre-release versions (like `5.2rc1`) appeared before stable versions (like `5.2.5`) when they should come after.
### Specific Issue
- **Problem**: `"5.2rc1"` was appearing before `"5.2.5"` in version lists
- **Root Cause**: Using `sorted(releases.keys(), reverse=True)` performs lexicographic string sorting
- **Impact**: Misleading version order in package version queries
## Solution Implemented
### 1. Added Semantic Version Sorting Function
**File**: `/pypi_query_mcp/core/version_utils.py`
```python
def sort_versions_semantically(versions: list[str], reverse: bool = True) -> list[str]:
"""Sort package versions using semantic version ordering.
This function properly sorts versions by parsing them as semantic versions,
ensuring that pre-release versions (alpha, beta, rc) are ordered correctly
relative to stable releases.
"""
```
**Key Features**:
- Uses the `packaging.version.Version` class for proper semantic parsing
- Handles pre-release versions correctly (alpha < beta < rc < stable)
- Gracefully handles invalid versions by falling back to string sorting
- Maintains original version strings in output
- Comprehensive logging for debugging
### 2. Updated Package Query Functions
**File**: `/pypi_query_mcp/tools/package_query.py`
**Changes Made**:
1. **Import**: Added `from ..core.version_utils import sort_versions_semantically`
2. **format_version_info()**: Replaced basic sorting with semantic sorting
3. **format_package_info()**: Updated available_versions to use semantic sorting
**Before**:
```python
# Sort versions (basic sorting, could be improved with proper version parsing)
sorted_versions = sorted(releases.keys(), reverse=True)
```
**After**:
```python
# Sort versions using semantic version ordering
sorted_versions = sort_versions_semantically(list(releases.keys()), reverse=True)
```
## Test Results
### 1. Unit Tests - Semantic Version Sorting
```
Test 1 - Pre-release ordering:
Input: ['5.2rc1', '5.2.5', '5.2.0', '5.2a1', '5.2b1']
Output: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']
✅ PASS: Correct pre-release ordering
```
### 2. Task Requirement Validation
```
Task requirement validation:
Input: ['5.2rc1', '5.2.5']
Output: ['5.2.5', '5.2rc1']
Requirement: '5.2rc1' should come after '5.2.5'
✅ PASS: Requirement met!
```
### 3. Pre-release Ordering Validation
```
Pre-release ordering validation:
Input: ['1.0.0', '1.0.0rc1', '1.0.0b1', '1.0.0a1']
Output: ['1.0.0', '1.0.0rc1', '1.0.0b1', '1.0.0a1']
Expected order: stable > rc > beta > alpha
✅ PASS: Pre-release ordering correct!
```
### 4. Real Package Testing
**Django** (complex versioning with pre-releases):
```
Recent versions: ['5.2.5', '5.2.4', '5.2.3', '5.2.2', '5.2.1', '5.2', '5.2rc1', '5.2b1', '5.2a1', '5.1.11']
String-sorted: ['5.2rc1', '5.2b1', '5.2a1', '5.2.5', '5.2.4', '5.2.3', '5.2.2', '5.2.1', '5.2', '5.1.9']
✅ Semantic sorting correctly places stable versions before pre-releases
```
**NumPy** (simple versioning):
```
Recent versions: ['2.3.2', '2.3.1', '2.3.0', '2.2.6', '2.2.5', '2.2.4', '2.2.3', '2.2.2', '2.2.1', '2.2.0']
✅ Both sorting methods produce identical results (as expected for simple versions)
```
### 5. Edge Cases Testing
**Complex versions with dev, post, and invalid versions**:
```
Input: ['1.0.0', '1.0.0.post1', '1.0.0.dev0', '1.0.0a1', '1.0.0b1', '1.0.0rc1', '1.0.1', 'invalid-version', '1.0']
Output: ['1.0.1', '1.0.0.post1', '1.0.0', '1.0', '1.0.0rc1', '1.0.0b1', '1.0.0a1', '1.0.0.dev0', 'invalid-version']
✅ Handles all edge cases correctly
```
### 6. Regression Testing
```bash
poetry run python -m pytest tests/ -v
============================= test session starts ==============================
64 passed in 9.25s
✅ All existing tests continue to pass
```
## Implementation Details
### Semantic Version Ordering Rules
1. **Stable versions** come before **pre-release versions** of the same base version
2. **Pre-release ordering**: `alpha < beta < rc < stable`
3. **Development versions** (`dev`) come before **alpha versions**
4. **Post-release versions** (`post`) come after **stable versions**
5. **Invalid versions** are sorted lexicographically and placed after valid versions
### Error Handling
- Invalid version strings are gracefully handled
- Falls back to string sorting for unparseable versions
- Logs warnings for invalid versions (debug level)
- Maintains all original version strings in output
### Performance Considerations
- Minimal performance impact (parsing is fast)
- Uses efficient sorting algorithms
- Caches parsed versions during single sort operation
- No breaking changes to existing API
## Verification Commands
```bash
# Run standalone semantic version tests
python test_version_sorting_standalone.py
# Test with real PyPI packages
poetry run python test_real_packages.py
# Test specific task requirement
poetry run python test_specific_case.py
# Run full test suite
poetry run python -m pytest tests/ -v
```
## Files Modified
1. **`/pypi_query_mcp/core/version_utils.py`**: Added `sort_versions_semantically()` function
2. **`/pypi_query_mcp/tools/package_query.py`**: Updated to use semantic version sorting
## Dependencies
- Uses existing `packaging` library (already a dependency in `pyproject.toml`)
- No new dependencies added
- Compatible with Python 3.10+
## Conclusion
The semantic version sorting fix successfully resolves the issue where pre-release versions were incorrectly appearing before stable versions. The implementation:
- ✅ Fixes the specific problem mentioned (`5.2rc1` vs `5.2.5`)
- ✅ Handles all pre-release types correctly (alpha, beta, rc)
- ✅ Manages edge cases (dev, post, invalid versions)
- ✅ Maintains backward compatibility
- ✅ Passes all existing tests
- ✅ Uses robust, industry-standard version parsing
The fix provides accurate, intuitive version ordering that matches user expectations and semantic versioning standards.

View File

@ -284,3 +284,62 @@ class VersionCompatibility:
)
return recommendations
def sort_versions_semantically(versions: list[str], reverse: bool = True) -> list[str]:
"""Sort package versions using semantic version ordering.
This function properly sorts versions by parsing them as semantic versions,
ensuring that pre-release versions (alpha, beta, rc) are ordered correctly
relative to stable releases.
Args:
versions: List of version strings to sort
reverse: If True, sort in descending order (newest first). Default True.
Returns:
List of version strings sorted semantically
Examples:
>>> sort_versions_semantically(['1.0.0', '2.0.0a1', '1.5.0', '2.0.0'])
['2.0.0', '2.0.0a1', '1.5.0', '1.0.0']
>>> sort_versions_semantically(['5.2rc1', '5.2.5', '5.2.0'])
['5.2.5', '5.2.0', '5.2rc1']
"""
if not versions:
return []
def parse_version_safe(version_str: str) -> tuple[Version | None, str]:
"""Safely parse a version string, returning (parsed_version, original_string).
Returns (None, original_string) if parsing fails.
"""
try:
return (Version(version_str), version_str)
except InvalidVersion:
logger.debug(f"Failed to parse version '{version_str}' as semantic version")
return (None, version_str)
# Parse all versions, keeping track of originals
parsed_versions = [parse_version_safe(v) for v in versions]
# Separate valid and invalid versions
valid_versions = [(v, orig) for v, orig in parsed_versions if v is not None]
invalid_versions = [orig for v, orig in parsed_versions if v is None]
# Sort valid versions semantically
valid_versions.sort(key=lambda x: x[0], reverse=reverse)
# Sort invalid versions lexicographically as fallback
invalid_versions.sort(reverse=reverse)
# Combine results: valid versions first, then invalid ones
result = [orig for _, orig in valid_versions] + invalid_versions
logger.debug(
f"Sorted {len(versions)} versions: {len(valid_versions)} valid, "
f"{len(invalid_versions)} invalid"
)
return result

View File

@ -4,6 +4,7 @@ import logging
from typing import Any
from ..core import InvalidPackageNameError, NetworkError, PyPIClient, PyPIError
from ..core.version_utils import sort_versions_semantically
logger = logging.getLogger(__name__)
@ -46,7 +47,12 @@ def format_package_info(package_data: dict[str, Any]) -> dict[str, Any]:
# Add release information
releases = package_data.get("releases", {})
formatted["total_versions"] = len(releases)
formatted["available_versions"] = list(releases.keys())[-10:] # Last 10 versions
# Sort versions semantically and get the most recent 10
if releases:
sorted_versions = sort_versions_semantically(list(releases.keys()), reverse=True)
formatted["available_versions"] = sorted_versions[:10] # Most recent 10 versions
else:
formatted["available_versions"] = []
# Add download statistics if available
if "urls" in package_data:
@ -79,8 +85,8 @@ def format_version_info(package_data: dict[str, Any]) -> dict[str, Any]:
info = package_data.get("info", {})
releases = package_data.get("releases", {})
# Sort versions (basic sorting, could be improved with proper version parsing)
sorted_versions = sorted(releases.keys(), reverse=True)
# Sort versions using semantic version ordering
sorted_versions = sort_versions_semantically(list(releases.keys()), reverse=True)
return {
"package_name": info.get("name", ""),

105
test_real_packages.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Test script to verify semantic version sorting with real PyPI packages."""
import asyncio
import logging
from pypi_query_mcp.tools.package_query import query_package_versions
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def test_real_package_versions():
"""Test with real PyPI packages that have complex version histories."""
print("=" * 60)
print("Testing Real Package Version Sorting")
print("=" * 60)
# Test packages known to have complex version histories
test_packages = [
"django", # Known for alpha, beta, rc versions
"numpy", # Long history with various formats
"requests" # Simple but well-known package
]
for package_name in test_packages:
try:
print(f"\nTesting {package_name}:")
result = await query_package_versions(package_name)
recent_versions = result.get("recent_versions", [])[:10]
print(f" Recent versions (first 10): {recent_versions}")
# Show older-style string sorting for comparison
all_versions = result.get("versions", [])
if all_versions:
# Use basic string sorting (the old way)
string_sorted = sorted(all_versions[:20], reverse=True)
print(f" String-sorted (first 10): {string_sorted[:10]}")
print(f" Semantic vs String comparison:")
for i in range(min(5, len(recent_versions))):
semantic = recent_versions[i] if i < len(recent_versions) else "N/A"
string_sort = string_sorted[i] if i < len(string_sorted) else "N/A"
match = "" if semantic == string_sort else ""
print(f" {i+1}: {semantic} vs {string_sort} {match}")
except Exception as e:
print(f" Error querying {package_name}: {e}")
print()
async def test_specific_version_ordering():
"""Test specific version ordering scenarios."""
print("=" * 60)
print("Specific Version Ordering Tests")
print("=" * 60)
# Let's test django which is known to have alpha, beta, rc versions
try:
print("Testing Django version ordering:")
result = await query_package_versions("django")
all_versions = result.get("versions", [])
# Find versions around a specific release to verify ordering
django_4_versions = [v for v in all_versions if v.startswith("4.2")][:15]
print(f" Django 4.2.x versions: {django_4_versions}")
# Check if pre-release versions are properly ordered
pre_release_pattern = ["4.2a1", "4.2b1", "4.2rc1", "4.2.0"]
found_versions = [v for v in django_4_versions if v in pre_release_pattern]
print(f" Found pre-release sequence: {found_versions}")
if len(found_versions) > 1:
print(" Checking pre-release ordering:")
for i in range(len(found_versions) - 1):
current = found_versions[i]
next_ver = found_versions[i + 1]
print(f" {current} comes before {next_ver}")
except Exception as e:
print(f" Error testing Django versions: {e}")
print()
async def main():
"""Main test function."""
print("Real Package Version Sorting Test")
print("="*60)
# Test with real packages
await test_real_package_versions()
# Test specific version ordering scenarios
await test_specific_version_ordering()
print("=" * 60)
print("Real package test completed!")
if __name__ == "__main__":
asyncio.run(main())

60
test_specific_case.py Normal file
View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Test the specific case mentioned in the task: 5.2rc1 vs 5.2.5"""
from pypi_query_mcp.core.version_utils import sort_versions_semantically
def test_specific_case():
"""Test the exact case mentioned in the task requirements."""
print("=" * 60)
print("Testing Specific Task Requirement")
print("=" * 60)
# The exact problem mentioned in the task
versions = ["5.2rc1", "5.2.5"]
# Old way (string sorting)
old_sorted = sorted(versions, reverse=True)
# New way (semantic sorting)
new_sorted = sort_versions_semantically(versions, reverse=True)
print(f"Original versions: {versions}")
print(f"Old string sorting: {old_sorted}")
print(f"New semantic sorting: {new_sorted}")
print()
print("Analysis:")
print(f" Problem: '5.2rc1' was appearing before '5.2.5' in string sorting")
print(f" String sorting result: {old_sorted[0]} comes first")
print(f" Semantic sorting result: {new_sorted[0]} comes first")
print()
if new_sorted == ["5.2.5", "5.2rc1"]:
print(" ✅ SUCCESS: Semantic sorting correctly places 5.2.5 before 5.2rc1")
print(" ✅ This fixes the issue described in the task!")
else:
print(" ❌ FAILED: The issue is not resolved")
print()
# Test a more comprehensive example
comprehensive_test = [
"5.2.5", "5.2rc1", "5.2.0", "5.2a1", "5.2b1",
"5.1.0", "5.3.0", "5.2.1"
]
old_comprehensive = sorted(comprehensive_test, reverse=True)
new_comprehensive = sort_versions_semantically(comprehensive_test, reverse=True)
print("Comprehensive version sorting test:")
print(f" Input: {comprehensive_test}")
print(f" String sorted: {old_comprehensive}")
print(f" Semantic sorted: {new_comprehensive}")
print()
print("Expected semantic order (newest to oldest):")
print(" 5.3.0 > 5.2.5 > 5.2.1 > 5.2.0 > 5.2rc1 > 5.2b1 > 5.2a1 > 5.1.0")
if __name__ == "__main__":
test_specific_case()

164
test_version_sorting.py Normal file
View File

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Test script to verify semantic version sorting functionality."""
import asyncio
import logging
from pypi_query_mcp.core.version_utils import sort_versions_semantically
from pypi_query_mcp.tools.package_query import query_package_versions
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_semantic_version_sorting():
"""Test the semantic version sorting function with various edge cases."""
print("=" * 60)
print("Testing Semantic Version Sorting Function")
print("=" * 60)
# Test case 1: Basic pre-release ordering
test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"]
sorted1 = sort_versions_semantically(test1_versions)
print(f"Test 1 - Pre-release ordering:")
print(f" Input: {test1_versions}")
print(f" Output: {sorted1}")
print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']")
print()
# Test case 2: Complex Django-like versions
test2_versions = [
"4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7",
"4.0.0", "3.2.18", "4.2.1", "4.2.2"
]
sorted2 = sort_versions_semantically(test2_versions)
print(f"Test 2 - Django-like versions:")
print(f" Input: {test2_versions}")
print(f" Output: {sorted2}")
print()
# Test case 3: TensorFlow-like versions with dev builds
test3_versions = [
"2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1",
"2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly
]
sorted3 = sort_versions_semantically(test3_versions)
print(f"Test 3 - TensorFlow-like versions:")
print(f" Input: {test3_versions}")
print(f" Output: {sorted3}")
print()
# Test case 4: Edge cases and malformed versions
test4_versions = [
"1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1",
"1.0.0rc1", "1.0.1", "invalid-version", "1.0"
]
sorted4 = sort_versions_semantically(test4_versions)
print(f"Test 4 - Edge cases and malformed versions:")
print(f" Input: {test4_versions}")
print(f" Output: {sorted4}")
print()
# Test case 5: Empty and single item lists
test5_empty = []
test5_single = ["1.0.0"]
sorted5_empty = sort_versions_semantically(test5_empty)
sorted5_single = sort_versions_semantically(test5_single)
print(f"Test 5 - Edge cases:")
print(f" Empty list: {sorted5_empty}")
print(f" Single item: {sorted5_single}")
print()
async def test_real_package_versions():
"""Test with real PyPI packages that have complex version histories."""
print("=" * 60)
print("Testing Real Package Version Sorting")
print("=" * 60)
# Test packages known to have complex version histories
test_packages = [
"django", # Known for alpha, beta, rc versions
"tensorflow", # Complex versioning with dev builds
"numpy", # Long history with various formats
"requests" # Simple but well-known package
]
for package_name in test_packages:
try:
print(f"\nTesting {package_name}:")
result = await query_package_versions(package_name)
recent_versions = result.get("recent_versions", [])[:10]
print(f" Recent versions (first 10): {recent_versions}")
# Check if versions seem to be properly sorted
if len(recent_versions) >= 3:
print(f" First three versions: {recent_versions[:3]}")
except Exception as e:
print(f" Error querying {package_name}: {e}")
print()
def validate_sorting_correctness():
"""Validate that our sorting meets the requirements."""
print("=" * 60)
print("Validation Tests")
print("=" * 60)
# The specific example from the task: "5.2rc1" should come after "5.2.5"
task_example = ["5.2rc1", "5.2.5"]
sorted_task = sort_versions_semantically(task_example)
print("Task requirement validation:")
print(f" Input: {task_example}")
print(f" Output: {sorted_task}")
print(f" Requirement: '5.2rc1' should come after '5.2.5'")
if sorted_task == ["5.2.5", "5.2rc1"]:
print(" ✅ PASS: Requirement met!")
else:
print(" ❌ FAIL: Requirement not met!")
print()
# Test pre-release ordering: alpha < beta < rc < stable
pre_release_test = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
sorted_pre = sort_versions_semantically(pre_release_test)
print("Pre-release ordering validation:")
print(f" Input: {pre_release_test}")
print(f" Output: {sorted_pre}")
print(f" Expected order: stable > rc > beta > alpha")
expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
if sorted_pre == expected_order:
print(" ✅ PASS: Pre-release ordering correct!")
else:
print(" ❌ FAIL: Pre-release ordering incorrect!")
print()
async def main():
"""Main test function."""
print("Semantic Version Sorting Test Suite")
print("="*60)
# Run unit tests
test_semantic_version_sorting()
# Validate specific requirements
validate_sorting_correctness()
# Test with real packages
await test_real_package_versions()
print("=" * 60)
print("Test suite completed!")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""Standalone test script to verify semantic version sorting functionality."""
import logging
from packaging.version import Version, InvalidVersion
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def sort_versions_semantically(versions: list[str], reverse: bool = True) -> list[str]:
"""Sort package versions using semantic version ordering.
This function properly sorts versions by parsing them as semantic versions,
ensuring that pre-release versions (alpha, beta, rc) are ordered correctly
relative to stable releases.
Args:
versions: List of version strings to sort
reverse: If True, sort in descending order (newest first). Default True.
Returns:
List of version strings sorted semantically
Examples:
>>> sort_versions_semantically(['1.0.0', '2.0.0a1', '1.5.0', '2.0.0'])
['2.0.0', '2.0.0a1', '1.5.0', '1.0.0']
>>> sort_versions_semantically(['5.2rc1', '5.2.5', '5.2.0'])
['5.2.5', '5.2.0', '5.2rc1']
"""
if not versions:
return []
def parse_version_safe(version_str: str) -> tuple[Version | None, str]:
"""Safely parse a version string, returning (parsed_version, original_string).
Returns (None, original_string) if parsing fails.
"""
try:
return (Version(version_str), version_str)
except InvalidVersion:
logger.debug(f"Failed to parse version '{version_str}' as semantic version")
return (None, version_str)
# Parse all versions, keeping track of originals
parsed_versions = [parse_version_safe(v) for v in versions]
# Separate valid and invalid versions
valid_versions = [(v, orig) for v, orig in parsed_versions if v is not None]
invalid_versions = [orig for v, orig in parsed_versions if v is None]
# Sort valid versions semantically
valid_versions.sort(key=lambda x: x[0], reverse=reverse)
# Sort invalid versions lexicographically as fallback
invalid_versions.sort(reverse=reverse)
# Combine results: valid versions first, then invalid ones
result = [orig for _, orig in valid_versions] + invalid_versions
logger.debug(
f"Sorted {len(versions)} versions: {len(valid_versions)} valid, "
f"{len(invalid_versions)} invalid"
)
return result
def test_semantic_version_sorting():
"""Test the semantic version sorting function with various edge cases."""
print("=" * 60)
print("Testing Semantic Version Sorting Function")
print("=" * 60)
# Test case 1: Basic pre-release ordering
test1_versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"]
sorted1 = sort_versions_semantically(test1_versions)
print(f"Test 1 - Pre-release ordering:")
print(f" Input: {test1_versions}")
print(f" Output: {sorted1}")
print(f" Expected: ['5.2.5', '5.2.0', '5.2rc1', '5.2b1', '5.2a1']")
print()
# Test case 2: Complex Django-like versions
test2_versions = [
"4.2.0", "4.2a1", "4.2b1", "4.2rc1", "4.1.0", "4.1.7",
"4.0.0", "3.2.18", "4.2.1", "4.2.2"
]
sorted2 = sort_versions_semantically(test2_versions)
print(f"Test 2 - Django-like versions:")
print(f" Input: {test2_versions}")
print(f" Output: {sorted2}")
print()
# Test case 3: TensorFlow-like versions with dev builds
test3_versions = [
"2.13.0", "2.13.0rc1", "2.13.0rc0", "2.12.0", "2.12.1",
"2.14.0dev20230517", "2.13.0rc2" # This might not parse correctly
]
sorted3 = sort_versions_semantically(test3_versions)
print(f"Test 3 - TensorFlow-like versions:")
print(f" Input: {test3_versions}")
print(f" Output: {sorted3}")
print()
# Test case 4: Edge cases and malformed versions
test4_versions = [
"1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.0a1", "1.0.0b1",
"1.0.0rc1", "1.0.1", "invalid-version", "1.0"
]
sorted4 = sort_versions_semantically(test4_versions)
print(f"Test 4 - Edge cases and malformed versions:")
print(f" Input: {test4_versions}")
print(f" Output: {sorted4}")
print()
# Test case 5: Empty and single item lists
test5_empty = []
test5_single = ["1.0.0"]
sorted5_empty = sort_versions_semantically(test5_empty)
sorted5_single = sort_versions_semantically(test5_single)
print(f"Test 5 - Edge cases:")
print(f" Empty list: {sorted5_empty}")
print(f" Single item: {sorted5_single}")
print()
def validate_sorting_correctness():
"""Validate that our sorting meets the requirements."""
print("=" * 60)
print("Validation Tests")
print("=" * 60)
# The specific example from the task: "5.2rc1" should come after "5.2.5"
task_example = ["5.2rc1", "5.2.5"]
sorted_task = sort_versions_semantically(task_example)
print("Task requirement validation:")
print(f" Input: {task_example}")
print(f" Output: {sorted_task}")
print(f" Requirement: '5.2rc1' should come after '5.2.5'")
if sorted_task == ["5.2.5", "5.2rc1"]:
print(" ✅ PASS: Requirement met!")
else:
print(" ❌ FAIL: Requirement not met!")
print()
# Test pre-release ordering: alpha < beta < rc < stable
pre_release_test = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
sorted_pre = sort_versions_semantically(pre_release_test)
print("Pre-release ordering validation:")
print(f" Input: {pre_release_test}")
print(f" Output: {sorted_pre}")
print(f" Expected order: stable > rc > beta > alpha")
expected_order = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
if sorted_pre == expected_order:
print(" ✅ PASS: Pre-release ordering correct!")
else:
print(" ❌ FAIL: Pre-release ordering incorrect!")
print()
def test_version_comparison_details():
"""Test detailed version comparison to understand packaging behavior."""
print("=" * 60)
print("Version Comparison Details")
print("=" * 60)
test_versions = [
("1.0.0", "1.0.0a1"),
("1.0.0", "1.0.0b1"),
("1.0.0", "1.0.0rc1"),
("1.0.0rc1", "1.0.0b1"),
("1.0.0b1", "1.0.0a1"),
("5.2.5", "5.2rc1"),
("5.2.0", "5.2rc1"),
("1.0.0.post1", "1.0.0"),
("1.0.0.dev0", "1.0.0"),
]
for v1, v2 in test_versions:
try:
ver1 = Version(v1)
ver2 = Version(v2)
comparison = ">" if ver1 > ver2 else "<" if ver1 < ver2 else "="
print(f" {v1} {comparison} {v2}")
except Exception as e:
print(f" Error comparing {v1} and {v2}: {e}")
print()
def main():
"""Main test function."""
print("Semantic Version Sorting Test Suite")
print("="*60)
# Run unit tests
test_semantic_version_sorting()
# Validate specific requirements
validate_sorting_correctness()
# Show detailed version comparisons
test_version_comparison_details()
print("=" * 60)
print("Test suite completed!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,87 @@
"""Tests for semantic version sorting functionality."""
import pytest
from pypi_query_mcp.core.version_utils import sort_versions_semantically
class TestSemanticVersionSorting:
"""Test semantic version sorting function."""
def test_basic_version_sorting(self):
"""Test basic version sorting with stable versions."""
versions = ["1.0.0", "2.0.0", "1.5.0", "1.0.1"]
expected = ["2.0.0", "1.5.0", "1.0.1", "1.0.0"]
result = sort_versions_semantically(versions, reverse=True)
assert result == expected
def test_pre_release_ordering(self):
"""Test that pre-release versions are ordered correctly."""
versions = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
expected = ["1.0.0", "1.0.0rc1", "1.0.0b1", "1.0.0a1"]
result = sort_versions_semantically(versions, reverse=True)
assert result == expected
def test_task_requirement(self):
"""Test the specific requirement from the task: 5.2rc1 vs 5.2.5."""
versions = ["5.2rc1", "5.2.5"]
expected = ["5.2.5", "5.2rc1"]
result = sort_versions_semantically(versions, reverse=True)
assert result == expected
def test_complex_pre_release_scenario(self):
"""Test complex pre-release scenario with multiple types."""
versions = ["5.2rc1", "5.2.5", "5.2.0", "5.2a1", "5.2b1"]
expected = ["5.2.5", "5.2.0", "5.2rc1", "5.2b1", "5.2a1"]
result = sort_versions_semantically(versions, reverse=True)
assert result == expected
def test_dev_and_post_versions(self):
"""Test development and post-release versions."""
versions = ["1.0.0", "1.0.0.post1", "1.0.0.dev0", "1.0.1"]
result = sort_versions_semantically(versions, reverse=True)
# 1.0.1 should be first, then 1.0.0.post1, then 1.0.0, then 1.0.0.dev0
assert result[0] == "1.0.1"
assert result[1] == "1.0.0.post1"
assert result[2] == "1.0.0"
assert result[3] == "1.0.0.dev0"
def test_invalid_versions_fallback(self):
"""Test that invalid versions fall back to string sorting."""
versions = ["1.0.0", "invalid-version", "another-invalid", "2.0.0"]
result = sort_versions_semantically(versions, reverse=True)
# Valid versions should come first
assert result[0] == "2.0.0"
assert result[1] == "1.0.0"
# Invalid versions should be at the end, string-sorted
assert "invalid-version" in result[2:]
assert "another-invalid" in result[2:]
def test_empty_list(self):
"""Test that empty list returns empty list."""
result = sort_versions_semantically([])
assert result == []
def test_single_version(self):
"""Test that single version returns single version."""
result = sort_versions_semantically(["1.0.0"])
assert result == ["1.0.0"]
def test_ascending_order(self):
"""Test sorting in ascending order."""
versions = ["2.0.0", "1.0.0", "1.5.0"]
expected = ["1.0.0", "1.5.0", "2.0.0"]
result = sort_versions_semantically(versions, reverse=False)
assert result == expected
def test_mixed_version_formats(self):
"""Test sorting with mixed version formats."""
versions = ["1.0", "1.0.0", "1.0.1", "v1.0.2"] # v1.0.2 might be invalid
result = sort_versions_semantically(versions, reverse=True)
# Should handle mixed formats gracefully
assert len(result) == 4
assert "1.0.1" in result
assert "1.0.0" in result
assert "1.0" in result