from __future__ import annotations as _annotations from dataclasses import dataclass, field from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum from typing import Any from uuid import UUID import pytest from pydantic import BaseModel, Field, computed_field from pydantic.dataclasses import dataclass as pydantic_dataclass from typing_extensions import Self from pydantic_ai import format_as_xml from ._inline_snapshot import snapshot @dataclass class ExampleDataclass: name: str age: int class ExamplePydanticModel(BaseModel): name: str age: int class ExampleEnum(Enum): FOO = 1 BAR = 2 class ExampleStrEnum(str, Enum): FOO = 'foo' BAR = 'bar' class ExamplePydanticFields(BaseModel): name: str = Field(description="The person's name") age: int = Field(description='Years', title='Age', default=18) height: float = Field(description="The person's height", exclude=True) children: list[Self] | None = Field(title='child', alias='child_list', default=None) @computed_field(title='Location') def location(self) -> str | None: if self.name == 'John': return 'Australia' return None @pytest.mark.parametrize( 'input_obj,output', [ pytest.param('a string', snapshot('a string'), id='string'), pytest.param(42, snapshot('42'), id='int'), pytest.param(None, snapshot('null'), id='null'), # regression test for https://github.com/pydantic/pydantic-ai/pull/4131 pytest.param(ExampleEnum.FOO, snapshot('ExampleEnum.FOO'), id='enum'), pytest.param(ExampleStrEnum.FOO, snapshot('foo'), id='str enum'), pytest.param( ExampleDataclass(name='John', age=42), snapshot("""\ John 42 \ """), id='dataclass', ), pytest.param( ExamplePydanticModel(name='John', age=42), snapshot("""\ John 42 \ """), id='pydantic model', ), pytest.param( [ExampleDataclass(name='John', age=42)], snapshot("""\ John 42 \ """), id='list[dataclass]', ), pytest.param( [ExamplePydanticModel(name='John', age=42)], snapshot("""\ John 42 \ """), id='list[pydantic model]', ), pytest.param( [1, 2, 3], snapshot("""\ 1 2 3 \ """), id='list[int]', ), pytest.param( (1, 'x'), snapshot("""\ 1 x \ """), id='tuple[int,str]', ), pytest.param( [[1, 2], [3]], snapshot("""\ 1 2 3 \ """), id='list[list[int]]', ), pytest.param( {'x': 1, 'y': 3, 3: 'z', 4: {'a': -1, 'b': -2}}, snapshot("""\ 1 3 <3>z <4> -1 -2 \ """), id='dict', ), ], ) def test_root_tag(input_obj: Any, output: str): assert format_as_xml(input_obj, root_tag='examples', item_tag='example', include_field_info=False) == output assert format_as_xml(input_obj, root_tag='examples', item_tag='example', include_field_info='once') == output @pytest.mark.parametrize( 'input_obj,use_fields,output', [ pytest.param( ExamplePydanticFields( name='John', age=42, height=160.0, child_list=[ ExamplePydanticFields(name='Liam', height=150), ExamplePydanticFields(name='Alice', height=160), ], ), 'once', snapshot("""\ John 42 Liam 18 null null Alice 18 null null Australia\ """), id='pydantic model with fields', ), pytest.param( [ ExamplePydanticFields( name='John', age=42, height=160.0, child_list=[ ExamplePydanticFields(name='Liam', height=150), ExamplePydanticFields(name='Alice', height=160), ], ) ], 'once', snapshot("""\ John 42 Liam 18 null null Alice 18 null null Australia \ """), id='list[pydantic model with fields]', ), pytest.param( ExamplePydanticFields( name='John', age=42, height=160.0, child_list=[ ExamplePydanticFields(name='Liam', height=150), ExamplePydanticFields(name='Alice', height=160), ], ), False, snapshot("""\ John 42 Liam 18 null null Alice 18 null null Australia\ """), id='pydantic model without fields', ), ], ) def test_fields(input_obj: Any, use_fields: bool, output: str): assert format_as_xml(input_obj, include_field_info=use_fields) == output def test_repeated_field_attributes(): class DataItem(BaseModel): user1: ExamplePydanticFields user2: ExamplePydanticFields data = ExamplePydanticFields( name='John', age=42, height=160.0, child_list=[ ExamplePydanticFields(name='Liam', height=150), ExamplePydanticFields(name='Alice', height=160), ], ) assert ( format_as_xml(data, include_field_info=True) == """\ John 42 Liam 18 null null Alice 18 null null Australia\ """ ) assert ( format_as_xml(DataItem(user1=data, user2=data.model_copy()), include_field_info=True) == """\ John 42 Liam 18 null null Alice 18 null null Australia John 42 Liam 18 null null Alice 18 null null Australia \ """ ) assert ( format_as_xml(DataItem(user1=data, user2=data.model_copy()), include_field_info='once') == """\ John 42 Liam 18 null null Alice 18 null null Australia John 42 Liam 18 null null Alice 18 null null Australia \ """ ) def test_nested_data(): @dataclass class DataItem1: id: str | None = None source: str = field(default='none', metadata={'description': 'the source', 'date': '19990805'}) class ModelItem1(BaseModel): name: str = Field(description='Name') value: int items: list[DataItem1] = Field(description='Items') @pydantic_dataclass class DataItem2: model: ModelItem1 = field(metadata={'title': 'the model', 'description': 'info'}) others: tuple[ModelItem1] | None = None count: int = field(default=10, metadata={'info': 'a count'}) data = { 'values': [ DataItem2( ModelItem1(name='Alice', value=42, items=[DataItem1('xyz')]), (ModelItem1(name='Liam', value=3, items=[]),), ), DataItem2( ModelItem1( name='Bob', value=7, items=[ DataItem1('a'), DataItem1(source='xx'), ], ), count=42, ), ] } assert ( format_as_xml(data, include_field_info='once') == """ Alice 42 xyz none Liam 3 10 Bob 7 a none null xx null 42 """.strip() ) assert ( format_as_xml(data, include_field_info=False) == """ Alice 42 xyz none Liam 3 10 Bob 7 a none null xx null 42 """.strip() ) @pytest.mark.parametrize( 'input_obj,output', [ pytest.param('a string', snapshot('a string'), id='string'), pytest.param('a foo', snapshot('a <ex>foo</ex>'), id='string'), pytest.param(42, snapshot('42'), id='int'), pytest.param( [1, 2, 3], snapshot("""\ 1 2 3\ """), id='list[int]', ), pytest.param( [[1, 2], [3]], snapshot("""\ 1 2 3 \ """), id='list[list[int]]', ), pytest.param( {'binary': b'my bytes', 'barray': bytearray(b'foo')}, snapshot("""\ my bytes foo\ """), id='dict[str, bytes]', ), pytest.param( [datetime(2025, 1, 1, 12, 13), date(2025, 1, 2)], snapshot("""\ 2025-01-01T12:13:00 2025-01-02\ """), id='list[date]', ), pytest.param( [time(12, 30, 45), time(8, 15)], snapshot("""\ 12:30:45 08:15:00\ """), id='list[time]', ), pytest.param( [timedelta(days=1, hours=2, minutes=30), timedelta(seconds=90)], snapshot("""\ 1 day, 2:30:00 0:01:30\ """), id='list[timedelta]', ), ], ) def test_no_root(input_obj: Any, output: str): assert format_as_xml(input_obj) == output def test_no_indent(): assert format_as_xml([1, 2, 3], indent=None, root_tag='example') == snapshot( '123' ) assert format_as_xml([1, 2, 3], indent=None) == snapshot('123') def test_invalid_value(): with pytest.raises(TypeError, match='Unsupported type'): format_as_xml(object()) def test_invalid_key(): with pytest.raises(TypeError, match='Unsupported key type for XML formatting'): format_as_xml({(1, 2): 42}) def test_parse_invalid_value(): class Invalid(BaseModel): name: str = Field(default='Alice', title='Name') bad: Any = object() with pytest.raises(TypeError, match='Unsupported type'): format_as_xml(Invalid(), include_field_info='once') def test_set(): assert '1' in format_as_xml({1, 2, 3}, item_tag='example') def test_custom_null(): assert format_as_xml(None, none_str='nil') == snapshot('nil') def test_non_primitive_types(): assert format_as_xml(Decimal('123.45')) == snapshot('123.45') assert format_as_xml(UUID('123e4567-e89b-12d3-a456-426614174000')) == snapshot( '123e4567-e89b-12d3-a456-426614174000' ) def test_model_with_non_primitive_types(): class ExamplePydanticModelWithNonPrimitiveTypes(BaseModel): name: str age: Decimal uuid: UUID assert format_as_xml( ExamplePydanticModelWithNonPrimitiveTypes( name='John', age=Decimal('123.45'), uuid=UUID('123e4567-e89b-12d3-a456-426614174000') ) ) == snapshot("""\ John 123.45 123e4567-e89b-12d3-a456-426614174000\ """)