Formatron v0.4.9
Formatron empowers everyone to control the output format of language models with minimal overhead.
Loading...
Searching...
No Matches
pydantic.py
Go to the documentation of this file.
1"""
2A module that implements the Schema interface using pydantic.
3"""
4import inspect
5import json
6import typing
7
8import pydantic.fields
9from pydantic import BaseModel, validate_call, ConfigDict, Field
10
11import formatron.schemas.schema as schema
12from formatron.schemas.schema import Schema, TypeWithMetadata
13
14
16 """
17 A wrapper for pydantic FieldInfo.
18 """
19 __slots__ = ("_field",)
20
21 def __init__(self, field: pydantic.fields.FieldInfo):
22 """
23 Initialize the field information.
24 """
25 self._field = field
26 self._annotation = field.annotation
27 if field.metadata:
28 metadata = {}
29 for constraint in ["min_length", "max_length", "pattern", "gt", "ge", "lt", "le", "substring_of"]:
30 value = next((getattr(m, constraint) for m in self._field.metadata if hasattr(m, constraint)), None)
31 if value is not None:
32 metadata[constraint] = value
33 if metadata:
34 self._annotation = TypeWithMetadata(self._annotation, metadata)
35
36 @property
37 def annotation(self) -> typing.Type[typing.Any] | None:
38 return self._annotation
39
40 @property
41 def required(self) -> bool:
42 return self._field.is_required()
43
44 def __repr__(self):
45 return repr(self._field)
46
47 def __str__(self):
48 return str(self._field)
49
50
51class ClassSchema(BaseModel, Schema):
52 """
53 A wrapper for pydantic BaseModel that implements the Schema interface.
54 """
55 __cached_fields__ = None
56
57 @classmethod
58 def fields(cls) -> typing.Dict[str, typing.Any]:
59 if cls.__cached_fields__ is not None:
60 return cls.__cached_fields__
61 cls.__cached_fields__ = {k: FieldInfo(v) for k, v in cls.model_fields.items()}
62 return cls.__cached_fields__
64 @classmethod
65 def from_json(cls, _json: str) -> "ClassSchema":
66 """
67 Create a ClassSchema from a JSON string.
68 """
69 return cls.model_validate_json(_json)
70
71
72CallableT = typing.TypeVar('CallableT', bound=typing.Callable)
74
75def callable_schema(func: CallableT, /, *, config: ConfigDict = None, validate_return: bool = False) -> CallableT:
76 """
77 A decorator that wraps pydantic's validate_call. The decorated callable also implements the Schema interface.
78
79 Args:
80 func: The function to decorate.
81 config: The pydantic configuration of validate_call.
82 validate_return: Whether to validate the return value.
83
84 Returns:
85 The decorated callable.
86 """
87 pydantic_wrapper = validate_call(config=config, validate_return=validate_return)(func)
88 signature = inspect.signature(func, eval_str=True)
89 fields = {}
90 for k, p in signature.parameters.items():
91 default = None
92 if p.default is not inspect.Signature.empty:
93 default = p.default
94 actual_type = p.annotation
95 metadata = []
96 if isinstance(p.default, pydantic.fields.FieldInfo):
97 fields[k] = p.default
98 if typing.get_origin(p.annotation) is typing.Annotated:
99 actual_type, *meta = typing.get_args(p.annotation)
100 fieldinfo = None
101 for i in meta:
102 if isinstance(i, pydantic.fields.FieldInfo):
103 fieldinfo = i
104 else:
105 metadata.append(i)
106 if fieldinfo is not None:
107 fields[k] = fieldinfo
108 if k in fields:
109 fields[k].default = default
110 fields[k].annotation = actual_type
111 fields[k].metadata.extend(metadata)
112 continue
113 if default is not None:
114 fields[k] = Field(default)
115 else:
116 fields[k] = Field()
117 fields[k].annotation = actual_type
118 fields[k].metadata.extend(metadata)
119 for k in fields:
120 fields[k] = FieldInfo(fields[k])
121
122 def from_json(cls, json_str):
123 json_data = json.loads(json_str)
124 positional_only = []
125 others = {}
126 for k, p in signature.parameters.items():
127 if p.kind == p.POSITIONAL_ONLY:
128 positional_only.append(json_data[k])
129 else:
130 others[k] = json_data[k]
131 return cls(*positional_only, **others)
132
133 _class = type(
134 f'{func.__qualname__}_PydanticSchema_{id(func)}',
135 (Schema,),
136 {
137 "_func": pydantic_wrapper,
138 '__new__': lambda cls, *args, **kwargs: pydantic_wrapper(*args, **kwargs),
139 '__call__': lambda *args, **kwargs: pydantic_wrapper(*args, **kwargs) # make duck typer happy
140 }
141 )
142 _class.from_json = classmethod(from_json)
143 _class.fields = classmethod(lambda cls: fields)
144 return _class
A wrapper for pydantic BaseModel that implements the Schema interface.
Definition pydantic.py:73
A wrapper for pydantic FieldInfo.
Definition pydantic.py:19
__init__(self, pydantic.fields.FieldInfo field)
Initialize the field information.
Definition pydantic.py:25
typing.Type[typing.Any]|None annotation(self)
Get the type annotation of the field.
Definition pydantic.py:45
bool required(self)
Check if the field is required for the schema.
Definition pydantic.py:58
An abstract field info that describes a data field in a schema.
Definition schema.py:13
An abstract schema that describes some data.
Definition schema.py:91
CallableT callable_schema(CallableT func, *, ConfigDict config=None, bool validate_return=False)
A decorator that wraps pydantic's validate_call.
Definition pydantic.py:123