-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathannotation_to_json_schema.py
More file actions
149 lines (135 loc) · 5.17 KB
/
annotation_to_json_schema.py
File metadata and controls
149 lines (135 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
'''
Converts annotated Python functions into JSON Schema representations.
Handles annotation `T` where `T` is one of:
- Primitive: `int`, `float`, `str`, `bool`.
- Typed list: `list[T]`, `tp.List[T]`.
- "Metadata" dict: `dict[str, tp.Any]`, `tp.Dict[str, tp.Any]`.
- Nullable: `tp.Optional[T]`, `T | None`, `tp.Union[T, None]`.
- Literal enum: `tp.Literal[..., ...]`.
- Annotated: `tp.Annotated[T, description: str]`.
In particular, doesn't support:
- NamedTuple, TypedDict. This is a todo.
- Union beyond nullable.
- `tp.Any`.
- `list[tp.Any]`.
- Dict beyond `str -> tp.Any`.
- Tuple.
- Enum.
Recursive list types? Undefined behavior. Probably stack overflow,
or Ctrl+C to see huge stack. So it's diagnosable downstream.
'''
# from __future__ import annotations # stress test
import typing as tp
import types
import builtins
LOOKUP = {
builtins.int : 'integer',
builtins.float: 'number',
builtins.str : 'string',
builtins.bool : 'boolean',
}
def non_null_to_json_schema(anno: tp.Any, /) -> dict[str, tp.Any]:
# print('non_null_to_json_schema', type(anno))
match anno:
case builtins.int | builtins.float | builtins.str | builtins.bool:
return dict(
type=LOOKUP[anno],
)
case types.GenericAlias() | tp._GenericAlias(): # type: ignore
origin = tp.get_origin(anno)
args = tp.get_args(anno)
match origin:
case builtins.list: # also matches tp.List
try:
member_anno, = args
except ValueError:
raise TypeError('List must have exactly one type argument.')
return dict(
type='array',
items=annotation_to_json_schema(member_anno),
)
case builtins.dict: # also matches tp.Dict
try:
key_anno, value_anno = args
except ValueError:
raise TypeError('Dict must have exactly two type arguments.')
if key_anno is not builtins.str:
raise TypeError('Only dicts with string keys are supported.')
if value_anno is not tp.Any:
raise TypeError('You should use TypedDict instead. Unfortunately we don\'t support TypedDict yet. So just use `dict[str, tp.Any]`.')
return dict(
type='object',
)
case tp.Literal:
enums = tp.get_args(anno)
unique_types = {type(e) for e in enums}
if len(unique_types) != 1:
raise TypeError('All Literal enum values must have the same type.')
return dict(
type=LOOKUP[unique_types.pop()],
enum=list(enums),
)
case _:
raise TypeError(f'Unsupported generic type: {origin!r}')
case _:
raise TypeError(f'Unsupported type annotation: {anno!r}')
def maybe_null_to_json_schema(anno: tp.Any, /) -> dict[str, tp.Any]:
# print('maybe_null_to_json_schema', type(anno))
origin = tp.get_origin(anno)
if origin is types.UnionType or origin is tp.Union:
args = tp.get_args(anno)
inner = extract_nullable(*args)
return dict(
type=[non_null_to_json_schema(inner)['type'], 'null'],
)
else:
return non_null_to_json_schema(anno)
def annotation_to_json_schema(anno: tp.Any, /) -> dict[str, tp.Any]:
# print('annotation_to_json_schema', type(anno))
if isinstance(anno, tp._AnnotatedAlias): # type: ignore
inner, description = tp.get_args(anno)
return dict(
**maybe_null_to_json_schema(inner),
description=description,
)
return maybe_null_to_json_schema(anno)
def extract_nullable(*types_: tp.Any) -> tp.Any:
non_null = [t for t in types_ if t is not types.NoneType]
if len(non_null) == 1:
return non_null[0]
else:
raise TypeError('We don\'t yet support unions beyond nullable.')
def test():
from pprint import pprint
def f(
a: tp.Annotated[int, "Number of bananas"],
d: tp.Optional[float],
e: tp.Union[float, None],
f: float | None,
g: int | str,
b: tp.Annotated[list[tp.Annotated[
float, "Angle between John and Mary",
]], "Historic angles"] = [3.14],
c: str = "Hello, World!",
aa: tp.Literal['A', 'B', 'C'] = 'A',
ab: tp.Annotated[tp.Literal[3, 4], "Number of apples"] = 3,
) -> int:
'''
Goes to the Moon.
'''
return 42
for name, anno in tp.get_type_hints(f, include_extras=True).items():
if name == 'return':
continue
print(f"Parameter: {name}")
try:
pprint(annotation_to_json_schema(anno))
except TypeError:
print('invalid')
if name != 'g':
raise
else:
assert name != 'g'
print()
if __name__ == '__main__':
test()