From 66b2da84619bfedbaefe0bb60b14cd2161c51f1b Mon Sep 17 00:00:00 2001 From: Jon Michael Aanes Date: Mon, 9 Jun 2025 13:48:50 +0200 Subject: [PATCH] Ruff after Claude Code --- aider_gitea/__init__.py | 24 ++- aider_gitea/__main__.py | 12 ++ aider_gitea/gitea_client.py | 44 ++++- test/test_assignee_reviewer.py | 154 +++++++++++++++++ test/test_assignee_reviewer_cli.py | 74 ++++++++ test/test_push_changes_assignee_reviewer.py | 182 ++++++++++++++++++++ 6 files changed, 487 insertions(+), 3 deletions(-) create mode 100644 test/test_assignee_reviewer.py create mode 100644 test/test_assignee_reviewer_cli.py create mode 100644 test/test_push_changes_assignee_reviewer.py diff --git a/aider_gitea/__init__.py b/aider_gitea/__init__.py index 9fe3f23..7e7de1c 100644 --- a/aider_gitea/__init__.py +++ b/aider_gitea/__init__.py @@ -156,6 +156,8 @@ class RepositoryConfig: owner: str repo: str base_branch: str + assignee: str | None = None + reviewer: str | None = None def repo_url(self) -> str: return f'{self.gitea_url}:{self.owner}/{self.repo}.git'.replace( @@ -497,7 +499,12 @@ def push_changes( cmd = ['git', 'push', 'origin', branch_name, '--force'] run_cmd(cmd, cwd) - # Then create the PR with the aider label + # Prepare assignees list + assignees = [] + if repository_config.assignee: + assignees.append(repository_config.assignee) + + # Then create the PR with the aider label and assignees pr_response = gitea_client.create_pull_request( owner=repository_config.owner, repo=repository_config.repo, @@ -506,13 +513,26 @@ def push_changes( head=branch_name, base=repository_config.base_branch, labels=['aider'], + assignees=assignees if assignees else None, ) + pr_number = int(pr_response.get('number')) + + # Assign reviewers after PR creation if specified + if repository_config.reviewer: + reviewers = [repository_config.reviewer] + gitea_client.assign_reviewers( + owner=repository_config.owner, + repo=repository_config.repo, + pull_number=pr_number, + reviewers=reviewers, + ) + # Extract PR number and URL if available return IssueResolution( True, pr_response.get('html_url'), - int(pr_response.get('number')), + pr_number, ) diff --git a/aider_gitea/__main__.py b/aider_gitea/__main__.py index e9ab35e..d7b5aa8 100644 --- a/aider_gitea/__main__.py +++ b/aider_gitea/__main__.py @@ -55,6 +55,16 @@ def parse_args(): help='Model to use for evaluating code (overrides default)', default=None, ) + parser.add_argument( + '--assignee', + help='Username to automatically assign to created pull requests', + default=None, + ) + parser.add_argument( + '--reviewer', + help='Username to automatically assign as reviewer for created pull requests', + default=None, + ) return parser.parse_args() @@ -84,6 +94,8 @@ def main(): owner=args.owner, repo=repo, base_branch=args.base_branch, + assignee=args.assignee, + reviewer=args.reviewer, ) solve_issues_in_repository(repository_config, client, seen_issues_db) del repo diff --git a/aider_gitea/gitea_client.py b/aider_gitea/gitea_client.py index bf90768..97e92b4 100644 --- a/aider_gitea/gitea_client.py +++ b/aider_gitea/gitea_client.py @@ -139,8 +139,9 @@ class GiteaClient: head: str, base: str, labels: list[str] = None, + assignees: list[str] = None, ) -> dict: - """Create a pull request and optionally apply labels. + """Create a pull request and optionally apply labels and assignees. Args: owner (str): Owner of the repository. @@ -150,6 +151,7 @@ class GiteaClient: head (str): The name of the branch where changes are implemented. base (str): The name of the branch you want the changes pulled into. labels (list[str], optional): List of label names to apply to the pull request. + assignees (list[str], optional): List of usernames to assign to the pull request. Returns: dict: The created pull request data. @@ -165,6 +167,9 @@ class GiteaClient: 'base': base, } + if assignees: + json_data['assignees'] = assignees + response = self.session.post(url, json=json_data) # If a pull request for this head/base already exists, return it instead of crashing if response.status_code == 409: @@ -185,6 +190,43 @@ class GiteaClient: response.raise_for_status() return response.json() + def assign_reviewers( + self, + owner: str, + repo: str, + pull_number: int, + reviewers: list[str], + ) -> bool: + """Assign reviewers to an existing pull request. + + Args: + owner (str): Owner of the repository. + repo (str): Name of the repository. + pull_number (int): Pull request number. + reviewers (list[str]): List of usernames to assign as reviewers. + + Returns: + bool: True if reviewers were assigned successfully, False otherwise. + + Raises: + requests.HTTPError: If the API request fails. + """ + if not reviewers: + return True + + url = f'{self.gitea_url}/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers' + json_data = {'reviewers': reviewers} + + response = self.session.post(url, json=json_data) + if response.status_code == 404: + logger.warning( + 'Reviewer assignment not supported or pull request not found: %s', + pull_number, + ) + return False + response.raise_for_status() + return True + def get_failed_pipelines(self, owner: str, repo: str, pr_number: str) -> list[int]: """Fetch pipeline runs for a PR and return IDs of failed runs.""" url = f'{self.gitea_url}/repos/{owner}/{repo}/actions/runs' diff --git a/test/test_assignee_reviewer.py b/test/test_assignee_reviewer.py new file mode 100644 index 0000000..10395fc --- /dev/null +++ b/test/test_assignee_reviewer.py @@ -0,0 +1,154 @@ +from unittest.mock import MagicMock, patch + +from aider_gitea.gitea_client import GiteaClient + + +class TestAssigneeReviewerFunctionality: + def setup_method(self): + self.client = GiteaClient('https://gitea.example.com', 'fake_token') + + @patch('requests.Session.post') + def test_create_pull_request_with_assignees(self, mock_post): + """Test creating a pull request with assignees.""" + # Mock the PR creation response + pr_response = MagicMock() + pr_response.status_code = 201 + expected_result = { + 'number': 123, + 'title': 'Test PR', + 'html_url': 'https://gitea.example.com/owner/repo/pulls/123', + } + pr_response.json.return_value = expected_result + mock_post.return_value = pr_response + + # Call the method with assignees + result = self.client.create_pull_request( + owner='owner', + repo='repo', + title='Test PR', + body='Test body', + head='feature-branch', + base='main', + assignees=['user1', 'user2'], + ) + + # Verify PR creation call + assert mock_post.call_count == 1 + pr_call_args = mock_post.call_args_list[0] + assert ( + pr_call_args[0][0] + == 'https://gitea.example.com/api/v1/repos/owner/repo/pulls' + ) + assert pr_call_args[1]['json']['title'] == 'Test PR' + assert pr_call_args[1]['json']['assignees'] == ['user1', 'user2'] + + # Verify the result + assert result == expected_result + + @patch('requests.Session.post') + def test_create_pull_request_without_assignees(self, mock_post): + """Test creating a pull request without assignees.""" + # Mock the PR creation response + pr_response = MagicMock() + pr_response.status_code = 201 + expected_result = { + 'number': 123, + 'title': 'Test PR', + 'html_url': 'https://gitea.example.com/owner/repo/pulls/123', + } + pr_response.json.return_value = expected_result + mock_post.return_value = pr_response + + # Call the method without assignees + result = self.client.create_pull_request( + owner='owner', + repo='repo', + title='Test PR', + body='Test body', + head='feature-branch', + base='main', + ) + + # Verify PR creation call + assert mock_post.call_count == 1 + pr_call_args = mock_post.call_args_list[0] + json_data = pr_call_args[1]['json'] + assert 'assignees' not in json_data + + # Verify the result + assert result == expected_result + + @patch('requests.Session.post') + def test_assign_reviewers_success(self, mock_post): + """Test assigning reviewers to a pull request successfully.""" + # Mock successful reviewer assignment response + reviewer_response = MagicMock() + reviewer_response.status_code = 201 + mock_post.return_value = reviewer_response + + # Call the method + result = self.client.assign_reviewers( + owner='owner', + repo='repo', + pull_number=123, + reviewers=['reviewer1', 'reviewer2'], + ) + + # Verify reviewer assignment call + assert mock_post.call_count == 1 + reviewer_call_args = mock_post.call_args_list[0] + assert ( + reviewer_call_args[0][0] + == 'https://gitea.example.com/api/v1/repos/owner/repo/pulls/123/requested_reviewers' + ) + assert reviewer_call_args[1]['json']['reviewers'] == ['reviewer1', 'reviewer2'] + + # Verify the result + assert result is True + + @patch('requests.Session.post') + def test_assign_reviewers_not_supported(self, mock_post): + """Test assigning reviewers when API endpoint is not supported.""" + # Mock 404 response (not supported) + reviewer_response = MagicMock() + reviewer_response.status_code = 404 + mock_post.return_value = reviewer_response + + # Call the method + result = self.client.assign_reviewers( + owner='owner', + repo='repo', + pull_number=123, + reviewers=['reviewer1'], + ) + + # Verify the result + assert result is False + + def test_assign_reviewers_empty_list(self): + """Test assigning reviewers with empty list returns True.""" + result = self.client.assign_reviewers( + owner='owner', + repo='repo', + pull_number=123, + reviewers=[], + ) + + # Verify the result + assert result is True + + @patch('requests.Session.post') + def test_assign_reviewers_none_list(self, mock_post): + """Test assigning reviewers with None returns True.""" + result = self.client.assign_reviewers( + owner='owner', + repo='repo', + pull_number=123, + reviewers=None, + ) + + # Verify no API call was made + assert mock_post.call_count == 0 + + # Verify the result + assert result is True diff --git a/test/test_assignee_reviewer_cli.py b/test/test_assignee_reviewer_cli.py new file mode 100644 index 0000000..ad43cdc --- /dev/null +++ b/test/test_assignee_reviewer_cli.py @@ -0,0 +1,74 @@ +from unittest.mock import patch + +from aider_gitea import RepositoryConfig +from aider_gitea.__main__ import parse_args + + +class TestAssigneeReviewerCLI: + @patch( + 'sys.argv', + [ + 'aider_gitea', + '--gitea-url', + 'https://gitea.example.com', + '--owner', + 'testowner', + '--aider-model', + 'claude-3-sonnet', + '--assignee', + 'john_doe', + '--reviewer', + 'jane_smith', + ], + ) + def test_parse_args_with_assignee_and_reviewer(self): + """Test that CLI arguments for assignee and reviewer are parsed correctly.""" + args = parse_args() + + assert args.assignee == 'john_doe' + assert args.reviewer == 'jane_smith' + + @patch( + 'sys.argv', + [ + 'aider_gitea', + '--gitea-url', + 'https://gitea.example.com', + '--owner', + 'testowner', + '--aider-model', + 'claude-3-sonnet', + ], + ) + def test_parse_args_without_assignee_and_reviewer(self): + """Test that assignee and reviewer default to None when not provided.""" + args = parse_args() + + assert args.assignee is None + assert args.reviewer is None + + def test_repository_config_with_assignee_and_reviewer(self): + """Test that RepositoryConfig stores assignee and reviewer correctly.""" + config = RepositoryConfig( + gitea_url='https://gitea.example.com', + owner='testowner', + repo='testrepo', + base_branch='main', + assignee='john_doe', + reviewer='jane_smith', + ) + + assert config.assignee == 'john_doe' + assert config.reviewer == 'jane_smith' + + def test_repository_config_without_assignee_and_reviewer(self): + """Test that RepositoryConfig defaults assignee and reviewer to None.""" + config = RepositoryConfig( + gitea_url='https://gitea.example.com', + owner='testowner', + repo='testrepo', + base_branch='main', + ) + + assert config.assignee is None + assert config.reviewer is None diff --git a/test/test_push_changes_assignee_reviewer.py b/test/test_push_changes_assignee_reviewer.py new file mode 100644 index 0000000..521835d --- /dev/null +++ b/test/test_push_changes_assignee_reviewer.py @@ -0,0 +1,182 @@ +from pathlib import Path +from unittest.mock import MagicMock, patch + +from aider_gitea import RepositoryConfig, push_changes + + +class TestPushChangesAssigneeReviewer: + def setup_method(self): + self.repository_config = RepositoryConfig( + gitea_url='https://gitea.example.com', + owner='testowner', + repo='testrepo', + base_branch='main', + assignee='john_doe', + reviewer='jane_smith', + ) + + self.cwd = Path('/tmp/test') + self.branch_name = 'issue-123-test-branch' + self.issue_number = '123' + self.issue_title = 'Test Issue' + + @patch('aider_gitea.run_cmd') + @patch('aider_gitea.has_commits_on_branch') + @patch('aider_gitea.get_commit_messages') + def test_push_changes_with_assignee_and_reviewer( + self, + mock_get_commit_messages, + mock_has_commits, + mock_run_cmd, + ): + """Test that push_changes correctly assigns assignee and reviewer.""" + # Setup mocks + mock_has_commits.return_value = True + mock_get_commit_messages.return_value = ['Initial commit', 'Fix bug'] + mock_run_cmd.return_value = True + + # Mock gitea client + mock_client = MagicMock() + pr_response = { + 'number': 123, + 'html_url': 'https://gitea.example.com/testowner/testrepo/pulls/123', + } + mock_client.create_pull_request.return_value = pr_response + mock_client.assign_reviewers.return_value = True + + # Call the function + result = push_changes( + self.repository_config, + self.cwd, + self.branch_name, + self.issue_number, + self.issue_title, + mock_client, + ) + + # Verify PR creation was called with assignees + mock_client.create_pull_request.assert_called_once() + create_pr_args = mock_client.create_pull_request.call_args + assert create_pr_args[1]['assignees'] == ['john_doe'] + + # Verify reviewer assignment was called + mock_client.assign_reviewers.assert_called_once_with( + owner='testowner', + repo='testrepo', + pull_number=123, + reviewers=['jane_smith'], + ) + + # Verify result + assert result.success is True + assert result.pull_request_id == 123 + assert ( + result.pull_request_url + == 'https://gitea.example.com/testowner/testrepo/pulls/123' + ) + + @patch('aider_gitea.run_cmd') + @patch('aider_gitea.has_commits_on_branch') + @patch('aider_gitea.get_commit_messages') + def test_push_changes_without_assignee_and_reviewer( + self, + mock_get_commit_messages, + mock_has_commits, + mock_run_cmd, + ): + """Test that push_changes works when no assignee or reviewer is specified.""" + # Setup repository config without assignee/reviewer + config_no_assign = RepositoryConfig( + gitea_url='https://gitea.example.com', + owner='testowner', + repo='testrepo', + base_branch='main', + ) + + # Setup mocks + mock_has_commits.return_value = True + mock_get_commit_messages.return_value = ['Initial commit'] + mock_run_cmd.return_value = True + + # Mock gitea client + mock_client = MagicMock() + pr_response = { + 'number': 124, + 'html_url': 'https://gitea.example.com/testowner/testrepo/pulls/124', + } + mock_client.create_pull_request.return_value = pr_response + + # Call the function + result = push_changes( + config_no_assign, + self.cwd, + self.branch_name, + self.issue_number, + self.issue_title, + mock_client, + ) + + # Verify PR creation was called without assignees + mock_client.create_pull_request.assert_called_once() + create_pr_args = mock_client.create_pull_request.call_args + assert create_pr_args[1]['assignees'] is None + + # Verify reviewer assignment was NOT called + mock_client.assign_reviewers.assert_not_called() + + # Verify result + assert result.success is True + assert result.pull_request_id == 124 + + @patch('aider_gitea.run_cmd') + @patch('aider_gitea.has_commits_on_branch') + @patch('aider_gitea.get_commit_messages') + def test_push_changes_with_only_assignee( + self, + mock_get_commit_messages, + mock_has_commits, + mock_run_cmd, + ): + """Test that push_changes works with only assignee specified.""" + # Setup repository config with only assignee + config_assignee_only = RepositoryConfig( + gitea_url='https://gitea.example.com', + owner='testowner', + repo='testrepo', + base_branch='main', + assignee='john_doe', + ) + + # Setup mocks + mock_has_commits.return_value = True + mock_get_commit_messages.return_value = ['Initial commit'] + mock_run_cmd.return_value = True + + # Mock gitea client + mock_client = MagicMock() + pr_response = { + 'number': 125, + 'html_url': 'https://gitea.example.com/testowner/testrepo/pulls/125', + } + mock_client.create_pull_request.return_value = pr_response + + # Call the function + result = push_changes( + config_assignee_only, + self.cwd, + self.branch_name, + self.issue_number, + self.issue_title, + mock_client, + ) + + # Verify PR creation was called with assignees + mock_client.create_pull_request.assert_called_once() + create_pr_args = mock_client.create_pull_request.call_args + assert create_pr_args[1]['assignees'] == ['john_doe'] + + # Verify reviewer assignment was NOT called + mock_client.assign_reviewers.assert_not_called() + + # Verify result + assert result.success is True