Skip to content

Commit faeed9f

Browse files
romanlutzCopilot
andauthored
MAINT Deduplicate pretty output color formatting (#2318)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 026dff00-745e-42fb-9748-5555ee006073
1 parent 53073e5 commit faeed9f

7 files changed

Lines changed: 67 additions & 86 deletions

File tree

pyrit/output/_formatting.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
from colorama import Style
5+
6+
7+
class _PrettyPrinterMixin:
8+
"""Shared ANSI line formatting for pretty printers."""
9+
10+
_enable_colors: bool
11+
12+
def _format_colored(self, text: str, *colors: str) -> str:
13+
"""
14+
Format text with color codes if colors are enabled.
15+
16+
Args:
17+
text (str): The text to format.
18+
*colors: Variable number of colorama color constants to apply.
19+
20+
Returns:
21+
str: The formatted line with trailing newline.
22+
"""
23+
if self._enable_colors and colors:
24+
color_prefix = "".join(colors)
25+
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
26+
return f"{text}\n"

pyrit/output/attack_result/pretty.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@
77
from colorama import Back, Fore, Style
88

99
from pyrit.models import AttackOutcome, AttackResult, ConversationType, Message, Score
10+
from pyrit.output._formatting import _PrettyPrinterMixin
1011
from pyrit.output.attack_result.base import AttackResultPrinterBase
1112
from pyrit.output.conversation.pretty import PrettyConversationPrinter
1213
from pyrit.output.score.pretty import PrettyScorePrinter
1314
from pyrit.output.sink import Sink
1415

1516

16-
class PrettyAttackResultPrinter(AttackResultPrinterBase):
17+
class PrettyAttackResultPrinter(_PrettyPrinterMixin, AttackResultPrinterBase):
1718
"""
1819
Pretty printer for attack results with ANSI-colored formatting.
1920
@@ -68,22 +69,6 @@ def __init__(
6869
blur_radius=blur_radius,
6970
)
7071

71-
def _format_colored(self, text: str, *colors: str) -> str:
72-
"""
73-
Format text with color codes if colors are enabled.
74-
75-
Args:
76-
text (str): The text to format.
77-
*colors: Variable number of colorama color constants to apply.
78-
79-
Returns:
80-
str: The formatted line with trailing newline.
81-
"""
82-
if self._enable_colors and colors:
83-
color_prefix = "".join(colors)
84-
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
85-
return f"{text}\n"
86-
8772
async def render_async(
8873
self,
8974
result: AttackResult,

pyrit/output/conversation/pretty.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
from colorama import Fore, Style
88

99
from pyrit.models import Message, MessagePiece, Score
10+
from pyrit.output._formatting import _PrettyPrinterMixin
1011
from pyrit.output.conversation.base import ConversationPrinterBase
1112
from pyrit.output.score.pretty import PrettyScorePrinter
1213
from pyrit.output.sink import Sink
1314

1415
logger = logging.getLogger(__name__)
1516

1617

17-
class PrettyConversationPrinter(ConversationPrinterBase):
18+
class PrettyConversationPrinter(_PrettyPrinterMixin, ConversationPrinterBase):
1819
"""
1920
Pretty printer for conversation message histories with ANSI-colored formatting.
2021
@@ -174,22 +175,6 @@ async def render_async(
174175

175176
return "".join(lines)
176177

177-
def _format_colored(self, text: str, *colors: str) -> str:
178-
"""
179-
Format text with color codes if colors are enabled.
180-
181-
Args:
182-
text (str): The text to format.
183-
*colors: Variable number of colorama color constants to apply.
184-
185-
Returns:
186-
str: The formatted line with trailing newline.
187-
"""
188-
if self._enable_colors and colors:
189-
color_prefix = "".join(colors)
190-
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
191-
return f"{text}\n"
192-
193178
def _render_wrapped_text(self, text: str, color: str) -> str:
194179
"""
195180
Render text with proper wrapping and indentation, preserving newlines.

pyrit/output/scenario_result/pretty.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from colorama import Fore, Style
77

88
from pyrit.models import AttackOutcome, ScenarioResult
9+
from pyrit.output._formatting import _PrettyPrinterMixin
910
from pyrit.output.scenario_result.base import ScenarioResultPrinterBase
1011
from pyrit.output.scorer.base import ScorerPrinterBase
1112
from pyrit.output.sink import Sink
1213

1314

14-
class PrettyScenarioResultPrinter(ScenarioResultPrinterBase):
15+
class PrettyScenarioResultPrinter(_PrettyPrinterMixin, ScenarioResultPrinterBase):
1516
"""
1617
Pretty printer for scenario results with ANSI-colored formatting.
1718
@@ -51,22 +52,6 @@ def __init__(
5152
self._scorer_printer = scorer_printer
5253
self._sort_groups_by_success_rate = sort_groups_by_success_rate
5354

54-
def _format_colored(self, text: str, *colors: str) -> str:
55-
"""
56-
Format text with color codes if colors are enabled.
57-
58-
Args:
59-
text (str): The text to format.
60-
*colors: Variable number of colorama color constants to apply.
61-
62-
Returns:
63-
str: The formatted line with trailing newline.
64-
"""
65-
if self._enable_colors and colors:
66-
color_prefix = "".join(colors)
67-
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
68-
return f"{text}\n"
69-
7055
def _render_section_header(self, title: str) -> str:
7156
"""
7257
Render a section header with visual separation.

pyrit/output/score/pretty.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
import textwrap
55

6-
from colorama import Fore, Style
6+
from colorama import Fore
77

88
from pyrit.models import Score
9+
from pyrit.output._formatting import _PrettyPrinterMixin
910
from pyrit.output.base import PrinterBase
1011
from pyrit.output.sink import Sink
1112

1213

13-
class PrettyScorePrinter(PrinterBase):
14+
class PrettyScorePrinter(_PrettyPrinterMixin, PrinterBase):
1415
"""
1516
Pretty printer for individual Score objects with ANSI-colored formatting.
1617
@@ -36,22 +37,6 @@ def __init__(
3637
self._indent = " " * indent_size
3738
self._enable_colors = enable_colors
3839

39-
def _format_colored(self, text: str, *colors: str) -> str:
40-
"""
41-
Format text with color codes if colors are enabled.
42-
43-
Args:
44-
text (str): The text to format.
45-
*colors: Variable number of colorama color constants to apply.
46-
47-
Returns:
48-
str: The formatted line with trailing newline.
49-
"""
50-
if self._enable_colors and colors:
51-
color_prefix = "".join(colors)
52-
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
53-
return f"{text}\n"
54-
5540
def _render_score(self, score: Score, indent_level: int = 3) -> str:
5641
"""
5742
Render a single score with proper formatting.

pyrit/output/scorer/pretty.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from colorama import Fore, Style
77

88
from pyrit.models import ComponentIdentifier
9+
from pyrit.output._formatting import _PrettyPrinterMixin
910
from pyrit.output.scorer.base import ScorerPrinterBase
1011
from pyrit.output.sink import Sink
1112

1213

13-
class PrettyScorerPrinter(ScorerPrinterBase):
14+
class PrettyScorerPrinter(_PrettyPrinterMixin, ScorerPrinterBase):
1415
"""
1516
Pretty printer for scorer information with ANSI-colored formatting.
1617
@@ -39,22 +40,6 @@ def __init__(self, *, sink: Sink | None = None, indent_size: int = 2, enable_col
3940
self._indent = " " * indent_size
4041
self._enable_colors = enable_colors
4142

42-
def _format_colored(self, text: str, *colors: str) -> str:
43-
"""
44-
Format text with color codes if colors are enabled.
45-
46-
Args:
47-
text (str): The text to format.
48-
*colors: Variable number of colorama color constants to apply.
49-
50-
Returns:
51-
str: The formatted line with trailing newline.
52-
"""
53-
if self._enable_colors and colors:
54-
color_prefix = "".join(colors)
55-
return f"{color_prefix}{text}{Style.RESET_ALL}\n"
56-
return f"{text}\n"
57-
5843
def _get_quality_color(
5944
self, value: float, *, higher_is_better: bool, good_threshold: float, bad_threshold: float
6045
) -> str:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
import pytest
5+
from colorama import Fore, Style
6+
7+
from pyrit.output._formatting import _PrettyPrinterMixin
8+
9+
10+
class _TestPrettyPrinter(_PrettyPrinterMixin):
11+
def __init__(self, *, enable_colors: bool) -> None:
12+
self._enable_colors = enable_colors
13+
14+
15+
@pytest.mark.parametrize(
16+
"enable_colors,colors,expected",
17+
[
18+
(True, (Style.BRIGHT, Fore.RED), f"{Style.BRIGHT}{Fore.RED}text{Style.RESET_ALL}\n"),
19+
(False, (Style.BRIGHT, Fore.RED), "text\n"),
20+
(True, (), "text\n"),
21+
],
22+
)
23+
def test_format_colored_preserves_line_output(
24+
enable_colors: bool,
25+
colors: tuple[str, ...],
26+
expected: str,
27+
) -> None:
28+
printer = _TestPrettyPrinter(enable_colors=enable_colors)
29+
30+
assert printer._format_colored("text", *colors) == expected

0 commit comments

Comments
 (0)