Skip to content

Commit 6b397a6

Browse files
authored
BREAKING CHANGE (minor) move format_as_xml (#1484)
1 parent 28f8bde commit 6b397a6

File tree

8 files changed

+133
-126
lines changed

8 files changed

+133
-126
lines changed

docs/evals.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ from typing import Any
163163

164164
from pydantic import BaseModel
165165

166-
from pydantic_ai import Agent
167-
from pydantic_ai.format_as_xml import format_as_xml
166+
from pydantic_ai import Agent, format_as_xml
168167
from pydantic_evals import Case, Dataset
169168
from pydantic_evals.evaluators import IsInstance, LLMJudge
170169

docs/graph.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,7 @@ from dataclasses import dataclass, field
359359

360360
from pydantic import BaseModel, EmailStr
361361

362-
from pydantic_ai import Agent
363-
from pydantic_ai.format_as_xml import format_as_xml
362+
from pydantic_ai import Agent, format_as_xml
364363
from pydantic_ai.messages import ModelMessage
365364
from pydantic_graph import BaseNode, End, Graph, GraphRunContext
366365

@@ -662,8 +661,7 @@ Instead of running the entire graph in a single process invocation, we run the g
662661
GraphRunContext,
663662
)
664663

665-
from pydantic_ai import Agent
666-
from pydantic_ai.format_as_xml import format_as_xml
664+
from pydantic_ai import Agent, format_as_xml
667665
from pydantic_ai.messages import ModelMessage
668666

669667
ask_agent = Agent('openai:gpt-4o', output_type=str, instrument=True)

examples/pydantic_ai_examples/question_graph.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
)
2121
from pydantic_graph.persistence.file import FileStatePersistence
2222

23-
from pydantic_ai import Agent
24-
from pydantic_ai.format_as_xml import format_as_xml
23+
from pydantic_ai import Agent, format_as_xml
2524
from pydantic_ai.messages import ModelMessage
2625

2726
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured

examples/pydantic_ai_examples/sql_gen.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
from pydantic import BaseModel, Field
2626
from typing_extensions import TypeAlias
2727

28-
from pydantic_ai import Agent, ModelRetry, RunContext
29-
from pydantic_ai.format_as_xml import format_as_xml
28+
from pydantic_ai import Agent, ModelRetry, RunContext, format_as_xml
3029

3130
# 'if-token-present' means nothing will be sent (and the example will work) if you don't have logfire configured
3231
logfire.configure(send_to_logfire='if-token-present')

pydantic_ai_slim/pydantic_ai/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from importlib.metadata import version
1+
from importlib.metadata import version as _metadata_version
22

33
from .agent import Agent, CallToolsNode, EndStrategy, ModelRequestNode, UserPromptNode, capture_run_messages
44
from .exceptions import (
@@ -10,6 +10,7 @@
1010
UsageLimitExceeded,
1111
UserError,
1212
)
13+
from .format_prompt import format_as_xml
1314
from .messages import AudioUrl, BinaryContent, DocumentUrl, ImageUrl, VideoUrl
1415
from .result import ToolOutput
1516
from .tools import RunContext, Tool
@@ -42,5 +43,7 @@
4243
'RunContext',
4344
# result
4445
'ToolOutput',
46+
# format_prompt
47+
'format_as_xml',
4548
)
46-
__version__ = version('pydantic_ai_slim')
49+
__version__ = _metadata_version('pydantic_ai_slim')
Lines changed: 6 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,9 @@
1-
from __future__ import annotations as _annotations
1+
from typing_extensions import deprecated
22

3-
from collections.abc import Iterable, Iterator, Mapping
4-
from dataclasses import asdict, dataclass, is_dataclass
5-
from datetime import date
6-
from typing import Any
7-
from xml.etree import ElementTree
3+
from .format_prompt import format_as_xml as _format_as_xml
84

9-
from pydantic import BaseModel
105

11-
__all__ = ('format_as_xml',)
12-
13-
14-
def format_as_xml(
15-
obj: Any,
16-
root_tag: str = 'examples',
17-
item_tag: str = 'example',
18-
include_root_tag: bool = True,
19-
none_str: str = 'null',
20-
indent: str | None = ' ',
21-
) -> str:
22-
"""Format a Python object as XML.
23-
24-
This is useful since LLMs often find it easier to read semi-structured data (e.g. examples) as XML,
25-
rather than JSON etc.
26-
27-
Supports: `str`, `bytes`, `bytearray`, `bool`, `int`, `float`, `date`, `datetime`, `Mapping`,
28-
`Iterable`, `dataclass`, and `BaseModel`.
29-
30-
Args:
31-
obj: Python Object to serialize to XML.
32-
root_tag: Outer tag to wrap the XML in, use `None` to omit the outer tag.
33-
item_tag: Tag to use for each item in an iterable (e.g. list), this is overridden by the class name
34-
for dataclasses and Pydantic models.
35-
include_root_tag: Whether to include the root tag in the output
36-
(The root tag is always included if it includes a body - e.g. when the input is a simple value).
37-
none_str: String to use for `None` values.
38-
indent: Indentation string to use for pretty printing.
39-
40-
Returns:
41-
XML representation of the object.
42-
43-
Example:
44-
```python {title="format_as_xml_example.py" lint="skip"}
45-
from pydantic_ai.format_as_xml import format_as_xml
46-
47-
print(format_as_xml({'name': 'John', 'height': 6, 'weight': 200}, root_tag='user'))
48-
'''
49-
<user>
50-
<name>John</name>
51-
<height>6</height>
52-
<weight>200</weight>
53-
</user>
54-
'''
55-
```
56-
"""
57-
el = _ToXml(item_tag=item_tag, none_str=none_str).to_xml(obj, root_tag)
58-
if not include_root_tag and el.text is None:
59-
join = '' if indent is None else '\n'
60-
return join.join(_rootless_xml_elements(el, indent))
61-
else:
62-
if indent is not None:
63-
ElementTree.indent(el, space=indent)
64-
return ElementTree.tostring(el, encoding='unicode')
65-
66-
67-
@dataclass
68-
class _ToXml:
69-
item_tag: str
70-
none_str: str
71-
72-
def to_xml(self, value: Any, tag: str | None) -> ElementTree.Element:
73-
element = ElementTree.Element(self.item_tag if tag is None else tag)
74-
if value is None:
75-
element.text = self.none_str
76-
elif isinstance(value, str):
77-
element.text = value
78-
elif isinstance(value, (bytes, bytearray)):
79-
element.text = value.decode(errors='ignore')
80-
elif isinstance(value, (bool, int, float)):
81-
element.text = str(value)
82-
elif isinstance(value, date):
83-
element.text = value.isoformat()
84-
elif isinstance(value, Mapping):
85-
self._mapping_to_xml(element, value) # pyright: ignore[reportUnknownArgumentType]
86-
elif is_dataclass(value) and not isinstance(value, type):
87-
if tag is None:
88-
element = ElementTree.Element(value.__class__.__name__)
89-
dc_dict = asdict(value)
90-
self._mapping_to_xml(element, dc_dict)
91-
elif isinstance(value, BaseModel):
92-
if tag is None:
93-
element = ElementTree.Element(value.__class__.__name__)
94-
self._mapping_to_xml(element, value.model_dump(mode='python'))
95-
elif isinstance(value, Iterable):
96-
for item in value: # pyright: ignore[reportUnknownVariableType]
97-
item_el = self.to_xml(item, None)
98-
element.append(item_el)
99-
else:
100-
raise TypeError(f'Unsupported type for XML formatting: {type(value)}')
101-
return element
102-
103-
def _mapping_to_xml(self, element: ElementTree.Element, mapping: Mapping[Any, Any]) -> None:
104-
for key, value in mapping.items():
105-
if isinstance(key, int):
106-
key = str(key)
107-
elif not isinstance(key, str):
108-
raise TypeError(f'Unsupported key type for XML formatting: {type(key)}, only str and int are allowed')
109-
element.append(self.to_xml(value, key))
110-
111-
112-
def _rootless_xml_elements(root: ElementTree.Element, indent: str | None) -> Iterator[str]:
113-
for sub_element in root:
114-
if indent is not None:
115-
ElementTree.indent(sub_element, space=indent)
116-
yield ElementTree.tostring(sub_element, encoding='unicode')
6+
@deprecated('`format_as_xml` has moved, import it via `from pydantic_ai import format_as_xml`')
7+
def format_as_xml(prompt: str) -> str:
8+
"""`format_as_xml` has moved, import it via `from pydantic_ai import format_as_xml` instead."""
9+
return _format_as_xml(prompt)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from __future__ import annotations as _annotations
2+
3+
from collections.abc import Iterable, Iterator, Mapping
4+
from dataclasses import asdict, dataclass, is_dataclass
5+
from datetime import date
6+
from typing import Any
7+
from xml.etree import ElementTree
8+
9+
from pydantic import BaseModel
10+
11+
__all__ = ('format_as_xml',)
12+
13+
14+
def format_as_xml(
15+
obj: Any,
16+
root_tag: str = 'examples',
17+
item_tag: str = 'example',
18+
include_root_tag: bool = True,
19+
none_str: str = 'null',
20+
indent: str | None = ' ',
21+
) -> str:
22+
"""Format a Python object as XML.
23+
24+
This is useful since LLMs often find it easier to read semi-structured data (e.g. examples) as XML,
25+
rather than JSON etc.
26+
27+
Supports: `str`, `bytes`, `bytearray`, `bool`, `int`, `float`, `date`, `datetime`, `Mapping`,
28+
`Iterable`, `dataclass`, and `BaseModel`.
29+
30+
Args:
31+
obj: Python Object to serialize to XML.
32+
root_tag: Outer tag to wrap the XML in, use `None` to omit the outer tag.
33+
item_tag: Tag to use for each item in an iterable (e.g. list), this is overridden by the class name
34+
for dataclasses and Pydantic models.
35+
include_root_tag: Whether to include the root tag in the output
36+
(The root tag is always included if it includes a body - e.g. when the input is a simple value).
37+
none_str: String to use for `None` values.
38+
indent: Indentation string to use for pretty printing.
39+
40+
Returns:
41+
XML representation of the object.
42+
43+
Example:
44+
```python {title="format_as_xml_example.py" lint="skip"}
45+
from pydantic_ai import format_as_xml
46+
47+
print(format_as_xml({'name': 'John', 'height': 6, 'weight': 200}, root_tag='user'))
48+
'''
49+
<user>
50+
<name>John</name>
51+
<height>6</height>
52+
<weight>200</weight>
53+
</user>
54+
'''
55+
```
56+
"""
57+
el = _ToXml(item_tag=item_tag, none_str=none_str).to_xml(obj, root_tag)
58+
if not include_root_tag and el.text is None:
59+
join = '' if indent is None else '\n'
60+
return join.join(_rootless_xml_elements(el, indent))
61+
else:
62+
if indent is not None:
63+
ElementTree.indent(el, space=indent)
64+
return ElementTree.tostring(el, encoding='unicode')
65+
66+
67+
@dataclass
68+
class _ToXml:
69+
item_tag: str
70+
none_str: str
71+
72+
def to_xml(self, value: Any, tag: str | None) -> ElementTree.Element:
73+
element = ElementTree.Element(self.item_tag if tag is None else tag)
74+
if value is None:
75+
element.text = self.none_str
76+
elif isinstance(value, str):
77+
element.text = value
78+
elif isinstance(value, (bytes, bytearray)):
79+
element.text = value.decode(errors='ignore')
80+
elif isinstance(value, (bool, int, float)):
81+
element.text = str(value)
82+
elif isinstance(value, date):
83+
element.text = value.isoformat()
84+
elif isinstance(value, Mapping):
85+
self._mapping_to_xml(element, value) # pyright: ignore[reportUnknownArgumentType]
86+
elif is_dataclass(value) and not isinstance(value, type):
87+
if tag is None:
88+
element = ElementTree.Element(value.__class__.__name__)
89+
dc_dict = asdict(value)
90+
self._mapping_to_xml(element, dc_dict)
91+
elif isinstance(value, BaseModel):
92+
if tag is None:
93+
element = ElementTree.Element(value.__class__.__name__)
94+
self._mapping_to_xml(element, value.model_dump(mode='python'))
95+
elif isinstance(value, Iterable):
96+
for item in value: # pyright: ignore[reportUnknownVariableType]
97+
item_el = self.to_xml(item, None)
98+
element.append(item_el)
99+
else:
100+
raise TypeError(f'Unsupported type for XML formatting: {type(value)}')
101+
return element
102+
103+
def _mapping_to_xml(self, element: ElementTree.Element, mapping: Mapping[Any, Any]) -> None:
104+
for key, value in mapping.items():
105+
if isinstance(key, int):
106+
key = str(key)
107+
elif not isinstance(key, str):
108+
raise TypeError(f'Unsupported key type for XML formatting: {type(key)}, only str and int are allowed')
109+
element.append(self.to_xml(value, key))
110+
111+
112+
def _rootless_xml_elements(root: ElementTree.Element, indent: str | None) -> Iterator[str]:
113+
for sub_element in root:
114+
if indent is not None:
115+
ElementTree.indent(sub_element, space=indent)
116+
yield ElementTree.tostring(sub_element, encoding='unicode')

tests/test_format_as_xml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from inline_snapshot import snapshot
77
from pydantic import BaseModel
88

9-
from pydantic_ai.format_as_xml import format_as_xml
9+
from pydantic_ai import format_as_xml
1010

1111

1212
@dataclass

0 commit comments

Comments
 (0)