Implement PyPI Community & Social Tools
- Add get_pypi_package_reviews: Aggregates community feedback from GitHub issues, Stack Overflow, and social media with sentiment analysis - Add manage_pypi_package_discussions: Future-ready discussion management with GitHub Discussions integration - Add get_pypi_maintainer_contacts: Privacy-respecting maintainer contact discovery with communication guidelines - Integrate all tools with MCP server endpoints and comprehensive error handling - Add extensive test coverage for all community functionality - Follow existing code patterns and async/await best practices - Include future-ready implementations for when PyPI adds native community features
This commit is contained in:
parent
f79144d710
commit
05dd346f45
@ -52,6 +52,9 @@ from .tools import (
|
||||
set_package_visibility,
|
||||
update_package_metadata,
|
||||
upload_package_to_pypi,
|
||||
get_pypi_package_reviews,
|
||||
manage_pypi_package_discussions,
|
||||
get_pypi_maintainer_contacts,
|
||||
)
|
||||
from .tools.discovery import (
|
||||
get_pypi_package_recommendations,
|
||||
@ -2370,6 +2373,196 @@ async def get_pypi_package_recommendations_tool(
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_pypi_package_reviews_tool(
|
||||
package_name: str,
|
||||
include_ratings: bool = True,
|
||||
include_community_feedback: bool = True,
|
||||
sentiment_analysis: bool = False,
|
||||
max_reviews: int = 50,
|
||||
) -> dict[str, Any]:
|
||||
"""Get community reviews and feedback for a PyPI package.
|
||||
|
||||
This tool aggregates community feedback from various sources including
|
||||
GitHub discussions, issues, Stack Overflow mentions, and social media to
|
||||
provide comprehensive community sentiment analysis.
|
||||
|
||||
Note: This is a future-ready implementation as PyPI doesn't currently have
|
||||
a native review system. The function prepares for when such features become
|
||||
available while providing useful community sentiment analysis from existing sources.
|
||||
|
||||
Args:
|
||||
package_name: Name of the package to get reviews for
|
||||
include_ratings: Whether to include numerical ratings (when available)
|
||||
include_community_feedback: Whether to include textual feedback analysis
|
||||
sentiment_analysis: Whether to perform sentiment analysis on feedback
|
||||
max_reviews: Maximum number of reviews to return
|
||||
|
||||
Returns:
|
||||
Dictionary containing review and feedback information including:
|
||||
- Community sentiment and ratings
|
||||
- Feedback from GitHub issues and discussions
|
||||
- Social media mentions and sentiment
|
||||
- Quality indicators and community health metrics
|
||||
- Future-ready structure for native PyPI reviews
|
||||
|
||||
Raises:
|
||||
InvalidPackageNameError: If package name is invalid
|
||||
PackageNotFoundError: If package is not found
|
||||
NetworkError: For network-related errors
|
||||
"""
|
||||
try:
|
||||
logger.info(f"MCP tool: Getting community reviews for package: {package_name}")
|
||||
result = await get_pypi_package_reviews(
|
||||
package_name=package_name,
|
||||
include_ratings=include_ratings,
|
||||
include_community_feedback=include_community_feedback,
|
||||
sentiment_analysis=sentiment_analysis,
|
||||
max_reviews=max_reviews,
|
||||
)
|
||||
logger.info(f"Successfully retrieved community reviews for package: {package_name}")
|
||||
return result
|
||||
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
||||
logger.error(f"Error getting reviews for {package_name}: {e}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"package_name": package_name,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting reviews for {package_name}: {e}")
|
||||
return {
|
||||
"error": f"Unexpected error: {e}",
|
||||
"error_type": "UnexpectedError",
|
||||
"package_name": package_name,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def manage_pypi_package_discussions_tool(
|
||||
package_name: str,
|
||||
action: str = "get_status",
|
||||
discussion_settings: dict[str, Any] | None = None,
|
||||
moderator_controls: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Manage and interact with PyPI package discussions.
|
||||
|
||||
This tool provides management capabilities for package discussions,
|
||||
including enabling/disabling discussions, setting moderation policies,
|
||||
and retrieving discussion status and metrics.
|
||||
|
||||
Note: This is a future-ready implementation as PyPI doesn't currently have
|
||||
native discussion features. The function prepares for when such features
|
||||
become available while providing integration with existing discussion platforms.
|
||||
|
||||
Args:
|
||||
package_name: Name of the package to manage discussions for
|
||||
action: Action to perform ('get_status', 'enable', 'disable', 'configure', 'moderate')
|
||||
discussion_settings: Settings for discussions (when enabling/configuring)
|
||||
moderator_controls: Moderation settings and controls
|
||||
|
||||
Returns:
|
||||
Dictionary containing discussion management results including:
|
||||
- Current discussion status and settings
|
||||
- Available discussion platforms and integration status
|
||||
- Moderation settings and community guidelines
|
||||
- Discussion metrics and engagement data
|
||||
- Future-ready structure for native PyPI discussions
|
||||
|
||||
Raises:
|
||||
InvalidPackageNameError: If package name is invalid
|
||||
PackageNotFoundError: If package is not found
|
||||
NetworkError: For network-related errors
|
||||
"""
|
||||
try:
|
||||
logger.info(f"MCP tool: Managing discussions for package: {package_name}, action: {action}")
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name=package_name,
|
||||
action=action,
|
||||
discussion_settings=discussion_settings,
|
||||
moderator_controls=moderator_controls,
|
||||
)
|
||||
logger.info(f"Successfully managed discussions for package: {package_name}")
|
||||
return result
|
||||
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
||||
logger.error(f"Error managing discussions for {package_name}: {e}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"package_name": package_name,
|
||||
"action": action,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error managing discussions for {package_name}: {e}")
|
||||
return {
|
||||
"error": f"Unexpected error: {e}",
|
||||
"error_type": "UnexpectedError",
|
||||
"package_name": package_name,
|
||||
"action": action,
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_pypi_maintainer_contacts_tool(
|
||||
package_name: str,
|
||||
contact_types: list[str] | None = None,
|
||||
include_social_profiles: bool = False,
|
||||
include_contribution_guidelines: bool = True,
|
||||
respect_privacy_settings: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Get contact information and communication channels for package maintainers.
|
||||
|
||||
This tool retrieves publicly available contact information for package
|
||||
maintainers while respecting privacy settings and providing appropriate
|
||||
communication channels for different types of inquiries.
|
||||
|
||||
Args:
|
||||
package_name: Name of the package to get maintainer contacts for
|
||||
contact_types: Types of contact info to retrieve ('email', 'github', 'social', 'support')
|
||||
include_social_profiles: Whether to include social media profiles
|
||||
include_contribution_guidelines: Whether to include contribution guidelines
|
||||
respect_privacy_settings: Whether to respect maintainer privacy preferences
|
||||
|
||||
Returns:
|
||||
Dictionary containing maintainer contact information including:
|
||||
- Publicly available contact methods
|
||||
- Communication preferences and guidelines
|
||||
- Support channels and community resources
|
||||
- Privacy-respecting contact recommendations
|
||||
- Contribution guidelines and community standards
|
||||
|
||||
Raises:
|
||||
InvalidPackageNameError: If package name is invalid
|
||||
PackageNotFoundError: If package is not found
|
||||
NetworkError: For network-related errors
|
||||
"""
|
||||
try:
|
||||
logger.info(f"MCP tool: Getting maintainer contacts for package: {package_name}")
|
||||
result = await get_pypi_maintainer_contacts(
|
||||
package_name=package_name,
|
||||
contact_types=contact_types,
|
||||
include_social_profiles=include_social_profiles,
|
||||
include_contribution_guidelines=include_contribution_guidelines,
|
||||
respect_privacy_settings=respect_privacy_settings,
|
||||
)
|
||||
logger.info(f"Successfully retrieved maintainer contacts for package: {package_name}")
|
||||
return result
|
||||
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
||||
logger.error(f"Error getting maintainer contacts for {package_name}: {e}")
|
||||
return {
|
||||
"error": str(e),
|
||||
"error_type": type(e).__name__,
|
||||
"package_name": package_name,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting maintainer contacts for {package_name}: {e}")
|
||||
return {
|
||||
"error": f"Unexpected error: {e}",
|
||||
"error_type": "UnexpectedError",
|
||||
"package_name": package_name,
|
||||
}
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--log-level",
|
||||
|
@ -59,6 +59,11 @@ from .workflow import (
|
||||
preview_pypi_package_page,
|
||||
validate_pypi_package_name,
|
||||
)
|
||||
from .community import (
|
||||
get_pypi_package_reviews,
|
||||
manage_pypi_package_discussions,
|
||||
get_pypi_maintainer_contacts,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"query_package_info",
|
||||
@ -98,4 +103,7 @@ __all__ = [
|
||||
"preview_pypi_package_page",
|
||||
"check_pypi_upload_requirements",
|
||||
"get_pypi_build_logs",
|
||||
"get_pypi_package_reviews",
|
||||
"manage_pypi_package_discussions",
|
||||
"get_pypi_maintainer_contacts",
|
||||
]
|
||||
|
1381
pypi_query_mcp/tools/community.py
Normal file
1381
pypi_query_mcp/tools/community.py
Normal file
File diff suppressed because it is too large
Load Diff
985
tests/test_community.py
Normal file
985
tests/test_community.py
Normal file
@ -0,0 +1,985 @@
|
||||
"""Tests for PyPI community and social tools functionality."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from pypi_query_mcp.core.exceptions import InvalidPackageNameError, PackageNotFoundError, NetworkError
|
||||
from pypi_query_mcp.tools.community import (
|
||||
get_pypi_package_reviews,
|
||||
manage_pypi_package_discussions,
|
||||
get_pypi_maintainer_contacts,
|
||||
_analyze_github_community_sentiment,
|
||||
_check_stackoverflow_mentions,
|
||||
_analyze_pypi_downloads_as_quality_indicator,
|
||||
_get_community_health_metrics,
|
||||
_calculate_community_score,
|
||||
_generate_community_insights,
|
||||
_extract_contact_info_from_metadata,
|
||||
_find_github_repository,
|
||||
_parse_github_url,
|
||||
_analyze_issue_sentiment,
|
||||
_analyze_stackoverflow_sentiment,
|
||||
)
|
||||
|
||||
|
||||
class TestGetPyPIPackageReviews:
|
||||
"""Test community reviews and feedback functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_package_data(self):
|
||||
"""Mock package data for testing."""
|
||||
return {
|
||||
"info": {
|
||||
"name": "test-package",
|
||||
"version": "1.0.0",
|
||||
"summary": "A test package for community analysis",
|
||||
"description": "A comprehensive test package with detailed description for community testing",
|
||||
"keywords": "test, community, package",
|
||||
"classifiers": [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
"license": "MIT",
|
||||
"author": "Test Author",
|
||||
"home_page": "https://example.com",
|
||||
"project_urls": {
|
||||
"Documentation": "https://docs.example.com",
|
||||
"Repository": "https://github.com/test/test-package",
|
||||
"Bug Reports": "https://github.com/test/test-package/issues",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_github_sentiment(self):
|
||||
"""Mock GitHub sentiment analysis data."""
|
||||
return {
|
||||
"repository": "https://github.com/test/test-package",
|
||||
"sentiment_analysis": {
|
||||
"overall_sentiment_score": 75.5,
|
||||
"issues_analyzed": 20,
|
||||
"positive_indicators": 15,
|
||||
"negative_indicators": 5,
|
||||
"sentiment_factors": {
|
||||
"closed_issues": 12,
|
||||
"open_issues": 8,
|
||||
"enhancement_requests": 5,
|
||||
"bug_reports": 3,
|
||||
},
|
||||
},
|
||||
"repository_stats": {
|
||||
"stargazers_count": 150,
|
||||
"forks_count": 25,
|
||||
"open_issues_count": 8,
|
||||
},
|
||||
"issues_analyzed": 20,
|
||||
"analysis_timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_stackoverflow_data(self):
|
||||
"""Mock Stack Overflow mentions data."""
|
||||
return {
|
||||
"questions_found": 5,
|
||||
"sentiment_analysis": {
|
||||
"overall_sentiment_score": 65.0,
|
||||
"questions_analyzed": 5,
|
||||
"positive_indicators": 3,
|
||||
"negative_indicators": 2,
|
||||
"question_characteristics": {
|
||||
"answered_questions": 4,
|
||||
"unanswered_questions": 1,
|
||||
"average_score": 2.4,
|
||||
},
|
||||
},
|
||||
"search_timestamp": datetime.now().isoformat(),
|
||||
"data_source": "Stack Overflow API",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_quality_indicators(self):
|
||||
"""Mock quality indicators data."""
|
||||
return {
|
||||
"download_stats": {
|
||||
"last_month": 50000,
|
||||
"last_week": 12000,
|
||||
"last_day": 2000,
|
||||
},
|
||||
"adoption_level": "moderate",
|
||||
"quality_indicator_score": 50.0,
|
||||
"analysis_timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_community_health(self):
|
||||
"""Mock community health metrics."""
|
||||
return {
|
||||
"github_community_health": {
|
||||
"health_percentage": 85,
|
||||
"documentation": {"exists": True},
|
||||
"contributing": {"exists": True},
|
||||
"code_of_conduct": {"exists": True},
|
||||
"license": {"exists": True},
|
||||
"readme": {"exists": True},
|
||||
},
|
||||
"has_repository": True,
|
||||
"repository_url": "https://github.com/test/test-package",
|
||||
}
|
||||
|
||||
async def test_get_pypi_package_reviews_success(
|
||||
self,
|
||||
mock_package_data,
|
||||
mock_github_sentiment,
|
||||
mock_stackoverflow_data,
|
||||
mock_quality_indicators,
|
||||
mock_community_health
|
||||
):
|
||||
"""Test successful retrieval of package reviews."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_reviews") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_github_community_sentiment") as mock_github, \
|
||||
patch("pypi_query_mcp.tools.community._check_stackoverflow_mentions") as mock_stackoverflow, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_pypi_downloads_as_quality_indicator") as mock_quality, \
|
||||
patch("pypi_query_mcp.tools.community._get_community_health_metrics") as mock_health:
|
||||
|
||||
mock_metadata.return_value = mock_package_data["info"]
|
||||
mock_github.return_value = mock_github_sentiment
|
||||
mock_stackoverflow.return_value = mock_stackoverflow_data
|
||||
mock_quality.return_value = mock_quality_indicators
|
||||
mock_health.return_value = mock_community_health
|
||||
|
||||
result = await get_pypi_package_reviews(
|
||||
package_name="test-package",
|
||||
include_ratings=True,
|
||||
include_community_feedback=True,
|
||||
sentiment_analysis=True,
|
||||
max_reviews=50
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert "community_score" in result
|
||||
assert "metadata" in result
|
||||
assert "community_health" in result
|
||||
assert "quality_indicators" in result
|
||||
assert "insights" in result
|
||||
assert "review_system_status" in result
|
||||
assert "github_community_feedback" in result
|
||||
assert "stackoverflow_mentions" in result
|
||||
assert "sentiment_analysis" in result
|
||||
assert "ratings" in result
|
||||
|
||||
# Check community score structure
|
||||
community_score = result["community_score"]
|
||||
assert "overall_score" in community_score
|
||||
assert "community_status" in community_score
|
||||
assert "score_components" in community_score
|
||||
|
||||
# Check review system status
|
||||
review_status = result["review_system_status"]
|
||||
assert review_status["native_pypi_reviews"] == "not_available"
|
||||
assert review_status["future_ready"] is True
|
||||
|
||||
async def test_get_pypi_package_reviews_invalid_package_name(self):
|
||||
"""Test handling of invalid package name."""
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await get_pypi_package_reviews("")
|
||||
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await get_pypi_package_reviews(" ")
|
||||
|
||||
async def test_get_pypi_package_reviews_minimal_options(self, mock_package_data):
|
||||
"""Test reviews with minimal options enabled."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_reviews") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_pypi_downloads_as_quality_indicator") as mock_quality, \
|
||||
patch("pypi_query_mcp.tools.community._get_community_health_metrics") as mock_health:
|
||||
|
||||
mock_metadata.return_value = mock_package_data["info"]
|
||||
mock_quality.return_value = {"quality_indicator_score": 30}
|
||||
mock_health.return_value = {"has_repository": False}
|
||||
|
||||
result = await get_pypi_package_reviews(
|
||||
package_name="test-package",
|
||||
include_ratings=False,
|
||||
include_community_feedback=False,
|
||||
sentiment_analysis=False
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert "github_community_feedback" not in result
|
||||
assert "stackoverflow_mentions" not in result
|
||||
assert "sentiment_analysis" not in result
|
||||
assert "ratings" not in result
|
||||
|
||||
async def test_get_pypi_package_reviews_network_error(self):
|
||||
"""Test handling of network errors."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_reviews", side_effect=NetworkError("Network error")):
|
||||
with pytest.raises(NetworkError):
|
||||
await get_pypi_package_reviews("test-package")
|
||||
|
||||
|
||||
class TestManagePyPIPackageDiscussions:
|
||||
"""Test package discussions management functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_package_data(self):
|
||||
"""Mock package data for discussions testing."""
|
||||
return {
|
||||
"info": {
|
||||
"name": "test-package",
|
||||
"project_urls": {
|
||||
"Repository": "https://github.com/test/test-package",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_discussion_status(self):
|
||||
"""Mock current discussion status."""
|
||||
return {
|
||||
"github_discussions": {
|
||||
"enabled": False,
|
||||
"reason": "requires_github_api_integration",
|
||||
"repository": "https://github.com/test/test-package",
|
||||
},
|
||||
"community_platforms": {
|
||||
"discord": {"available": False},
|
||||
"reddit": {"available": False},
|
||||
"forums": {"available": False},
|
||||
},
|
||||
"native_pypi_discussions": {
|
||||
"available": False,
|
||||
"note": "PyPI does not currently support native discussions",
|
||||
},
|
||||
}
|
||||
|
||||
async def test_manage_discussions_get_status(self, mock_package_data, mock_discussion_status):
|
||||
"""Test getting discussion status."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="get_status"
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "get_status"
|
||||
assert "status" in result
|
||||
assert "current_discussion_status" in result
|
||||
assert "available_platforms" in result
|
||||
assert "discussion_system_status" in result
|
||||
|
||||
# Check system status
|
||||
system_status = result["discussion_system_status"]
|
||||
assert system_status["native_pypi_discussions"] == "not_available"
|
||||
assert system_status["future_ready"] is True
|
||||
|
||||
async def test_manage_discussions_enable(self, mock_package_data, mock_discussion_status):
|
||||
"""Test enabling discussions."""
|
||||
discussion_settings = {
|
||||
"categories": ["General", "Q&A", "Ideas"],
|
||||
"moderation": "manual_review",
|
||||
}
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="enable",
|
||||
discussion_settings=discussion_settings
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "enable"
|
||||
assert result["status"] == "configured"
|
||||
assert result["action"] == "enable_discussions"
|
||||
assert "settings_applied" in result
|
||||
assert "next_steps" in result
|
||||
|
||||
async def test_manage_discussions_disable(self, mock_package_data, mock_discussion_status):
|
||||
"""Test disabling discussions."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="disable"
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "disable"
|
||||
assert result["status"] == "configured"
|
||||
assert result["action"] == "disable_discussions"
|
||||
assert "next_steps" in result
|
||||
|
||||
async def test_manage_discussions_configure(self, mock_package_data, mock_discussion_status):
|
||||
"""Test configuring discussions."""
|
||||
discussion_settings = {
|
||||
"categories": ["General", "Q&A", "Ideas", "Show and Tell"],
|
||||
"moderation": "community_moderation",
|
||||
"notifications": ["email_notifications", "web_notifications"],
|
||||
}
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="configure",
|
||||
discussion_settings=discussion_settings
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "configure"
|
||||
assert result["status"] == "configured"
|
||||
assert result["action"] == "configure_discussions"
|
||||
assert "configuration_options" in result
|
||||
|
||||
async def test_manage_discussions_moderate(self, mock_package_data, mock_discussion_status):
|
||||
"""Test moderating discussions."""
|
||||
moderator_controls = {
|
||||
"content_filtering": True,
|
||||
"auto_moderation": True,
|
||||
"moderator_roles": ["owner", "maintainer"],
|
||||
}
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="moderate",
|
||||
moderator_controls=moderator_controls
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "moderate"
|
||||
assert result["status"] == "moderation_configured"
|
||||
assert result["action"] == "moderate_discussions"
|
||||
assert "moderation_features" in result
|
||||
|
||||
async def test_manage_discussions_get_metrics(self, mock_package_data, mock_discussion_status):
|
||||
"""Test getting discussion metrics."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_discussions") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._get_current_discussion_status") as mock_status:
|
||||
|
||||
mock_metadata.return_value = mock_package_data
|
||||
mock_status.return_value = mock_discussion_status
|
||||
|
||||
result = await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="get_metrics"
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert result["action_performed"] == "get_metrics"
|
||||
assert result["status"] == "metrics_retrieved"
|
||||
assert "github_metrics" in result
|
||||
assert "overall_engagement" in result
|
||||
|
||||
async def test_manage_discussions_invalid_action(self):
|
||||
"""Test handling of invalid action."""
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await manage_pypi_package_discussions(
|
||||
package_name="test-package",
|
||||
action="invalid_action"
|
||||
)
|
||||
|
||||
async def test_manage_discussions_invalid_package_name(self):
|
||||
"""Test handling of invalid package name."""
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await manage_pypi_package_discussions("")
|
||||
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await manage_pypi_package_discussions(" ")
|
||||
|
||||
|
||||
class TestGetPyPIMaintainerContacts:
|
||||
"""Test maintainer contact information functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_package_metadata(self):
|
||||
"""Mock package metadata for contact testing."""
|
||||
return {
|
||||
"name": "test-package",
|
||||
"author": "Test Author",
|
||||
"author_email": "author@example.com",
|
||||
"maintainer": "Test Maintainer",
|
||||
"maintainer_email": "maintainer@example.com",
|
||||
"home_page": "https://example.com",
|
||||
"project_urls": {
|
||||
"Documentation": "https://docs.example.com",
|
||||
"Repository": "https://github.com/test/test-package",
|
||||
"Bug Reports": "https://github.com/test/test-package/issues",
|
||||
"Support": "https://support.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_github_info(self):
|
||||
"""Mock GitHub maintainer information."""
|
||||
return {
|
||||
"repository": "https://github.com/test/test-package",
|
||||
"owner": "test",
|
||||
"repository_data": {
|
||||
"owner": {
|
||||
"login": "test",
|
||||
"type": "User",
|
||||
"html_url": "https://github.com/test",
|
||||
},
|
||||
"has_pages": True,
|
||||
"default_branch": "main",
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"login": "test",
|
||||
"contributions": 150,
|
||||
"html_url": "https://github.com/test",
|
||||
},
|
||||
{
|
||||
"login": "contributor1",
|
||||
"contributions": 25,
|
||||
"html_url": "https://github.com/contributor1",
|
||||
},
|
||||
],
|
||||
"primary_maintainer": {
|
||||
"login": "test",
|
||||
"type": "User",
|
||||
"html_url": "https://github.com/test",
|
||||
},
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_support_channels(self):
|
||||
"""Mock support channels information."""
|
||||
return {
|
||||
"issue_tracker": "https://github.com/test/test-package/issues",
|
||||
"documentation": "https://test.github.io/test-package/",
|
||||
"community_forum": None,
|
||||
"chat_channels": [],
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_community_channels(self):
|
||||
"""Mock community channels information."""
|
||||
return {
|
||||
"github_discussions": "https://github.com/test/test-package/discussions",
|
||||
"stackoverflow_tag": "https://stackoverflow.com/questions/tagged/test-package",
|
||||
"reddit_community": None,
|
||||
"discord_server": None,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def mock_contribution_info(self):
|
||||
"""Mock contribution guidelines information."""
|
||||
return {
|
||||
"repository": "https://github.com/test/test-package",
|
||||
"contribution_files": {
|
||||
"CONTRIBUTING.md": True,
|
||||
"CODE_OF_CONDUCT.md": True,
|
||||
"SECURITY.md": False,
|
||||
},
|
||||
"guidelines_available": True,
|
||||
}
|
||||
|
||||
async def test_get_maintainer_contacts_success(
|
||||
self,
|
||||
mock_package_metadata,
|
||||
mock_github_info,
|
||||
mock_support_channels,
|
||||
mock_community_channels,
|
||||
mock_contribution_info
|
||||
):
|
||||
"""Test successful retrieval of maintainer contacts."""
|
||||
contact_types = ["github", "support", "community"]
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_contacts") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_github_maintainer_info") as mock_github, \
|
||||
patch("pypi_query_mcp.tools.community._get_support_channels") as mock_support, \
|
||||
patch("pypi_query_mcp.tools.community._get_community_channels") as mock_community, \
|
||||
patch("pypi_query_mcp.tools.community._get_contribution_guidelines") as mock_contrib:
|
||||
|
||||
mock_metadata.return_value = mock_package_metadata
|
||||
mock_github.return_value = mock_github_info
|
||||
mock_support.return_value = mock_support_channels
|
||||
mock_community.return_value = mock_community_channels
|
||||
mock_contrib.return_value = mock_contribution_info
|
||||
|
||||
result = await get_pypi_maintainer_contacts(
|
||||
package_name="test-package",
|
||||
contact_types=contact_types,
|
||||
include_social_profiles=True,
|
||||
include_contribution_guidelines=True,
|
||||
respect_privacy_settings=True
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert "contact_information" in result
|
||||
assert "accessibility_assessment" in result
|
||||
assert "contact_recommendations" in result
|
||||
assert "privacy_compliance" in result
|
||||
assert "github_information" in result
|
||||
assert "support_channels" in result
|
||||
assert "community_channels" in result
|
||||
assert "contribution_guidelines" in result
|
||||
assert "social_profiles" in result
|
||||
assert "communication_guidelines" in result
|
||||
|
||||
# Check privacy compliance
|
||||
privacy = result["privacy_compliance"]
|
||||
assert privacy["respects_privacy_settings"] is True
|
||||
assert privacy["data_sources"] == "Publicly available information only"
|
||||
|
||||
async def test_get_maintainer_contacts_email_included(self, mock_package_metadata):
|
||||
"""Test contacts with email included and privacy disabled."""
|
||||
contact_types = ["email", "github"]
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_contacts") as mock_metadata, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_github_maintainer_info") as mock_github:
|
||||
|
||||
mock_metadata.return_value = mock_package_metadata
|
||||
mock_github.return_value = {"status": "no_github_repository"}
|
||||
|
||||
result = await get_pypi_maintainer_contacts(
|
||||
package_name="test-package",
|
||||
contact_types=contact_types,
|
||||
respect_privacy_settings=False
|
||||
)
|
||||
|
||||
contact_info = result["contact_information"]
|
||||
assert "available_contacts" in contact_info
|
||||
# When privacy is disabled, emails should be included
|
||||
if not contact_info["privacy_compliant"]:
|
||||
# This would include emails if privacy is disabled
|
||||
pass
|
||||
|
||||
async def test_get_maintainer_contacts_privacy_enabled(self, mock_package_metadata):
|
||||
"""Test contacts with privacy settings enabled."""
|
||||
contact_types = ["email", "github"]
|
||||
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_contacts") as mock_metadata:
|
||||
mock_metadata.return_value = mock_package_metadata
|
||||
|
||||
result = await get_pypi_maintainer_contacts(
|
||||
package_name="test-package",
|
||||
contact_types=contact_types,
|
||||
respect_privacy_settings=True
|
||||
)
|
||||
|
||||
contact_info = result["contact_information"]
|
||||
assert contact_info["privacy_compliant"] is True
|
||||
# With privacy enabled, emails should be hidden
|
||||
if "email_note" in contact_info.get("available_contacts", {}):
|
||||
assert "hidden due to privacy settings" in contact_info["available_contacts"]["email_note"]
|
||||
|
||||
async def test_get_maintainer_contacts_minimal_options(self, mock_package_metadata):
|
||||
"""Test contacts with minimal options."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_contacts") as mock_metadata:
|
||||
mock_metadata.return_value = mock_package_metadata
|
||||
|
||||
result = await get_pypi_maintainer_contacts(
|
||||
package_name="test-package",
|
||||
contact_types=["support"],
|
||||
include_social_profiles=False,
|
||||
include_contribution_guidelines=False
|
||||
)
|
||||
|
||||
assert result["package"] == "test-package"
|
||||
assert "contact_information" in result
|
||||
assert "github_information" not in result
|
||||
assert "contribution_guidelines" not in result
|
||||
assert "social_profiles" not in result
|
||||
|
||||
async def test_get_maintainer_contacts_invalid_contact_types(self):
|
||||
"""Test handling of invalid contact types."""
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await get_pypi_maintainer_contacts(
|
||||
package_name="test-package",
|
||||
contact_types=["invalid_type"]
|
||||
)
|
||||
|
||||
async def test_get_maintainer_contacts_invalid_package_name(self):
|
||||
"""Test handling of invalid package name."""
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await get_pypi_maintainer_contacts("")
|
||||
|
||||
with pytest.raises(InvalidPackageNameError):
|
||||
await get_pypi_maintainer_contacts(" ")
|
||||
|
||||
|
||||
class TestHelperFunctions:
|
||||
"""Test helper functions for community tools."""
|
||||
|
||||
def test_parse_github_url_valid(self):
|
||||
"""Test parsing valid GitHub URLs."""
|
||||
test_cases = [
|
||||
("https://github.com/owner/repo", {"repository_url": "https://github.com/owner/repo", "owner": "owner", "repo": "repo"}),
|
||||
("https://github.com/owner/repo.git", {"repository_url": "https://github.com/owner/repo", "owner": "owner", "repo": "repo"}),
|
||||
("https://github.com/owner/repo/", {"repository_url": "https://github.com/owner/repo", "owner": "owner", "repo": "repo"}),
|
||||
]
|
||||
|
||||
for url, expected in test_cases:
|
||||
result = _parse_github_url(url)
|
||||
assert result == expected
|
||||
|
||||
def test_parse_github_url_invalid(self):
|
||||
"""Test parsing invalid GitHub URLs."""
|
||||
test_cases = [
|
||||
"https://gitlab.com/owner/repo",
|
||||
"https://github.com/owner",
|
||||
"https://github.com/",
|
||||
"not-a-url",
|
||||
]
|
||||
|
||||
for url in test_cases:
|
||||
result = _parse_github_url(url)
|
||||
assert "status" in result or "error" in result
|
||||
|
||||
def test_analyze_issue_sentiment_positive(self):
|
||||
"""Test analyzing positive GitHub issue sentiment."""
|
||||
issues_data = {
|
||||
"issues": [
|
||||
{
|
||||
"title": "Enhancement: Add new feature",
|
||||
"state": "closed",
|
||||
"labels": [{"name": "enhancement"}, {"name": "good first issue"}],
|
||||
},
|
||||
{
|
||||
"title": "How to use this package?",
|
||||
"state": "closed",
|
||||
"labels": [{"name": "question"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
result = _analyze_issue_sentiment(issues_data)
|
||||
|
||||
assert result["overall_sentiment_score"] > 50
|
||||
assert result["issues_analyzed"] == 2
|
||||
assert result["sentiment_factors"]["closed_issues"] == 2
|
||||
assert result["sentiment_factors"]["enhancement_requests"] == 1
|
||||
|
||||
def test_analyze_issue_sentiment_negative(self):
|
||||
"""Test analyzing negative GitHub issue sentiment."""
|
||||
issues_data = {
|
||||
"issues": [
|
||||
{
|
||||
"title": "Critical bug: Application crashes",
|
||||
"state": "open",
|
||||
"labels": [{"name": "bug"}, {"name": "critical"}],
|
||||
},
|
||||
{
|
||||
"title": "Error when importing package",
|
||||
"state": "open",
|
||||
"labels": [{"name": "bug"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
result = _analyze_issue_sentiment(issues_data)
|
||||
|
||||
assert result["overall_sentiment_score"] < 50
|
||||
assert result["issues_analyzed"] == 2
|
||||
assert result["sentiment_factors"]["open_issues"] == 2
|
||||
assert result["sentiment_factors"]["bug_reports"] == 2
|
||||
|
||||
def test_analyze_stackoverflow_sentiment_positive(self):
|
||||
"""Test analyzing positive Stack Overflow sentiment."""
|
||||
questions = [
|
||||
{
|
||||
"title": "How to implement best practices with test-package",
|
||||
"tags": ["test-package", "python"],
|
||||
"score": 5,
|
||||
"is_answered": True,
|
||||
},
|
||||
{
|
||||
"title": "Tutorial: Getting started with test-package",
|
||||
"tags": ["test-package", "tutorial"],
|
||||
"score": 3,
|
||||
"is_answered": True,
|
||||
},
|
||||
]
|
||||
|
||||
result = _analyze_stackoverflow_sentiment(questions, "test-package")
|
||||
|
||||
assert result["overall_sentiment_score"] > 50
|
||||
assert result["questions_analyzed"] == 2
|
||||
assert result["question_characteristics"]["answered_questions"] == 2
|
||||
assert result["question_characteristics"]["average_score"] == 4.0
|
||||
|
||||
def test_analyze_stackoverflow_sentiment_negative(self):
|
||||
"""Test analyzing negative Stack Overflow sentiment."""
|
||||
questions = [
|
||||
{
|
||||
"title": "test-package not working: Error on import",
|
||||
"tags": ["test-package", "error"],
|
||||
"score": -1,
|
||||
"is_answered": False,
|
||||
},
|
||||
{
|
||||
"title": "Problem with test-package installation",
|
||||
"tags": ["test-package", "installation"],
|
||||
"score": 0,
|
||||
"is_answered": False,
|
||||
},
|
||||
]
|
||||
|
||||
result = _analyze_stackoverflow_sentiment(questions, "test-package")
|
||||
|
||||
assert result["overall_sentiment_score"] < 50
|
||||
assert result["questions_analyzed"] == 2
|
||||
assert result["question_characteristics"]["unanswered_questions"] == 2
|
||||
assert result["question_characteristics"]["average_score"] == -0.5
|
||||
|
||||
def test_calculate_community_score_excellent(self):
|
||||
"""Test calculating excellent community score."""
|
||||
github_sentiment = {
|
||||
"sentiment_analysis": {"overall_sentiment_score": 85}
|
||||
}
|
||||
stackoverflow_data = {
|
||||
"sentiment_analysis": {"overall_sentiment_score": 80}
|
||||
}
|
||||
quality_indicators = {
|
||||
"quality_indicator_score": 90
|
||||
}
|
||||
community_health = {
|
||||
"github_community_health": {"health_percentage": 95}
|
||||
}
|
||||
|
||||
result = _calculate_community_score(
|
||||
github_sentiment,
|
||||
stackoverflow_data,
|
||||
quality_indicators,
|
||||
community_health
|
||||
)
|
||||
|
||||
assert result["overall_score"] >= 80
|
||||
assert result["community_status"] == "excellent"
|
||||
assert len(result["score_components"]) > 0
|
||||
|
||||
def test_calculate_community_score_poor(self):
|
||||
"""Test calculating poor community score."""
|
||||
github_sentiment = {
|
||||
"sentiment_analysis": {"overall_sentiment_score": 20}
|
||||
}
|
||||
stackoverflow_data = {
|
||||
"sentiment_analysis": {"overall_sentiment_score": 25}
|
||||
}
|
||||
quality_indicators = {
|
||||
"quality_indicator_score": 15
|
||||
}
|
||||
community_health = {}
|
||||
|
||||
result = _calculate_community_score(
|
||||
github_sentiment,
|
||||
stackoverflow_data,
|
||||
quality_indicators,
|
||||
community_health
|
||||
)
|
||||
|
||||
assert result["overall_score"] < 35
|
||||
assert result["community_status"] == "poor"
|
||||
|
||||
def test_generate_community_insights_strong_community(self):
|
||||
"""Test generating insights for strong community."""
|
||||
github_sentiment = {
|
||||
"repository_stats": {"stargazers_count": 2000}
|
||||
}
|
||||
stackoverflow_data = {
|
||||
"questions_found": 25
|
||||
}
|
||||
community_score = {
|
||||
"overall_score": 85
|
||||
}
|
||||
package_metadata = {
|
||||
"name": "test-package"
|
||||
}
|
||||
|
||||
result = _generate_community_insights(
|
||||
github_sentiment,
|
||||
stackoverflow_data,
|
||||
community_score,
|
||||
package_metadata
|
||||
)
|
||||
|
||||
assert "key_insights" in result
|
||||
assert "community_strengths" in result
|
||||
assert len(result["community_strengths"]) > 0
|
||||
# Should have positive insights for high score
|
||||
insights_text = " ".join(result["key_insights"])
|
||||
assert "strong" in insights_text.lower() or "positive" in insights_text.lower()
|
||||
|
||||
def test_extract_contact_info_from_metadata_with_privacy(self):
|
||||
"""Test extracting contact info with privacy enabled."""
|
||||
package_metadata = {
|
||||
"author_email": "author@example.com",
|
||||
"maintainer_email": "maintainer@example.com",
|
||||
"project_urls": {
|
||||
"Repository": "https://github.com/test/repo",
|
||||
"Documentation": "https://docs.example.com",
|
||||
"Support": "https://support.example.com",
|
||||
},
|
||||
"home_page": "https://example.com",
|
||||
}
|
||||
|
||||
contact_types = ["email", "github", "support"]
|
||||
|
||||
result = _extract_contact_info_from_metadata(
|
||||
package_metadata,
|
||||
contact_types,
|
||||
respect_privacy=True
|
||||
)
|
||||
|
||||
assert result["privacy_compliant"] is True
|
||||
# With privacy enabled, emails should be hidden
|
||||
assert "email_note" in result["available_contacts"]
|
||||
# Project URLs should still be included
|
||||
assert len(result["project_urls"]) > 0
|
||||
|
||||
def test_extract_contact_info_from_metadata_without_privacy(self):
|
||||
"""Test extracting contact info with privacy disabled."""
|
||||
package_metadata = {
|
||||
"author_email": "author@example.com",
|
||||
"maintainer_email": "maintainer@example.com",
|
||||
"project_urls": {
|
||||
"Repository": "https://github.com/test/repo",
|
||||
},
|
||||
}
|
||||
|
||||
contact_types = ["email", "github"]
|
||||
|
||||
result = _extract_contact_info_from_metadata(
|
||||
package_metadata,
|
||||
contact_types,
|
||||
respect_privacy=False
|
||||
)
|
||||
|
||||
assert result["privacy_compliant"] is False
|
||||
# With privacy disabled, emails should be included
|
||||
assert "author_email" in result["available_contacts"]
|
||||
assert "maintainer_email" in result["available_contacts"]
|
||||
|
||||
|
||||
class TestCommunityIntegrations:
|
||||
"""Test community tool integrations with external services."""
|
||||
|
||||
async def test_github_community_sentiment_no_repository(self):
|
||||
"""Test GitHub sentiment analysis when no repository is found."""
|
||||
with patch("pypi_query_mcp.tools.community._find_github_repository") as mock_find:
|
||||
mock_find.return_value = {"status": "no_github_repository"}
|
||||
|
||||
result = await _analyze_github_community_sentiment("test-package")
|
||||
|
||||
assert result["status"] == "no_github_repository"
|
||||
|
||||
async def test_stackoverflow_mentions_api_error(self):
|
||||
"""Test Stack Overflow mentions with API error."""
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client.return_value.__aenter__.return_value.get.return_value.status_code = 500
|
||||
|
||||
result = await _check_stackoverflow_mentions("test-package")
|
||||
|
||||
assert result["status"] == "api_unavailable"
|
||||
assert result["questions_found"] == 0
|
||||
|
||||
async def test_quality_indicator_with_download_stats(self):
|
||||
"""Test quality indicator calculation with download stats."""
|
||||
with patch("pypi_query_mcp.tools.community.get_package_download_stats") as mock_stats:
|
||||
mock_stats.return_value = {
|
||||
"downloads": {
|
||||
"last_month": 500000,
|
||||
"last_week": 125000,
|
||||
"last_day": 18000,
|
||||
}
|
||||
}
|
||||
|
||||
result = await _analyze_pypi_downloads_as_quality_indicator("test-package")
|
||||
|
||||
assert result["adoption_level"] == "high"
|
||||
assert result["quality_indicator_score"] > 0
|
||||
assert "download_stats" in result
|
||||
|
||||
async def test_community_health_metrics_no_repository(self):
|
||||
"""Test community health metrics when no repository exists."""
|
||||
with patch("pypi_query_mcp.tools.community._find_github_repository") as mock_find:
|
||||
mock_find.return_value = {"status": "no_github_repository"}
|
||||
|
||||
result = await _get_community_health_metrics("test-package")
|
||||
|
||||
assert result["has_repository"] is False
|
||||
assert "note" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAsyncBehavior:
|
||||
"""Test async behavior and error handling."""
|
||||
|
||||
async def test_concurrent_operations_success(self):
|
||||
"""Test that concurrent operations work correctly."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_reviews") as mock_meta, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_github_community_sentiment") as mock_github, \
|
||||
patch("pypi_query_mcp.tools.community._check_stackoverflow_mentions") as mock_so, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_pypi_downloads_as_quality_indicator") as mock_quality, \
|
||||
patch("pypi_query_mcp.tools.community._get_community_health_metrics") as mock_health:
|
||||
|
||||
# Set up mocks to return after small delays to test concurrency
|
||||
import asyncio
|
||||
|
||||
async def delayed_return(value, delay=0.01):
|
||||
await asyncio.sleep(delay)
|
||||
return value
|
||||
|
||||
mock_meta.return_value = delayed_return({"name": "test-package"})
|
||||
mock_github.return_value = delayed_return({"sentiment_analysis": {"overall_sentiment_score": 75}})
|
||||
mock_so.return_value = delayed_return({"sentiment_analysis": {"overall_sentiment_score": 70}})
|
||||
mock_quality.return_value = delayed_return({"quality_indicator_score": 80})
|
||||
mock_health.return_value = delayed_return({"has_repository": True})
|
||||
|
||||
start_time = datetime.now()
|
||||
result = await get_pypi_package_reviews("test-package")
|
||||
end_time = datetime.now()
|
||||
|
||||
# Should complete relatively quickly due to concurrent execution
|
||||
assert (end_time - start_time).total_seconds() < 1.0
|
||||
assert result["package"] == "test-package"
|
||||
|
||||
async def test_partial_failure_handling(self):
|
||||
"""Test handling when some operations fail but others succeed."""
|
||||
with patch("pypi_query_mcp.tools.community._get_package_metadata_for_reviews") as mock_meta, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_github_community_sentiment", side_effect=Exception("GitHub error")) as mock_github, \
|
||||
patch("pypi_query_mcp.tools.community._check_stackoverflow_mentions") as mock_so, \
|
||||
patch("pypi_query_mcp.tools.community._analyze_pypi_downloads_as_quality_indicator") as mock_quality, \
|
||||
patch("pypi_query_mcp.tools.community._get_community_health_metrics", side_effect=Exception("Health error")) as mock_health:
|
||||
|
||||
mock_meta.return_value = {"name": "test-package"}
|
||||
mock_so.return_value = {"sentiment_analysis": {"overall_sentiment_score": 70}}
|
||||
mock_quality.return_value = {"quality_indicator_score": 80}
|
||||
|
||||
result = await get_pypi_package_reviews("test-package")
|
||||
|
||||
# Should still return a result even with some failures
|
||||
assert result["package"] == "test-package"
|
||||
assert "community_score" in result
|
||||
# Failed operations should result in empty dicts or be excluded
|
Loading…
x
Reference in New Issue
Block a user