97e91a83f3
Ruff / Ruff (push) Waiting to run
Test / Core Tests (push) Waiting to run
Test / Offline Coverage Tests (Python 3.10) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.11) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.12) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.13) (push) Waiting to run
Test / Offline Coverage Tests (Python 3.9) (push) Waiting to run
Test / Full Coverage (Python 3.11) (push) Waiting to run
Test / Core Provider Tests (OpenAI) (push) Blocked by required conditions
Test / Core Provider Tests (Anthropic) (push) Blocked by required conditions
Test / Core Provider Tests (Google) (push) Blocked by required conditions
Test / Core Provider Tests (Other) (push) Blocked by required conditions
Test / Anthropic Tests (push) Blocked by required conditions
Test / Gemini Tests (push) Blocked by required conditions
Test / Google GenAI Tests (push) Blocked by required conditions
Test / Vertex AI Tests (push) Blocked by required conditions
Test / OpenAI Tests (push) Blocked by required conditions
Test / Writer Tests (push) Blocked by required conditions
Test / Auto Client Tests (push) Blocked by required conditions
ty / type-check (push) Waiting to run
438 lines
15 KiB
Python
438 lines
15 KiB
Python
"""Test that all instructor exceptions can be imported and caught properly."""
|
|
|
|
import pytest
|
|
from json import JSONDecodeError
|
|
from instructor.core.exceptions import (
|
|
InstructorError,
|
|
IncompleteOutputException,
|
|
InstructorRetryException,
|
|
ValidationError,
|
|
ProviderError,
|
|
ConfigurationError,
|
|
ModeError,
|
|
ClientError,
|
|
FailedAttempt,
|
|
)
|
|
|
|
|
|
def test_exception_hierarchy():
|
|
"""Test that all exceptions inherit from InstructorError."""
|
|
assert issubclass(IncompleteOutputException, InstructorError)
|
|
assert issubclass(InstructorRetryException, InstructorError)
|
|
assert issubclass(ValidationError, InstructorError)
|
|
assert issubclass(ProviderError, InstructorError)
|
|
assert issubclass(ConfigurationError, InstructorError)
|
|
assert issubclass(ModeError, InstructorError)
|
|
assert issubclass(ClientError, InstructorError)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"exception_factory",
|
|
[
|
|
lambda: IncompleteOutputException(),
|
|
lambda: InstructorRetryException(n_attempts=3, total_usage=100),
|
|
lambda: ValidationError("Validation failed"),
|
|
lambda: ProviderError("openai", "API error"),
|
|
lambda: ConfigurationError("Invalid config"),
|
|
lambda: ModeError("tools", "openai", ["json"]),
|
|
lambda: ClientError("Client initialization failed"),
|
|
],
|
|
)
|
|
def test_base_instructor_error_can_be_caught(exception_factory):
|
|
"""Test that InstructorError can catch all instructor exceptions."""
|
|
with pytest.raises(InstructorError):
|
|
raise exception_factory()
|
|
|
|
|
|
def test_incomplete_output_exception():
|
|
"""Test IncompleteOutputException attributes and catching."""
|
|
last_completion = {"content": "partial response"}
|
|
|
|
with pytest.raises(IncompleteOutputException) as exc_info:
|
|
raise IncompleteOutputException(last_completion=last_completion)
|
|
|
|
assert exc_info.value.last_completion == last_completion
|
|
assert "incomplete due to a max_tokens length limit" in str(exc_info.value)
|
|
|
|
|
|
def test_instructor_retry_exception():
|
|
"""Test InstructorRetryException attributes and catching."""
|
|
last_completion = {"content": "failed response"}
|
|
messages = [{"role": "user", "content": "test"}]
|
|
n_attempts = 3
|
|
total_usage = 150
|
|
create_kwargs = {"model": "gpt-4.1-mini"}
|
|
|
|
with pytest.raises(InstructorRetryException) as exc_info:
|
|
raise InstructorRetryException(
|
|
last_completion=last_completion,
|
|
messages=messages,
|
|
n_attempts=n_attempts,
|
|
total_usage=total_usage,
|
|
create_kwargs=create_kwargs,
|
|
)
|
|
|
|
exception = exc_info.value
|
|
assert exception.last_completion == last_completion
|
|
assert exception.messages == messages
|
|
assert exception.n_attempts == n_attempts
|
|
assert exception.total_usage == total_usage
|
|
assert exception.create_kwargs == create_kwargs
|
|
|
|
|
|
def test_validation_error():
|
|
"""Test ValidationError can be caught."""
|
|
error_message = "Field validation failed"
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
raise ValidationError(error_message)
|
|
|
|
assert str(exc_info.value) == error_message
|
|
|
|
|
|
def test_provider_error():
|
|
"""Test ProviderError attributes and catching."""
|
|
provider = "anthropic"
|
|
message = "Rate limit exceeded"
|
|
|
|
with pytest.raises(ProviderError) as exc_info:
|
|
raise ProviderError(provider, message)
|
|
|
|
exception = exc_info.value
|
|
assert exception.provider == provider
|
|
assert f"{provider}: {message}" in str(exception)
|
|
|
|
|
|
def test_configuration_error():
|
|
"""Test ConfigurationError can be caught."""
|
|
error_message = "Missing required configuration"
|
|
|
|
with pytest.raises(ConfigurationError) as exc_info:
|
|
raise ConfigurationError(error_message)
|
|
|
|
assert str(exc_info.value) == error_message
|
|
|
|
|
|
def test_mode_error():
|
|
"""Test ModeError attributes and catching."""
|
|
mode = "invalid_mode"
|
|
provider = "openai"
|
|
valid_modes = ["json", "tools", "functions"]
|
|
|
|
with pytest.raises(ModeError) as exc_info:
|
|
raise ModeError(mode, provider, valid_modes)
|
|
|
|
exception = exc_info.value
|
|
assert exception.mode == mode
|
|
assert exception.provider == provider
|
|
assert exception.valid_modes == valid_modes
|
|
assert f"Invalid mode '{mode}' for provider '{provider}'" in str(exception)
|
|
assert "json, tools, functions" in str(exception)
|
|
|
|
|
|
def test_client_error():
|
|
"""Test ClientError can be caught."""
|
|
error_message = "Client not properly initialized"
|
|
|
|
with pytest.raises(ClientError) as exc_info:
|
|
raise ClientError(error_message)
|
|
|
|
assert str(exc_info.value) == error_message
|
|
|
|
|
|
def test_multiple_exception_handling():
|
|
"""Test handling multiple exception types in a single try-except block."""
|
|
|
|
def raise_exception(exc_type: str):
|
|
if exc_type == "incomplete":
|
|
raise IncompleteOutputException()
|
|
elif exc_type == "retry":
|
|
raise InstructorRetryException(n_attempts=3, total_usage=100)
|
|
elif exc_type == "validation":
|
|
raise ValidationError("validation failed")
|
|
else:
|
|
raise ValueError("unknown exception type")
|
|
|
|
# Test catching multiple specific exceptions
|
|
for exc_type in ["incomplete", "retry", "validation"]:
|
|
with pytest.raises(
|
|
(IncompleteOutputException, InstructorRetryException, ValidationError)
|
|
):
|
|
raise_exception(exc_type)
|
|
|
|
# Test that base exception catches all instructor exceptions
|
|
for exc_type in ["incomplete", "retry", "validation"]:
|
|
with pytest.raises(InstructorError):
|
|
raise_exception(exc_type)
|
|
|
|
# Test that non-instructor exceptions are not caught
|
|
with pytest.raises(ValueError):
|
|
raise_exception("unknown")
|
|
|
|
|
|
def test_instructor_error_from_exception():
|
|
"""Test InstructorError.from_exception() class method."""
|
|
# Test with basic exception
|
|
original_exception = ValueError("Original error message")
|
|
instructor_error = InstructorError.from_exception(original_exception)
|
|
|
|
assert isinstance(instructor_error, InstructorError)
|
|
assert str(instructor_error) == "Original error message"
|
|
assert instructor_error.failed_attempts is None
|
|
|
|
# Test with failed attempts
|
|
failed_attempts = [
|
|
FailedAttempt(1, Exception("First failure"), "partial completion"),
|
|
FailedAttempt(2, Exception("Second failure"), None),
|
|
]
|
|
instructor_error_with_attempts = InstructorError.from_exception(
|
|
original_exception, failed_attempts=failed_attempts
|
|
)
|
|
|
|
assert isinstance(instructor_error_with_attempts, InstructorError)
|
|
assert instructor_error_with_attempts.failed_attempts == failed_attempts
|
|
|
|
# Test with different exception types
|
|
runtime_error = RuntimeError("Runtime issue")
|
|
instructor_error_runtime = InstructorError.from_exception(runtime_error)
|
|
assert str(instructor_error_runtime) == "Runtime issue"
|
|
|
|
|
|
def test_instructor_error_str_with_failed_attempts():
|
|
"""Test InstructorError.__str__() XML template rendering with failed attempts."""
|
|
# Create failed attempts
|
|
failed_attempts = [
|
|
FailedAttempt(1, ValueError("Validation failed"), "incomplete response"),
|
|
FailedAttempt(2, KeyError("Missing key"), {"partial": "data"}),
|
|
FailedAttempt(3, RuntimeError("Process failed"), None),
|
|
]
|
|
|
|
error = InstructorError("Final error message", failed_attempts=failed_attempts)
|
|
error_str = str(error)
|
|
|
|
# Check that XML structure is present
|
|
assert "<failed_attempts>" in error_str
|
|
assert "</failed_attempts>" in error_str
|
|
assert "<last_exception>" in error_str
|
|
assert "</last_exception>" in error_str
|
|
|
|
# Check that all attempts are included
|
|
assert 'number="1"' in error_str
|
|
assert 'number="2"' in error_str
|
|
assert 'number="3"' in error_str
|
|
|
|
# Check that exceptions are included
|
|
assert "Validation failed" in error_str
|
|
assert "Missing key" in error_str
|
|
assert "Process failed" in error_str
|
|
|
|
# Check that completions are included
|
|
assert "incomplete response" in error_str
|
|
assert "partial" in error_str
|
|
|
|
# Check that final exception is included
|
|
assert "Final error message" in error_str
|
|
|
|
|
|
def test_instructor_retry_exception_with_failed_attempts():
|
|
"""Test InstructorRetryException inherits failed_attempts functionality."""
|
|
failed_attempts = [
|
|
FailedAttempt(1, Exception("First error"), "first completion"),
|
|
FailedAttempt(2, Exception("Second error"), "second completion"),
|
|
]
|
|
|
|
retry_exception = InstructorRetryException(
|
|
"Retry exhausted",
|
|
n_attempts=3,
|
|
total_usage=100,
|
|
failed_attempts=failed_attempts,
|
|
)
|
|
|
|
# Check that it inherits the XML formatting
|
|
error_str = str(retry_exception)
|
|
assert "<failed_attempts>" in error_str
|
|
assert "First error" in error_str
|
|
assert "Second error" in error_str
|
|
assert "first completion" in error_str
|
|
assert "second completion" in error_str
|
|
|
|
|
|
def test_multiple_exception_types_with_failed_attempts():
|
|
"""Test that various exception types work with failed attempts."""
|
|
failed_attempts = [FailedAttempt(1, Exception("Test"), None)]
|
|
|
|
# Test various exception types can be created with failed attempts
|
|
validation_error = ValidationError(
|
|
"Validation failed", failed_attempts=failed_attempts
|
|
)
|
|
assert validation_error.failed_attempts == failed_attempts
|
|
|
|
provider_error = ProviderError(
|
|
"openai", "API error", failed_attempts=failed_attempts
|
|
)
|
|
assert provider_error.failed_attempts == failed_attempts
|
|
|
|
config_error = ConfigurationError("Config error", failed_attempts=failed_attempts)
|
|
assert config_error.failed_attempts == failed_attempts
|
|
|
|
|
|
def test_failed_attempts_propagation_through_retry_cycles():
|
|
"""Test that failed attempts accumulate and propagate correctly through retry cycles."""
|
|
# Simulate multiple retry attempts with different exceptions
|
|
attempt1 = FailedAttempt(1, ValidationError("Invalid format"), "partial response 1")
|
|
attempt2 = FailedAttempt(2, KeyError("missing_field"), "partial response 2")
|
|
attempt3 = FailedAttempt(3, ValueError("invalid value"), "partial response 3")
|
|
|
|
failed_attempts = [attempt1, attempt2, attempt3]
|
|
|
|
# Create final retry exception with accumulated failed attempts
|
|
final_exception = InstructorRetryException(
|
|
"All retries exhausted",
|
|
n_attempts=3,
|
|
total_usage=250,
|
|
failed_attempts=failed_attempts,
|
|
)
|
|
|
|
# Verify failed attempts are properly stored
|
|
assert final_exception.failed_attempts == failed_attempts
|
|
assert final_exception.failed_attempts is not None
|
|
assert len(final_exception.failed_attempts) == 3
|
|
|
|
# Verify attempt numbers are sequential
|
|
attempt_numbers = [
|
|
attempt.attempt_number for attempt in final_exception.failed_attempts
|
|
]
|
|
assert attempt_numbers == [1, 2, 3]
|
|
|
|
# Verify each attempt has different exceptions
|
|
exception_types = [
|
|
type(attempt.exception).__name__ for attempt in final_exception.failed_attempts
|
|
]
|
|
assert exception_types == ["ValidationError", "KeyError", "ValueError"]
|
|
|
|
# Verify completions are preserved
|
|
completions = [attempt.completion for attempt in final_exception.failed_attempts]
|
|
assert completions == [
|
|
"partial response 1",
|
|
"partial response 2",
|
|
"partial response 3",
|
|
]
|
|
|
|
|
|
def test_failed_attempts_propagation_in_exception_hierarchy():
|
|
"""Test that failed attempts propagate correctly through exception inheritance."""
|
|
# Test base class propagation
|
|
base_failed_attempts = [FailedAttempt(1, Exception("Base error"), None)]
|
|
base_error = InstructorError("Base error", failed_attempts=base_failed_attempts)
|
|
|
|
# Convert to more specific exception type using from_exception
|
|
specific_error = ValidationError.from_exception(
|
|
base_error, failed_attempts=base_failed_attempts
|
|
)
|
|
assert isinstance(specific_error, ValidationError)
|
|
assert isinstance(specific_error, InstructorError) # Should still inherit from base
|
|
assert specific_error.failed_attempts == base_failed_attempts
|
|
|
|
# Test that derived exceptions maintain failed attempts
|
|
retry_failed_attempts = [
|
|
FailedAttempt(1, Exception("Retry 1"), "completion 1"),
|
|
FailedAttempt(2, Exception("Retry 2"), "completion 2"),
|
|
]
|
|
retry_error = InstructorRetryException(
|
|
"Retries failed",
|
|
n_attempts=2,
|
|
total_usage=100,
|
|
failed_attempts=retry_failed_attempts,
|
|
)
|
|
|
|
# Convert to base type should preserve failed attempts
|
|
base_from_retry = InstructorError.from_exception(
|
|
retry_error, failed_attempts=retry_failed_attempts
|
|
)
|
|
assert base_from_retry.failed_attempts == retry_failed_attempts
|
|
|
|
|
|
def test_failed_attempts_accumulation_simulation():
|
|
"""Test simulation of how failed attempts would accumulate in a real retry scenario."""
|
|
# Simulate a retry scenario where attempts accumulate
|
|
attempts = []
|
|
|
|
# First attempt fails
|
|
attempts.append(
|
|
FailedAttempt(
|
|
1, ValidationError("Schema validation failed"), {"invalid": "data"}
|
|
)
|
|
)
|
|
|
|
# Second attempt fails differently
|
|
attempts.append(
|
|
FailedAttempt(2, JSONDecodeError("Invalid JSON", "", 0), "malformed json")
|
|
)
|
|
|
|
# Third attempt fails again
|
|
attempts.append(
|
|
FailedAttempt(
|
|
3, ValidationError("Required field missing"), {"partial": "response"}
|
|
)
|
|
)
|
|
|
|
# Final retry exception with all attempts
|
|
final_error = InstructorRetryException(
|
|
"Maximum retries exceeded",
|
|
n_attempts=3,
|
|
total_usage=500,
|
|
failed_attempts=attempts,
|
|
last_completion={"final": "attempt"},
|
|
messages=[{"role": "user", "content": "test"}],
|
|
create_kwargs={"model": "gpt-4.1-mini", "max_retries": 3},
|
|
)
|
|
|
|
# Verify all data is preserved
|
|
assert final_error.n_attempts == 3
|
|
assert final_error.total_usage == 500
|
|
assert final_error.failed_attempts is not None
|
|
assert len(final_error.failed_attempts) == 3
|
|
assert final_error.last_completion == {"final": "attempt"}
|
|
|
|
# Test string representation includes all attempts
|
|
error_str = str(final_error)
|
|
assert "<failed_attempts>" in error_str
|
|
assert "Schema validation failed" in error_str
|
|
assert "Invalid JSON" in error_str
|
|
assert "Required field missing" in error_str
|
|
assert "Maximum retries exceeded" in error_str
|
|
|
|
# Verify attempt sequence integrity
|
|
assert final_error.failed_attempts is not None
|
|
for i, attempt in enumerate(final_error.failed_attempts, 1):
|
|
assert attempt.attempt_number == i
|
|
|
|
|
|
def test_failed_attempts_exception_chaining():
|
|
"""Test that exception chaining works properly with failed attempts."""
|
|
# Create original exception with failed attempts
|
|
original_attempts = [
|
|
FailedAttempt(1, Exception("Original failure"), "original completion")
|
|
]
|
|
original_error = InstructorError(
|
|
"Original error", failed_attempts=original_attempts
|
|
)
|
|
|
|
try:
|
|
raise original_error
|
|
except InstructorError as e:
|
|
assert e.failed_attempts is not None
|
|
# Create new exception from caught exception, preserving failed attempts
|
|
chained_error = InstructorRetryException(
|
|
"Chained error",
|
|
n_attempts=2,
|
|
total_usage=150,
|
|
failed_attempts=e.failed_attempts,
|
|
)
|
|
|
|
# Verify failed attempts are preserved through chaining
|
|
assert chained_error.failed_attempts == original_attempts
|
|
assert chained_error.failed_attempts is not None
|
|
assert len(chained_error.failed_attempts) == 1
|
|
assert chained_error.failed_attempts[0].exception.args[0] == "Original failure"
|