Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ fixable = [
"ISC",
"NPY",
"PD",
"PERF",
"PGH",
"PIE",
"PL",
Expand All @@ -259,6 +260,7 @@ select = [
"DOC", # https://docs.astral.sh/ruff/rules/#pydoclint-doc
"F401", # unused-import
"I", # isort
"PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PERF rule prefix was added to select but not to fixable. Looking at other multi-character rule codes in the fixable list (PIE, RET, SIM, TCH, etc.), there's a consistent pattern of including selected multi-character rule codes in fixable as well. Without PERF in the fixable list, ruff --fix will not auto-fix any future PERF violations, requiring manual fixes every time. Consider adding "PERF" to the fixable list for consistency.

Copilot uses AI. Check for mistakes.
"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
Expand All @@ -274,6 +276,7 @@ ignore = [
"D212", # Multi-line docstring summary should start at the first line
"D301", # Use r""" if any backslashes in a docstring
"DOC502", # Raised exception is not explicitly raised
"PERF203", # try-except-in-loop (intentional per-item error handling)
"SIM117", # multiple-with-statements (combining often exceeds line length)
"UP007", # non-pep604-annotation-union (keep Union[X, Y] syntax)
"UP045", # non-pep604-annotation-optional (keep Optional[X] syntax)
Expand Down
24 changes: 10 additions & 14 deletions pyrit/analytics/conversation_analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,16 @@ def get_prompt_entries_with_same_converted_content(
the similar chat messages based on content.
"""
all_memories = self.memory_interface.get_message_pieces()
similar_messages = []

for memory in all_memories:
if memory.converted_value == chat_message_content:
similar_messages.append(
ConversationMessageWithSimilarity(
score=1.0,
role=memory.role,
content=memory.converted_value,
metric="exact_match", # Exact match
)
)

return similar_messages
return [
ConversationMessageWithSimilarity(
score=1.0,
role=memory.role,
content=memory.converted_value,
metric="exact_match", # Exact match
)
for memory in all_memories
if memory.converted_value == chat_message_content
]

def get_similar_chat_messages_by_embedding(
self, *, chat_message_embedding: list[float], threshold: float = 0.8
Expand Down
5 changes: 1 addition & 4 deletions pyrit/auxiliary_attacks/gcg/attack/base/attack_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,7 @@ def get_nonascii_toks(tokenizer: Any, device: str = "cpu") -> torch.Tensor:
def is_ascii(s: str) -> bool:
return s.isascii() and s.isprintable()

ascii_toks = []
for i in range(3, tokenizer.vocab_size):
if not is_ascii(tokenizer.decode([i])):
ascii_toks.append(i)
ascii_toks = [i for i in range(3, tokenizer.vocab_size) if not is_ascii(tokenizer.decode([i]))]

if tokenizer.bos_token_id is not None:
ascii_toks.append(tokenizer.bos_token_id)
Expand Down
12 changes: 4 additions & 8 deletions pyrit/executor/attack/printer/markdown_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ def _format_score(self, score: Score, indent: str = "") -> str:
rationale_lines = score.score_rationale.split("\n")
if len(rationale_lines) > 1:
lines.append(f"{indent}- **Rationale:**")
for line in rationale_lines:
lines.append(f"{indent} {line}")
lines.extend(f"{indent} {line}" for line in rationale_lines)
else:
lines.append(f"{indent}- **Rationale:** {score.score_rationale}")

Expand Down Expand Up @@ -273,8 +272,7 @@ def _format_system_message(self, message: Message) -> list[str]:
List[str]: List of markdown strings representing the system message.
"""
lines = ["\n### System Message\n"]
for piece in message.message_pieces:
lines.append(f"{piece.converted_value}\n")
lines.extend(f"{piece.converted_value}\n" for piece in message.message_pieces)
return lines

async def _format_user_message_async(self, *, message: Message, turn_number: int) -> list[str]:
Expand Down Expand Up @@ -461,8 +459,7 @@ def _format_message_scores(self, message: Message) -> list[str]:
scores = self._memory.get_prompt_scores(prompt_ids=[str(piece.id)])
if scores:
lines.append("\n##### Scores\n")
for score in scores:
lines.append(self._format_score(score, indent=""))
lines.extend(self._format_score(score, indent="") for score in scores)
lines.append("")
return lines

Expand Down Expand Up @@ -572,8 +569,7 @@ async def _get_pruned_conversations_markdown_async(self, result: AttackResult) -
scores = self._memory.get_prompt_scores(prompt_ids=[str(piece.id)])
if scores:
markdown_lines.append("\n**Score:**\n")
for score in scores:
markdown_lines.append(self._format_score(score, indent=""))
markdown_lines.extend(self._format_score(score, indent="") for score in scores)

return markdown_lines

Expand Down
11 changes: 5 additions & 6 deletions pyrit/executor/promptgen/fuzzer/fuzzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,14 +957,13 @@ def _get_other_templates(self, context: FuzzerContext) -> list[str]:
Returns:
List of template strings.
"""
other_templates = []
node_ids_on_path = {node.id for node in context.mcts_selected_path}

for prompt_node in context.initial_prompt_nodes + context.new_prompt_nodes:
if prompt_node.id not in node_ids_on_path:
other_templates.append(prompt_node.template)

return other_templates
return [
prompt_node.template
for prompt_node in context.initial_prompt_nodes + context.new_prompt_nodes
if prompt_node.id not in node_ids_on_path
]

def _generate_prompts_from_template(self, *, template: SeedPrompt, prompts: list[str]) -> list[str]:
"""
Expand Down
4 changes: 1 addition & 3 deletions pyrit/identifiers/component_identifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,7 @@ def _build_hash_dict(

# Only include non-None params — adding an optional param with None default
# won't change existing hashes, making the schema backward-compatible.
for key, value in sorted(params.items()):
if value is not None:
hash_dict[key] = value
hash_dict.update({key: value for key, value in sorted(params.items()) if value is not None})

# Children contribute their hashes, not their full structure.
if children:
Expand Down
8 changes: 2 additions & 6 deletions pyrit/memory/memory_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ def export_to_json(self, data: list[MessagePiece], file_path: Optional[Path] = N
raise ValueError("Please provide a valid file path for exporting data.")
if not data:
raise ValueError("No data to export.")
export_data = []
for piece in data:
export_data.append(piece.to_dict())
export_data = [piece.to_dict() for piece in data]
with open(file_path, "w") as f:
json.dump(export_data, f, indent=4)

Expand All @@ -92,9 +90,7 @@ def export_to_csv(self, data: list[MessagePiece], file_path: Optional[Path] = No
raise ValueError("Please provide a valid file path for exporting data.")
if not data:
raise ValueError("No data to export.")
export_data = []
for piece in data:
export_data.append(piece.to_dict())
export_data = [piece.to_dict() for piece in data]
fieldnames = list(export_data[0].keys())
with open(file_path, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
Expand Down
3 changes: 1 addition & 2 deletions pyrit/memory/memory_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -954,8 +954,7 @@ def _add_list_conditions(
self, field: InstrumentedAttribute[Any], conditions: list[Any], values: Optional[Sequence[str]] = None
) -> None:
if values:
for value in values:
conditions.append(field.contains(value))
conditions.extend(field.contains(value) for value in values)

async def _serialize_seed_value(self, prompt: Seed) -> str:
"""
Expand Down
3 changes: 1 addition & 2 deletions pyrit/models/scenario_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ def get_objectives(self, *, atomic_attack_name: Optional[str] = None) -> list[st
strategies_to_process = []

for results in strategies_to_process:
for result in results:
objectives.append(result.objective)
objectives.extend(result.objective for result in results)

return list(set(objectives))

Expand Down
4 changes: 1 addition & 3 deletions pyrit/prompt_converter/audio_echo_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ async def convert_async(self, *, prompt: str, input_type: PromptDataType = "audi
if data.ndim == 1:
echo_data = self._apply_echo(data, sample_rate).astype(original_dtype)
else:
channels = []
for ch in range(data.shape[1]):
channels.append(self._apply_echo(data[:, ch], sample_rate))
channels = [self._apply_echo(data[:, ch], sample_rate) for ch in range(data.shape[1])]
echo_data = np.column_stack(channels).astype(original_dtype)

# Write the processed data as a new WAV file
Expand Down
4 changes: 1 addition & 3 deletions pyrit/prompt_converter/audio_volume_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ async def convert_async(self, *, prompt: str, input_type: PromptDataType = "audi
volume_data = self._apply_volume(data).astype(original_dtype)
else:
# Multi-channel audio (e.g., stereo)
channels = []
for ch in range(data.shape[1]):
channels.append(self._apply_volume(data[:, ch]))
channels = [self._apply_volume(data[:, ch]) for ch in range(data.shape[1])]
volume_data = np.column_stack(channels).astype(original_dtype)

# Write the processed data as a new WAV file
Expand Down
4 changes: 1 addition & 3 deletions pyrit/prompt_converter/audio_white_noise_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,7 @@ async def convert_async(self, *, prompt: str, input_type: PromptDataType = "audi
if data.ndim == 1:
noisy_data = self._add_noise(data).astype(original_dtype)
else:
channels = []
for ch in range(data.shape[1]):
channels.append(self._add_noise(data[:, ch]))
channels = [self._add_noise(data[:, ch]) for ch in range(data.shape[1])]
noisy_data = np.column_stack(channels).astype(original_dtype)

# Write the processed data as a new WAV file
Expand Down
5 changes: 1 addition & 4 deletions pyrit/prompt_converter/nato_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ def _convert_to_nato(self, text: str) -> str:
Returns:
str: The NATO phonetic alphabet representation, with code words separated by spaces.
"""
output = []
for char in text.upper():
if char in self._NATO_MAP:
output.append(self._NATO_MAP[char])
output = [self._NATO_MAP[char] for char in text.upper() if char in self._NATO_MAP]

return " ".join(output)
3 changes: 1 addition & 2 deletions pyrit/prompt_target/openai/openai_chat_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,7 @@ async def _construct_request_body(
}

if self._extra_body_parameters:
for key, value in self._extra_body_parameters.items():
body_parameters[key] = value
body_parameters.update(self._extra_body_parameters)

# Filter out None values
return {k: v for k, v in body_parameters.items() if v is not None}
Expand Down
4 changes: 1 addition & 3 deletions pyrit/prompt_target/openai/openai_response_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,7 @@ async def _build_input_for_multi_modal_async(self, conversation: MutableSequence

# System message (remapped to developer)
if pieces[0].api_role == "system":
system_content = []
for piece in pieces:
system_content.append({"type": "input_text", "text": piece.converted_value})
system_content = [{"type": "input_text", "text": piece.converted_value} for piece in pieces]
input_items.append({"role": "developer", "content": system_content})
continue

Expand Down
5 changes: 3 additions & 2 deletions pyrit/scenario/core/scenario_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,8 +556,9 @@ def normalize_compositions(
aggregate = aggregates_in_composition[0]
expanded = strategy_type.normalize_strategies({aggregate})
# Each expanded strategy becomes its own composition
for strategy in expanded:
normalized_compositions.append(ScenarioCompositeStrategy(strategies=[strategy]))
normalized_compositions.extend(
ScenarioCompositeStrategy(strategies=[strategy]) for strategy in expanded
)
else:
# Concrete composition - validate and preserve as-is
strategy_type.validate_composition(typed_strategies)
Expand Down
5 changes: 1 addition & 4 deletions pyrit/scenario/scenarios/airt/cyber.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,8 @@ async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:
# Resolve seed groups from deprecated objectives or dataset config
self._seed_groups = self._resolve_seed_groups()

atomic_attacks: list[AtomicAttack] = []
strategies = ScenarioCompositeStrategy.extract_single_strategy_values(
composites=self._scenario_composites, strategy_type=CyberStrategy
)

for strategy in strategies:
atomic_attacks.append(self._get_atomic_attack_from_strategy(strategy))
return atomic_attacks
return [self._get_atomic_attack_from_strategy(strategy) for strategy in strategies]
5 changes: 1 addition & 4 deletions pyrit/scenario/scenarios/airt/leakage_scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,8 @@ async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:
# Resolve objectives to seed groups format
self._seed_groups = self._resolve_seed_groups()

atomic_attacks: list[AtomicAttack] = []
strategies = ScenarioCompositeStrategy.extract_single_strategy_values(
composites=self._scenario_composites, strategy_type=LeakageStrategy
)

for strategy in strategies:
atomic_attacks.append(await self._get_atomic_attack_from_strategy_async(strategy))
return atomic_attacks
return [await self._get_atomic_attack_from_strategy_async(strategy) for strategy in strategies]
6 changes: 1 addition & 5 deletions pyrit/scenario/scenarios/airt/scam.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,8 @@ async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:
# Resolve seed groups from deprecated objectives or dataset config
self._seed_groups = self._resolve_seed_groups()

atomic_attacks: list[AtomicAttack] = []
strategies = ScenarioCompositeStrategy.extract_single_strategy_values(
composites=self._scenario_composites, strategy_type=ScamStrategy
)

for strategy in strategies:
atomic_attacks.append(self._get_atomic_attack_from_strategy(strategy))

return atomic_attacks
return [self._get_atomic_attack_from_strategy(strategy) for strategy in strategies]
5 changes: 1 addition & 4 deletions pyrit/scenario/scenarios/foundry/red_team_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,10 +345,7 @@ async def _get_atomic_attacks_async(self) -> list[AtomicAttack]:
# Resolve seed groups now that initialize_async has been called
self._seed_groups = self._resolve_seed_groups()

atomic_attacks = []
for composition in self._scenario_composites:
atomic_attacks.append(self._get_attack_from_strategy(composition))
return atomic_attacks
return [self._get_attack_from_strategy(composition) for composition in self._scenario_composites]

def _get_default_adversarial_target(self) -> OpenAIChatTarget:
return OpenAIChatTarget(
Expand Down
6 changes: 1 addition & 5 deletions pyrit/scenario/scenarios/garak/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,7 @@ def _resolve_seed_groups(self) -> list[SeedAttackGroup]:

# Use deprecated seed_prompts if provided
if self._deprecated_seed_prompts is not None:
seed_groups = []
for seed in self._deprecated_seed_prompts:
seed_groups.append(SeedAttackGroup(seeds=[SeedObjective(value=seed)]))

return seed_groups
return [SeedAttackGroup(seeds=[SeedObjective(value=seed)]) for seed in self._deprecated_seed_prompts]

# Use dataset_config (guaranteed to be set by initialize_async)
seed_groups = self._dataset_config.get_all_seed_attack_groups()
Expand Down
18 changes: 8 additions & 10 deletions pyrit/score/scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,17 +783,15 @@ async def score_response_multiple_scorers_async(
return []

# Create all scoring tasks, note TEMPORARY fix to prevent multi-piece responses from breaking scoring logic
tasks = []

for scorer in scorers:
tasks.append(
scorer.score_async(
message=response,
objective=objective,
role_filter=role_filter,
skip_on_error_result=skip_on_error_result,
)
tasks = [
scorer.score_async(
message=response,
objective=objective,
role_filter=role_filter,
skip_on_error_result=skip_on_error_result,
)
for scorer in scorers
]

if not tasks:
return []
Expand Down
10 changes: 7 additions & 3 deletions pyrit/score/scorer_evaluation/scorer_metrics_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,13 @@ def _build_eval_dict(
ComponentIdentifier.KEY_CLASS_MODULE: identifier.class_module,
}

for key, value in sorted(identifier.params.items()):
if value is not None and (param_allowlist is None or key in param_allowlist):
eval_dict[key] = value
eval_dict.update(
{
k: v
for k, v in sorted(identifier.params.items())
if v is not None and (param_allowlist is None or k in param_allowlist)
}
)

if identifier.children:
eval_children: dict[str, Any] = {}
Expand Down
2 changes: 1 addition & 1 deletion pyrit/setup/initializers/pyrit_initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def _track_initialization_changes(self) -> Iterator[dict[str, Any]]:
new_main_dict = sys.modules["__main__"].__dict__

# Track default values that were added - just collect class.parameter pairs
for scope, _value in new_defaults.items():
for scope in new_defaults:
if scope not in current_default_keys:
class_param = f"{scope.class_type.__name__}.{scope.parameter_name}"
if class_param not in tracking_info["default_values"]:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/datasets/test_jailbreak_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def teardown_method(self) -> None:
def test_scan_template_files_excludes_multi_parameter(self) -> None:
"""Test that _scan_template_files excludes files under multi_parameter directories."""
result = TextJailBreak._scan_template_files()
for _filename, paths in result.items():
for paths in result.values():
for path in paths:
assert "multi_parameter" not in path.parts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -912,9 +912,9 @@ def _get_adversarial_chat_text_values(*, adversarial_chat_conversation_id: str)

text_values = []
for msg in conversation:
for piece in msg.message_pieces:
if piece.original_value_data_type == "text":
text_values.append(piece.original_value)
text_values.extend(
piece.original_value for piece in msg.message_pieces if piece.original_value_data_type == "text"
)

return text_values

Expand Down
Loading