From 718bf4c8bae3bb6e45e9613b1df3a745a6b8ada9 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Fri, 27 Feb 2026 10:19:37 -0800 Subject: [PATCH 1/2] Enable ruff rules: DTZ, N, T10, TID, YTT Enable 5 additional ruff rule categories: - DTZ: datetime timezone awareness (use timezone.utc everywhere, add _ensure_utc helper in memory_models to re-attach UTC to naive datetimes returned by SQLite) - N: PEP 8 naming conventions (renamed camelCase variables to snake_case, noqa for intentional patterns like do_EOF, PyritException, Mock fixtures) - T10: debugger calls (0 violations) - TID: tidy imports (0 violations) - YTT: sys.version checks (0 violations) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyproject.toml | 7 +++- pyrit/auth/azure_storage_auth.py | 6 +-- pyrit/cli/frontend_core.py | 6 +-- pyrit/cli/pyrit_shell.py | 2 +- .../local/local_dataset_loader.py | 2 +- pyrit/exceptions/exception_classes.py | 2 +- .../attack/printer/console_printer.py | 6 +-- .../attack/printer/markdown_printer.py | 6 ++- pyrit/memory/memory_interface.py | 6 +-- pyrit/memory/memory_models.py | 31 +++++++++++---- pyrit/models/message.py | 4 +- pyrit/models/message_piece.py | 4 +- pyrit/models/score.py | 4 +- pyrit/models/seeds/seed.py | 4 +- pyrit/models/seeds/seed_dataset.py | 4 +- pyrit/prompt_converter/braille_converter.py | 22 +++++------ .../insert_punctuation_converter.py | 6 +-- pyrit/prompt_converter/morse_converter.py | 4 +- .../openai/openai_realtime_target.py | 8 ++-- .../prompt_target/websocket_copilot_target.py | 6 +-- .../scorer_evaluation/scorer_evaluator.py | 4 +- .../ai_recruiter/test_ai_recruiter.py | 12 +++--- .../test_azure_sql_memory_integration.py | 4 +- .../score/test_hitl_gradio_integration.py | 22 +++++------ tests/unit/auth/test_azure_storage_auth.py | 4 +- tests/unit/backend/test_converter_service.py | 10 ++--- .../converter/test_azure_speech_converter.py | 6 ++- .../test_azure_speech_text_converter.py | 4 +- .../attack/core/test_attack_executor.py | 2 +- .../attack/core/test_attack_parameters.py | 8 ++-- .../test_interface_prompts.py | 14 +++---- tests/unit/memory/test_azure_sql_memory.py | 5 ++- tests/unit/memory/test_sqlite_memory.py | 3 +- tests/unit/models/test_message_piece.py | 38 ++++++++++++------- tests/unit/models/test_score.py | 4 +- 35 files changed, 160 insertions(+), 120 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d87582c389..281bf6ab3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -257,14 +257,19 @@ select = [ "CPY001", # missing-copyright-notice "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d "DOC", # https://docs.astral.sh/ruff/rules/#pydoclint-doc + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz "F401", # unused-import "I", # isort + "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "T10", # https://docs.astral.sh/ruff/rules/#flake8-debugger-t10 "TCH", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tch + "TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-w + "YTT", # https://docs.astral.sh/ruff/rules/#flake8-2020-ytt ] ignore = [ "B903", # class-as-data-structure (test helper classes use @apply_defaults pattern) @@ -300,7 +305,7 @@ notice-rgx = "Copyright \\(c\\) Microsoft Corporation\\.\\s*\\n.*Licensed under # Temporary ignores for pyrit/ subdirectories until issue #1176 # https://github.com/Azure/PyRIT/issues/1176 is fully resolved # TODO: Remove these ignores once the issues are fixed -"pyrit/{auxiliary_attacks,ui}/**/*.py" = ["B905", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D401", "D404", "D417", "D418", "DOC102", "DOC201", "DOC202", "DOC402", "DOC501", "SIM101", "SIM108"] +"pyrit/{auxiliary_attacks,ui}/**/*.py" = ["B905", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D401", "D404", "D417", "D418", "DOC102", "DOC201", "DOC202", "DOC402", "DOC501", "N", "SIM101", "SIM108"] # Backend API routes raise HTTPException handled by FastAPI, not true exceptions "pyrit/backend/**/*.py" = ["DOC501", "B008"] "pyrit/__init__.py" = ["D104"] diff --git a/pyrit/auth/azure_storage_auth.py b/pyrit/auth/azure_storage_auth.py index f24576aa62..4c206f439b 100644 --- a/pyrit/auth/azure_storage_auth.py +++ b/pyrit/auth/azure_storage_auth.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from urllib.parse import urlparse from azure.identity.aio import DefaultAzureCredential @@ -31,7 +31,7 @@ async def get_user_delegation_key(blob_service_client: BlobServiceClient) -> Use Returns: UserDelegationKey: A user delegation key valid for one day. """ - delegation_key_start_time = datetime.now() + delegation_key_start_time = datetime.now(tz=timezone.utc) delegation_key_expiry_time = delegation_key_start_time + timedelta(days=1) return await blob_service_client.get_user_delegation_key( @@ -79,7 +79,7 @@ async def get_sas_token(container_url: str) -> str: storage_account_name = parsed_url.netloc.split(".")[0] # Set start_time 5 minutes before the current time to account for any clock skew - start_time = datetime.now() - timedelta(minutes=5) + start_time = datetime.now(tz=timezone.utc) - timedelta(minutes=5) expiry_time = start_time + timedelta(days=1) sas_token = generate_container_sas( diff --git a/pyrit/cli/frontend_core.py b/pyrit/cli/frontend_core.py index 5c49525ec5..86e9fd2483 100644 --- a/pyrit/cli/frontend_core.py +++ b/pyrit/cli/frontend_core.py @@ -32,7 +32,7 @@ HAS_TERMCOLOR = False # Create a dummy termcolor module for fallback - class termcolor: # type: ignore + class termcolor: # type: ignore # noqa: N801 """Dummy termcolor fallback for colored printing if termcolor is not installed.""" @staticmethod @@ -718,8 +718,8 @@ def get_default_initializer_discovery_path() -> Path: Returns: Path to the scenarios initializers directory. """ - PYRIT_PATH = Path(__file__).parent.parent.resolve() - return PYRIT_PATH / "setup" / "initializers" / "scenarios" + pyrit_path = Path(__file__).parent.parent.resolve() + return pyrit_path / "setup" / "initializers" / "scenarios" async def print_scenarios_list_async(*, context: FrontendCore) -> int: diff --git a/pyrit/cli/pyrit_shell.py b/pyrit/cli/pyrit_shell.py index 2c218f237b..d667b2a53d 100644 --- a/pyrit/cli/pyrit_shell.py +++ b/pyrit/cli/pyrit_shell.py @@ -411,7 +411,7 @@ def do_clear(self, arg: str) -> None: # Shortcuts and aliases do_quit = do_exit do_q = do_exit - do_EOF = do_exit # Ctrl+D on Unix, Ctrl+Z on Windows + do_EOF = do_exit # Ctrl+D on Unix, Ctrl+Z on Windows # noqa: N815 def emptyline(self) -> bool: """ diff --git a/pyrit/datasets/seed_datasets/local/local_dataset_loader.py b/pyrit/datasets/seed_datasets/local/local_dataset_loader.py index 51d3790fc9..270fba1568 100644 --- a/pyrit/datasets/seed_datasets/local/local_dataset_loader.py +++ b/pyrit/datasets/seed_datasets/local/local_dataset_loader.py @@ -92,7 +92,7 @@ def _register_local_datasets() -> None: # We override __init__ to pass the specific file_path def make_init(path: Path) -> Callable[[Any], None]: - def __init__(self: Any) -> None: + def __init__(self: Any) -> None: # noqa: N807 super(self.__class__, self).__init__(file_path=path) return __init__ diff --git a/pyrit/exceptions/exception_classes.py b/pyrit/exceptions/exception_classes.py index eeb9e16535..e7541d8565 100644 --- a/pyrit/exceptions/exception_classes.py +++ b/pyrit/exceptions/exception_classes.py @@ -113,7 +113,7 @@ def __call__(self, retry_state: RetryCallState) -> float: return wait_strategy(retry_state) -class PyritException(Exception, ABC): +class PyritException(Exception, ABC): # noqa: N818 """Base exception class for PyRIT components.""" def __init__(self, *, status_code: int = 500, message: str = "An error occurred") -> None: diff --git a/pyrit/executor/attack/printer/console_printer.py b/pyrit/executor/attack/printer/console_printer.py index ef703e1a85..a26e7851a1 100644 --- a/pyrit/executor/attack/printer/console_printer.py +++ b/pyrit/executor/attack/printer/console_printer.py @@ -3,7 +3,7 @@ import json import textwrap -from datetime import datetime +from datetime import datetime, timezone from typing import Any from colorama import Back, Fore, Style @@ -345,10 +345,10 @@ def _print_footer(self) -> None: Displays the current timestamp when the report was generated. """ - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + timestamp = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S") print() self._print_colored("─" * self._width, Style.DIM, Fore.WHITE) - footer_text = f"Report generated at: {timestamp}" + footer_text = f"Report generated at: {timestamp} UTC" self._print_colored(footer_text.center(self._width), Style.DIM, Fore.WHITE) def _print_section_header(self, title: str) -> None: diff --git a/pyrit/executor/attack/printer/markdown_printer.py b/pyrit/executor/attack/printer/markdown_printer.py index 357abe55ae..418be19b3d 100644 --- a/pyrit/executor/attack/printer/markdown_printer.py +++ b/pyrit/executor/attack/printer/markdown_printer.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. import os -from datetime import datetime +from datetime import datetime, timezone from pyrit.executor.attack.printer.attack_result_printer import AttackResultPrinter from pyrit.memory import CentralMemory @@ -171,7 +171,9 @@ async def print_result_async( # Footer markdown_lines.append("\n---") - markdown_lines.append(f"*Report generated at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*") + markdown_lines.append( + f"*Report generated at {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC*" + ) self._render_markdown(markdown_lines) diff --git a/pyrit/memory/memory_interface.py b/pyrit/memory/memory_interface.py index 67e6dcfb6d..f9626833de 100644 --- a/pyrit/memory/memory_interface.py +++ b/pyrit/memory/memory_interface.py @@ -9,7 +9,7 @@ import weakref from collections.abc import MutableSequence, Sequence from contextlib import closing -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union @@ -1001,7 +1001,7 @@ async def add_seeds_to_memory_async(self, *, seeds: Sequence[Seed], added_by: Op ValueError: If the 'added_by' attribute is not set for each prompt. """ entries: MutableSequence[SeedEntry] = [] - current_time = datetime.now() + current_time = datetime.now(tz=timezone.utc) for prompt in seeds: if added_by: prompt.added_by = added_by @@ -1245,7 +1245,7 @@ def export_conversations( # If file_path is not provided, construct a default using the exporter's results_path if not file_path: - file_name = f"exported_conversations_on_{datetime.now().strftime('%Y_%m_%d')}.{export_type}" + file_name = f"exported_conversations_on_{datetime.now(tz=timezone.utc).strftime('%Y_%m_%d')}.{export_type}" file_path = DB_DATA_PATH / file_name self.exporter.export_data(list(data), file_path=file_path, export_type=export_type) diff --git a/pyrit/memory/memory_models.py b/pyrit/memory/memory_models.py index 04be633df5..2ea5dfc116 100644 --- a/pyrit/memory/memory_models.py +++ b/pyrit/memory/memory_models.py @@ -4,7 +4,7 @@ import json import logging import uuid -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Literal, Optional, Union from pydantic import BaseModel, ConfigDict @@ -59,6 +59,21 @@ MAX_IDENTIFIER_VALUE_LENGTH: int = 80 +def _ensure_utc(dt: Optional[datetime]) -> Optional[datetime]: + """ + Attach UTC tzinfo to a naive datetime (as returned by SQLite). + + Args: + dt (Optional[datetime]): The datetime to normalize, or None. + + Returns: + Optional[datetime]: The datetime with UTC tzinfo attached if it was naive, or None. + """ + if dt is not None and dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt + + class CustomUUID(TypeDecorator[uuid.UUID]): """ A custom UUID type that works consistently across different database backends. @@ -291,7 +306,7 @@ def get_message_piece(self) -> MessagePiece: converted_value_data_type=self.converted_value_data_type, response_error=self.response_error, original_prompt_id=self.original_prompt_id, - timestamp=self.timestamp, + timestamp=_ensure_utc(self.timestamp), ) message_piece.scores = [score.get_score() for score in self.scores] return message_piece @@ -416,7 +431,7 @@ def get_score(self) -> Score: score_metadata=self.score_metadata, scorer_class_identifier=scorer_identifier, message_piece_id=self.prompt_request_response_id, - timestamp=self.timestamp, + timestamp=_ensure_utc(self.timestamp), objective=self.objective, ) @@ -621,7 +636,7 @@ def get_seed(self) -> Seed: authors=self.authors, groups=self.groups, source=self.source, - date_added=self.date_added, + date_added=_ensure_utc(self.date_added), added_by=self.added_by, metadata=self.prompt_metadata, prompt_group_id=self.prompt_group_id, @@ -641,7 +656,7 @@ def get_seed(self) -> Seed: authors=self.authors, groups=self.groups, source=self.source, - date_added=self.date_added, + date_added=_ensure_utc(self.date_added), added_by=self.added_by, metadata=self.prompt_metadata, prompt_group_id=self.prompt_group_id, @@ -663,7 +678,7 @@ def get_seed(self) -> Seed: authors=self.authors, groups=self.groups, source=self.source, - date_added=self.date_added, + date_added=_ensure_utc(self.date_added), added_by=self.added_by, metadata=self.prompt_metadata, parameters=self.parameters, @@ -774,7 +789,7 @@ def __init__(self, *, entry: AttackResult): ref.conversation_id for ref in entry.get_conversations_by_type(ConversationType.ADVERSARIAL) ] or None - self.timestamp = datetime.now() + self.timestamp = datetime.now(tz=timezone.utc) self.pyrit_version = pyrit.__version__ @staticmethod @@ -957,7 +972,7 @@ def __init__(self, *, entry: ScenarioResult): serialized_attack_results[attack_name] = [result.conversation_id for result in results] self.attack_results_json = json.dumps(serialized_attack_results) - self.timestamp = datetime.now() + self.timestamp = datetime.now(tz=timezone.utc) def get_scenario_result(self) -> ScenarioResult: """ diff --git a/pyrit/models/message.py b/pyrit/models/message.py index ba6c09c937..e4446c2cc5 100644 --- a/pyrit/models/message.py +++ b/pyrit/models/message.py @@ -6,7 +6,7 @@ import copy import uuid import warnings -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Optional, Union from pyrit.common.utils import combine_dict @@ -415,7 +415,7 @@ def duplicate_message(self) -> Message: """ new_pieces = copy.deepcopy(self.message_pieces) - new_timestamp = datetime.now() + new_timestamp = datetime.now(tz=timezone.utc) for piece in new_pieces: piece.id = uuid.uuid4() piece.timestamp = new_timestamp diff --git a/pyrit/models/message_piece.py b/pyrit/models/message_piece.py index 62a6e4890d..49ec2e672d 100644 --- a/pyrit/models/message_piece.py +++ b/pyrit/models/message_piece.py @@ -4,7 +4,7 @@ from __future__ import annotations import uuid -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_args from uuid import uuid4 @@ -108,7 +108,7 @@ def __init__( self.conversation_id = conversation_id if conversation_id else str(uuid4()) self.sequence = sequence - self.timestamp = timestamp if timestamp else datetime.now() + self.timestamp = timestamp if timestamp else datetime.now(tz=timezone.utc) self.labels = labels or {} self.prompt_metadata = prompt_metadata or {} diff --git a/pyrit/models/score.py b/pyrit/models/score.py index fa01d5432c..3f15e05ae3 100644 --- a/pyrit/models/score.py +++ b/pyrit/models/score.py @@ -5,7 +5,7 @@ import uuid from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Literal, Optional, Union, get_args if TYPE_CHECKING: @@ -90,7 +90,7 @@ def __init__( from pyrit.identifiers.component_identifier import ComponentIdentifier self.id = id if id else uuid.uuid4() - self.timestamp = timestamp if timestamp else datetime.now() + self.timestamp = timestamp if timestamp else datetime.now(tz=timezone.utc) self.validate(score_type, score_value) diff --git a/pyrit/models/seeds/seed.py b/pyrit/models/seeds/seed.py index c506a49a12..7854b87874 100644 --- a/pyrit/models/seeds/seed.py +++ b/pyrit/models/seeds/seed.py @@ -14,7 +14,7 @@ import re import uuid from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union from jinja2 import BaseLoader, Environment, StrictUndefined, Template, Undefined @@ -113,7 +113,7 @@ class Seed(YamlLoadable): source: Optional[str] = None # Date when the prompt was added to the dataset - date_added: Optional[datetime] = field(default_factory=lambda: datetime.now()) + date_added: Optional[datetime] = field(default_factory=lambda: datetime.now(tz=timezone.utc)) # User who added the prompt to the dataset added_by: Optional[str] = None diff --git a/pyrit/models/seeds/seed_dataset.py b/pyrit/models/seeds/seed_dataset.py index 413f216a97..c55f84490a 100644 --- a/pyrit/models/seeds/seed_dataset.py +++ b/pyrit/models/seeds/seed_dataset.py @@ -12,7 +12,7 @@ import uuid import warnings from collections import defaultdict -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING, Any, Optional, Union from pyrit.common import utils @@ -121,7 +121,7 @@ def __init__( self.authors = authors or [] self.groups = groups or [] self.source = source - self.date_added = date_added or datetime.now() + self.date_added = date_added or datetime.now(tz=timezone.utc) self.added_by = added_by # Convert any dictionaries in `seeds` to SeedPrompt and/or SeedObjective objects diff --git a/pyrit/prompt_converter/braille_converter.py b/pyrit/prompt_converter/braille_converter.py index 70ee6b19a5..4c72dd54a2 100644 --- a/pyrit/prompt_converter/braille_converter.py +++ b/pyrit/prompt_converter/braille_converter.py @@ -58,7 +58,7 @@ def _get_braile(self, text: str) -> str: Returns: str: The braille representation of the input text. """ - characterUnicodes = { + character_unicodes = { "a": "\u2801", "b": "\u2803", "k": "\u2805", @@ -111,25 +111,25 @@ def _get_braile(self, text: str) -> str: "0": "\u281a", " ": " ", } - numberPunctuations = [".", ",", "-", "/", "$"] - escapeCharacters = ["\n", "\r", "\t"] + number_punctuations = [".", ",", "-", "/", "$"] + escape_characters = ["\n", "\r", "\t"] output = "" for char in text: is_number = False - if char in escapeCharacters: + if char in escape_characters: output += char elif char.isupper(): - if char.lower() in characterUnicodes: - output += characterUnicodes["caps"] - output += characterUnicodes[char.lower()] - elif char in characterUnicodes: + if char.lower() in character_unicodes: + output += character_unicodes["caps"] + output += character_unicodes[char.lower()] + elif char in character_unicodes: if char.isdigit() and not is_number: is_number = True - output += characterUnicodes["num"] - output += characterUnicodes[char] - if is_number and char not in numberPunctuations: + output += character_unicodes["num"] + output += character_unicodes[char] + if is_number and char not in number_punctuations: is_number = False return output diff --git a/pyrit/prompt_converter/insert_punctuation_converter.py b/pyrit/prompt_converter/insert_punctuation_converter.py index 759781449c..66ac4840c1 100644 --- a/pyrit/prompt_converter/insert_punctuation_converter.py +++ b/pyrit/prompt_converter/insert_punctuation_converter.py @@ -150,10 +150,10 @@ def _insert_between_words( """ insert_indices = random.sample(word_indices, num_insertions) # Randomly choose num_insertions indices from actual word indices. - INSERT_BEFORE = 0 - INSERT_AFTER = 1 + insert_before = 0 + insert_after = 1 for index in insert_indices: - if random.randint(INSERT_BEFORE, INSERT_AFTER) == INSERT_AFTER: + if random.randint(insert_before, insert_after) == insert_after: words[index] += random.choice(punctuation_list) else: words[index] = random.choice(punctuation_list) + words[index] diff --git a/pyrit/prompt_converter/morse_converter.py b/pyrit/prompt_converter/morse_converter.py index 2641f91b56..f9033d3d66 100644 --- a/pyrit/prompt_converter/morse_converter.py +++ b/pyrit/prompt_converter/morse_converter.py @@ -165,9 +165,9 @@ def _morse(self, text: str) -> str: "Ź": "--..-.", "Ż": "--..-", } - EXTENDED_CHAR_SUPPORT = True + extended_char_support = True supported_charset = "".join(morse_mapping.keys()) - if EXTENDED_CHAR_SUPPORT: + if extended_char_support: supported_charset += "".join(extended_mapping.keys()) morse_mapping = {**morse_mapping, **extended_mapping} error_char = "........" diff --git a/pyrit/prompt_target/openai/openai_realtime_target.py b/pyrit/prompt_target/openai/openai_realtime_target.py index 774eeb7733..1de40cfa08 100644 --- a/pyrit/prompt_target/openai/openai_realtime_target.py +++ b/pyrit/prompt_target/openai/openai_realtime_target.py @@ -461,7 +461,7 @@ async def receive_events(self, conversation_id: str) -> RealtimeTargetResult: result = RealtimeTargetResult() audio_done_received = False - GRACE_PERIOD_SEC = 1.0 # Wait 1 second after audio.done before soft-finishing + grace_period_sec = 1.0 # Wait 1 second after audio.done before soft-finishing try: # Create event iterator @@ -470,7 +470,7 @@ async def receive_events(self, conversation_id: str) -> RealtimeTargetResult: while True: # If we've seen audio.done, wait with a short timeout for response.done # Otherwise, wait indefinitely for events - timeout = GRACE_PERIOD_SEC if audio_done_received else None + timeout = grace_period_sec if audio_done_received else None try: event = await asyncio.wait_for(event_iter.__anext__(), timeout=timeout) @@ -478,7 +478,7 @@ async def receive_events(self, conversation_id: str) -> RealtimeTargetResult: # Soft-finish: audio.done was received but no response.done after grace period if audio_done_received: logger.warning( - f"Soft-finishing: No response.done {GRACE_PERIOD_SEC}s after audio.done. " + f"Soft-finishing: No response.done {grace_period_sec}s after audio.done. " f"Audio bytes: {len(result.audio_bytes)}" ) break @@ -519,7 +519,7 @@ async def receive_events(self, conversation_id: str) -> RealtimeTargetResult: logger.debug(f"Decoded {len(audio_data)} bytes of audio data") elif event_type in ["response.audio.done", "response.output_audio.done"]: - logger.debug(f"Received audio.done - will soft-finish in {GRACE_PERIOD_SEC}s if no response.done") + logger.debug(f"Received audio.done - will soft-finish in {grace_period_sec}s if no response.done") audio_done_received = True elif event_type in ["response.audio_transcript.delta", "response.output_audio_transcript.delta"]: diff --git a/pyrit/prompt_target/websocket_copilot_target.py b/pyrit/prompt_target/websocket_copilot_target.py index 9b72b9f052..1a3bcfa7a9 100644 --- a/pyrit/prompt_target/websocket_copilot_target.py +++ b/pyrit/prompt_target/websocket_copilot_target.py @@ -506,16 +506,16 @@ async def _connect_and_send( is_user_input = input_msg.get("type") == CopilotMessageType.USER_PROMPT - MAX_MESSAGE_ITERATIONS = 1000 + max_message_iterations = 1000 iteration_count = 0 stop_polling = False while not stop_polling: # Prevent infinite loops (e.g. if Copilot somehow never sends a terminating message) iteration_count += 1 - if iteration_count > MAX_MESSAGE_ITERATIONS: + if iteration_count > max_message_iterations: raise RuntimeError( - f"Exceeded maximum message iterations ({MAX_MESSAGE_ITERATIONS}) " + f"Exceeded maximum message iterations ({max_message_iterations}) " "while waiting for Copilot response." ) diff --git a/pyrit/score/scorer_evaluation/scorer_evaluator.py b/pyrit/score/scorer_evaluation/scorer_evaluator.py index d5ecfe1480..b5a8ac95ec 100644 --- a/pyrit/score/scorer_evaluation/scorer_evaluator.py +++ b/pyrit/score/scorer_evaluation/scorer_evaluator.py @@ -108,9 +108,9 @@ def from_scorer(cls, scorer: Scorer, metrics_type: Optional[MetricsType] = None) if not metrics_type: metrics_type = MetricsType.OBJECTIVE if isinstance(scorer, TrueFalseScorer) else MetricsType.HARM - _EVALUATOR_MAP = {MetricsType.HARM: HarmScorerEvaluator, MetricsType.OBJECTIVE: ObjectiveScorerEvaluator} + evaluator_map = {MetricsType.HARM: HarmScorerEvaluator, MetricsType.OBJECTIVE: ObjectiveScorerEvaluator} - evaluator = _EVALUATOR_MAP.get(metrics_type, HarmScorerEvaluator) + evaluator = evaluator_map.get(metrics_type, HarmScorerEvaluator) return evaluator(scorer=scorer) async def run_evaluation_async( diff --git a/tests/integration/ai_recruiter/test_ai_recruiter.py b/tests/integration/ai_recruiter/test_ai_recruiter.py index 84039a4237..9ee79b8d79 100644 --- a/tests/integration/ai_recruiter/test_ai_recruiter.py +++ b/tests/integration/ai_recruiter/test_ai_recruiter.py @@ -83,21 +83,21 @@ def ensure_ai_recruiter_running(): 4. After tests, shuts down and deletes the cloned repo. """ with tempfile.TemporaryDirectory() as temp_root: - CLONE_DIR = pathlib.Path(temp_root) / "cloned_ai_recruiter" + clone_dir = pathlib.Path(temp_root) / "cloned_ai_recruiter" # Clone and pin to commit - subprocess.run(["git", "clone", AI_RECRUITER_REPO, str(CLONE_DIR)], check=True) - subprocess.run(["git", "checkout", AI_RECRUITER_COMMIT], cwd=CLONE_DIR, check=True) + subprocess.run(["git", "clone", AI_RECRUITER_REPO, str(clone_dir)], check=True) + subprocess.run(["git", "checkout", AI_RECRUITER_COMMIT], cwd=clone_dir, check=True) # Ensure .env is available to Docker Compose original_env_path = HOME_PATH / ".env" if original_env_path.exists(): - shutil.copy(original_env_path, CLONE_DIR / ".env") + shutil.copy(original_env_path, clone_dir / ".env") else: raise FileNotFoundError(f".env not found at {original_env_path}") # Start container from inside the cloned repo - subprocess.run(["docker-compose", "up", "-d", "--build"], cwd=CLONE_DIR, check=True) + subprocess.run(["docker-compose", "up", "-d", "--build"], cwd=clone_dir, check=True) # Poll the health endpoint until it's live (or we time out) health_url = "http://localhost:8000/health" @@ -118,7 +118,7 @@ def ensure_ai_recruiter_running(): yield # Shut down container - subprocess.run(["docker-compose", "down"], cwd=CLONE_DIR, check=True) + subprocess.run(["docker-compose", "down"], cwd=clone_dir, check=True) @pytest.mark.run_only_if_all_tests diff --git a/tests/integration/memory/test_azure_sql_memory_integration.py b/tests/integration/memory/test_azure_sql_memory_integration.py index a50aad7ff5..0f3bbd8065 100644 --- a/tests/integration/memory/test_azure_sql_memory_integration.py +++ b/tests/integration/memory/test_azure_sql_memory_integration.py @@ -3,7 +3,7 @@ from collections.abc import Generator from contextlib import closing, contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from uuid import uuid4 import numpy as np @@ -648,7 +648,7 @@ async def test_get_scenario_results_combined_filters(azuresql_instance: AzureSQL """ # Use unique identifiers to avoid test pollution test_id = generate_test_id() - now = datetime.utcnow() + now = datetime.now(tz=timezone.utc) yesterday = now - timedelta(days=1) with cleanup_scenario_data(azuresql_instance, test_id): diff --git a/tests/integration/score/test_hitl_gradio_integration.py b/tests/integration/score/test_hitl_gradio_integration.py index f0fcaf540c..11873ec30f 100644 --- a/tests/integration/score/test_hitl_gradio_integration.py +++ b/tests/integration/score/test_hitl_gradio_integration.py @@ -35,7 +35,7 @@ def score() -> Score: @pytest.fixture -def promptOriginal() -> MessagePiece: +def prompt_original() -> MessagePiece: return MessagePiece( role="assistant", original_value="This is the original value", @@ -86,7 +86,7 @@ class TestHiTLGradioIntegration: @patch("pyrit.ui.rpc.is_app_running") @pytest.mark.asyncio @pytest.mark.timeout(30) - async def test_scorer_can_start(self, mock_is_app_running, promptOriginal: MessagePiece): + async def test_scorer_can_start(self, mock_is_app_running, prompt_original: MessagePiece): memory = MagicMock(MemoryInterface) with patch.object(CentralMemory, "get_memory_instance", return_value=memory): disconnected_event = Event() @@ -96,8 +96,8 @@ def disconnected(): disconnected_event.set() def score_callback(prompt: MessagePiece) -> bool: - assert prompt.original_value == promptOriginal.original_value - assert prompt.converted_value == promptOriginal.converted_value + assert prompt.original_value == prompt_original.original_value + assert prompt.converted_value == prompt_original.converted_value return True mock_is_app_running.return_value = True @@ -106,7 +106,7 @@ def score_callback(prompt: MessagePiece) -> bool: scorer = HumanInTheLoopScorerGradio() rpc_client.start() - score_result = await scorer.score_async(message=promptOriginal.to_message()) + score_result = await scorer.score_async(message=prompt_original.to_message()) assert score_result[0].score_value == "true" rpc_client.stop() @@ -120,7 +120,7 @@ def score_callback(prompt: MessagePiece) -> bool: @patch("pyrit.ui.rpc.is_app_running") @pytest.mark.asyncio @pytest.mark.timeout(30) - async def test_scorer_receive_multiple(self, mock_is_app_running, promptOriginal: MessagePiece): + async def test_scorer_receive_multiple(self, mock_is_app_running, prompt_original: MessagePiece): memory = MagicMock(MemoryInterface) with patch.object(CentralMemory, "get_memory_instance", return_value=memory): disconnected_event = Event() @@ -134,8 +134,8 @@ def disconnected(): def score_callback(prompt: MessagePiece) -> bool: nonlocal i i += 1 - assert prompt.original_value == promptOriginal.original_value - assert prompt.converted_value == promptOriginal.converted_value + assert prompt.original_value == prompt_original.original_value + assert prompt.converted_value == prompt_original.converted_value return i % 2 == 0 @@ -146,14 +146,14 @@ def score_callback(prompt: MessagePiece) -> bool: rpc_client.start() - score_result = await scorer.score_async(message=promptOriginal.to_message()) + score_result = await scorer.score_async(message=prompt_original.to_message()) assert score_result[0].score_value == "true" # Next prompt - score_result = await scorer.score_async(message=promptOriginal.to_message()) + score_result = await scorer.score_async(message=prompt_original.to_message()) assert score_result[0].score_value == "false" - score_result = await scorer.score_async(message=promptOriginal.to_message()) + score_result = await scorer.score_async(message=prompt_original.to_message()) assert score_result[0].score_value == "true" rpc_client.stop() diff --git a/tests/unit/auth/test_azure_storage_auth.py b/tests/unit/auth/test_azure_storage_auth.py index 7d58958a14..fba56e919f 100644 --- a/tests/unit/auth/test_azure_storage_auth.py +++ b/tests/unit/auth/test_azure_storage_auth.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -17,7 +17,7 @@ @pytest.mark.asyncio async def test_get_user_delegation_key(): mock_blob_service_client = AsyncMock(spec=BlobServiceClient) - expected_start_time = datetime.now() + expected_start_time = datetime.now(tz=timezone.utc) expected_expiry_time = expected_start_time + timedelta(days=1) mock_user_delegation_key = UserDelegationKey() diff --git a/tests/unit/backend/test_converter_service.py b/tests/unit/backend/test_converter_service.py index 1f7eaf7566..0deb273b2b 100644 --- a/tests/unit/backend/test_converter_service.py +++ b/tests/unit/backend/test_converter_service.py @@ -390,14 +390,14 @@ def _try_instantiate_converter(converter_name: str): # Converters requiring external credentials or resources that can't be mocked # at the constructor level — these validate env vars / files in __init__ body - _SKIP_CONVERTERS = { + skip_converters = { "AzureSpeechAudioToTextConverter", # requires AZURE_SPEECH_REGION env var "AzureSpeechTextToAudioConverter", # requires AZURE_SPEECH_REGION env var "TransparencyAttackConverter", # requires a real JPEG image file on disk } # Converter-specific overrides for params with validation - _OVERRIDES: dict = { + overrides: dict = { "CodeChameleonConverter": {"encrypt_type": "reverse"}, "SearchReplaceConverter": {"pattern": "foo", "replace": "bar"}, "PersuasionConverter": {"persuasion_technique": "logical_appeal"}, @@ -407,7 +407,7 @@ def _try_instantiate_converter(converter_name: str): if converter_cls is None: return None, f"Converter {converter_name} not found in prompt_converter module" - if converter_name in _SKIP_CONVERTERS: + if converter_name in skip_converters: return None, None # Signal to skip without failure # Build minimal kwargs based on constructor signature @@ -426,8 +426,8 @@ def _try_instantiate_converter(converter_name: str): continue # Has a real default — skip # Check overrides first - if converter_name in _OVERRIDES and pname in _OVERRIDES[converter_name]: - kwargs[pname] = _OVERRIDES[converter_name][pname] + if converter_name in overrides and pname in overrides[converter_name]: + kwargs[pname] = overrides[converter_name][pname] continue ann = param.annotation diff --git a/tests/unit/converter/test_azure_speech_converter.py b/tests/unit/converter/test_azure_speech_converter.py index ca8a817884..415ba02007 100644 --- a/tests/unit/converter/test_azure_speech_converter.py +++ b/tests/unit/converter/test_azure_speech_converter.py @@ -28,7 +28,11 @@ class TestAzureSpeechTextToAudioConverter: side_effect=lambda env_var_name, passed_value: passed_value or "dummy_value", ) async def test_azure_speech_text_to_audio_convert_async( - self, mock_get_required_value, MockSpeechConfig, MockSpeechSynthesizer, sqlite_instance + self, + mock_get_required_value, + MockSpeechConfig, # noqa: N803 + MockSpeechSynthesizer, # noqa: N803 + sqlite_instance, ): import azure.cognitiveservices.speech as speechsdk diff --git a/tests/unit/converter/test_azure_speech_text_converter.py b/tests/unit/converter/test_azure_speech_text_converter.py index 21fae8ca87..af0f84d34b 100644 --- a/tests/unit/converter/test_azure_speech_text_converter.py +++ b/tests/unit/converter/test_azure_speech_text_converter.py @@ -37,7 +37,7 @@ def test_azure_speech_audio_text_converter_initialization(self, mock_get_require ) @patch("azure.cognitiveservices.speech.SpeechRecognizer") @patch("pyrit.prompt_converter.azure_speech_audio_to_text_converter.logger") - def test_stop_cb(self, mock_logger, MockSpeechRecognizer, mock_get_required_value): + def test_stop_cb(self, mock_logger, MockSpeechRecognizer, mock_get_required_value): # noqa: N803 import azure.cognitiveservices.speech as speechsdk # Create a mock event @@ -66,7 +66,7 @@ def test_stop_cb(self, mock_logger, MockSpeechRecognizer, mock_get_required_valu ) @patch("azure.cognitiveservices.speech.SpeechRecognizer") @patch("pyrit.prompt_converter.azure_speech_audio_to_text_converter.logger") - def test_transcript_cb(self, mock_logger, MockSpeechRecognizer, mock_get_required_value): + def test_transcript_cb(self, mock_logger, MockSpeechRecognizer, mock_get_required_value): # noqa: N803 import azure.cognitiveservices.speech as speechsdk # Create a mock event diff --git a/tests/unit/executor/attack/core/test_attack_executor.py b/tests/unit/executor/attack/core/test_attack_executor.py index ba848903fa..2401311206 100644 --- a/tests/unit/executor/attack/core/test_attack_executor.py +++ b/tests/unit/executor/attack/core/test_attack_executor.py @@ -517,7 +517,7 @@ class TestParamsTypeIntegration: async def test_excluded_params_type_rejects_excluded_fields(self): """Test that params_type.excluding() properly rejects fields.""" # Create a params type that excludes next_message - LimitedParams = AttackParameters.excluding("next_message", "prepended_conversation") + LimitedParams = AttackParameters.excluding("next_message", "prepended_conversation") # noqa: N806 attack = create_mock_attack(params_type=LimitedParams) attack.execute_with_context_async.return_value = create_attack_result("Test") diff --git a/tests/unit/executor/attack/core/test_attack_parameters.py b/tests/unit/executor/attack/core/test_attack_parameters.py index d70036dfec..47f853328e 100644 --- a/tests/unit/executor/attack/core/test_attack_parameters.py +++ b/tests/unit/executor/attack/core/test_attack_parameters.py @@ -266,7 +266,7 @@ class TestExcluding: def test_excluding_creates_class_without_specified_fields(self) -> None: """Test that excluding() creates a class without the specified fields.""" - ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") + ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") # noqa: N806 field_names = {f.name for f in dataclasses.fields(ExcludedParams)} @@ -282,13 +282,13 @@ def test_excluding_raises_for_invalid_fields(self) -> None: async def test_excluded_class_has_from_seed_group_async(self) -> None: """Test that the excluded class has from_seed_group_async method.""" - ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") + ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") # noqa: N806 assert hasattr(ExcludedParams, "from_seed_group_async") async def test_excluded_class_from_seed_group_async_works(self) -> None: """Test that from_seed_group_async works on excluded class.""" - ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") + ExcludedParams = AttackParameters.excluding("next_message", "prepended_conversation") # noqa: N806 objective = SeedObjective(value="Test objective") seed_group = SeedAttackGroup(seeds=[objective]) @@ -298,7 +298,7 @@ async def test_excluded_class_from_seed_group_async_works(self) -> None: async def test_excluded_class_rejects_excluded_field_overrides(self) -> None: """Test that from_seed_group_async rejects overrides for excluded fields.""" - ExcludedParams = AttackParameters.excluding("next_message") + ExcludedParams = AttackParameters.excluding("next_message") # noqa: N806 objective = SeedObjective(value="Test objective") seed_group = SeedAttackGroup(seeds=[objective]) diff --git a/tests/unit/memory/memory_interface/test_interface_prompts.py b/tests/unit/memory/memory_interface/test_interface_prompts.py index 99f973f3f1..1eacce67c9 100644 --- a/tests/unit/memory/memory_interface/test_interface_prompts.py +++ b/tests/unit/memory/memory_interface/test_interface_prompts.py @@ -4,7 +4,7 @@ import uuid from collections.abc import MutableSequence, Sequence -from datetime import datetime +from datetime import datetime, timezone from unittest.mock import MagicMock, patch from uuid import uuid4 @@ -866,12 +866,12 @@ def test_get_message_pieces_sent_after(sqlite_instance: MemoryInterface): ), ] - entries[0].timestamp = datetime(2022, 12, 25, 15, 30, 0) - entries[1].timestamp = datetime(2022, 12, 25, 15, 30, 0) + entries[0].timestamp = datetime(2022, 12, 25, 15, 30, 0, tzinfo=timezone.utc) + entries[1].timestamp = datetime(2022, 12, 25, 15, 30, 0, tzinfo=timezone.utc) sqlite_instance._insert_entries(entries=entries) - retrieved_entries = sqlite_instance.get_message_pieces(sent_after=datetime(2024, 1, 1)) + retrieved_entries = sqlite_instance.get_message_pieces(sent_after=datetime(2024, 1, 1, tzinfo=timezone.utc)) assert len(retrieved_entries) == 1 assert "Hello 3" in retrieved_entries[0].original_value @@ -899,12 +899,12 @@ def test_get_message_pieces_sent_before(sqlite_instance: MemoryInterface): ), ] - entries[0].timestamp = datetime(2022, 12, 25, 15, 30, 0) - entries[1].timestamp = datetime(2021, 12, 25, 15, 30, 0) + entries[0].timestamp = datetime(2022, 12, 25, 15, 30, 0, tzinfo=timezone.utc) + entries[1].timestamp = datetime(2021, 12, 25, 15, 30, 0, tzinfo=timezone.utc) sqlite_instance._insert_entries(entries=entries) - retrieved_entries = sqlite_instance.get_message_pieces(sent_before=datetime(2024, 1, 1)) + retrieved_entries = sqlite_instance.get_message_pieces(sent_before=datetime(2024, 1, 1, tzinfo=timezone.utc)) assert len(retrieved_entries) == 2 assert_original_value_in_list("Hello 1", retrieved_entries) diff --git a/tests/unit/memory/test_azure_sql_memory.py b/tests/unit/memory/test_azure_sql_memory.py index 4316270259..f3bccd24f2 100644 --- a/tests/unit/memory/test_azure_sql_memory.py +++ b/tests/unit/memory/test_azure_sql_memory.py @@ -4,6 +4,7 @@ import os import uuid from collections.abc import Generator, MutableSequence, Sequence +from datetime import timezone from typing import TYPE_CHECKING import pytest @@ -203,7 +204,9 @@ def test_get_memories_with_json_properties(memory_interface: AzureSQLMemory): assert retrieved_entry.api_role == "user" assert retrieved_entry.original_value == "Test content" # For timestamp, you might want to check if it's close to the current time instead of an exact match - assert abs((retrieved_entry.timestamp - entry.timestamp).total_seconds()) < 10 # Assuming the test runs quickly + assert ( + abs((retrieved_entry.timestamp - entry.timestamp.replace(tzinfo=timezone.utc)).total_seconds()) < 10 + ) # Assuming the test runs quickly converter_identifiers = retrieved_entry.converter_identifiers assert len(converter_identifiers) == 1 diff --git a/tests/unit/memory/test_sqlite_memory.py b/tests/unit/memory/test_sqlite_memory.py index f99b725258..8a486e4f81 100644 --- a/tests/unit/memory/test_sqlite_memory.py +++ b/tests/unit/memory/test_sqlite_memory.py @@ -4,6 +4,7 @@ import os import uuid from collections.abc import Sequence +from datetime import timezone from unittest.mock import MagicMock import pytest @@ -369,7 +370,7 @@ def test_get_memories_with_json_properties(sqlite_instance): assert retrieved_entry.original_value == "Test content" # For timestamp, you might want to check if it's close to the current time instead of an exact match assert abs((retrieved_entry.timestamp - piece.timestamp).total_seconds()) < 0.1 - assert abs((retrieved_entry.timestamp - entry.timestamp).total_seconds()) < 0.1 + assert abs((retrieved_entry.timestamp - entry.timestamp.replace(tzinfo=timezone.utc)).total_seconds()) < 0.1 converter_identifiers = retrieved_entry.converter_identifiers assert len(converter_identifiers) == 1 diff --git a/tests/unit/models/test_message_piece.py b/tests/unit/models/test_message_piece.py index 469c94e87b..b59bf4acf6 100644 --- a/tests/unit/models/test_message_piece.py +++ b/tests/unit/models/test_message_piece.py @@ -6,7 +6,7 @@ import time import uuid from collections.abc import MutableSequence -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from unit.mocks import MockPromptTarget, get_mock_target, get_sample_conversations @@ -40,7 +40,7 @@ def test_id_set(): def test_datetime_set(): - now = datetime.now() + now = datetime.now(tz=timezone.utc) time.sleep(0.1) entry = MessagePiece( role="user", @@ -402,7 +402,7 @@ def test_order_message_pieces_by_conversation_single_conversation(): id="prompt-1", original_value="Hello 1", conversation_id="conv1", - timestamp=datetime.now() - timedelta(seconds=10), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=10), sequence=2, ), MessagePiece( @@ -410,7 +410,7 @@ def test_order_message_pieces_by_conversation_single_conversation(): id="prompt-2", original_value="Hello 2", conversation_id="conv1", - timestamp=datetime.now() - timedelta(seconds=10), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=10), sequence=1, ), MessagePiece( @@ -418,7 +418,7 @@ def test_order_message_pieces_by_conversation_single_conversation(): id="prompt-3", original_value="Hello 3", conversation_id="conv1", - timestamp=datetime.now(), + timestamp=datetime.now(tz=timezone.utc), sequence=3, ), ] @@ -460,7 +460,7 @@ def test_order_message_pieces_by_conversation_multiple_conversations(): role="user", original_value="Hello 4", conversation_id="conv2", - timestamp=datetime.now() - timedelta(seconds=5), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=5), sequence=2, id="4", ), @@ -468,7 +468,7 @@ def test_order_message_pieces_by_conversation_multiple_conversations(): role="user", original_value="Hello 1", conversation_id="conv1", - timestamp=datetime.now() - timedelta(seconds=15), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=15), sequence=1, id="1", ), @@ -476,7 +476,7 @@ def test_order_message_pieces_by_conversation_multiple_conversations(): role="user", original_value="Hello 3", conversation_id="conv2", - timestamp=datetime.now() - timedelta(seconds=10), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=10), sequence=1, id="3", ), @@ -484,7 +484,7 @@ def test_order_message_pieces_by_conversation_multiple_conversations(): role="user", original_value="Hello 2", conversation_id="conv1", - timestamp=datetime.now() - timedelta(seconds=10), + timestamp=datetime.now(tz=timezone.utc) - timedelta(seconds=10), sequence=2, id="2", ), @@ -529,7 +529,7 @@ def test_order_message_pieces_by_conversation_multiple_conversations(): def test_order_message_pieces_by_conversation_same_timestamp(): - timestamp = datetime.now() + timestamp = datetime.now(tz=timezone.utc) pieces = [ MessagePiece( @@ -621,10 +621,20 @@ def test_order_message_pieces_by_conversation_single_message(): def test_order_message_pieces_by_conversation_same_timestamp_different_sequences(): pieces = [ MessagePiece( - role="user", original_value="Hello 2", conversation_id="conv1", timestamp=datetime.now(), sequence=2, id="2" + role="user", + original_value="Hello 2", + conversation_id="conv1", + timestamp=datetime.now(tz=timezone.utc), + sequence=2, + id="2", ), MessagePiece( - role="user", original_value="Hello 1", conversation_id="conv1", timestamp=datetime.now(), sequence=1, id="1" + role="user", + original_value="Hello 1", + conversation_id="conv1", + timestamp=datetime.now(tz=timezone.utc), + sequence=1, + id="1", ), ] for i, piece in enumerate(pieces): @@ -685,7 +695,7 @@ def test_message_piece_to_dict(): response_error="none", originator="undefined", original_prompt_id=uuid.uuid4(), - timestamp=datetime.now(), + timestamp=datetime.now(tz=timezone.utc), scores=[ Score( id=str(uuid.uuid4()), @@ -700,7 +710,7 @@ def test_message_piece_to_dict(): class_module="pyrit.score", ), message_piece_id=str(uuid.uuid4()), - timestamp=datetime.now(), + timestamp=datetime.now(tz=timezone.utc), objective="Task1", ) ], diff --git a/tests/unit/models/test_score.py b/tests/unit/models/test_score.py index 5940cbae17..dc35a0c0d8 100644 --- a/tests/unit/models/test_score.py +++ b/tests/unit/models/test_score.py @@ -2,7 +2,7 @@ # Licensed under the MIT license. import uuid -from datetime import datetime +from datetime import datetime, timezone import pytest @@ -26,7 +26,7 @@ async def test_score_to_dict(): score_metadata={"key": "value"}, scorer_class_identifier=scorer_identifier, message_piece_id=str(uuid.uuid4()), - timestamp=datetime.now(), + timestamp=datetime.now(tz=timezone.utc), objective="Task1", ) result = sample_score.to_dict() From 2291d33a14075e94bb1b7f74ee90f6f74292d67e Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Mon, 2 Mar 2026 21:06:18 -0800 Subject: [PATCH 2/2] fix: address copilot comments - Optional type, UTC timestamp, braille is_number bug Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyrit/executor/attack/printer/markdown_printer.py | 5 ++--- pyrit/prompt_converter/braille_converter.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrit/executor/attack/printer/markdown_printer.py b/pyrit/executor/attack/printer/markdown_printer.py index 418be19b3d..2e9112349b 100644 --- a/pyrit/executor/attack/printer/markdown_printer.py +++ b/pyrit/executor/attack/printer/markdown_printer.py @@ -171,9 +171,8 @@ async def print_result_async( # Footer markdown_lines.append("\n---") - markdown_lines.append( - f"*Report generated at {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC*" - ) + timestamp_utc = datetime.now(tz=timezone.utc).isoformat().replace("+00:00", "Z") + markdown_lines.append(f"*Report generated at {timestamp_utc}*") self._render_markdown(markdown_lines) diff --git a/pyrit/prompt_converter/braille_converter.py b/pyrit/prompt_converter/braille_converter.py index 4c72dd54a2..a6ddc62561 100644 --- a/pyrit/prompt_converter/braille_converter.py +++ b/pyrit/prompt_converter/braille_converter.py @@ -116,8 +116,8 @@ def _get_braile(self, text: str) -> str: output = "" + is_number = False for char in text: - is_number = False if char in escape_characters: output += char elif char.isupper():