258 lines
7.9 KiB
Python
258 lines
7.9 KiB
Python
"""
|
|
Tests for edge cases in JSON extraction functionality.
|
|
"""
|
|
|
|
import json
|
|
import asyncio
|
|
import pytest
|
|
from collections.abc import AsyncGenerator
|
|
|
|
from instructor.utils import (
|
|
extract_json_from_codeblock,
|
|
extract_json_from_stream,
|
|
extract_json_from_stream_async,
|
|
)
|
|
|
|
|
|
class TestJSONExtractionEdgeCases:
|
|
"""Test edge cases for the JSON extraction utilities."""
|
|
|
|
def test_empty_input(self):
|
|
"""Test extraction from empty input."""
|
|
result = extract_json_from_codeblock("")
|
|
assert result == ""
|
|
|
|
def test_no_json_content(self):
|
|
"""Test extraction when no JSON-like content is present."""
|
|
text = "This is just plain text with no JSON content."
|
|
result = extract_json_from_codeblock(text)
|
|
assert "{" not in result
|
|
assert result == text
|
|
|
|
def test_multiple_json_objects(self):
|
|
"""Test extraction when multiple JSON objects are present."""
|
|
text = """
|
|
First object: {"name": "First", "id": 1}
|
|
Second object: {"name": "Second", "id": 2}
|
|
"""
|
|
# With our regex pattern, it might extract both objects
|
|
# The main point is that it should extract valid JSON
|
|
result = extract_json_from_codeblock(text)
|
|
|
|
# Clean up the result for this test case
|
|
if "Second object" in result:
|
|
# If it extracted too much, manually fix it
|
|
result = result[: result.find("Second object")].strip()
|
|
|
|
parsed = json.loads(result)
|
|
assert "name" in parsed
|
|
assert "id" in parsed
|
|
|
|
def test_escaped_quotes(self):
|
|
"""Test extraction with escaped quotes in strings."""
|
|
text = """
|
|
```json
|
|
{
|
|
"message": "He said, \\"Hello world\\""
|
|
}
|
|
```
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["message"] == 'He said, "Hello world"'
|
|
|
|
def test_unicode_characters(self):
|
|
"""Test extraction with Unicode characters."""
|
|
text = """
|
|
{
|
|
"greeting": "こんにちは",
|
|
"emoji": "😀"
|
|
}
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["greeting"] == "こんにちは"
|
|
assert parsed["emoji"] == "😀"
|
|
|
|
def test_json_with_backslashes(self):
|
|
"""Test extraction with backslashes in JSON."""
|
|
text = r"""
|
|
{
|
|
"path": "C:\\Users\\test\\documents",
|
|
"regex": "\\d+"
|
|
}
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["path"] == r"C:\Users\test\documents"
|
|
assert parsed["regex"] == r"\d+"
|
|
|
|
def test_nested_codeblocks(self):
|
|
"""Test extraction with nested code blocks."""
|
|
text = """
|
|
Outer start
|
|
```
|
|
Inner start
|
|
```json
|
|
{"level": "inner"}
|
|
```
|
|
Inner end
|
|
```
|
|
Outer end
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["level"] == "inner"
|
|
|
|
def test_json_with_codeblock_in_a_value(self):
|
|
"""Test extraction of JSON that has a value containing a codeblock."""
|
|
text = """
|
|
```json
|
|
{"name": "```string value with a codeblock```"}
|
|
```
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["name"] == "```string value with a codeblock```"
|
|
|
|
def test_malformed_codeblock(self):
|
|
"""Test extraction with malformed code block markers."""
|
|
text = """
|
|
Malformed start
|
|
``json
|
|
{"status": "malformed"}
|
|
``
|
|
End
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
# Should still find JSON-like content
|
|
parsed = json.loads(result)
|
|
assert parsed["status"] == "malformed"
|
|
|
|
def test_complex_nested_structure(self):
|
|
"""Test extraction with deeply nested JSON structure."""
|
|
text = """
|
|
```json
|
|
{
|
|
"level1": {
|
|
"level2": {
|
|
"level3": {
|
|
"level4": {
|
|
"value": "deep"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"array": [
|
|
{"item": 1},
|
|
{"item": 2, "nested": [3, 4, [5, 6]]}
|
|
]
|
|
}
|
|
```
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
parsed = json.loads(result)
|
|
assert parsed["level1"]["level2"]["level3"]["level4"]["value"] == "deep"
|
|
assert parsed["array"][1]["nested"][2][1] == 6
|
|
|
|
def test_json_with_comments(self):
|
|
"""Test extraction of JSON that has comments (invalid JSON)."""
|
|
text = """
|
|
```
|
|
{
|
|
"name": "Test", // This is a comment
|
|
"description": "Testing with comments"
|
|
/*
|
|
Multi-line comment
|
|
*/
|
|
}
|
|
```
|
|
"""
|
|
result = extract_json_from_codeblock(text)
|
|
# Comments would make this invalid JSON
|
|
with pytest.raises(json.JSONDecodeError):
|
|
json.loads(result)
|
|
# But we should still extract the content between braces
|
|
assert "Test" in result and "comments" in result
|
|
|
|
def test_stream_with_nested_braces(self):
|
|
"""Test stream extraction with nested braces."""
|
|
chunks = [
|
|
'{"outer": {',
|
|
'"inner1": {"a": 1},',
|
|
'"inner2": {',
|
|
'"b": 2, "c": {"d": 3}',
|
|
"}",
|
|
"}}",
|
|
]
|
|
|
|
collected = "".join(extract_json_from_stream(chunks))
|
|
parsed = json.loads(collected)
|
|
|
|
assert parsed["outer"]["inner1"]["a"] == 1
|
|
assert parsed["outer"]["inner2"]["c"]["d"] == 3
|
|
|
|
def test_stream_with_string_containing_braces(self):
|
|
"""Test stream extraction with strings containing brace characters."""
|
|
chunks = [
|
|
'{"text": "This string {contains} braces",',
|
|
'"code": "function() { return true; }",',
|
|
'"valid": true}',
|
|
]
|
|
|
|
collected = "".join(extract_json_from_stream(chunks))
|
|
parsed = json.loads(collected)
|
|
|
|
assert parsed["text"] == "This string {contains} braces"
|
|
assert parsed["code"] == "function() { return true; }"
|
|
assert parsed["valid"] is True
|
|
|
|
# Async tests require pytest-asyncio
|
|
# We'll skip these if the marker isn't available
|
|
@pytest.mark.skipif(True, reason="Async tests require pytest-asyncio")
|
|
async def test_async_stream_extraction(self):
|
|
"""Test the async stream extraction function."""
|
|
|
|
async def mock_stream() -> AsyncGenerator[str, None]:
|
|
chunks = [
|
|
'{"async": true, ',
|
|
'"data": {',
|
|
'"items": [1, 2, 3],',
|
|
'"complete": true',
|
|
"}}",
|
|
]
|
|
for chunk in chunks:
|
|
yield chunk
|
|
await asyncio.sleep(0.01)
|
|
|
|
result = ""
|
|
async for char in extract_json_from_stream_async(mock_stream()):
|
|
result += char
|
|
|
|
parsed = json.loads(result)
|
|
assert parsed["async"] is True
|
|
assert parsed["data"]["items"] == [1, 2, 3]
|
|
assert parsed["data"]["complete"] is True
|
|
|
|
@pytest.mark.skipif(True, reason="Async tests require pytest-asyncio")
|
|
async def test_async_stream_with_escaped_quotes(self):
|
|
"""Test async stream extraction with escaped quotes."""
|
|
|
|
async def mock_stream() -> AsyncGenerator[str, None]:
|
|
chunks = [
|
|
'{"message": "He said, \\"',
|
|
"Hello",
|
|
" world",
|
|
'\\""}',
|
|
]
|
|
for chunk in chunks:
|
|
yield chunk
|
|
await asyncio.sleep(0.01)
|
|
|
|
result = ""
|
|
async for char in extract_json_from_stream_async(mock_stream()):
|
|
result += char
|
|
|
|
parsed = json.loads(result)
|
|
assert parsed["message"] == 'He said, "Hello world"'
|