Skip to content

Commit 7d4cc16

Browse files
committed
Apply ruff formatting
1 parent 4cd9bb1 commit 7d4cc16

File tree

3 files changed

+73
-74
lines changed

3 files changed

+73
-74
lines changed

src/mcp/client/sse.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,19 @@
2020

2121
def remove_request_params(url: str) -> str:
2222
return urljoin(url, urlparse(url).path)
23-
23+
24+
2425
async def compliant_aiter_sse(event_source: EventSource) -> AsyncIterator[ServerSentEvent]:
2526
"""
2627
Safely iterate over SSE events, working around httpx issue where U+2028 and U+2029
2728
are incorrectly treated as newlines, breaking SSE stream parsing.
28-
29+
2930
This function replaces event_source.aiter_sse() to handle these Unicode characters
3031
correctly by processing the raw byte stream and only splitting on actual newlines.
31-
32+
3233
Args:
3334
event_source: The EventSource to iterate over
34-
35+
3536
Yields:
3637
ServerSentEvent objects parsed from the stream
3738
"""
@@ -44,10 +45,10 @@ async def compliant_aiter_sse(event_source: EventSource) -> AsyncIterator[Server
4445
# Note: this is tricky, because we could have a "\r" at the end of a chunk and not yet
4546
# know if the next chunk starts with a "\n" or not.
4647
skip_leading_lf = False
47-
48+
4849
async for chunk in event_source.response.aiter_bytes():
4950
buffer += chunk
50-
51+
5152
while len(buffer) != 0:
5253
if skip_leading_lf and buffer.startswith(b"\n"):
5354
buffer = buffer[1:]
@@ -63,21 +64,21 @@ async def compliant_aiter_sse(event_source: EventSource) -> AsyncIterator[Server
6364
break
6465

6566
line_bytes = buffer[:pos]
66-
buffer = buffer[pos + 1:]
67+
buffer = buffer[pos + 1 :]
6768

6869
# If we have a CR first, skip any LF immediately after (may be in next chunk)
69-
skip_leading_lf = (pos == cr)
70+
skip_leading_lf = pos == cr
7071

71-
line = line_bytes.decode('utf-8', errors='replace')
72+
line = line_bytes.decode("utf-8", errors="replace")
7273
sse = decoder.decode(line)
7374
if sse is not None:
7475
yield sse
75-
76+
7677
# Process any remaining data in buffer
7778
if buffer:
7879
assert b"\n" not in buffer
7980
assert b"\r" not in buffer
80-
line = buffer.decode('utf-8', errors='replace')
81+
line = buffer.decode("utf-8", errors="replace")
8182
sse = decoder.decode(line)
8283
if sse is not None:
8384
yield sse

tests/client/test_sse_unicode.py

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,146 +16,146 @@ def create_mock_event_source(data_chunks: list[bytes]) -> EventSource:
1616
event_source = MagicMock(spec=EventSource)
1717
response = AsyncMock()
1818
event_source.response = response
19-
19+
2020
async def mock_aiter_bytes() -> AsyncIterator[bytes]:
2121
for chunk in data_chunks:
2222
yield chunk
23-
23+
2424
response.aiter_bytes = mock_aiter_bytes
2525
return event_source
2626

2727

2828
async def test_compliant_aiter_sse_handles_unicode_line_separators():
2929
"""Test that compliant_aiter_sse correctly handles U+2028 and U+2029 characters."""
30-
30+
3131
# Simulate SSE data with U+2028 in JSON content
3232
# The server sends: event: message\ndata: {"text":"Hello\u2028World"}\n\n
3333
test_data = [
34-
b'event: message\n',
34+
b"event: message\n",
3535
b'data: {"text":"Hello',
36-
b'\xe2\x80\xa8', # UTF-8 encoding of U+2028
36+
b"\xe2\x80\xa8", # UTF-8 encoding of U+2028
3737
b'World"}\n',
38-
b'\n',
38+
b"\n",
3939
]
40-
40+
4141
event_source = create_mock_event_source(test_data)
42-
42+
4343
# Collect events
4444
events = [event async for event in compliant_aiter_sse(event_source)]
45-
45+
4646
# Should receive one message event
4747
assert len(events) == 1
4848
assert events[0].event == "message"
4949
# The U+2028 should be preserved in the data
50-
assert '\u2028' in events[0].data
50+
assert "\u2028" in events[0].data
5151
assert events[0].data == '{"text":"Hello\u2028World"}'
5252

5353

5454
async def test_compliant_aiter_sse_handles_paragraph_separator():
5555
"""Test that compliant_aiter_sse correctly handles U+2029 (PARAGRAPH SEPARATOR)."""
56-
56+
5757
# Simulate SSE data with U+2029
5858
test_data = [
59-
b'event: test\ndata: Line1',
60-
b'\xe2\x80\xa9', # UTF-8 encoding of U+2029
61-
b'Line2\n\n',
59+
b"event: test\ndata: Line1",
60+
b"\xe2\x80\xa9", # UTF-8 encoding of U+2029
61+
b"Line2\n\n",
6262
]
63-
63+
6464
event_source = create_mock_event_source(test_data)
65-
65+
6666
events = [event async for event in compliant_aiter_sse(event_source)]
67-
67+
6868
assert len(events) == 1
6969
assert events[0].event == "test"
7070
# U+2029 should be preserved, not treated as a newline
71-
assert '\u2029' in events[0].data
72-
assert events[0].data == 'Line1\u2029Line2'
71+
assert "\u2029" in events[0].data
72+
assert events[0].data == "Line1\u2029Line2"
7373

7474

7575
async def test_compliant_aiter_sse_handles_crlf():
7676
"""Test that compliant_aiter_sse correctly handles \\r\\n line endings."""
77-
77+
7878
# Simulate SSE data with CRLF line endings
7979
test_data = [
80-
b'event: message\r\n',
81-
b'data: test data\r\n',
82-
b'\r\n',
80+
b"event: message\r\n",
81+
b"data: test data\r\n",
82+
b"\r\n",
8383
]
84-
84+
8585
event_source = create_mock_event_source(test_data)
86-
86+
8787
events = [event async for event in compliant_aiter_sse(event_source)]
88-
88+
8989
assert len(events) == 1
9090
assert events[0].event == "message"
9191
assert events[0].data == "test data"
9292

9393

9494
async def test_compliant_aiter_sse_handles_split_utf8():
9595
"""Test that compliant_aiter_sse handles UTF-8 characters split across chunks."""
96-
96+
9797
# Split a UTF-8 emoji (🎉 = \xf0\x9f\x8e\x89) across chunks
9898
test_data = [
99-
b'event: message\n',
100-
b'data: Party ',
101-
b'\xf0\x9f', # First half of emoji
102-
b'\x8e\x89', # Second half of emoji
103-
b' time!\n\n',
99+
b"event: message\n",
100+
b"data: Party ",
101+
b"\xf0\x9f", # First half of emoji
102+
b"\x8e\x89", # Second half of emoji
103+
b" time!\n\n",
104104
]
105-
105+
106106
event_source = create_mock_event_source(test_data)
107-
107+
108108
events = [event async for event in compliant_aiter_sse(event_source)]
109-
109+
110110
assert len(events) == 1
111111
assert events[0].event == "message"
112112
assert events[0].data == "Party 🎉 time!"
113113

114114

115115
async def test_compliant_aiter_sse_handles_multiple_events():
116116
"""Test that compliant_aiter_sse correctly handles multiple SSE events."""
117-
117+
118118
# Multiple events with problematic Unicode
119119
test_data = [
120-
b'event: first\ndata: Hello\xe2\x80\xa8World\n\n',
121-
b'event: second\ndata: Test\xe2\x80\xa9Data\n\n',
122-
b'data: No event name\n\n',
120+
b"event: first\ndata: Hello\xe2\x80\xa8World\n\n",
121+
b"event: second\ndata: Test\xe2\x80\xa9Data\n\n",
122+
b"data: No event name\n\n",
123123
]
124-
124+
125125
event_source = create_mock_event_source(test_data)
126-
126+
127127
events = [event async for event in compliant_aiter_sse(event_source)]
128-
128+
129129
assert len(events) == 3
130-
130+
131131
assert events[0].event == "first"
132-
assert '\u2028' in events[0].data
133-
132+
assert "\u2028" in events[0].data
133+
134134
assert events[1].event == "second"
135-
assert '\u2029' in events[1].data
136-
135+
assert "\u2029" in events[1].data
136+
137137
# Default event type is "message"
138138
assert events[2].event == "message"
139139
assert events[2].data == "No event name"
140140

141141

142142
async def test_compliant_aiter_sse_handles_split_crlf():
143143
"""Test that \r at end of chunk followed by \n in next chunk is treated as one newline."""
144-
144+
145145
# Test case where \r is at the end of one chunk and \n starts the next
146146
# This should be treated as a single CRLF line ending, not two separate newlines
147147
test_data = [
148-
b'event: test\r', # \r at end of chunk
149-
b'\ndata: line1\r', # \n at start of next chunk, then another \r at end
150-
b'\ndata: line2\n\n', # \n at start, completing the CRLF
148+
b"event: test\r", # \r at end of chunk
149+
b"\ndata: line1\r", # \n at start of next chunk, then another \r at end
150+
b"\ndata: line2\n\n", # \n at start, completing the CRLF
151151
]
152-
152+
153153
event_source = create_mock_event_source(test_data)
154-
154+
155155
events = [event async for event in compliant_aiter_sse(event_source)]
156-
156+
157157
# Should get exactly one event with both data lines
158158
assert len(events) == 1
159159
assert events[0].event == "test"
160160
# The SSE decoder concatenates multiple data fields with \n
161-
assert events[0].data == "line1\nline2"
161+
assert events[0].data == "line1\nline2"

tests/issues/test_1356_sse_parsing_line_separator.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ async def handle_sse(request: Request) -> Response:
7878
def run_problematic_server(server_port: int) -> None:
7979
"""Run the problematic Unicode test server."""
8080
app = make_problematic_server_app()
81-
server = uvicorn.Server(
82-
config=uvicorn.Config(app=app, host="127.0.0.1", port=server_port, log_level="error")
83-
)
81+
server = uvicorn.Server(config=uvicorn.Config(app=app, host="127.0.0.1", port=server_port, log_level="error"))
8482
server.run()
8583

8684

@@ -123,7 +121,7 @@ def problematic_server(problematic_server_port: int) -> Generator[str, None, Non
123121

124122
async def test_json_parsing_with_problematic_unicode(problematic_server: str) -> None:
125123
"""Test that special Unicode characters like U+2028 are handled properly.
126-
124+
127125
This test reproduces issue #1356 where special Unicode characters
128126
cause JSON parsing to fail and the raw exception is sent to the stream,
129127
preventing proper error handling.
@@ -137,20 +135,20 @@ async def test_json_parsing_with_problematic_unicode(problematic_server: str) ->
137135

138136
# Call the tool that returns problematic Unicode
139137
# This should succeed and not hang
140-
138+
141139
# Use a timeout to detect if we're hanging
142140
with anyio.fail_after(5): # 5 second timeout
143141
try:
144142
response = await session.call_tool("get_problematic_unicode", {})
145-
143+
146144
# If we get here, the Unicode was handled properly
147145
assert len(response.content) == 1
148146
text_content = response.content[0]
149147
assert hasattr(text_content, "text"), f"Response doesn't have text: {text_content}"
150-
148+
151149
expected = "This text contains a line separator\u2028character that may break JSON parsing"
152150
assert text_content.text == expected, f"Expected: {expected!r}, Got: {text_content.text!r}"
153-
151+
154152
except McpError:
155153
pytest.fail("Unexpected error with tool call")
156154
except TimeoutError:

0 commit comments

Comments
 (0)