Files
wehub-resource-sync 9201ef759e
Harness Compat / harness compat (push) Failing after 0s
CI / test on 3.12 (standard) (push) Has been cancelled
CI / test on 3.13 (standard) (push) Has been cancelled
CI / test on 3.14 (standard) (push) Has been cancelled
CI / test on 3.10 (all-extras) (push) Has been cancelled
CI / test on 3.11 (all-extras) (push) Has been cancelled
CI / test on 3.12 (all-extras) (push) Has been cancelled
CI / test on 3.14 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.10 (pydantic-evals) (push) Has been cancelled
CI / test on 3.11 (pydantic-evals) (push) Has been cancelled
CI / test on 3.12 (pydantic-evals) (push) Has been cancelled
CI / deploy-docs-preview (push) Has been cancelled
CI / build release artifacts (push) Has been cancelled
CI / publish to PyPI (push) Has been cancelled
CI / Send tweet (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / mypy (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / test on 3.10 (standard) (push) Has been cancelled
CI / test on 3.11 (standard) (push) Has been cancelled
CI / test on 3.13 (all-extras) (push) Has been cancelled
CI / test on 3.14 (all-extras) (push) Has been cancelled
CI / test on 3.10 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.11 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.12 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-ai-slim) (push) Has been cancelled
CI / test on 3.13 (pydantic-evals) (push) Has been cancelled
CI / test on 3.14 (pydantic-evals) (push) Has been cancelled
CI / test on 3.10 (lowest-versions) (push) Has been cancelled
CI / test on 3.11 (lowest-versions) (push) Has been cancelled
CI / test on 3.12 (lowest-versions) (push) Has been cancelled
CI / test on 3.13 (lowest-versions) (push) Has been cancelled
CI / test on 3.14 (lowest-versions) (push) Has been cancelled
CI / test examples on 3.11 (push) Has been cancelled
CI / test examples on 3.12 (push) Has been cancelled
CI / test examples on 3.13 (push) Has been cancelled
CI / test examples on 3.14 (push) Has been cancelled
CI / coverage (push) Has been cancelled
CI / check (push) Has been cancelled
CI / deploy-docs (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:27:52 +08:00

258 lines
8.5 KiB
Python

from __future__ import annotations as _annotations
from datetime import datetime, timezone
from typing import Any
import pytest
from inline_snapshot import snapshot
from pydantic import TypeAdapter
from ..conftest import try_import
with try_import() as imports_successful:
from pydantic_evals.evaluators.context import EvaluatorContext
from pydantic_evals.otel._errors import SpanTreeRecordingError
from pydantic_evals.otel.span_tree import SpanNode, SpanTree
pytestmark = [pytest.mark.skipif(not imports_successful(), reason='pydantic-evals not installed'), pytest.mark.anyio]
def test_evaluator_context_basic():
"""Test basic EvaluatorContext functionality."""
# Create a mock span tree
span_tree = SpanTree()
# Create a context with all fields populated
ctx = EvaluatorContext(
name='test_case',
inputs={'input': 'value'},
metadata={'meta': 'data'},
expected_output={'expected': 'output'},
output={'actual': 'output'},
duration=1.0,
_span_tree=span_tree,
attributes={'attr': 'value'},
metrics={'metric': 1.0},
)
# Test basic attribute access
assert ctx.name == 'test_case'
assert ctx.inputs == {'input': 'value'}
assert ctx.metadata == {'meta': 'data'}
assert ctx.expected_output == {'expected': 'output'}
assert ctx.output == {'actual': 'output'}
assert ctx.duration == 1.0
assert ctx.attributes == {'attr': 'value'}
assert ctx.metrics == {'metric': 1.0}
# Test span_tree property
assert ctx.span_tree == span_tree
def test_evaluator_context_span_tree_error():
"""Test EvaluatorContext with SpanTreeRecordingError."""
ctx = EvaluatorContext(
name='test',
inputs=dict[str, Any](),
metadata=None,
expected_output=None,
output=dict[str, Any](),
duration=0.0,
_span_tree=SpanTreeRecordingError('Test error'),
attributes={},
metrics={},
)
# Test that accessing span_tree raises the error
with pytest.raises(SpanTreeRecordingError) as exc_info:
_ = ctx.span_tree
assert str(exc_info.value) == 'Test error'
def test_evaluator_context_with_custom_types():
"""Test EvaluatorContext with custom generic types."""
class CustomInput:
def __init__(self, value: str):
self.value = value
class CustomOutput:
def __init__(self, result: int):
self.result = result
class CustomMetadata:
def __init__(self, info: Any):
self.info = info
# Create context with custom types
ctx = EvaluatorContext[CustomInput, CustomOutput, CustomMetadata](
name='test',
inputs=CustomInput('test_input'),
metadata=CustomMetadata({'info': 'test'}),
expected_output=CustomOutput(42),
output=CustomOutput(42),
duration=1.0,
_span_tree=SpanTreeRecordingError('Test error'),
attributes={},
metrics={},
)
assert isinstance(ctx.inputs, CustomInput)
assert ctx.inputs.value == 'test_input'
assert isinstance(ctx.output, CustomOutput)
assert ctx.output.result == 42
assert isinstance(ctx.metadata, CustomMetadata)
assert ctx.metadata.info == {'info': 'test'}
def test_span_tree_recording_error_serde():
"""Test that SpanTreeRecordingError round-trips in message field through JSON serialization."""
adapter = TypeAdapter(SpanTreeRecordingError)
error = SpanTreeRecordingError('test error message')
json_bytes = adapter.dump_json(error)
restored = adapter.validate_json(json_bytes)
assert isinstance(restored, SpanTreeRecordingError)
assert restored.message == 'test error message'
def test_span_tree_recording_error_serde_json_shape():
"""Test the JSON shape produced by SpanTreeRecordingError serialization."""
adapter = TypeAdapter(SpanTreeRecordingError)
error = SpanTreeRecordingError('some message')
data = adapter.dump_python(error, mode='json')
assert data == {'message': 'some message', 'kind': 'span-tree-recording-error'}
def test_evaluator_context_serde_with_span_tree():
"""Test EvaluatorContext round-trips through `TypeAdapter(EvaluatorContext)` when _span_tree is a populated SpanTree.
The tree is built with `add_spans` rather than `SpanTree(roots=[...])`: `__post_init__` rebuilds the
tree from `nodes_by_id`, so passing `roots` directly would leave the parent/child path untested.
"""
adapter = TypeAdapter(EvaluatorContext)
root = SpanNode(
name='root',
trace_id=1,
span_id=1,
parent_span_id=None,
start_timestamp=datetime(2025, 1, 1, tzinfo=timezone.utc),
end_timestamp=datetime(2025, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
attributes={'key': 'value'},
)
child = SpanNode(
name='child',
trace_id=1,
span_id=2,
parent_span_id=1,
start_timestamp=datetime(2025, 1, 1, 0, 0, 0, 500_000, tzinfo=timezone.utc),
end_timestamp=datetime(2025, 1, 1, 0, 0, 1, tzinfo=timezone.utc),
attributes={},
)
span_tree = SpanTree()
span_tree.add_spans([root, child])
ctx = EvaluatorContext(
name='test_case',
inputs='hello',
metadata=None,
expected_output=None,
output='world',
duration=0.1,
_span_tree=span_tree,
attributes={},
metrics={},
)
assert adapter.dump_python(ctx, mode='json') == snapshot(
{
'name': 'test_case',
'inputs': 'hello',
'metadata': None,
'expected_output': None,
'output': 'world',
'duration': 0.1,
'_span_tree': {
'roots': [
{
'name': 'root',
'trace_id': 1,
'span_id': 1,
'parent_span_id': None,
'start_timestamp': '2025-01-01T00:00:00Z',
'end_timestamp': '2025-01-01T00:00:01Z',
'attributes': {'key': 'value'},
'status': 'unset',
}
],
'nodes_by_id': {
'00000000000000000000000000000001:0000000000000001': {
'name': 'root',
'trace_id': 1,
'span_id': 1,
'parent_span_id': None,
'start_timestamp': '2025-01-01T00:00:00Z',
'end_timestamp': '2025-01-01T00:00:01Z',
'attributes': {'key': 'value'},
'status': 'unset',
},
'00000000000000000000000000000001:0000000000000002': {
'name': 'child',
'trace_id': 1,
'span_id': 2,
'parent_span_id': 1,
'start_timestamp': '2025-01-01T00:00:00.500000Z',
'end_timestamp': '2025-01-01T00:00:01Z',
'attributes': {},
'status': 'unset',
},
},
},
'attributes': {},
'metrics': {},
}
)
json_bytes = adapter.dump_json(ctx)
restored = adapter.validate_json(json_bytes)
assert isinstance(restored.span_tree, SpanTree)
assert [node.name for node in restored.span_tree.roots] == ['root']
assert [node.name for node in restored.span_tree.roots[0].children] == ['child']
def test_evaluator_context_serde_with_error():
"""Test EvaluatorContext round-trips through `TypeAdapter(EvaluatorContext)` when _span_tree is an error."""
adapter = TypeAdapter(EvaluatorContext)
ctx = EvaluatorContext(
name='test_case',
inputs={'key': 'value'},
metadata={'meta': 1},
expected_output='expected',
output='actual',
duration=2.5,
_span_tree=SpanTreeRecordingError('spans not recorded'),
attributes={'attr': 'val'},
metrics={'m': 42},
)
json_bytes = adapter.dump_json(ctx)
restored = adapter.validate_json(json_bytes)
assert restored.name == ctx.name
assert restored.inputs == ctx.inputs
assert restored.metadata == ctx.metadata
assert restored.expected_output == ctx.expected_output
assert restored.output == ctx.output
assert restored.duration == ctx.duration
assert restored.attributes == ctx.attributes
assert restored.metrics == ctx.metrics
with pytest.raises(SpanTreeRecordingError, match='spans not recorded'):
_ = restored.span_tree