diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5971b70d4..afed6e00ab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,11 +17,12 @@ repos: # rev: v1.7.3 # hooks: # - id: actionlint - - repo: https://github.com/pycqa/isort - # rev must match what's in dev-requirements.txt - rev: 5.13.2 + # ruff handles linting and import sorting (config in pyproject.toml). + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.20 hooks: - - id: isort + - id: ruff + args: [--fix] # - repo: https://github.com/PyCQA/bandit # rev: 1.7.10 # hooks: @@ -29,18 +30,3 @@ repos: # args: [ # "-c", "pyproject.toml", # ] - # - repo: https://github.com/astral-sh/ruff-pre-commit - # rev: v0.7.1 - # hooks: - # - id: ruff - # args: - # - --fix - # - id: ruff-format - # - repo: https://github.com/PyCQA/autoflake - # rev: v2.3.1 - # hooks: - # - id: autoflake - # args: - # - --in-place - # - --remove-all-unused-imports - # - --remove-unused-variables diff --git a/AGENTS.md b/AGENTS.md index 4e1404fd6e..818cf69724 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,8 +34,8 @@ PR-Agent automates AI-assisted reviews for pull requests across multiple git pro ## Coding Style and Naming Conventions - Python sources follow the Ruff configuration in `pyproject.toml` (`line-length = 120`, Pyflakes plus `flake8-bugbear` checks, and isort ordering). Keep imports grouped as isort would produce and prefer double quotes for strings. -- Pre-commit (`.pre-commit-config.yaml`) enforces trailing whitespace cleanup, final newlines, TOML/YAML validity, and optional `isort`; run `pre-commit run --all-files` before submitting patches if installed. -- Before committing, run `flake8` and fix every issue it reports. Keep fixes mechanical (rename, reformat, remove unused imports, add missing newlines); do not alter program logic while cleaning up—if a lint fix would change behavior, surface it instead of applying it silently. +- Pre-commit (`.pre-commit-config.yaml`) enforces trailing whitespace cleanup, final newlines, TOML/YAML validity, and `ruff` (lint + import sorting); run `pre-commit run --all-files` before submitting patches if installed. +- Before committing, run `uv run ruff check --fix` and fix every issue it reports. Keep fixes mechanical (rename, reformat, remove unused imports, add missing newlines); do not alter program logic while cleaning up—if a lint fix would change behavior, surface it instead of applying it silently. - Match existing docstring and comment style—concise English comments using imperative phrasing only where necessary. - Configuration files in `pr_agent/settings/` are TOML; preserve formatting, section order, and comments when editing prompts or defaults. - Markdown in `docs/` uses MkDocs conventions (YAML front matter absent; rely on heading hierarchy already in place). diff --git a/pr_agent/algo/ai_handlers/langchain_ai_handler.py b/pr_agent/algo/ai_handlers/langchain_ai_handler.py index 2d4fa08bd3..051f55a5e0 100644 --- a/pr_agent/algo/ai_handlers/langchain_ai_handler.py +++ b/pr_agent/algo/ai_handlers/langchain_ai_handler.py @@ -7,11 +7,10 @@ except: # we don't enforce langchain as a dependency, so if it's not installed, just move on pass -import functools import openai -from tenacity import retry, retry_if_exception_type, retry_if_not_exception_type, stop_after_attempt from langchain_core.runnables import Runnable +from tenacity import retry, retry_if_exception_type, retry_if_not_exception_type, stop_after_attempt from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.config_loader import get_settings diff --git a/pr_agent/algo/ai_handlers/litellm_ai_handler.py b/pr_agent/algo/ai_handlers/litellm_ai_handler.py index 6a306bc90a..4e2b613a5a 100644 --- a/pr_agent/algo/ai_handlers/litellm_ai_handler.py +++ b/pr_agent/algo/ai_handlers/litellm_ai_handler.py @@ -7,18 +7,22 @@ import openai import requests from litellm import acompletion -from tenacity import (retry, retry_if_exception_type, - retry_if_not_exception_type, stop_after_attempt) - -from pr_agent.algo import (CLAUDE_EXTENDED_THINKING_MODELS, - NO_SUPPORT_TEMPERATURE_MODELS, - STREAMING_REQUIRED_MODELS, - SUPPORT_REASONING_EFFORT_MODELS, - USER_MESSAGE_ONLY_MODELS) +from tenacity import retry, retry_if_exception_type, retry_if_not_exception_type, stop_after_attempt + +from pr_agent.algo import ( + CLAUDE_EXTENDED_THINKING_MODELS, + NO_SUPPORT_TEMPERATURE_MODELS, + STREAMING_REQUIRED_MODELS, + SUPPORT_REASONING_EFFORT_MODELS, + USER_MESSAGE_ONLY_MODELS, +) from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_helpers import ( - MockResponse, _get_azure_ad_token, _handle_streaming_response, - _process_litellm_extra_body) + MockResponse, + _get_azure_ad_token, + _handle_streaming_response, + _process_litellm_extra_body, +) from pr_agent.algo.utils import ReasoningEffort, get_version from pr_agent.config_loader import get_settings from pr_agent.log import get_logger @@ -456,7 +460,7 @@ async def chat_completion(self, model: str, system: str, user: str, temperature: # check if the image link is alive r = requests.head(img_path, allow_redirects=True) if r.status_code == 404: - error_msg = f"The image link is not [alive](img_path).\nPlease repost the original image as a comment, and send the question again with 'quote reply' (see [instructions](https://pr-agent-docs.codium.ai/tools/ask/#ask-on-images-using-the-pr-code-as-context))." + error_msg = "The image link is not [alive](img_path).\nPlease repost the original image as a comment, and send the question again with 'quote reply' (see [instructions](https://pr-agent-docs.codium.ai/tools/ask/#ask-on-images-using-the-pr-code-as-context))." get_logger().error(error_msg) return f"{error_msg}", "error" except Exception as e: diff --git a/pr_agent/algo/ai_handlers/openai_ai_handler.py b/pr_agent/algo/ai_handlers/openai_ai_handler.py index f5fb99f6ad..70316d7cf1 100644 --- a/pr_agent/algo/ai_handlers/openai_ai_handler.py +++ b/pr_agent/algo/ai_handlers/openai_ai_handler.py @@ -1,5 +1,5 @@ from os import environ -from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler + import openai from openai import AsyncOpenAI from tenacity import retry, retry_if_exception_type, retry_if_not_exception_type, stop_after_attempt diff --git a/pr_agent/algo/cli_args.py b/pr_agent/algo/cli_args.py index 246864cefa..0436ff45d7 100644 --- a/pr_agent/algo/cli_args.py +++ b/pr_agent/algo/cli_args.py @@ -1,5 +1,5 @@ -from base64 import b64decode, encode, b64encode -import hashlib +from base64 import b64decode + class CliArgs: @staticmethod diff --git a/pr_agent/algo/git_patch_processing.py b/pr_agent/algo/git_patch_processing.py index 553914e8d9..1a0689a1cd 100644 --- a/pr_agent/algo/git_patch_processing.py +++ b/pr_agent/algo/git_patch_processing.py @@ -3,7 +3,7 @@ import re import traceback -from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo +from pr_agent.algo.types import EDIT_TYPE from pr_agent.config_loader import get_settings from pr_agent.log import get_logger diff --git a/pr_agent/algo/pr_processing.py b/pr_agent/algo/pr_processing.py index 17c5bc1fb6..89e595257c 100644 --- a/pr_agent/algo/pr_processing.py +++ b/pr_agent/algo/pr_processing.py @@ -5,13 +5,14 @@ from github import RateLimitExceededException -from pr_agent.algo.file_filter import filter_ignored from pr_agent.algo.git_patch_processing import ( - extend_patch, handle_patch_deletions, - decouple_and_convert_to_hunks_with_lines_numbers) + decouple_and_convert_to_hunks_with_lines_numbers, + extend_patch, + handle_patch_deletions, +) from pr_agent.algo.language_handler import sort_files_by_main_languages from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo +from pr_agent.algo.types import EDIT_TYPE from pr_agent.algo.utils import ModelType, clip_tokens, get_max_tokens, get_model from pr_agent.config_loader import get_settings from pr_agent.git_providers.git_provider import GitProvider @@ -506,7 +507,7 @@ def add_ai_metadata_to_diff_files(git_provider, pr_description_files): """ try: if not pr_description_files: - get_logger().warning(f"PR description files are empty.") + get_logger().warning("PR description files are empty.") return available_files = {pr_file['full_file_name'].strip(): pr_file for pr_file in pr_description_files} diff_files = git_provider.get_diff_files() @@ -517,7 +518,7 @@ def add_ai_metadata_to_diff_files(git_provider, pr_description_files): file.ai_file_summary = available_files[filename] found_any_match = True if not found_any_match: - get_logger().error(f"Failed to find any matching files between PR description and diff files.", + get_logger().error("Failed to find any matching files between PR description and diff files.", artifact={"pr_description_files": pr_description_files}) except Exception as e: get_logger().error(f"Failed to add AI metadata to diff files: {e}", diff --git a/pr_agent/algo/token_handler.py b/pr_agent/algo/token_handler.py index cb313f023f..56b2e005fb 100644 --- a/pr_agent/algo/token_handler.py +++ b/pr_agent/algo/token_handler.py @@ -1,6 +1,6 @@ -from threading import Lock -from math import ceil import re +from math import ceil +from threading import Lock from jinja2 import Environment, StrictUndefined from tiktoken import encoding_for_model, get_encoding @@ -99,6 +99,7 @@ def _get_system_user_tokens(self, pr, encoder, vars: dict, system, user): def _calc_claude_tokens(self, patch: str) -> int: try: import anthropic + from pr_agent.algo import MAX_TOKENS client = anthropic.Anthropic(api_key=get_settings(use_context=False).get('anthropic.key')) diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 35bf9e0a36..32c08ebab4 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -1,6 +1,5 @@ from __future__ import annotations -import ast import copy import difflib import hashlib @@ -164,7 +163,7 @@ def convert_to_markdown_v2(output_data: dict, return "" if get_settings().get("pr_reviewer.enable_intro_text", False): - markdown_text += f"Here are some key observations to aid the review process:\n\n" + markdown_text += "Here are some key observations to aid the review process:\n\n" if gfm_supported: markdown_text += "\n" @@ -190,20 +189,20 @@ def convert_to_markdown_v2(output_data: dict, white_bars = '⚪' * (5 - value_int) value = f"{value_int} {blue_bars}{white_bars}" if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: markdown_text += f"### {emoji} {key_nice}: {value}\n\n" elif 'relevant tests' in key_nice.lower(): value = str(value).strip().lower() if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: if is_value_no(value): markdown_text += f'### {emoji} No relevant tests\n\n' @@ -215,20 +214,20 @@ def convert_to_markdown_v2(output_data: dict, if gfm_supported: markdown_text += f"\n" + markdown_text += "\n" else: markdown_text += f"### {emoji} Contribution time estimate (best, average, worst case): " markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}\n\n" elif 'security concerns' in key_nice.lower(): if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: if is_value_no(value): markdown_text += f'### {emoji} No security concerns identified\n\n' @@ -240,7 +239,7 @@ def convert_to_markdown_v2(output_data: dict, if gfm_supported: markdown_text += "\n" else: if is_value_no(value): - markdown_text += f"### ✅ No TODO sections\n\n" + markdown_text += "### ✅ No TODO sections\n\n" else: markdown_todo_items = format_todo_items(value, git_provider, gfm_supported) markdown_text += f"### {emoji} TODO sections\n\n" markdown_text += markdown_todo_items elif 'can be split' in key_nice.lower(): if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" elif 'key issues to review' in key_nice.lower(): # value is a list of issues if is_value_no(value): if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: markdown_text += f"### {emoji} No major issues detected\n\n" else: issues = value if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: markdown_text += f"### {emoji} {key_nice}: {value}\n\n" @@ -383,7 +382,7 @@ def ticket_markdown_logic(emoji, markdown_text, value, gfm_supported) -> str: '').strip() if not fully_compliant_str and not not_compliant_str: - get_logger().debug(f"Ticket compliance has no requirements", + get_logger().debug("Ticket compliance has no requirements", artifact={'ticket_url': ticket_url}) continue @@ -414,7 +413,7 @@ def ticket_markdown_logic(emoji, markdown_text, value, gfm_supported) -> str: # for debugging if requires_further_human_verification: - get_logger().debug(f"Ticket compliance requires further human verification", + get_logger().debug("Ticket compliance requires further human verification", artifact={'ticket_url': ticket_url, 'requires_further_human_verification': requires_further_human_verification, 'compliance_level': ticket_compliance_level}) @@ -451,10 +450,10 @@ def ticket_markdown_logic(emoji, markdown_text, value, gfm_supported) -> str: # editing table row for ticket compliance analysis if gfm_supported: - markdown_text += f"\n" + markdown_text += "\n" else: markdown_text += f"### {emoji} Ticket compliance analysis {compliance_emoji}\n\n" markdown_text += ticket_compliance_str + "\n\n" @@ -478,11 +477,11 @@ def process_can_be_split(emoji, value): title = split.get('title', '') relevant_files = split.get('relevant_files', []) markdown_text += f"
\nSub-PR theme: {title}\n\n" - markdown_text += f"___\n\nRelevant files:\n\n" + markdown_text += "___\n\nRelevant files:\n\n" for file in relevant_files: markdown_text += f"- {file}\n" - markdown_text += f"___\n\n" - markdown_text += f"
\n\n" + markdown_text += "___\n\n" + markdown_text += "\n\n" # markdown_text += f"#### Sub-PR theme: {title}\n\n" # markdown_text += f"Relevant files:\n\n" @@ -539,7 +538,7 @@ def parse_code_suggestion(code_suggestion: dict, i: int = 0, gfm_supported: bool markdown_text += (f"" f"") elif sub_key.lower() == 'relevant_line': - markdown_text += f"" + markdown_text += "" sub_value_list = sub_value.split('](') relevant_line = sub_value_list[0].lstrip('`').lstrip('[') if len(sub_value_list) > 1: @@ -759,10 +758,10 @@ def load_yaml(response_text: str, keys_fix_yaml: List[str] = [], first_key="", l data = try_fix_yaml(response_text, keys_fix_yaml=keys_fix_yaml, first_key=first_key, last_key=last_key, response_text_original=response_text_original) if not data: - get_logger().error(f"Failed to parse AI prediction after fallbacks", + get_logger().error("Failed to parse AI prediction after fallbacks", artifact={'response_text': response_text}) else: - get_logger().info(f"Successfully parsed AI prediction after fallbacks", + get_logger().info("Successfully parsed AI prediction after fallbacks", artifact={'response_text': response_text}) return data @@ -788,7 +787,7 @@ def try_fix_yaml(response_text: str, f'{key} |\n ') try: data = yaml.safe_load('\n'.join(response_text_lines_copy)) - get_logger().info(f"Successfully parsed AI prediction after adding |-\n") + get_logger().info("Successfully parsed AI prediction after adding |-\n") return data except: pass @@ -798,7 +797,7 @@ def try_fix_yaml(response_text: str, response_text_copy = response_text_copy.replace('|\n', '|2\n') try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after replacing | with |2") + get_logger().info("Successfully parsed AI prediction after replacing | with |2") return data except: # if it fails, we can try to add spaces to the lines that are not indented properly, and contain '}'. @@ -809,7 +808,7 @@ def try_fix_yaml(response_text: str, response_text_lines_copy[i] = ' ' + response_text_lines_copy[i].lstrip() try: data = yaml.safe_load('\n'.join(response_text_lines_copy)) - get_logger().info(f"Successfully parsed AI prediction after replacing | with |2 and adding spaces") + get_logger().info("Successfully parsed AI prediction after replacing | with |2 and adding spaces") return data except: pass @@ -824,7 +823,7 @@ def try_fix_yaml(response_text: str, snippet_text = snippet.group(2) try: data = yaml.safe_load(snippet_text) - get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet") + get_logger().info("Successfully parsed AI prediction after extracting yaml snippet") return data except Exception as e: get_logger().debug(f"Failed to parse AI prediction after extracting yaml snippet: {e}") @@ -834,7 +833,7 @@ def try_fix_yaml(response_text: str, response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}').rstrip(':\n') try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after removing curly brackets") + get_logger().info("Successfully parsed AI prediction after removing curly brackets") return data except: pass @@ -855,7 +854,7 @@ def try_fix_yaml(response_text: str, if response_text_copy: try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet") + get_logger().info("Successfully parsed AI prediction after extracting yaml snippet") return data except: pass @@ -867,7 +866,7 @@ def try_fix_yaml(response_text: str, response_text_lines_copy[i] = ' ' + response_text_lines_copy[i][1:] try: data = yaml.safe_load('\n'.join(response_text_lines_copy)) - get_logger().info(f"Successfully parsed AI prediction after removing leading '+'") + get_logger().info("Successfully parsed AI prediction after removing leading '+'") return data except: pass @@ -878,7 +877,7 @@ def try_fix_yaml(response_text: str, response_text_copy = response_text_copy.replace('\t', ' ') try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after replacing tabs with spaces") + get_logger().info("Successfully parsed AI prediction after replacing tabs with spaces") return data except: pass @@ -901,7 +900,7 @@ def try_fix_yaml(response_text: str, response_text_copy = response_text_copy.replace(' |\n', ' |2\n') try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after adding indent for sections of code blocks") + get_logger().info("Successfully parsed AI prediction after adding indent for sections of code blocks") return data except: pass @@ -911,7 +910,7 @@ def try_fix_yaml(response_text: str, response_text_copy = response_text_copy.lstrip('|\n') try: data = yaml.safe_load(response_text_copy) - get_logger().info(f"Successfully parsed AI prediction after removing pipe chars") + get_logger().info("Successfully parsed AI prediction after removing pipe chars") return data except: pass @@ -1281,7 +1280,7 @@ def show_relevant_configurations(relevant_section: str) -> str: markdown_text = "" markdown_text += "\n
\n
🛠️ Relevant configurations: \n\n" markdown_text +="
These are the relevant [configurations](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) for this tool:\n\n" - markdown_text += f"**[config**]\n```yaml\n\n" + markdown_text += "**[config**]\n```yaml\n\n" for key, value in get_settings().config.items(): if key in skip_keys: continue @@ -1411,7 +1410,7 @@ def process_description(description_full: str) -> Tuple[str, List]: if '...' in file_data: pass # PR with many files. some did not get analyzed else: - get_logger().warning(f"Failed to parse description", artifact={'description': file_data}) + get_logger().warning("Failed to parse description", artifact={'description': file_data}) except Exception as e: get_logger().exception(f"Failed to process description: {e}", artifact={'description': file_data}) diff --git a/pr_agent/config_loader.py b/pr_agent/config_loader.py index ac7343f288..28a790916f 100644 --- a/pr_agent/config_loader.py +++ b/pr_agent/config_loader.py @@ -97,8 +97,8 @@ def apply_secrets_manager_config(): """ try: # Dynamic imports to avoid circular dependency (secret_providers imports config_loader) - from pr_agent.secret_providers import get_secret_provider from pr_agent.log import get_logger + from pr_agent.secret_providers import get_secret_provider secret_provider = get_secret_provider() if not secret_provider: diff --git a/pr_agent/custom_merge_loader.py b/pr_agent/custom_merge_loader.py index 2f82d29efb..8dc7d54b89 100644 --- a/pr_agent/custom_merge_loader.py +++ b/pr_agent/custom_merge_loader.py @@ -1,5 +1,5 @@ +import tomllib #tomllib should be used instead of Py toml for Python 3.11+ from pathlib import Path -import tomllib #tomllib should be used instead of Py toml for Python 3.11+ from jinja2.exceptions import SecurityError diff --git a/pr_agent/git_providers/__init__.py b/pr_agent/git_providers/__init__.py index f22194b399..31233e7b24 100644 --- a/pr_agent/git_providers/__init__.py +++ b/pr_agent/git_providers/__init__.py @@ -3,8 +3,7 @@ from pr_agent.config_loader import get_settings from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider -from pr_agent.git_providers.bitbucket_server_provider import \ - BitbucketServerProvider +from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider from pr_agent.git_providers.codecommit_provider import CodeCommitProvider from pr_agent.git_providers.gerrit_provider import GerritProvider from pr_agent.git_providers.git_provider import GitProvider @@ -12,7 +11,6 @@ from pr_agent.git_providers.github_provider import GithubProvider from pr_agent.git_providers.gitlab_provider import GitLabProvider from pr_agent.git_providers.local_git_provider import LocalGitProvider -from pr_agent.git_providers.gitea_provider import GiteaProvider _GIT_PROVIDERS = { 'github': GithubProvider, diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 39dc6b5b7c..1f9b41a7dd 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -9,9 +9,12 @@ from ..algo.file_filter import filter_ignored from ..algo.language_handler import is_valid_file -from ..algo.utils import (PRDescriptionHeader, PRReviewHeader, clip_tokens, - find_line_number_of_relevant_line_in_file, - load_large_diff) +from ..algo.utils import ( + PRDescriptionHeader, + PRReviewHeader, + find_line_number_of_relevant_line_in_file, + load_large_diff, +) from ..config_loader import get_settings from ..log import get_logger from .git_provider import GitProvider, IncrementalPR @@ -23,9 +26,19 @@ try: # noinspection PyUnresolvedReferences from azure.devops.connection import Connection + # noinspection PyUnresolvedReferences - from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition) + from azure.devops.released.git import ( + Comment, + CommentPosition, + CommentThread, + CommentThreadContext, + GitClient, + GitPullRequest, + GitVersionDescriptor, + ) from azure.devops.released.work_item_tracking import WorkItemTrackingClient + # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -476,7 +489,7 @@ def get_diff_files(self) -> list[FilePatchInfo]: diffs = filter_ignored(diffs_original, "azure") if diffs_original != diffs: try: - get_logger().info(f"Filtered out [ignore] files for pull request:", extra= + get_logger().info("Filtered out [ignore] files for pull request:", extra= {"files": diffs_original, # diffs is just a list of names "filtered_files": diffs}) except Exception: diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index e5da2ae399..bf5d9ec41f 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -264,7 +264,7 @@ def get_diff_files(self) -> list[FilePatchInfo]: names_original = [d.new.path for d in diffs_original] names_kept = [d.new.path for d in diffs] names_filtered = list(set(names_original) - set(names_kept)) - get_logger().info(f"Filtered out [ignore] files for PR", extra={ + get_logger().info("Filtered out [ignore] files for PR", extra={ 'original_files': names_original, 'names_kept': names_kept, 'names_filtered': names_filtered @@ -354,7 +354,7 @@ def get_diff_files(self) -> list[FilePatchInfo]: else: if counter_valid == MAX_FILES_ALLOWED_FULL // 2: get_logger().info( - f"Bitbucket too many files in PR, will avoid loading full content for rest of files") + "Bitbucket too many files in PR, will avoid loading full content for rest of files") original_file_content_str = "" new_file_content_str = "" except Exception as e: diff --git a/pr_agent/git_providers/bitbucket_server_provider.py b/pr_agent/git_providers/bitbucket_server_provider.py index 83426ee9f9..fe72dc7b50 100644 --- a/pr_agent/git_providers/bitbucket_server_provider.py +++ b/pr_agent/git_providers/bitbucket_server_provider.py @@ -1,21 +1,19 @@ import difflib import re - -from packaging.version import parse as parse_version +import shlex +import subprocess from typing import Optional, Tuple from urllib.parse import quote_plus, urlparse from atlassian.bitbucket import Bitbucket +from packaging.version import parse as parse_version from requests.exceptions import HTTPError -import shlex -import subprocess from ..algo.file_filter import filter_ignored from ..algo.git_patch_processing import decode_if_bytes from ..algo.language_handler import is_valid_file from ..algo.types import EDIT_TYPE, FilePatchInfo -from ..algo.utils import (find_line_number_of_relevant_line_in_file, - load_large_diff) +from ..algo.utils import find_line_number_of_relevant_line_in_file, load_large_diff from ..config_loader import get_settings from ..log import get_logger from .git_provider import GitProvider, get_git_ssl_env @@ -575,7 +573,7 @@ def _clone_inner(self, repo_url: str, dest_folder: str, operation_timeout_in_sec bearer_token = self.bearer_token if not bearer_token: #Shouldn't happen since this is checked in _prepare_clone, therefore - throwing an exception. - raise RuntimeError(f"Bearer token is required!") + raise RuntimeError("Bearer token is required!") cli_args = shlex.split(f"git clone -c http.extraHeader='Authorization: Bearer {bearer_token}' " f"--filter=blob:none --depth 1 {repo_url} {dest_folder}") diff --git a/pr_agent/git_providers/codecommit_client.py b/pr_agent/git_providers/codecommit_client.py index ef42efd67f..69aa1097d2 100644 --- a/pr_agent/git_providers/codecommit_client.py +++ b/pr_agent/git_providers/codecommit_client.py @@ -212,9 +212,9 @@ def publish_description(self, pr_number: int, pr_title: str, pr_body: str): raise ValueError(f"Invalid description for PR number: {pr_number}") from e if e.response["Error"]["Code"] == 'PullRequestAlreadyClosedException': raise ValueError(f"PR is already closed: PR number: {pr_number}") from e - raise ValueError(f"Boto3 client error calling publish_description") from e + raise ValueError("Boto3 client error calling publish_description") from e except Exception as e: - raise ValueError(f"Error calling publish_description") from e + raise ValueError("Error calling publish_description") from e def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str, annotation_file: str = None, annotation_line: int = None): """ @@ -273,6 +273,6 @@ def publish_comment(self, repo_name: str, pr_number: int, destination_commit: st raise ValueError(f"Repository does not exist: {repo_name}") from e if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException': raise ValueError(f"PR number does not exist: {pr_number}") from e - raise ValueError(f"Boto3 client error calling post_comment_for_pull_request") from e + raise ValueError("Boto3 client error calling post_comment_for_pull_request") from e except Exception as e: - raise ValueError(f"Error calling post_comment_for_pull_request") from e + raise ValueError("Error calling post_comment_for_pull_request") from e diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 70e72c8f0f..fe6e59f9a0 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -1,9 +1,9 @@ -from abc import ABC, abstractmethod # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) import os import shutil import subprocess import time +from abc import ABC, abstractmethod from typing import Optional, Tuple from pr_agent.algo.types import FilePatchInfo @@ -72,12 +72,12 @@ def get_git_ssl_env() -> dict[str, str]: if os.path.exists(ssl_cert_file): if ((requests_ca_bundle and requests_ca_bundle != ssl_cert_file) or (git_ssl_ca_info and git_ssl_ca_info != ssl_cert_file)): - get_logger().warning(f"Found mismatch among: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " - f"Using the SSL_CERT_FILE to resolve ambiguity.", + get_logger().warning("Found mismatch among: SSL_CERT_FILE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " + "Using the SSL_CERT_FILE to resolve ambiguity.", artifact={"ssl_cert_file": ssl_cert_file, "requests_ca_bundle": requests_ca_bundle, 'git_ssl_ca_info': git_ssl_ca_info}) else: - get_logger().info(f"Using SSL certificate bundle for git operations", artifact={"ssl_cert_file": ssl_cert_file}) + get_logger().info("Using SSL certificate bundle for git operations", artifact={"ssl_cert_file": ssl_cert_file}) chosen_cert_file = ssl_cert_file else: get_logger().warning("SSL certificate bundle not found for git operations", artifact={"ssl_cert_file": ssl_cert_file}) @@ -86,8 +86,8 @@ def get_git_ssl_env() -> dict[str, str]: elif requests_ca_bundle: if os.path.exists(requests_ca_bundle): if (git_ssl_ca_info and git_ssl_ca_info != requests_ca_bundle): - get_logger().warning(f"Found mismatch between: REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " - f"Using the REQUESTS_CA_BUNDLE to resolve ambiguity.", + get_logger().warning("Found mismatch between: REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO. " + "Using the REQUESTS_CA_BUNDLE to resolve ambiguity.", artifact = {"requests_ca_bundle": requests_ca_bundle, 'git_ssl_ca_info': git_ssl_ca_info}) else: get_logger().info("Using SSL certificate bundle from REQUESTS_CA_BUNDLE for git operations", @@ -192,7 +192,7 @@ def clone(self, repo_url_to_clone: str, dest_folder: str, remove_dest_folder: bo self._clone_inner(clone_url, dest_folder, operation_timeout_in_seconds) returned_obj = GitProvider.ScopedClonedRepo(dest_folder) except Exception as e: - get_logger().exception(f"Clone failed: Could not clone url.", + get_logger().exception("Clone failed: Could not clone url.", artifact={"error": str(e), "url": clone_url, "dest_folder": dest_folder}) finally: return returned_obj @@ -268,11 +268,11 @@ def get_user_description(self) -> str: description = (self.get_pr_description_full() or "").strip() description_lowercase = description.lower() - get_logger().debug(f"Existing description", description=description_lowercase) + get_logger().debug("Existing description", description=description_lowercase) # if the existing description wasn't generated by the pr-agent, just return it as-is if not self._is_generated_by_pr_agent(description_lowercase): - get_logger().info(f"Existing description was not generated by the pr-agent") + get_logger().info("Existing description was not generated by the pr-agent") self.user_description = description return description @@ -280,7 +280,7 @@ def get_user_description(self) -> str: # return nothing (empty string) because it means there is no user description user_description_header = "### **user description**" if user_description_header not in description_lowercase: - get_logger().info(f"Existing description was generated by the pr-agent, but it doesn't contain a user description") + get_logger().info("Existing description was generated by the pr-agent, but it doesn't contain a user description") return "" # otherwise, extract the original user description from the existing pr-agent description and return it @@ -303,7 +303,7 @@ def get_user_description(self) -> str: if original_user_description.lower().startswith(user_description_header): original_user_description = original_user_description[len(user_description_header):].strip() - get_logger().info(f"Extracted user description from existing description", + get_logger().info("Extracted user description from existing description", description=original_user_description) self.user_description = original_user_description return original_user_description diff --git a/pr_agent/git_providers/gitea_provider.py b/pr_agent/git_providers/gitea_provider.py index 5f3e17d9f7..e0d15939ca 100644 --- a/pr_agent/git_providers/gitea_provider.py +++ b/pr_agent/git_providers/gitea_provider.py @@ -9,12 +9,9 @@ from pr_agent.algo.git_patch_processing import decode_if_bytes from pr_agent.algo.language_handler import is_valid_file from pr_agent.algo.types import EDIT_TYPE -from pr_agent.algo.utils import (clip_tokens, - find_line_number_of_relevant_line_in_file) +from pr_agent.algo.utils import clip_tokens, find_line_number_of_relevant_line_in_file from pr_agent.config_loader import get_settings -from pr_agent.git_providers.git_provider import (MAX_FILES_ALLOWED_FULL, - FilePatchInfo, GitProvider, - IncrementalPR) +from pr_agent.git_providers.git_provider import MAX_FILES_ALLOWED_FULL, FilePatchInfo, GitProvider, IncrementalPR from pr_agent.log import get_logger diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 727912dbb5..e4c46eda96 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -11,8 +11,8 @@ from typing import Optional, Tuple from urllib.parse import urlparse -from github.Issue import Issue from github import AppAuthentication, Auth, Github, GithubException +from github.Issue import Issue from retry.api import retry_call from starlette_context import context @@ -20,14 +20,18 @@ from ..algo.git_patch_processing import extract_hunk_headers from ..algo.language_handler import is_valid_file from ..algo.types import EDIT_TYPE -from ..algo.utils import (PRReviewHeader, Range, clip_tokens, - find_line_number_of_relevant_line_in_file, - load_large_diff, set_file_languages) +from ..algo.utils import ( + PRReviewHeader, + Range, + clip_tokens, + find_line_number_of_relevant_line_in_file, + load_large_diff, + set_file_languages, +) from ..config_loader import get_settings from ..log import get_logger from ..servers.utils import RateLimitExceeded -from .git_provider import (MAX_FILES_ALLOWED_FULL, FilePatchInfo, GitProvider, - IncrementalPR, get_cached_global_settings) +from .git_provider import MAX_FILES_ALLOWED_FULL, FilePatchInfo, GitProvider, IncrementalPR, get_cached_global_settings def _next_page_url(headers: dict) -> str: @@ -150,7 +154,7 @@ def get_canonical_url_parts(self, repo_git_url:str, desired_branch:str) -> Tuple scheme_and_netloc = self.base_url_html desired_branch = self.repo_obj.default_branch if not all([scheme_and_netloc, owner, repo]): #"else": Not invoked from a PR context,but no provided git url for context - get_logger().error(f"Unable to get canonical url parts since missing context (PR or explicit git url)") + get_logger().error("Unable to get canonical url parts since missing context (PR or explicit git url)") return ("", "") prefix = f"{scheme_and_netloc}/{owner}/{repo}/blob/{desired_branch}" @@ -266,7 +270,7 @@ def _get_diff_files(self) -> list[FilePatchInfo]: try: names_original = [file.filename for file in files_original] names_new = [file.filename for file in files] - get_logger().info(f"Filtered out [ignore] files for pull request:", extra= + get_logger().info("Filtered out [ignore] files for pull request:", extra= {"files": names_original, "filtered_files": names_new}) except Exception: @@ -309,7 +313,7 @@ def _get_diff_files(self) -> list[FilePatchInfo]: if counter_valid >= MAX_FILES_ALLOWED_FULL and patch and not self.incremental.is_incremental: avoid_load = True if counter_valid == MAX_FILES_ALLOWED_FULL: - get_logger().info(f"Too many files in PR, will avoid loading full content for rest of files") + get_logger().info("Too many files in PR, will avoid loading full content for rest of files") if avoid_load: new_file_content_str = "" @@ -449,7 +453,7 @@ def _publish_check_run(self, text: str, name: str) -> bool: self._check_run_ids[name] = data["id"] return True except Exception: - get_logger().warning(f"Failed to create check run, falling back to comment") + get_logger().warning("Failed to create check run, falling back to comment") return False def _find_existing_check_run(self, check_run_name: str, head_sha: str) -> Optional[int]: @@ -516,7 +520,7 @@ def publish_inline_comments(self, comments: list[dict], disable_fallback: bool = # publish all comments in a single message self.pr.create_review(commit=self.last_commit_id, comments=comments) except Exception as e: - get_logger().info(f"Initially failed to publish inline comments as committable") + get_logger().info("Initially failed to publish inline comments as committable") if (getattr(e, "status", None) == 422 and not disable_fallback): pass # continue to try _publish_inline_comments_fallback_with_verification @@ -560,7 +564,7 @@ def get_review_thread_comments(self, comment_id: int) -> list[dict]: return thread_comments except Exception as e: - get_logger().exception(f"Failed to get review comments for an inline ask command", artifact={"comment_id": comment_id, "error": e}) + get_logger().exception("Failed to get review comments for an inline ask command", artifact={"comment_id": comment_id, "error": e}) return [] def _publish_inline_comments_fallback_with_verification(self, comments: list[dict]): @@ -708,7 +712,7 @@ def edit_comment(self, comment, body: str): "Failed to edit github comment due to permission restrictions", artifact={"error": e}) else: - get_logger().exception(f"Failed to edit github comment", artifact={"error": e}) + get_logger().exception("Failed to edit github comment", artifact={"error": e}) def edit_comment_from_comment_id(self, comment_id: int, body: str): try: diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 441eac69b0..908bc08aca 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -1,27 +1,21 @@ import difflib -import hashlib import re import urllib.parse -from typing import Any, Optional, Tuple, Union -from urllib.parse import parse_qs, urlparse +from typing import Optional, Tuple +from urllib.parse import urlparse import gitlab -import requests -from gitlab import (GitlabAuthenticationError, GitlabCreateError, - GitlabGetError, GitlabUpdateError) +from gitlab import GitlabAuthenticationError, GitlabCreateError, GitlabGetError, GitlabUpdateError from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo from ..algo.file_filter import filter_ignored from ..algo.git_patch_processing import decode_if_bytes from ..algo.language_handler import is_valid_file -from ..algo.utils import (clip_tokens, - find_line_number_of_relevant_line_in_file, - load_large_diff) +from ..algo.utils import clip_tokens, find_line_number_of_relevant_line_in_file, load_large_diff from ..config_loader import get_settings from ..log import get_logger -from .git_provider import (MAX_FILES_ALLOWED_FULL, GitProvider, - get_cached_global_settings) +from .git_provider import MAX_FILES_ALLOWED_FULL, GitProvider, get_cached_global_settings class DiffNotFoundError(Exception): @@ -439,7 +433,7 @@ def get_diff_files(self) -> list[FilePatchInfo]: new_file_content_str = self.get_pr_file_content(diff['new_path'], self.mr.diff_refs['head_sha']) else: if counter_valid == MAX_FILES_ALLOWED_FULL: - get_logger().info(f"Too many files in PR, will avoid loading full content for rest of files") + get_logger().info("Too many files in PR, will avoid loading full content for rest of files") original_file_content_str = '' new_file_content_str = '' @@ -612,7 +606,7 @@ def send_inline_comment(self, body: str, edit_type: str, found: bool, relevant_f link = self.get_line_link(relevant_file, line_start, line_end) body_fallback =f"**Suggestion:** {content} [{label}, importance: {score}]\n\n" body_fallback +=f"\n\n
[{target_file.filename} [{line_start}-{line_end}]]({link}):\n\n" - body_fallback += f"\n\n___\n\n`(Cannot implement directly - GitLab API allows committable suggestions strictly on MR diff lines)`" + body_fallback += "\n\n___\n\n`(Cannot implement directly - GitLab API allows committable suggestions strictly on MR diff lines)`" body_fallback+="
\n\n" diff_patch = difflib.unified_diff(old_code_snippet.split('\n'), new_code_snippet.split('\n'), n=999) diff --git a/pr_agent/git_providers/utils.py b/pr_agent/git_providers/utils.py index e8ab995d92..6411c4cd36 100644 --- a/pr_agent/git_providers/utils.py +++ b/pr_agent/git_providers/utils.py @@ -12,8 +12,7 @@ from starlette_context import context from pr_agent.config_loader import get_settings -from pr_agent.custom_merge_loader import (MAX_TOML_SIZE_IN_BYTES, - validate_file_security) +from pr_agent.custom_merge_loader import MAX_TOML_SIZE_IN_BYTES, validate_file_security from pr_agent.git_providers import get_git_provider_with_context from pr_agent.log import get_logger diff --git a/pr_agent/identity_providers/__init__.py b/pr_agent/identity_providers/__init__.py index aa617ef44f..73a31bfb45 100644 --- a/pr_agent/identity_providers/__init__.py +++ b/pr_agent/identity_providers/__init__.py @@ -1,6 +1,5 @@ from pr_agent.config_loader import get_settings -from pr_agent.identity_providers.default_identity_provider import \ - DefaultIdentityProvider +from pr_agent.identity_providers.default_identity_provider import DefaultIdentityProvider _IDENTITY_PROVIDERS = { 'default': DefaultIdentityProvider diff --git a/pr_agent/identity_providers/default_identity_provider.py b/pr_agent/identity_providers/default_identity_provider.py index 341f6cb958..c542e1c28a 100644 --- a/pr_agent/identity_providers/default_identity_provider.py +++ b/pr_agent/identity_providers/default_identity_provider.py @@ -1,5 +1,4 @@ -from pr_agent.identity_providers.identity_provider import (Eligibility, - IdentityProvider) +from pr_agent.identity_providers.identity_provider import Eligibility, IdentityProvider class DefaultIdentityProvider(IdentityProvider): diff --git a/pr_agent/log/__init__.py b/pr_agent/log/__init__.py index 1d02fec79f..e1c0e3adeb 100644 --- a/pr_agent/log/__init__.py +++ b/pr_agent/log/__init__.py @@ -1,4 +1,5 @@ import os + os.environ["AUTO_CAST_FOR_DYNACONF"] = "false" import json import logging diff --git a/pr_agent/mosaico/card.py b/pr_agent/mosaico/card.py index ee1ccbbf3b..8cb8a6e9bb 100644 --- a/pr_agent/mosaico/card.py +++ b/pr_agent/mosaico/card.py @@ -5,8 +5,7 @@ selects message/send vs message/stream from capabilities.streaming).""" import os -from a2a.types import (AgentCapabilities, AgentCard, AgentExtension, - AgentInterface, AgentSkill) +from a2a.types import AgentCapabilities, AgentCard, AgentExtension, AgentInterface, AgentSkill from pr_agent.algo.utils import get_version diff --git a/pr_agent/mosaico/executor.py b/pr_agent/mosaico/executor.py index 587a6c7e58..2201479af5 100644 --- a/pr_agent/mosaico/executor.py +++ b/pr_agent/mosaico/executor.py @@ -27,9 +27,7 @@ from pr_agent.config_loader import get_settings, global_settings from pr_agent.log import get_logger from pr_agent.mosaico.dispatch import route_and_run_result -from pr_agent.mosaico.observability import (langfuse_span, - mosaico_log_context, - parse_observability_metadata) +from pr_agent.mosaico.observability import langfuse_span, mosaico_log_context, parse_observability_metadata class PRAgentExecutor(AgentExecutor): @@ -95,8 +93,7 @@ async def health_check() -> str: # Construct the handler purely for its side effect of applying pr-agent's LLM # config (api_base/key/callbacks/etc.) onto the litellm module — do NOT call its # retry-wrapped chat_completion. - from pr_agent.algo.ai_handlers.litellm_ai_handler import \ - LiteLLMAIHandler + from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler handler = LiteLLMAIHandler() model = get_settings().get("CONFIG.MODEL", None) diff --git a/pr_agent/mosaico/server.py b/pr_agent/mosaico/server.py index db79bf533d..ad66582ffa 100644 --- a/pr_agent/mosaico/server.py +++ b/pr_agent/mosaico/server.py @@ -26,7 +26,6 @@ # that chain completes before any get_settings() call below, avoiding a partial-init # circular import when server.py is the first module loaded (e.g. at test collection). import pr_agent.config_loader # noqa: F401 (import-order load; do not remove) - from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.mosaico.card import build_agent_card from pr_agent.mosaico.env_bridge import apply_mosaico_env, langfuse_env_present diff --git a/pr_agent/secret_providers/__init__.py b/pr_agent/secret_providers/__init__.py index 204872e270..44114a91dd 100644 --- a/pr_agent/secret_providers/__init__.py +++ b/pr_agent/secret_providers/__init__.py @@ -8,15 +8,13 @@ def get_secret_provider(): provider_id = get_settings().config.secret_provider if provider_id == 'google_cloud_storage': try: - from pr_agent.secret_providers.google_cloud_storage_secret_provider import \ - GoogleCloudStorageSecretProvider + from pr_agent.secret_providers.google_cloud_storage_secret_provider import GoogleCloudStorageSecretProvider return GoogleCloudStorageSecretProvider() except Exception as e: raise ValueError(f"Failed to initialize google_cloud_storage secret provider {provider_id}") from e elif provider_id == 'aws_secrets_manager': try: - from pr_agent.secret_providers.aws_secrets_manager_provider import \ - AWSSecretsManagerProvider + from pr_agent.secret_providers.aws_secrets_manager_provider import AWSSecretsManagerProvider return AWSSecretsManagerProvider() except Exception as e: raise ValueError(f"Failed to initialize aws_secrets_manager secret provider {provider_id}") from e diff --git a/pr_agent/secret_providers/aws_secrets_manager_provider.py b/pr_agent/secret_providers/aws_secrets_manager_provider.py index 599369db04..03b7184bc7 100644 --- a/pr_agent/secret_providers/aws_secrets_manager_provider.py +++ b/pr_agent/secret_providers/aws_secrets_manager_provider.py @@ -1,6 +1,6 @@ import json + import boto3 -from botocore.exceptions import ClientError from pr_agent.config_loader import get_settings from pr_agent.log import get_logger diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 8eacbf664c..da73f4688c 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -49,7 +49,7 @@ async def handle_request_comment(url: str, body: str, thread_id: int, comment_id provider.set_thread_status(thread_id, "closed") provider.remove_initial_comment() except Exception as e: - get_logger().exception(f"Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e)) + get_logger().exception("Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e)) def handle_line_comment(body: str, thread_id: int, provider: AzureDevopsProvider): body = body.strip() diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index 272332767e..ad95d46406 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -129,10 +129,10 @@ async def _validate_time_from_last_commit_to_pr_update(data: dict) -> bool: if diff > 0 and diff < max_delta_seconds: is_valid_push = True else: - get_logger().debug(f"Too much time passed since last commit", + get_logger().debug("Too much time passed since last commit", artifact={'updated': time_pr_updated, 'last_commit': time_last_commit}) except Exception as e: - get_logger().exception(f"Failed to validate time difference between last commit and PR update", + get_logger().exception("Failed to validate time difference between last commit and PR update", artifact={'error': e, 'data': data}) return is_valid_push @@ -154,7 +154,7 @@ async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_ur if commands_conf == "push_commands": is_valid_push = await _validate_time_from_last_commit_to_pr_update(data) if not is_valid_push: - get_logger().info(f"Bitbucket skipping 'pullrequest:updated' for push commands") + get_logger().info("Bitbucket skipping 'pullrequest:updated' for push commands") return for command in commands: try: diff --git a/pr_agent/servers/bitbucket_server_webhook.py b/pr_agent/servers/bitbucket_server_webhook.py index e1da66d087..1c81977f9f 100644 --- a/pr_agent/servers/bitbucket_server_webhook.py +++ b/pr_agent/servers/bitbucket_server_webhook.py @@ -158,7 +158,7 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request): or (data["eventKey"] == "repo:refs_changed" and data.get("pullRequest", {}).get("id", -1) != -1)): # push event; -1 for push unassigned to a PR: #Check auto commands for creation/updating apply_repo_settings(pr_url) if not should_process_pr_logic(data): - get_logger().info(f"PR ignored due to config settings", **log_context) + get_logger().info("PR ignored due to config settings", **log_context) return JSONResponse( status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "PR ignored by config"}) ) diff --git a/pr_agent/servers/gitea_app.py b/pr_agent/servers/gitea_app.py index 4239cf51e2..51e2e97a60 100644 --- a/pr_agent/servers/gitea_app.py +++ b/pr_agent/servers/gitea_app.py @@ -73,7 +73,7 @@ async def handle_request(body: Dict[str, Any], event: str): # Handle different event types if event == "pull_request": if not should_process_pr_logic(body): - get_logger().debug(f"Request ignored: PR logic filtering") + get_logger().debug("Request ignored: PR logic filtering") return {} if action in ["opened", "reopened", "synchronized"]: await handle_pr_event(body, event, action, agent) @@ -101,8 +101,8 @@ async def handle_pr_event(body: Dict[str, Any], event: str, action: str, agent: # await agent.handle_request(api_url, command) elif action == "synchronized": # Handle push to PR - commands_on_push = get_settings().get(f"gitea.push_commands", {}) - handle_push_trigger = get_settings().get(f"gitea.handle_push_trigger", False) + commands_on_push = get_settings().get("gitea.push_commands", {}) + handle_push_trigger = get_settings().get("gitea.handle_push_trigger", False) if not commands_on_push or not handle_push_trigger: get_logger().info("Push event, but no push commands found or push trigger is disabled") return @@ -136,7 +136,7 @@ async def _perform_commands_gitea(commands_conf: str, agent: PRAgent, body: dict return {} commands = get_settings().get(f"gitea.{commands_conf}") if not commands: - get_logger().info(f"New PR, but no auto commands configured") + get_logger().info("New PR, but no auto commands configured") return get_settings().set("config.is_auto_command", True) for command in commands: diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index b94b79e32f..b947ae5a02 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -15,9 +15,7 @@ from pr_agent.agent.pr_agent import PRAgent from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings, global_settings -from pr_agent.git_providers import (get_git_provider, - get_git_provider_with_context) -from pr_agent.git_providers.git_provider import IncrementalPR +from pr_agent.git_providers import get_git_provider, get_git_provider_with_context from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.identity_providers import get_identity_provider from pr_agent.identity_providers.identity_provider import Eligibility @@ -233,7 +231,7 @@ def get_log_context(body, event, action, build_number): "request_id": uuid.uuid4().hex, "build_number": build_number, "app_name": app_name, "repo": repo, "git_org": git_org, "installation_id": installation_id} except Exception as e: - get_logger().error(f"Failed to get log context", artifact={'error': e}) + get_logger().error("Failed to get log context", artifact={'error': e}) log_context = {} return log_context, sender, sender_id, sender_type @@ -320,18 +318,18 @@ async def handle_request(body: Dict[str, Any], event: str): action = body.get("action") # "created", "opened", "reopened", "ready_for_review", "review_requested", "synchronize" get_logger().debug(f"Handling request with event: {event}, action: {action}") if not action: - get_logger().debug(f"No action found in request body, exiting handle_request") + get_logger().debug("No action found in request body, exiting handle_request") return {} agent = PRAgent() log_context, sender, sender_id, sender_type = get_log_context(body, event, action, build_number) # logic to ignore PRs opened by bot, PRs with specific titles, labels, source branches, or target branches if is_bot_user(sender, sender_type) and 'check_run' not in body: - get_logger().debug(f"Request ignored: bot user detected") + get_logger().debug("Request ignored: bot user detected") return {} if action != 'created' and 'check_run' not in body: if not should_process_pr_logic(body): - get_logger().debug(f"Request ignored: PR logic filtering") + get_logger().debug("Request ignored: PR logic filtering") return {} if 'check_run' in body: # handle failed checks @@ -339,11 +337,11 @@ async def handle_request(body: Dict[str, Any], event: str): pass # handle comments on PRs elif action == 'created': - get_logger().debug(f'Request body', artifact=body, event=event) + get_logger().debug('Request body', artifact=body, event=event) await handle_comments_on_pr(body, event, sender, sender_id, action, log_context, agent) # handle new PRs elif event == 'pull_request' and action != 'synchronize' and action != 'closed': - get_logger().debug(f'Request body', artifact=body, event=event) + get_logger().debug('Request body', artifact=body, event=event) await handle_new_pr_opened(body, event, sender, sender_id, action, log_context, agent) elif event == "issue_comment" and 'edited' in action: pass # handle_checkbox_clicked @@ -402,7 +400,7 @@ async def _perform_auto_commands_github(commands_conf: str, agent: PRAgent, body return {} commands = get_settings().get(f"github_app.{commands_conf}") if not commands: - get_logger().info(f"New PR, but no auto commands configured") + get_logger().info("New PR, but no auto commands configured") return get_settings().set("config.is_auto_command", True) for command in commands: diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index ab02339109..318776060c 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -1,6 +1,5 @@ import asyncio import multiprocessing -import time import traceback from collections import deque from datetime import datetime, timezone @@ -80,7 +79,7 @@ async def is_valid_notification(notification, headers, handled_ids, session, use pr_url = notification['subject']['url'] latest_comment = notification['subject']['latest_comment_url'] if not latest_comment or not isinstance(latest_comment, str): - get_logger().debug(f"no latest_comment") + get_logger().debug("no latest_comment") return False, handled_ids async with session.get(latest_comment, headers=headers) as comment_response: check_prev_comments = False @@ -89,21 +88,21 @@ async def is_valid_notification(notification, headers, handled_ids, session, use comment = await comment_response.json() if 'id' in comment: if comment['id'] in handled_ids: - get_logger().debug(f"comment['id'] in handled_ids") + get_logger().debug("comment['id'] in handled_ids") return False, handled_ids else: handled_ids.add(comment['id']) if 'user' in comment and 'login' in comment['user']: if comment['user']['login'] == user_id: - get_logger().debug(f"comment['user']['login'] == user_id") + get_logger().debug("comment['user']['login'] == user_id") check_prev_comments = True comment_body = comment.get('body', '') if not comment_body: - get_logger().debug(f"no comment_body") + get_logger().debug("no comment_body") check_prev_comments = True else: if user_tag not in comment_body: - get_logger().debug(f"user_tag not in comment_body") + get_logger().debug("user_tag not in comment_body") check_prev_comments = True else: get_logger().info(f"Polling, pr_url: {pr_url}", @@ -136,7 +135,7 @@ async def is_valid_notification(notification, headers, handled_ids, session, use return False, handled_ids except Exception as e: - get_logger().exception(f"Error processing polling notification", + get_logger().exception("Error processing polling notification", artifact={"notification": notification, "error": e}) return False, handled_ids @@ -211,7 +210,7 @@ async def polling_loop(): task_queue.append((process_comment_sync, (pr_url, rest_of_comment, comment_id))) get_logger().info(f"Queued comment processing for PR: {pr_url}") else: - get_logger().debug(f"Skipping comment processing for PR") + get_logger().debug("Skipping comment processing for PR") max_allowed_parallel_tasks = 10 if task_queue: diff --git a/pr_agent/servers/gitlab_webhook.py b/pr_agent/servers/gitlab_webhook.py index 6647a41b11..65173613b6 100644 --- a/pr_agent/servers/gitlab_webhook.py +++ b/pr_agent/servers/gitlab_webhook.py @@ -16,10 +16,10 @@ from pr_agent.agent.pr_agent import PRAgent from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings, global_settings +from pr_agent.git_providers import get_git_provider_with_context from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.secret_providers import get_secret_provider -from pr_agent.git_providers import get_git_provider_with_context setup_logger(fmt=LoggingFormat.JSON, level=get_settings().get("CONFIG.LOG_LEVEL", "DEBUG")) router = APIRouter() @@ -238,8 +238,8 @@ async def inner(data: dict): # Apply repo settings before checking push commands or handle_push_trigger apply_repo_settings(url) - commands_on_push = get_settings().get(f"gitlab.push_commands", {}) - handle_push_trigger = get_settings().get(f"gitlab.handle_push_trigger", False) + commands_on_push = get_settings().get("gitlab.push_commands", {}) + handle_push_trigger = get_settings().get("gitlab.handle_push_trigger", False) if not commands_on_push or not handle_push_trigger: get_logger().info("Push event, but no push commands found or push trigger is disabled") return JSONResponse(status_code=status.HTTP_200_OK, diff --git a/pr_agent/servers/help.py b/pr_agent/servers/help.py index 287d2e360f..0a525601c7 100644 --- a/pr_agent/servers/help.py +++ b/pr_agent/servers/help.py @@ -37,7 +37,7 @@ def get_review_usage_guide(): ``` """ - output += f"\n\nSee the review [usage page](https://pr-agent-docs.codium.ai/tools/review/) for a comprehensive guide on using this tool.\n\n" + output += "\n\nSee the review [usage page](https://pr-agent-docs.codium.ai/tools/review/) for a comprehensive guide on using this tool.\n\n" return output @@ -134,7 +134,7 @@ def get_describe_usage_guide(): output += "
" + markdown_text += "
" markdown_text += f"{emoji} {key_nice}: {value}" - markdown_text += f"
" + markdown_text += "
" if is_value_no(value): markdown_text += f"{emoji} No relevant tests" else: markdown_text += f"{emoji} PR contains tests" - markdown_text += f"
{emoji} Contribution time estimate (best, average, worst case): " markdown_text += f"{value['best_case'].replace('m', ' minutes')} | {value['average_case'].replace('m', ' minutes')} | {value['worst_case'].replace('m', ' minutes')}" - markdown_text += f"
" + markdown_text += "
" if is_value_no(value): markdown_text += f"{emoji} No security concerns identified" else: markdown_text += f"{emoji} Security concerns

\n\n" value = emphasize_header(value.strip()) markdown_text += f"{value}" - markdown_text += f"
" if is_value_no(value): - markdown_text += f"✅ No TODO sections" + markdown_text += "✅ No TODO sections" else: markdown_todo_items = format_todo_items(value, git_provider, gfm_supported) markdown_text += f"{emoji} TODO sections\n

\n" @@ -248,29 +247,29 @@ def convert_to_markdown_v2(output_data: dict, markdown_text += "
" + markdown_text += "
" markdown_text += process_can_be_split(emoji, value) - markdown_text += f"
" + markdown_text += "
" markdown_text += f"{emoji} No major issues detected" - markdown_text += f"
" + markdown_text += "
" # markdown_text += f"{emoji} {key_nice}

\n\n" markdown_text += f"{emoji} Recommended focus areas for review

\n\n" else: @@ -310,12 +309,12 @@ def convert_to_markdown_v2(output_data: dict, except Exception as e: get_logger().exception(f"Failed to process 'Recommended focus areas for review': {e}") if gfm_supported: - markdown_text += f"
" + markdown_text += "
" markdown_text += f"{emoji} {key_nice}: {value}" - markdown_text += f"
\n\n" + markdown_text += "
\n\n" markdown_text += f"**{emoji} Ticket compliance analysis {compliance_emoji}**\n\n" markdown_text += ticket_compliance_str - markdown_text += f"
{sub_key}      \n\n\n\n{sub_value.strip()}\n\n\n
relevant line
relevant line
" - output += f"\n\nSee the [describe usage](https://pr-agent-docs.codium.ai/tools/describe/) page for a comprehensive guide on using this tool.\n\n" + output += "\n\nSee the [describe usage](https://pr-agent-docs.codium.ai/tools/describe/) page for a comprehensive guide on using this tool.\n\n" return output @@ -160,7 +160,7 @@ def get_ask_usage_guide(): # # output += "" - output += f"\n\nSee the [ask usage](https://pr-agent-docs.codium.ai/tools/ask/) page for a comprehensive guide on using this tool.\n\n" + output += "\n\nSee the [ask usage](https://pr-agent-docs.codium.ai/tools/ask/) page for a comprehensive guide on using this tool.\n\n" return output @@ -187,7 +187,7 @@ def get_improve_usage_guide(): """ - output += f"\n\nSee the improve [usage page](https://pr-agent-docs.codium.ai/tools/improve/) for a comprehensive guide on using this tool.\n\n" + output += "\n\nSee the improve [usage page](https://pr-agent-docs.codium.ai/tools/improve/) for a comprehensive guide on using this tool.\n\n" return output @@ -202,5 +202,5 @@ def get_help_docs_usage_guide(): /help_docs "..." ``` """ - output += f"\n\nSee the [help_docs usage](https://pr-agent-docs.codium.ai/tools/help_docs/) page for a comprehensive guide on using this tool.\n\n" + output += "\n\nSee the [help_docs usage](https://pr-agent-docs.codium.ai/tools/help_docs/) page for a comprehensive guide on using this tool.\n\n" return output diff --git a/pr_agent/tools/pr_add_docs.py b/pr_agent/tools/pr_add_docs.py index 3ec97b31ce..e23c13c269 100644 --- a/pr_agent/tools/pr_add_docs.py +++ b/pr_agent/tools/pr_add_docs.py @@ -119,7 +119,7 @@ def push_inline_docs(self, data): new_code_snippet = self.dedent_code(relevant_file, relevant_line, documentation, doc_placement, add_original_line=True) - body = f"**Suggestion:** Proposed documentation\n```suggestion\n" + new_code_snippet + "\n```" + body = "**Suggestion:** Proposed documentation\n```suggestion\n" + new_code_snippet + "\n```" docs.append({'body': body, 'relevant_file': relevant_file, 'relevant_lines_start': relevant_line, 'relevant_lines_end': relevant_line}) diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index f950ce9e01..192be02cb0 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -14,19 +14,30 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler from pr_agent.algo.git_patch_processing import decouple_and_convert_to_hunks_with_lines_numbers -from pr_agent.algo.pr_processing import (add_ai_metadata_to_diff_files, - get_pr_diff, get_pr_multi_diffs, - retry_with_fallback_models) -from pr_agent.algo.skills_loader import get_skills_context +from pr_agent.algo.pr_processing import ( + add_ai_metadata_to_diff_files, + get_pr_diff, + get_pr_multi_diffs, + retry_with_fallback_models, +) from pr_agent.algo.repo_context import build_repo_context +from pr_agent.algo.skills_loader import get_skills_context from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import (ModelType, load_yaml, replace_code_tags, - show_relevant_configurations, get_max_tokens, clip_tokens, get_model) +from pr_agent.algo.utils import ( + ModelType, + clip_tokens, + get_max_tokens, + get_model, + load_yaml, + replace_code_tags, + show_relevant_configurations, +) from pr_agent.config_loader import get_settings -from pr_agent.git_providers import (AzureDevopsProvider, GithubProvider, - GitLabProvider, get_git_provider, - get_git_provider_with_context) -from pr_agent.git_providers.git_provider import get_main_pr_language, GitProvider +from pr_agent.git_providers import ( + GithubProvider, + get_git_provider_with_context, +) +from pr_agent.git_providers.git_provider import GitProvider, get_main_pr_language from pr_agent.log import get_logger from pr_agent.servers.help import HelpMessage from pr_agent.tools.pr_description import insert_br_after_x_chars @@ -55,10 +66,10 @@ def __init__(self, pr_url: str, cli_mode=False, args: list = None, if (self.pr_description_files and get_settings().get("config.is_auto_command", False) and get_settings().get("config.enable_ai_metadata", False)): add_ai_metadata_to_diff_files(self.git_provider, self.pr_description_files) - get_logger().debug(f"AI metadata added to the this command") + get_logger().debug("AI metadata added to the this command") else: get_settings().set("config.enable_ai_metadata", False) - get_logger().debug(f"AI metadata is disabled for this command") + get_logger().debug("AI metadata is disabled for this command") self.vars = { "title": self.git_provider.pr.title, @@ -138,7 +149,7 @@ async def run(self): # generate summarized suggestions pr_body = self.generate_summarized_suggestions(data) - get_logger().debug(f"PR output", artifact=pr_body) + get_logger().debug("PR output", artifact=pr_body) # require self-review if get_settings().pr_code_suggestions.demand_code_suggestions_self_review: @@ -194,7 +205,7 @@ async def run(self): else: try: self.git_provider.remove_initial_comment() - self.git_provider.publish_comment(f"Failed to generate code suggestions for PR") + self.git_provider.publish_comment("Failed to generate code suggestions for PR") except Exception as e: get_logger().exception(f"Failed to update persistent review, error: {e}") @@ -216,7 +227,7 @@ async def publish_no_suggestions(self): if (get_settings().config.publish_output and get_settings().pr_code_suggestions.get('publish_output_no_suggestions', True)): get_logger().warning('No code suggestions found for the PR.') - get_logger().debug(f"PR output", artifact=pr_body) + get_logger().debug("PR output", artifact=pr_body) if self.progress_response: self.git_provider.edit_comment(self.progress_response, body=pr_body) else: @@ -233,7 +244,7 @@ async def dual_publishing(self, data): and suggestion.get('improved_code'): data_above_threshold['code_suggestions'].append(suggestion) if not data_above_threshold['code_suggestions'][-1]['existing_code']: - get_logger().info(f'Identical existing and improved code for dual publishing found') + get_logger().info('Identical existing and improved code for dual publishing found') data_above_threshold['code_suggestions'][-1]['existing_code'] = suggestion[ 'improved_code'] if data_above_threshold['code_suggestions']: @@ -266,7 +277,7 @@ def _extract_link(comment_text: str): up_to_commit_txt = f" up to commit {match.group(0)[4:-3].strip()}" return up_to_commit_txt - history_header = f"#### Previous suggestions\n" + history_header = "#### Previous suggestions\n" last_commit_num = git_provider.get_latest_commit_url().split('/')[-1][:7] if only_fold: # A user clicked on the 'self-review' checkbox text = get_settings().pr_code_suggestions.code_suggestions_self_review_text @@ -381,10 +392,10 @@ async def _prepare_prediction(self, model: str) -> dict: self.patches_diff_no_line_number = self.remove_line_numbers([self.patches_diff])[0] if self.patches_diff: - get_logger().debug(f"PR diff", artifact=self.patches_diff) + get_logger().debug("PR diff", artifact=self.patches_diff) self.prediction = await self._get_prediction(model, self.patches_diff, self.patches_diff_no_line_number) else: - get_logger().warning(f"Empty PR diff") + get_logger().warning("Empty PR diff") self.prediction = None data = self.prediction @@ -412,7 +423,7 @@ async def _get_prediction(self, model: str, patches_diff: str, patches_diff_no_l if model_reflect_with_reasoning == get_settings().config.model and model != get_settings().config.model and fallbacks and model == \ fallbacks[0]: # we are using a fallback model (should not happen on regular conditions) - get_logger().warning(f"Using the same model for self-reflection as the one used for suggestions") + get_logger().warning("Using the same model for self-reflection as the one used for suggestions") model_reflect_with_reasoning = model response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"], patches_diff, model=model_reflect_with_reasoning) @@ -453,7 +464,7 @@ async def analyze_self_reflection_response(self, data, response_reflect): label = label.replace('
', ' ') suggestion_statistics_dict = {'score': score, 'label': label} - get_logger().info(f"PR-Agent suggestions statistics", + get_logger().info("PR-Agent suggestions statistics", statistics=suggestion_statistics_dict, analytics=True) except Exception as e: get_logger().error(f"Failed to log suggestion statistics, error: {e}") @@ -638,17 +649,17 @@ def validate_one_liner_suggestion_not_repeating_code(self, suggestion): if file.filename.strip() == relevant_file: # protections if not file.head_file: - get_logger().info(f"head_file is empty") + get_logger().info("head_file is empty") return suggestion head_file = file.head_file base_file = file.base_file if existing_code in base_file and existing_code not in head_file and new_code in head_file: suggestion["score"] = 0 get_logger().warning( - f"existing_code is in the base file but not in the head file, setting score to 0", + "existing_code is in the base file but not in the head file, setting score to 0", artifact={"suggestion": suggestion}) except Exception as e: - get_logger().exception(f"Error validating one-liner suggestion", artifact={"error": e}) + get_logger().exception("Error validating one-liner suggestion", artifact={"error": e}) return suggestion @@ -703,7 +714,7 @@ async def prepare_prediction_main(self, model: str) -> dict: if self.patches_diff_list: get_logger().info(f"Number of PR chunk calls: {len(self.patches_diff_list)}") - get_logger().debug(f"PR diff:", artifact=self.patches_diff_list) + get_logger().debug("PR diff:", artifact=self.patches_diff_list) # parallelize calls to AI: if get_settings().pr_code_suggestions.parallel_calls: @@ -736,7 +747,7 @@ async def prepare_prediction_main(self, model: str) -> dict: artifact={"prediction": prediction}) self.data = data else: - get_logger().warning(f"Empty PR diff list") + get_logger().warning("Empty PR diff list") self.data = data = None return data @@ -772,7 +783,7 @@ async def convert_to_decoupled_with_line_numbers(self, patches_diff_list_no_line patches_diff_list.append(patch_final) return patches_diff_list except Exception as e: - get_logger().exception(f"Error converting to decoupled with line numbers", + get_logger().exception("Error converting to decoupled with line numbers", artifact={'patches_diff_list_no_line_numbers': patches_diff_list_no_line_numbers}) return [] @@ -794,7 +805,7 @@ def generate_summarized_suggestions(self, data: Dict) -> str: extension_to_language[ext] = language pr_body += "" - header = f"Suggestion" + header = "Suggestion" delta = 66 header += "  " * delta pr_body += f"""""" @@ -851,9 +862,9 @@ def generate_summarized_suggestions(self, data: Dict) -> str: example_code = "" example_code += f"```diff\n{patch.rstrip()}\n```\n" if i == 0: - pr_body += f"""" + pr_body += "" counter_suggestions += 1 # pr_body += "" diff --git a/pr_agent/tools/pr_config.py b/pr_agent/tools/pr_config.py index 24ecaab97b..f97dd92766 100644 --- a/pr_agent/tools/pr_config.py +++ b/pr_agent/tools/pr_config.py @@ -66,7 +66,7 @@ def _prepare_pr_configs(self) -> str: markdown_text = "
🛠️ PR-Agent Configurations: \n\n" - markdown_text += f"\n\n```yaml\n\n" + markdown_text += "\n\n```yaml\n\n" for header, configs in relevant_configs.items(): if configs: markdown_text += "\n\n" @@ -80,5 +80,5 @@ def _prepare_pr_configs(self) -> str: markdown_text += " " markdown_text += "\n```" markdown_text += "\n
\n" - get_logger().info(f"Possible Configurations outputted to PR comment", artifact=markdown_text) + get_logger().info("Possible Configurations outputted to PR comment", artifact=markdown_text) return markdown_text diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 953568ca80..259db3721a 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -10,26 +10,33 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler -from pr_agent.algo.pr_processing import (OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD, - get_pr_diff, - get_pr_diff_multiple_patchs, - retry_with_fallback_models) -from pr_agent.algo.skills_loader import get_skills_context +from pr_agent.algo.pr_processing import ( + OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD, + get_pr_diff, + get_pr_diff_multiple_patchs, + retry_with_fallback_models, +) from pr_agent.algo.repo_context import build_repo_context +from pr_agent.algo.skills_loader import get_skills_context from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import (ModelType, PRDescriptionHeader, clip_tokens, - get_max_tokens, get_user_labels, load_yaml, - set_custom_labels, - show_relevant_configurations) +from pr_agent.algo.utils import ( + ModelType, + PRDescriptionHeader, + clip_tokens, + get_max_tokens, + get_user_labels, + load_yaml, + set_custom_labels, + show_relevant_configurations, +) from pr_agent.config_loader import get_settings -from pr_agent.git_providers import (GithubProvider, get_git_provider, - get_git_provider_with_context) +from pr_agent.git_providers import GithubProvider, get_git_provider_with_context from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.log import get_logger from pr_agent.servers.help import HelpMessage from pr_agent.tools.ticket_pr_compliance_check import ( - extract_and_cache_pr_tickets, extract_ticket_links_from_pr_description, - extract_tickets) + extract_and_cache_pr_tickets, +) class PRDescription: @@ -124,7 +131,7 @@ async def run(self): if get_settings().pr_description.publish_labels: pr_labels = self._prepare_labels() else: - get_logger().debug(f"Publishing labels disabled") + get_logger().debug("Publishing labels disabled") if get_settings().pr_description.use_description_markers: pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer_with_markers() @@ -162,15 +169,15 @@ async def run(self): # publish labels if get_settings().pr_description.publish_labels and pr_labels and self.git_provider.is_supported("get_labels"): original_labels = self.git_provider.get_pr_labels(update=True) - get_logger().debug(f"original labels", artifact=original_labels) + get_logger().debug("original labels", artifact=original_labels) user_labels = get_user_labels(original_labels) new_labels = pr_labels + user_labels - get_logger().debug(f"published labels", artifact=new_labels) + get_logger().debug("published labels", artifact=new_labels) if set(new_labels) != set(original_labels): get_logger().info(f"Setting describe labels:\n{new_labels}") self.git_provider.publish_labels(new_labels) else: - get_logger().debug(f"Labels are the same, not updating") + get_logger().debug("Labels are the same, not updating") # publish description if get_settings().pr_description.publish_description_as_comment: @@ -224,7 +231,7 @@ async def _prepare_prediction(self, model: str) -> None: self.patches_diff = patches_diff if patches_diff: # generate the prediction - get_logger().debug(f"PR diff", artifact=self.patches_diff) + get_logger().debug("PR diff", artifact=self.patches_diff) self.prediction = await self._get_prediction(model, patches_diff, prompt="pr_description_prompt") # extend the prediction with additional files not shown @@ -312,7 +319,7 @@ async def _prepare_prediction(self, model: str) -> None: num_input_tokens=tokens_files_walkthrough) # PR header inference - get_logger().debug(f"PR diff only description", artifact=files_walkthrough_prompt) + get_logger().debug("PR diff only description", artifact=files_walkthrough_prompt) prediction_headers = await self._get_prediction(model, patches_diff=files_walkthrough_prompt, prompt="pr_description_only_description_prompts") prediction_headers = prediction_headers.strip().removeprefix('```yaml').strip('`').strip() @@ -356,7 +363,7 @@ async def extend_uncovered_files(self, original_prediction: str) -> str: # add up to MAX_EXTRA_FILES_TO_OUTPUT files counter_extra_files += 1 if counter_extra_files > MAX_EXTRA_FILES_TO_OUTPUT: - extra_file_yaml = f"""\ + extra_file_yaml = """\ - filename: | Additional files not shown changes_title: | @@ -648,7 +655,7 @@ def _prepare_file_labels(self): filename = file['filename'].replace("'", "`").replace('"', '`') changes_summary = file.get('changes_summary', "") if not changes_summary and self.vars.get('include_file_summary_changes', True): - get_logger().warning(f"Empty changes summary in file label dict, skipping file", + get_logger().warning("Empty changes summary in file label dict, skipping file", artifact={"file": file}) continue changes_summary = changes_summary.strip() @@ -677,7 +684,7 @@ def process_pr_files_prediction(self, pr_body, value): return pr_body, pr_comments try: pr_body += "
Category{header}Impact
\n\n""" + pr_body += """\n\n""" else: - pr_body += f"""
\n\n""" + pr_body += """
\n\n""" suggestion_summary = suggestion['one_sentence_summary'].strip().rstrip('.') if "'<" in suggestion_summary and ">'" in suggestion_summary: # escape the '<' and '>' characters, otherwise they are interpreted as html tags @@ -874,9 +885,9 @@ def generate_summarized_suggestions(self, data: Dict) -> str: if suggestion.get('score_why'): pr_body += f"
Suggestion importance[1-10]: {suggestion['score']}\n\n" pr_body += f"__\n\nWhy: {suggestion['score_why']}\n\n" - pr_body += f"
" + pr_body += "" - pr_body += f"" + pr_body += "" # # add another column for 'score' score_int = int(suggestion.get('score', 0)) @@ -885,7 +896,7 @@ def generate_summarized_suggestions(self, data: Dict) -> str: score_str = self.get_score_str(score_int) pr_body += f"
{score_str}\n\n" - pr_body += f"
" - header = f"Relevant files" + header = "Relevant files" delta = 75 # header += "  " * delta pr_body += f"""""" @@ -690,7 +697,7 @@ def process_pr_files_prediction(self, pr_body, value): if use_collapsible_file_list: pr_body += f"""
{header}
{len(list_tuples)} files""" else: - pr_body += f"""
""" + pr_body += """
""" for filename, file_changes_title, file_change_description in list_tuples: filename = filename.replace("'", "`").rstrip() filename_publish = filename.split("/")[-1] diff --git a/pr_agent/tools/pr_generate_labels.py b/pr_agent/tools/pr_generate_labels.py index dae25d914e..94a4bc32f6 100644 --- a/pr_agent/tools/pr_generate_labels.py +++ b/pr_agent/tools/pr_generate_labels.py @@ -1,7 +1,6 @@ import copy -import re from functools import partial -from typing import List, Tuple +from typing import List from jinja2 import Environment, StrictUndefined diff --git a/pr_agent/tools/pr_help_docs.py b/pr_agent/tools/pr_help_docs.py index aa7002189c..ffcdb72cc0 100644 --- a/pr_agent/tools/pr_help_docs.py +++ b/pr_agent/tools/pr_help_docs.py @@ -1,18 +1,18 @@ import copy -from functools import partial - -from jinja2 import Environment, StrictUndefined import math import os import re +from functools import partial from tempfile import TemporaryDirectory +from jinja2 import Environment, StrictUndefined + from pr_agent.algo import MAX_TOKENS from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler from pr_agent.algo.pr_processing import retry_with_fallback_models from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import clip_tokens, get_max_tokens, load_yaml, ModelType +from pr_agent.algo.utils import ModelType, clip_tokens, get_max_tokens, load_yaml from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider_with_context from pr_agent.log import get_logger @@ -113,7 +113,7 @@ def return_document_headings(text: str, ext: str) -> str: return '\n'.join(headings) except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" # Load documentation files to memory: full file path (as will be given as prompt) -> doc contents @@ -139,7 +139,7 @@ def map_documentation_files_to_contents(base_path: str, doc_files: list[str], ma get_logger().error("Couldn't find any usable documentation files. Returning empty dict.") return returned_dict except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty dict.") + get_logger().exception("Unexpected exception thrown. Returning empty dict.") return {} # Goes over files' contents, generating payload for prompt while decorating them with a header to mark where each file begins, @@ -163,7 +163,7 @@ def aggregate_documentation_files_for_prompt_contents(file_path_to_contents: dic docs_prompt += f"\n==file name==\n\n{file_path}\n\n==file content==\n\n{file_contents}\n=========\n\n" return docs_prompt except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" def format_markdown_q_and_a_response(question_str: str, response_str: str, relevant_sections: list[dict[str, str]], @@ -173,7 +173,7 @@ def format_markdown_q_and_a_response(question_str: str, response_str: str, relev answer_str = "" answer_str += f"### Question: \n{question_str}\n\n" answer_str += f"### Answer:\n{response_str.strip()}\n\n" - answer_str += f"#### Relevant Sources:\n\n" + answer_str += "#### Relevant Sources:\n\n" for section in relevant_sections: file = section.get('file_name').lstrip('/').strip() #Remove any '/' in the beginning, since some models do it anyway ext = [suffix for suffix in supported_suffixes if file.endswith(suffix)] @@ -188,7 +188,7 @@ def format_markdown_q_and_a_response(question_str: str, response_str: str, relev answer_str += f"> - {base_url_prefix}/{file}{base_url_suffix}\n" return answer_str except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" def format_markdown_header(header: str) -> str: @@ -215,7 +215,7 @@ def format_markdown_header(header: str) -> str: # Perform replacements in a single pass and convert to lowercase return pattern.sub(lambda m: replacements[m.group()], cleaned).lower() except Exception: - get_logger().exception(f"Error while formatting markdown header", artifacts={'header': header}) + get_logger().exception("Error while formatting markdown header", artifacts={'header': header}) return "" def clean_markdown_content(content: str) -> str: @@ -253,7 +253,7 @@ def clean_markdown_content(content: str) -> str: r'\2', content, flags=re.DOTALL) return content.strip() except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" class PredictionPreparator: @@ -265,7 +265,7 @@ def __init__(self, ai_handler, vars, system_prompt, user_prompt): self.system_prompt = environment.from_string(system_prompt).render(variables) self.user_prompt = environment.from_string(user_prompt).render(variables) except Exception as e: - get_logger().exception(f"Caught exception during init. Setting ai_handler to None to prevent __call__.") + get_logger().exception("Caught exception during init. Setting ai_handler to None to prevent __call__.") self.ai_handler = None #Called by retry_with_fallback_models and therefore, on any failure must throw an exception: @@ -323,7 +323,7 @@ def __init__(self, ctx_url, ai_handler:partial[BaseAiHandler,] = LiteLLMAIHandle get_settings().pr_help_docs_prompts.system, get_settings().pr_help_docs_prompts.user) except Exception as e: - get_logger().exception(f"Caught exception during init. Setting self.question to None to prevent run() to do anything.") + get_logger().exception("Caught exception during init. Setting self.question to None to prevent run() to do anything.") self.question = None async def run(self): @@ -338,7 +338,7 @@ async def run(self): #Generate prompt for the AI model. This will be the full text of all the documentation files combined. docs_prompt = aggregate_documentation_files_for_prompt_contents(docs_filepath_to_contents) if not docs_filepath_to_contents or not docs_prompt: - get_logger().warning(f"Could not find any usable documentation. Returning with no result...") + get_logger().warning("Could not find any usable documentation. Returning with no result...") return None docs_prompt_to_send_to_model = docs_prompt @@ -376,7 +376,7 @@ async def run(self): artifacts={'raw_response': response, 'response_str': response_str, 'relevant_sections': relevant_sections}) return if int(response_yaml.get('question_is_relevant', '1')) == 0: - get_logger().warning(f"Question is not relevant. Returning without an answer...", + get_logger().warning("Question is not relevant. Returning without an answer...", artifacts={'raw_response': response}) return @@ -415,7 +415,7 @@ def _find_all_document_files_matching_exts(self, abs_docs_path: str, ignore_read return matching_files return matching_files except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty list.") + get_logger().exception("Unexpected exception thrown. Returning empty list.") return [] def _gen_filenames_to_contents_map_from_repo(self) -> dict[str, str]: @@ -426,7 +426,7 @@ def _gen_filenames_to_contents_map_from_repo(self) -> dict[str, str]: if not returned_cloned_repo_root: raise Exception(f"Failed to clone {self.repo_url} to {tmp_dir}") - get_logger().debug(f"About to gather relevant documentation files...") + get_logger().debug("About to gather relevant documentation files...") doc_files = [] if self.include_root_readme_file: for root, _, files in os.walk(returned_cloned_repo_root.path): @@ -451,7 +451,7 @@ def _gen_filenames_to_contents_map_from_repo(self) -> dict[str, str]: return map_documentation_files_to_contents(returned_cloned_repo_root.path, doc_files) except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty dict.") + get_logger().exception("Unexpected exception thrown. Returning empty dict.") return {} def _trim_docs_input(self, docs_input: str, max_allowed_txt_input: int, only_return_if_trim_needed=False) -> bool|str: @@ -488,7 +488,7 @@ def _trim_docs_input(self, docs_input: str, max_allowed_txt_input: int, only_ret # Unexpected exception. Rethrowing it since: # 1. This is an internal function. # 2. An empty str/False result is a valid one - would require now checking also for None. - get_logger().exception(f"Unexpected exception thrown. Rethrowing it...") + get_logger().exception("Unexpected exception thrown. Rethrowing it...") raise e async def _rank_docs_and_return_them_as_prompt(self, docs_filepath_to_contents: dict[str, str], max_allowed_txt_input: int) -> str: @@ -530,7 +530,7 @@ async def _rank_docs_and_return_them_as_prompt(self, docs_filepath_to_contents: return "" return docs_prompt_to_send_to_model except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" def _format_model_answer(self, response_str: str, relevant_sections: list[dict[str, str]]) -> str: @@ -545,10 +545,10 @@ def _format_model_answer(self, response_str: str, relevant_sections: list[dict[s answer_str = modify_answer_section(answer_str) #In case the response should not be published and returned as string, stop here: if answer_str and self.return_as_string: - get_logger().info(f"Chat help docs answer", artifacts={'answer_str': answer_str}) + get_logger().info("Chat help docs answer", artifacts={'answer_str': answer_str}) return answer_str if not answer_str: - get_logger().info(f"No answer found") + get_logger().info("No answer found") return "" if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_help_docs.enable_help_text: answer_str += "
\n\n
💡 Tool usage guide:
\n\n" @@ -556,5 +556,5 @@ def _format_model_answer(self, response_str: str, relevant_sections: list[dict[s answer_str += "\n
\n" return answer_str except Exception as e: - get_logger().exception(f"Unexpected exception thrown. Returning empty result.") + get_logger().exception("Unexpected exception thrown. Returning empty result.") return "" diff --git a/pr_agent/tools/pr_help_message.py b/pr_agent/tools/pr_help_message.py index a83e03e0c7..a33f0eac64 100644 --- a/pr_agent/tools/pr_help_message.py +++ b/pr_agent/tools/pr_help_message.py @@ -10,7 +10,7 @@ from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler from pr_agent.algo.pr_processing import retry_with_fallback_models from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import ModelType, clip_tokens, load_yaml, get_max_tokens +from pr_agent.algo.utils import ModelType, clip_tokens, get_max_tokens, load_yaml from pr_agent.config_loader import get_settings from pr_agent.git_providers import BitbucketServerProvider, GithubProvider, get_git_provider_with_context from pr_agent.log import get_logger @@ -89,7 +89,7 @@ def format_markdown_header(self, header: str) -> str: # Perform replacements in a single pass and convert to lowercase return pattern.sub(lambda m: replacements[m.group()], cleaned).lower() except Exception: - get_logger().exception(f"Error while formatting markdown header", artifacts={'header': header}) + get_logger().exception("Error while formatting markdown header", artifacts={'header': header}) return "" @@ -151,7 +151,7 @@ async def run(self): get_logger().warning(f"failing to parse response: {response_yaml}, publishing the response as is") if get_settings().config.publish_output: answer_str = f"### Question: \n{self.question_str}\n\n" - answer_str += f"### Answer:\n\n" + answer_str += "### Answer:\n\n" answer_str += response_yaml self.git_provider.publish_comment(answer_str) return "" @@ -162,8 +162,8 @@ async def run(self): get_logger().info(f"Could not find relevant answer for the question: {self.question_str}") if get_settings().config.publish_output: answer_str = f"### Question: \n{self.question_str}\n\n" - answer_str += f"### Answer:\n\n" - answer_str += f"Could not find relevant information to answer the question. Please provide more details and try again." + answer_str += "### Answer:\n\n" + answer_str += "Could not find relevant information to answer the question. Please provide more details and try again." self.git_provider.publish_comment(answer_str) return "" @@ -172,7 +172,7 @@ async def run(self): if response_str: answer_str += f"### Question: \n{self.question_str}\n\n" answer_str += f"### Answer:\n{response_str.strip()}\n\n" - answer_str += f"#### Relevant Sources:\n\n" + answer_str += "#### Relevant Sources:\n\n" base_path = "https://qodo-merge-docs.qodo.ai/" for section in relevant_sections: file = section.get('file_name').strip().removesuffix('.md') @@ -247,21 +247,21 @@ async def run(self): checkbox_list.append("[*]") if isinstance(self.git_provider, GithubProvider) and not get_settings().config.get('disable_checkboxes', False): - pr_comment += f"
" + pr_comment += "
ToolDescriptionTrigger Interactively :gem:
" for i in range(len(tool_names)): pr_comment += f"\n\n\n" pr_comment += "
ToolDescriptionTrigger Interactively :gem:
\n\n{tool_names[i]}{descriptions[i]}\n\n{checkbox_list[i]}\n
\n\n" - pr_comment += f"""\n\n(1) Note that each tool can be [triggered automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#github-app-automatic-tools-when-a-new-pr-is-opened) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage).""" - pr_comment += f"""\n\n(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the `/ask` tool, you need to comment on a PR: `/ask ""`. See the relevant documentation for each tool for more details.""" + pr_comment += """\n\n(1) Note that each tool can be [triggered automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#github-app-automatic-tools-when-a-new-pr-is-opened) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage).""" + pr_comment += """\n\n(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the `/ask` tool, you need to comment on a PR: `/ask ""`. See the relevant documentation for each tool for more details.""" elif isinstance(self.git_provider, BitbucketServerProvider): # only support basic commands in BBDC pr_comment = generate_bbdc_table(tool_names[:4], descriptions[:4]) else: - pr_comment += f"" + pr_comment += "
ToolCommandDescription
" for i in range(len(tool_names)): pr_comment += f"\n" pr_comment += "
ToolCommandDescription
\n\n{tool_names[i]}{commands[i]}{descriptions[i]}
\n\n" - pr_comment += f"""\n\nNote that each tool can be [invoked automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage).""" + pr_comment += """\n\nNote that each tool can be [invoked automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage).""" if get_settings().config.publish_output: self.git_provider.publish_comment(pr_comment) diff --git a/pr_agent/tools/pr_line_questions.py b/pr_agent/tools/pr_line_questions.py index 3ae5d0040b..025c5296ca 100644 --- a/pr_agent/tools/pr_line_questions.py +++ b/pr_agent/tools/pr_line_questions.py @@ -1,4 +1,3 @@ -import argparse import copy from functools import partial @@ -6,9 +5,8 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler -from pr_agent.algo.git_patch_processing import ( - decouple_and_convert_to_hunks_with_lines_numbers, extract_hunk_lines_from_patch) -from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models +from pr_agent.algo.git_patch_processing import extract_hunk_lines_from_patch +from pr_agent.algo.pr_processing import retry_with_fallback_models from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.utils import ModelType from pr_agent.config_loader import get_settings @@ -16,7 +14,7 @@ from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.github_provider import GithubProvider from pr_agent.log import get_logger -from pr_agent.servers.help import HelpMessage + class PR_LineQuestions: def __init__(self, pr_url: str, args=None, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler): diff --git a/pr_agent/tools/pr_questions.py b/pr_agent/tools/pr_questions.py index e03c707a3a..aad8e41e04 100644 --- a/pr_agent/tools/pr_questions.py +++ b/pr_agent/tools/pr_questions.py @@ -9,7 +9,7 @@ from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.utils import ModelType from pr_agent.config_loader import get_settings -from pr_agent.git_providers import get_git_provider, GitLabProvider +from pr_agent.git_providers import GitLabProvider, get_git_provider from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.log import get_logger from pr_agent.servers.help import HelpMessage @@ -62,12 +62,12 @@ async def run(self): # identify image img_path = self.identify_image_in_comment() if img_path: - get_logger().debug(f"Image path identified", artifact=img_path) + get_logger().debug("Image path identified", artifact=img_path) await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.WEAK) pr_comment = self._prepare_pr_answer() - get_logger().debug(f"PR output", artifact=pr_comment) + get_logger().debug("PR output", artifact=pr_comment) if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_questions.enable_help_text: pr_comment += "
\n\n
💡 Tool usage guide:
\n\n" @@ -95,10 +95,10 @@ def identify_image_in_comment(self): async def _prepare_prediction(self, model: str): self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) if self.patches_diff: - get_logger().debug(f"PR diff", artifact=self.patches_diff) + get_logger().debug("PR diff", artifact=self.patches_diff) self.prediction = await self._get_prediction(model) else: - get_logger().error(f"Error getting PR diff") + get_logger().error("Error getting PR diff") self.prediction = "" async def _get_prediction(self, model: str): @@ -136,7 +136,7 @@ def _prepare_pr_answer(self) -> str: if model_answer_sanitized.startswith("/"): model_answer_sanitized = " " + model_answer_sanitized if model_answer_sanitized != model_answer: - get_logger().debug(f"Sanitized model answer", + get_logger().debug("Sanitized model answer", artifact={"model_answer": model_answer, "sanitized_answer": model_answer_sanitized}) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 3ace41dc16..bfb52afa87 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -1,7 +1,5 @@ import copy import datetime -import traceback -from collections import OrderedDict from functools import partial from typing import List, Tuple @@ -9,24 +7,24 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler -from pr_agent.algo.pr_processing import (add_ai_metadata_to_diff_files, - get_pr_diff, - retry_with_fallback_models) -from pr_agent.algo.skills_loader import get_skills_context +from pr_agent.algo.pr_processing import add_ai_metadata_to_diff_files, get_pr_diff, retry_with_fallback_models from pr_agent.algo.repo_context import build_repo_context +from pr_agent.algo.skills_loader import get_skills_context from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import (ModelType, PRReviewHeader, - convert_to_markdown_v2, github_action_output, - load_yaml, show_relevant_configurations) +from pr_agent.algo.utils import ( + ModelType, + PRReviewHeader, + convert_to_markdown_v2, + github_action_output, + load_yaml, + show_relevant_configurations, +) from pr_agent.config_loader import get_settings -from pr_agent.git_providers import (get_git_provider, - get_git_provider_with_context) -from pr_agent.git_providers.git_provider import (IncrementalPR, - get_main_pr_language) +from pr_agent.git_providers import get_git_provider_with_context +from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language from pr_agent.log import get_logger from pr_agent.servers.help import HelpMessage -from pr_agent.tools.ticket_pr_compliance_check import ( - extract_and_cache_pr_tickets, extract_tickets) +from pr_agent.tools.ticket_pr_compliance_check import extract_and_cache_pr_tickets class PRReviewer: @@ -71,10 +69,10 @@ def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False, if (self.pr_description_files and get_settings().get("config.is_auto_command", False) and get_settings().get("config.enable_ai_metadata", False)): add_ai_metadata_to_diff_files(self.git_provider, self.pr_description_files) - get_logger().debug(f"AI metadata added to the this command") + get_logger().debug("AI metadata added to the this command") else: get_settings().set("config.enable_ai_metadata", False) - get_logger().debug(f"AI metadata is disabled for this command") + get_logger().debug("AI metadata is disabled for this command") self.vars = { "title": self.git_provider.pr.title, @@ -169,7 +167,7 @@ async def run(self) -> None: return None pr_review = self._prepare_pr_review() - get_logger().debug(f"PR output", artifact=pr_review) + get_logger().debug("PR output", artifact=pr_review) should_publish = get_settings().config.publish_output and self._should_publish_review_no_suggestions(pr_review) if not should_publish: @@ -205,7 +203,7 @@ async def _prepare_prediction(self, model: str) -> None: disable_extra_lines=False,) if self.patches_diff: - get_logger().debug(f"PR diff", diff=self.patches_diff) + get_logger().debug("PR diff", diff=self.patches_diff) self.prediction = await self._get_prediction(model) else: get_logger().warning(f"Empty diff for PR: {self.pr_url}") diff --git a/pr_agent/tools/pr_similar_issue.py b/pr_agent/tools/pr_similar_issue.py index f48756d793..26273ba212 100644 --- a/pr_agent/tools/pr_similar_issue.py +++ b/pr_agent/tools/pr_similar_issue.py @@ -35,9 +35,7 @@ def __init__(self, issue_url: str, ai_handler, args: list = None): if get_settings().pr_similar_issue.vectordb == "pinecone": try: - import pandas as pd import pinecone - from pinecone_datasets import Dataset, DatasetMetadata except: raise Exception("Please install 'pinecone' and 'pinecone_datasets' to use pinecone as vectordb") # assuming pinecone api key and environment are set in secrets file @@ -178,9 +176,7 @@ def __init__(self, issue_url: str, ai_handler, args: list = None): elif get_settings().pr_similar_issue.vectordb == "qdrant": try: import qdrant_client - from qdrant_client.models import (Distance, FieldCondition, - Filter, MatchValue, - PointStruct, VectorParams) + from qdrant_client.models import Distance, FieldCondition, Filter, MatchValue, VectorParams except Exception: raise Exception("Please install qdrant-client to use qdrant as vectordb") diff --git a/pr_agent/tools/pr_update_changelog.py b/pr_agent/tools/pr_update_changelog.py index 28f96128b5..39cca3e3b9 100644 --- a/pr_agent/tools/pr_update_changelog.py +++ b/pr_agent/tools/pr_update_changelog.py @@ -12,7 +12,7 @@ from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.utils import ModelType, show_relevant_configurations from pr_agent.config_loader import get_settings -from pr_agent.git_providers import GithubProvider, get_git_provider +from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.log import get_logger @@ -91,7 +91,7 @@ async def run(self): if get_settings().get('config', {}).get('output_relevant_configurations', False): answer += show_relevant_configurations(relevant_section='pr_update_changelog') - get_logger().debug(f"PR output", artifact=answer) + get_logger().debug("PR output", artifact=answer) if get_settings().config.publish_output: self.git_provider.remove_initial_comment() @@ -109,10 +109,10 @@ async def run(self): async def _prepare_prediction(self, model: str): self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) if self.patches_diff: - get_logger().debug(f"PR diff", artifact=self.patches_diff) + get_logger().debug("PR diff", artifact=self.patches_diff) self.prediction = await self._get_prediction(model) else: - get_logger().error(f"Error getting PR diff") + get_logger().error("Error getting PR diff") self.prediction = "" async def _get_prediction(self, model: str): diff --git a/pr_agent/tools/ticket_pr_compliance_check.py b/pr_agent/tools/ticket_pr_compliance_check.py index f2ae1dca78..12160cbde3 100644 --- a/pr_agent/tools/ticket_pr_compliance_check.py +++ b/pr_agent/tools/ticket_pr_compliance_check.py @@ -2,8 +2,7 @@ import traceback from pr_agent.config_loader import get_settings -from pr_agent.git_providers import GithubProvider -from pr_agent.git_providers import AzureDevopsProvider +from pr_agent.git_providers import AzureDevopsProvider, GithubProvider from pr_agent.log import get_logger # Compile the regex pattern once, outside the function diff --git a/pyproject.toml b/pyproject.toml index fdfec8acfb..77954e8d9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,7 @@ dev = [ "pytest-cov==7.0.0", "httpx==0.28.1", "pre-commit>=4,<5", - "flake8==7.3.0", + "ruff==0.15.20", ] [tool.ruff] @@ -126,9 +126,10 @@ lint.select = [ "I002", # isort missing-required-import ] -# First commit - only fixing isort lint.fixable = [ - "I001", # isort basic checks + "I001", # import sorting + "F401", # unused imports + "F541", # f-string without placeholders ] lint.unfixable = [ @@ -137,12 +138,39 @@ lint.unfixable = [ lint.exclude = ["api/code_completions"] -lint.ignore = ["E999", "B008"] +# B008 is a long-standing ignore. Everything below it is a pre-existing +# violation deferred so ruff runs green on adoption — fix the code and drop +# the entry. TODO: burn this list down. +lint.ignore = [ + "B008", # function call in argument defaults + "E402", # import not at top of file + "E501", # line too long + "E711", # comparison to None + "E713", # test for membership with `not ... in` + "E714", # test for object identity with `not ... is` + "E721", # type comparison with `==` + "E722", # bare `except` + "F811", # redefinition of unused name + "F841", # unused local variable + "B005", # strip() with multi-character string + "B006", # mutable argument default + "B007", # unused loop control variable + "B009", # getattr with constant attribute + "B011", # `assert False` + "B012", # break/continue/return inside finally + "B017", # assertRaises(Exception) + "B023", # function definition uses loop variable + "B027", # empty method without @abstractmethod + "B904", # raise without `from` inside `except` + "B905", # zip() without explicit strict= +] [tool.ruff.lint.per-file-ignores] -"__init__.py" = [ - "E402", -] # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +# Ignore import-position and unused-import in __init__.py (re-export modules). +"__init__.py" = ["E402", "F401"] +# The 'similar issue' tool imports its optional deps (pinecone, pandas, +# pinecone-datasets) lazily at runtime; ruff can't see them at module scope. +"pr_agent/tools/pr_similar_issue.py" = ["F821"] [tool.bandit] exclude_dirs = ["tests"] diff --git a/tests/e2e_tests/langchain_ai_handler.py b/tests/e2e_tests/langchain_ai_handler.py index 38b73ad08c..ddf9e61a78 100644 --- a/tests/e2e_tests/langchain_ai_handler.py +++ b/tests/e2e_tests/langchain_ai_handler.py @@ -43,7 +43,7 @@ async def measure_performance(handler, num_requests=3): total_time = end_time - start_time avg_time = total_time / num_requests - print(f'Performance results:') + print('Performance results:') print(f'Total time: {total_time:.2f} seconds') print(f'Average time per request: {avg_time:.2f} seconds') print(f'Requests per second: {num_requests/total_time:.2f}') diff --git a/tests/e2e_tests/test_bitbucket_app.py b/tests/e2e_tests/test_bitbucket_app.py index 7a070e7d4f..26b5fa3f50 100644 --- a/tests/e2e_tests/test_bitbucket_app.py +++ b/tests/e2e_tests/test_bitbucket_app.py @@ -1,10 +1,7 @@ -import hashlib import os -import re import time from datetime import datetime -import jwt import requests from atlassian.bitbucket import Cloud from requests.auth import HTTPBasicAuth @@ -13,11 +10,8 @@ from pr_agent.log import get_logger, setup_logger from tests.e2e_tests.e2e_utils import ( FILE_PATH, - IMPROVE_START_WITH_REGEX_PATTERN, NEW_FILE_CONTENT, NUM_MINUTES, - PR_HEADER_START_WITH, - REVIEW_START_WITH, ) log_level = os.environ.get("LOG_LEVEL", "INFO") @@ -66,7 +60,7 @@ def test_e2e_run_bitbucket_app(): # check every 1 minute, for 5 minutes if the PR has all the tool results for i in range(NUM_MINUTES): - logger.info(f"Waiting for the PR to get all the tool results...") + logger.info("Waiting for the PR to get all the tool results...") time.sleep(60) comments = list(pr.comments()) comments_raw = [c.raw for c in comments] @@ -79,7 +73,7 @@ def test_e2e_run_bitbucket_app(): if valid_review: break else: - logger.error(f"REVIEW feedback is invalid") + logger.error("REVIEW feedback is invalid") raise Exception("REVIEW feedback is invalid") else: logger.info(f"Waiting for the PR to get all the tool results. {i + 1} minute(s) passed") @@ -91,7 +85,7 @@ def test_e2e_run_bitbucket_app(): repo.branches.delete(new_branch) # If we reach here, the test is successful - logger.info(f"Succeeded in running e2e test for Bitbucket app on the PR") + logger.info("Succeeded in running e2e test for Bitbucket app on the PR") except Exception as e: logger.error(f"Failed to run e2e test for Bitbucket app: {e}") # delete the branch diff --git a/tests/e2e_tests/test_gitea_app.py b/tests/e2e_tests/test_gitea_app.py index e2c83ed284..5956dbc0ff 100644 --- a/tests/e2e_tests/test_gitea_app.py +++ b/tests/e2e_tests/test_gitea_app.py @@ -8,11 +8,8 @@ from pr_agent.log import get_logger, setup_logger from tests.e2e_tests.e2e_utils import ( FILE_PATH, - IMPROVE_START_WITH_REGEX_PATTERN, NEW_FILE_CONTENT, NUM_MINUTES, - PR_HEADER_START_WITH, - REVIEW_START_WITH, ) log_level = os.environ.get("LOG_LEVEL", "INFO") @@ -121,7 +118,7 @@ def test_e2e_run_gitea_app(): pr_number = pr['number'] for i in range(NUM_MINUTES): - logger.info(f"Waiting for the PR to get all the tool results...") + logger.info("Waiting for the PR to get all the tool results...") time.sleep(60) response = requests.get( @@ -163,7 +160,7 @@ def test_e2e_run_gitea_app(): ) response.raise_for_status() - logger.info(f"Succeeded in running e2e test for Gitea app on the PR") + logger.info("Succeeded in running e2e test for Gitea app on the PR") except Exception as e: logger.error(f"Failed to run e2e test for Gitea app: {e}") raise diff --git a/tests/e2e_tests/test_github_app.py b/tests/e2e_tests/test_github_app.py index 17e08627c1..691456b74f 100644 --- a/tests/e2e_tests/test_github_app.py +++ b/tests/e2e_tests/test_github_app.py @@ -67,7 +67,7 @@ def test_e2e_run_github_app(): # check every 1 minute, for 5, minutes if the PR has all the tool results for i in range(NUM_MINUTES): - logger.info(f"Waiting for the PR to get all the tool results...") + logger.info("Waiting for the PR to get all the tool results...") time.sleep(60) logger.info(f"Checking the PR {pr.html_url} after {i + 1} minute(s)") pr.update() diff --git a/tests/e2e_tests/test_gitlab_webhook.py b/tests/e2e_tests/test_gitlab_webhook.py index e304660e38..d775b6cbde 100644 --- a/tests/e2e_tests/test_gitlab_webhook.py +++ b/tests/e2e_tests/test_gitlab_webhook.py @@ -6,7 +6,6 @@ import gitlab from pr_agent.config_loader import get_settings -from pr_agent.git_providers import get_git_provider from pr_agent.log import get_logger, setup_logger from tests.e2e_tests.e2e_utils import ( FILE_PATH, @@ -59,7 +58,7 @@ def test_e2e_run_github_app(): # check every 1 minute, for 5, minutes if the PR has all the tool results for i in range(NUM_MINUTES): - logger.info(f"Waiting for the MR to get all the tool results...") + logger.info("Waiting for the MR to get all the tool results...") time.sleep(60) logger.info(f"Checking the MR {mr.web_url} after {i + 1} minute(s)") mr = project.mergerequests.get(mr.iid) diff --git a/tests/health_test/main.py b/tests/health_test/main.py index 11279f49b6..ed753bf576 100644 --- a/tests/health_test/main.py +++ b/tests/health_test/main.py @@ -1,16 +1,12 @@ -import argparse import asyncio import copy import os -from pathlib import Path from starlette_context import context, request_cycle_context -from pr_agent.agent.pr_agent import PRAgent, commands -from pr_agent.cli import run_command +from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings, global_settings from pr_agent.log import get_logger, setup_logger -from tests.e2e_tests import e2e_utils log_level = os.environ.get("LOG_LEVEL", "INFO") setup_logger(log_level) @@ -26,7 +22,7 @@ async def run_async() -> None: agent = PRAgent() try: # Run the 'describe' command - get_logger().info(f"\nSanity check for the 'describe' command...") + get_logger().info("\nSanity check for the 'describe' command...") original_settings = copy.deepcopy(get_settings()) await agent.handle_request(pr_url, ['describe']) pr_header_body = dict(get_settings().data)['artifact'] @@ -37,7 +33,7 @@ async def run_async() -> None: get_logger().info("PR description generated successfully\n") # Run the 'review' command - get_logger().info(f"\nSanity check for the 'review' command...") + get_logger().info("\nSanity check for the 'review' command...") original_settings = copy.deepcopy(get_settings()) await agent.handle_request(pr_url, ['review']) pr_review_body = dict(get_settings().data)['artifact'] @@ -48,7 +44,7 @@ async def run_async() -> None: get_logger().info("PR review generated successfully\n") # Run the 'improve' command - get_logger().info(f"\nSanity check for the 'improve' command...") + get_logger().info("\nSanity check for the 'improve' command...") original_settings = copy.deepcopy(get_settings()) await agent.handle_request(pr_url, ['improve']) pr_improve_body = dict(get_settings().data)['artifact'] @@ -58,10 +54,10 @@ async def run_async() -> None: context['settings'] = copy.deepcopy(original_settings) # Restore settings state after each test to prevent test interference get_logger().info("PR improvements generated successfully\n") - get_logger().info(f"\n\n========\nHealth test passed successfully\n========") + get_logger().info("\n\n========\nHealth test passed successfully\n========") except Exception as e: - get_logger().exception(f"\n\n========\nHealth test failed\n========") + get_logger().exception("\n\n========\nHealth test failed\n========") raise e diff --git a/tests/unittest/test_aws_secrets_manager_provider.py b/tests/unittest/test_aws_secrets_manager_provider.py index 4ca33b8b01..8eca25af78 100644 --- a/tests/unittest/test_aws_secrets_manager_provider.py +++ b/tests/unittest/test_aws_secrets_manager_provider.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch import pytest -from botocore.exceptions import ClientError from pr_agent.secret_providers.aws_secrets_manager_provider import AWSSecretsManagerProvider diff --git a/tests/unittest/test_clip_tokens.py b/tests/unittest/test_clip_tokens.py index 45b264cdba..d469871efe 100644 --- a/tests/unittest/test_clip_tokens.py +++ b/tests/unittest/test_clip_tokens.py @@ -1,7 +1,5 @@ from unittest.mock import MagicMock, patch -import pytest - from pr_agent.algo.token_handler import TokenEncoder from pr_agent.algo.utils import clip_tokens diff --git a/tests/unittest/test_codecommit_provider.py b/tests/unittest/test_codecommit_provider.py index c4f021256f..ab823a7b1c 100644 --- a/tests/unittest/test_codecommit_provider.py +++ b/tests/unittest/test_codecommit_provider.py @@ -2,7 +2,7 @@ import pytest -from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo +from pr_agent.algo.types import EDIT_TYPE from pr_agent.git_providers.codecommit_provider import CodeCommitFile, CodeCommitProvider, PullRequestCCMimic diff --git a/tests/unittest/test_extract_issue_from_branch.py b/tests/unittest/test_extract_issue_from_branch.py index e8a240448c..064aeb6aa4 100644 --- a/tests/unittest/test_extract_issue_from_branch.py +++ b/tests/unittest/test_extract_issue_from_branch.py @@ -1,4 +1,3 @@ -import pytest from pr_agent.tools.ticket_pr_compliance_check import ( extract_ticket_links_from_branch_name, diff --git a/tests/unittest/test_gitlab_provider.py b/tests/unittest/test_gitlab_provider.py index dc61a49525..4612dfa7fd 100644 --- a/tests/unittest/test_gitlab_provider.py +++ b/tests/unittest/test_gitlab_provider.py @@ -1,9 +1,8 @@ from unittest.mock import MagicMock, patch import pytest -from gitlab import Gitlab from gitlab.exceptions import GitlabGetError -from gitlab.v4.objects import Project, ProjectFile +from gitlab.v4.objects import ProjectFile from pr_agent.git_providers.gitlab_provider import GitLabProvider diff --git a/tests/unittest/test_handle_patch_deletions.py b/tests/unittest/test_handle_patch_deletions.py index c216f189d7..ec88baa713 100644 --- a/tests/unittest/test_handle_patch_deletions.py +++ b/tests/unittest/test_handle_patch_deletions.py @@ -1,8 +1,6 @@ # Generated by CodiumAI -import logging from pr_agent.algo.git_patch_processing import handle_patch_deletions -from pr_agent.config_loader import get_settings """ Code Analysis diff --git a/tests/unittest/test_litellm_reasoning_effort.py b/tests/unittest/test_litellm_reasoning_effort.py index 3c4905ef01..031c51ed74 100644 --- a/tests/unittest/test_litellm_reasoning_effort.py +++ b/tests/unittest/test_litellm_reasoning_effort.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, MagicMock, call, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/unittest/test_mosaico_a2a_roundtrip.py b/tests/unittest/test_mosaico_a2a_roundtrip.py index 5fd0a6a361..4ac85b230c 100644 --- a/tests/unittest/test_mosaico_a2a_roundtrip.py +++ b/tests/unittest/test_mosaico_a2a_roundtrip.py @@ -22,14 +22,13 @@ custom_merge_loader circular import (mirrors server.py).""" import os -import pr_agent.config_loader # noqa: F401 (import-order load; see module docstring) - import httpx import pytest +from a2a.types import Message, Part, Role, SendMessageRequest from google.protobuf.json_format import MessageToDict from httpx import ASGITransport -from a2a.types import Message, Part, Role, SendMessageRequest +import pr_agent.config_loader # noqa: F401 (import-order load; see module docstring) # A small, valid unified diff wrapped in a ```diff fence -> the supplied-diff (path b) # of the router: no PR URL, no network, parsed by the mosaico_diff provider. diff --git a/tests/unittest/test_mosaico_card.py b/tests/unittest/test_mosaico_card.py index 4507c961ec..656b17ce82 100644 --- a/tests/unittest/test_mosaico_card.py +++ b/tests/unittest/test_mosaico_card.py @@ -4,8 +4,7 @@ from google.protobuf.json_format import MessageToDict -from pr_agent.mosaico.card import (OBSERVABILITY_EXTENSION_URI, - build_agent_card) +from pr_agent.mosaico.card import OBSERVABILITY_EXTENSION_URI, build_agent_card _REGISTRATION_JSON = os.path.join( os.path.dirname(__file__), "..", "..", "docker", "mosaico", diff --git a/tests/unittest/test_mosaico_env_bridge.py b/tests/unittest/test_mosaico_env_bridge.py index 8fd1263d6f..2e00899bee 100644 --- a/tests/unittest/test_mosaico_env_bridge.py +++ b/tests/unittest/test_mosaico_env_bridge.py @@ -9,10 +9,8 @@ import pytest from pr_agent.config_loader import get_settings, global_settings -from pr_agent.mosaico.env_bridge import (apply_mosaico_env, - langfuse_env_present) -from pr_agent.mosaico.observability import (mosaico_log_context, - parse_observability_metadata) +from pr_agent.mosaico.env_bridge import apply_mosaico_env, langfuse_env_present +from pr_agent.mosaico.observability import mosaico_log_context, parse_observability_metadata _SNAPSHOT_KEYS = [ "OPENAI.API_BASE", diff --git a/tests/unittest/test_mosaico_metadata.py b/tests/unittest/test_mosaico_metadata.py index 4a9c2a2d64..9edcdcf5ff 100644 --- a/tests/unittest/test_mosaico_metadata.py +++ b/tests/unittest/test_mosaico_metadata.py @@ -11,9 +11,7 @@ import pytest -from pr_agent.mosaico.observability import (langfuse_span, - mosaico_log_context, - parse_observability_metadata) +from pr_agent.mosaico.observability import langfuse_span, mosaico_log_context, parse_observability_metadata class TestParseObservabilityMetadata: diff --git a/tests/unittest/test_mosaico_provider.py b/tests/unittest/test_mosaico_provider.py index 5db9ac9664..11760bd27f 100644 --- a/tests/unittest/test_mosaico_provider.py +++ b/tests/unittest/test_mosaico_provider.py @@ -252,8 +252,8 @@ def isolated_settings(): async def _run_tool_via_spy(monkeypatch, isolated_settings, verb, canned_yaml, second_yaml=""): """Patch the provider registry to a spy subclass, mock the LLM, run the tool with publish_output=False, and return (captured_artifact, incremental_accessed_list).""" - from pr_agent.git_providers import _GIT_PROVIDERS import pr_agent.algo.ai_handlers.litellm_ai_handler as litellm_mod + from pr_agent.git_providers import _GIT_PROVIDERS files = parse_unified_diff(TWO_FILE_DIFF) provider_input = {"files": files, "languages": {"Python": 100}, "title": "spike PR"} diff --git a/tests/unittest/test_mosaico_router.py b/tests/unittest/test_mosaico_router.py index b194a2526e..ded6ba36b1 100644 --- a/tests/unittest/test_mosaico_router.py +++ b/tests/unittest/test_mosaico_router.py @@ -5,15 +5,18 @@ with no exception escaping. asyncio_mode=auto.""" -import pytest - import aiohttp +import pytest from pr_agent.config_loader import global_settings from pr_agent.mosaico import dispatch -from pr_agent.mosaico.dispatch import (_detect_verb, _empty_fallback, - _error_fallback, route_and_run, - route_and_run_result, RouteResult) +from pr_agent.mosaico.dispatch import ( + _detect_verb, + _empty_fallback, + _error_fallback, + route_and_run, + route_and_run_result, +) PR_URL = "https://github.com/org/repo/pull/123" diff --git a/tests/unittest/test_mosaico_smoke.py b/tests/unittest/test_mosaico_smoke.py index ae9392b876..6b888fc8b8 100644 --- a/tests/unittest/test_mosaico_smoke.py +++ b/tests/unittest/test_mosaico_smoke.py @@ -13,11 +13,11 @@ provider->render chain works through the wire.""" import uuid +from a2a.types import Message, Part, Role, SendMessageRequest from google.protobuf.json_format import MessageToDict from starlette.testclient import TestClient import pr_agent.algo.ai_handlers.litellm_ai_handler as litellm_mod -from a2a.types import Message, Part, Role, SendMessageRequest from pr_agent.mosaico.server import build_app REVIEW_DIFF = """```diff diff --git a/tests/unittest/test_progress_comment.py b/tests/unittest/test_progress_comment.py index eb21224c21..e4be03a337 100644 --- a/tests/unittest/test_progress_comment.py +++ b/tests/unittest/test_progress_comment.py @@ -1,10 +1,12 @@ from unittest.mock import MagicMock, patch -from pr_agent.tools.progress_comment import (DEFAULT_PROGRESS_GIF_URL, - DEFAULT_PROGRESS_GIF_WIDTH, - build_progress_comment, - get_progress_gif_url, - get_progress_gif_width) +from pr_agent.tools.progress_comment import ( + DEFAULT_PROGRESS_GIF_URL, + DEFAULT_PROGRESS_GIF_WIDTH, + build_progress_comment, + get_progress_gif_url, + get_progress_gif_width, +) def _mock_settings(mock_get_settings, values): diff --git a/tests/unittest/test_skills_loader.py b/tests/unittest/test_skills_loader.py index 6aa9d492d3..cd74a6715a 100644 --- a/tests/unittest/test_skills_loader.py +++ b/tests/unittest/test_skills_loader.py @@ -5,10 +5,13 @@ from jinja2 import Environment, StrictUndefined -from pr_agent.algo.skills_loader import (Skill, _parse_skill_file, - discover_skills, - format_skills_context, - get_skills_context) +from pr_agent.algo.skills_loader import ( + Skill, + _parse_skill_file, + discover_skills, + format_skills_context, + get_skills_context, +) def _write_skill(directory: Path, name: str, body: str = "Body content."): diff --git a/uv.lock b/uv.lock index aa597a197c..e39dc2570e 100644 --- a/uv.lock +++ b/uv.lock @@ -762,20 +762,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693", size = 45073, upload-time = "2026-07-03T03:50:30.445Z" }, ] -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -1963,15 +1949,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "msal" version = "1.37.0" @@ -2577,12 +2554,12 @@ similar-issue = [ [package.dev-dependencies] dev = [ - { name = "flake8" }, { name = "httpx" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "ruff" }, ] [package.metadata] @@ -2635,12 +2612,12 @@ provides-extras = ["lambda", "similar-issue", "langchain"] [package.metadata.requires-dev] dev = [ - { name = "flake8", specifier = "==7.3.0" }, { name = "httpx", specifier = "==0.28.1" }, { name = "pre-commit", specifier = ">=4,<5" }, { name = "pytest", specifier = "==9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = "==7.0.0" }, + { name = "ruff", specifier = "==0.15.20" }, ] [[package]] @@ -2853,15 +2830,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -2961,15 +2929,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, ] -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - [[package]] name = "pygithub" version = "1.59.1" @@ -3474,6 +3433,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, ] +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + [[package]] name = "s3fs" version = "2025.12.0"