Skip to content

Commit fd694f9

Browse files
fix: use !r formatting in repr methods for proper escaping (#78)
Fix repr methods to use Python's !r format specifier instead of literal quotes. This properly escapes single quotes, backslashes, and newlines in text content. Add edge case tests for special chars.
1 parent 0e3b8e0 commit fd694f9

File tree

2 files changed

+37
-17
lines changed

2 files changed

+37
-17
lines changed

src/claude_agent_sdk/types.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ class TextBlock:
747747
text: str
748748

749749
def __repr__(self) -> str:
750-
return f"TextBlock(text='{_truncate(self.text)}')"
750+
return f"TextBlock(text={_truncate(self.text)!r})"
751751

752752

753753
@dataclass(repr=False)
@@ -758,7 +758,7 @@ class ThinkingBlock:
758758
signature: str
759759

760760
def __repr__(self) -> str:
761-
return f"ThinkingBlock(thinking='{_truncate(self.thinking)}')"
761+
return f"ThinkingBlock(thinking={_truncate(self.thinking)!r})"
762762

763763

764764
@dataclass(repr=False)
@@ -770,7 +770,7 @@ class ToolUseBlock:
770770
input: dict[str, Any]
771771

772772
def __repr__(self) -> str:
773-
return f"ToolUseBlock(id='{self.id}', name='{self.name}')"
773+
return f"ToolUseBlock(id={self.id!r}, name={self.name!r})"
774774

775775

776776
@dataclass(repr=False)
@@ -782,10 +782,10 @@ class ToolResultBlock:
782782
is_error: bool | None = None
783783

784784
def __repr__(self) -> str:
785-
parts = [f"tool_use_id='{self.tool_use_id}'"]
785+
parts = [f"tool_use_id={self.tool_use_id!r}"]
786786
if self.content is not None:
787787
if isinstance(self.content, str):
788-
parts.append(f"content='{_truncate(self.content)}'")
788+
parts.append(f"content={_truncate(self.content)!r}")
789789
else:
790790
parts.append(f"content={self.content!r}")
791791
if self.is_error:
@@ -818,7 +818,7 @@ class UserMessage:
818818

819819
def __repr__(self) -> str:
820820
if isinstance(self.content, str):
821-
return f"UserMessage(content='{_truncate(self.content)}')"
821+
return f"UserMessage(content={_truncate(self.content)!r})"
822822
return f"UserMessage(content={self.content!r})"
823823

824824

@@ -833,9 +833,9 @@ class AssistantMessage:
833833
usage: dict[str, Any] | None = None
834834

835835
def __repr__(self) -> str:
836-
parts = [f"model='{self.model}'", f"content={self.content!r}"]
836+
parts = [f"model={self.model!r}", f"content={self.content!r}"]
837837
if self.error is not None:
838-
parts.append(f"error='{self.error}'")
838+
parts.append(f"error={self.error!r}")
839839
return f"AssistantMessage({', '.join(parts)})"
840840

841841

@@ -847,7 +847,7 @@ class SystemMessage:
847847
data: dict[str, Any]
848848

849849
def __repr__(self) -> str:
850-
return f"SystemMessage(subtype='{self.subtype}')"
850+
return f"SystemMessage(subtype={self.subtype!r})"
851851

852852

853853
class TaskUsage(TypedDict):
@@ -879,7 +879,7 @@ class TaskStartedMessage(SystemMessage):
879879
task_type: str | None = None
880880

881881
def __repr__(self) -> str:
882-
return f"TaskStartedMessage(task_id='{self.task_id}', description='{_truncate(self.description)}')"
882+
return f"TaskStartedMessage(task_id={self.task_id!r}, description={_truncate(self.description)!r})"
883883

884884

885885
@dataclass(repr=False)
@@ -900,7 +900,7 @@ class TaskProgressMessage(SystemMessage):
900900
last_tool_name: str | None = None
901901

902902
def __repr__(self) -> str:
903-
return f"TaskProgressMessage(task_id='{self.task_id}', description='{_truncate(self.description)}')"
903+
return f"TaskProgressMessage(task_id={self.task_id!r}, description={_truncate(self.description)!r})"
904904

905905

906906
@dataclass(repr=False)
@@ -923,7 +923,7 @@ class TaskNotificationMessage(SystemMessage):
923923

924924
def __repr__(self) -> str:
925925
return (
926-
f"TaskNotificationMessage(task_id='{self.task_id}', status='{self.status}')"
926+
f"TaskNotificationMessage(task_id={self.task_id!r}, status={self.status!r})"
927927
)
928928

929929

@@ -950,7 +950,7 @@ def __repr__(self) -> str:
950950
if self.total_cost_usd is not None:
951951
parts.append(f"total_cost_usd={self.total_cost_usd}")
952952
if self.stop_reason is not None:
953-
parts.append(f"stop_reason='{self.stop_reason}'")
953+
parts.append(f"stop_reason={self.stop_reason!r}")
954954
return f"ResultMessage({', '.join(parts)})"
955955

956956

@@ -964,7 +964,7 @@ class StreamEvent:
964964
parent_tool_use_id: str | None = None
965965

966966
def __repr__(self) -> str:
967-
return f"StreamEvent(session_id='{self.session_id}')"
967+
return f"StreamEvent(session_id={self.session_id!r})"
968968

969969

970970
# Rate limit types — see https://docs.claude.com/en/docs/claude-code/rate-limits
@@ -1000,11 +1000,11 @@ class RateLimitInfo:
10001000
raw: dict[str, Any] = field(default_factory=dict)
10011001

10021002
def __repr__(self) -> str:
1003-
parts = [f"status='{self.status}'"]
1003+
parts = [f"status={self.status!r}"]
10041004
if self.utilization is not None:
10051005
parts.append(f"utilization={self.utilization}")
10061006
if self.rate_limit_type is not None:
1007-
parts.append(f"rate_limit_type='{self.rate_limit_type}'")
1007+
parts.append(f"rate_limit_type={self.rate_limit_type!r}")
10081008
return f"RateLimitInfo({', '.join(parts)})"
10091009

10101010

@@ -1022,7 +1022,7 @@ class RateLimitEvent:
10221022
session_id: str
10231023

10241024
def __repr__(self) -> str:
1025-
return f"RateLimitEvent(status='{self.rate_limit_info.status}')"
1025+
return f"RateLimitEvent(status={self.rate_limit_info.status!r})"
10261026

10271027

10281028
Message = (

tests/test_types.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,26 @@ def test_tool_result_block_no_content(self):
557557
r = repr(block)
558558
assert "None" not in r
559559

560+
def test_text_block_with_quotes(self):
561+
"""Ensure repr properly escapes quotes in text."""
562+
block = TextBlock(text='it\'s a "test"')
563+
r = repr(block)
564+
assert "TextBlock(text=" in r
565+
# !r formatting should produce a valid Python string literal
566+
assert r.count("TextBlock") == 1
567+
568+
def test_text_block_with_newlines(self):
569+
"""Ensure repr properly handles newlines."""
570+
block = TextBlock(text="line1\nline2")
571+
r = repr(block)
572+
assert "\\n" in r
573+
574+
def test_text_block_with_backslashes(self):
575+
"""Ensure repr properly handles backslashes."""
576+
block = TextBlock(text="path\\to\\file")
577+
r = repr(block)
578+
assert "TextBlock(text=" in r
579+
560580

561581
class TestMessageRepr:
562582
def test_user_message_string_content(self):

0 commit comments

Comments
 (0)