fastapi-voyager 0.4.2__tar.gz → 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/PKG-INFO +18 -6
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/README.md +17 -5
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/graph.py +19 -15
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/type.py +20 -11
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/type_helper.py +70 -62
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/version.py +1 -1
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/demo_anno.py +9 -0
- fastapi_voyager-0.4.4/tests/test_type_helper.py +99 -0
- fastapi_voyager-0.4.2/tests/test_type_alias.py +0 -30
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/.gitignore +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/LICENSE +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/pyproject.toml +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/__init__.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/cli.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/filter.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/module.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/server.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graph-ui.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/index.html +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/quasar.min.css +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/quasar.min.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/vue-main.js +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/__init__.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/demo.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/service/__init__.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/service/schema.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/test_analysis.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/test_import.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/tests/test_module.py +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/uv.lock +0 -0
- {fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/voyager.jpg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-voyager
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Visualize FastAPI application's routing tree and dependencies
|
|
5
5
|
Project-URL: Homepage, https://github.com/allmonday/fastapi-voyager
|
|
6
6
|
Project-URL: Source, https://github.com/allmonday/fastapi-voyager
|
|
@@ -144,9 +144,9 @@ dot -Tpng router_viz.dot -o router_viz.png
|
|
|
144
144
|
or you can open router_viz.dot with vscode extension `graphviz interactive preview`
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
## Plan
|
|
147
|
+
## Plan before v1.0
|
|
148
148
|
|
|
149
|
-
features
|
|
149
|
+
features
|
|
150
150
|
- [x] group schemas by module hierarchy
|
|
151
151
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
152
152
|
- [x] view in web browser
|
|
@@ -162,18 +162,30 @@ features:
|
|
|
162
162
|
- [x] alt+click to show field details
|
|
163
163
|
- [x] display source code of routes (including response_model)
|
|
164
164
|
- [x] handle excluded field
|
|
165
|
-
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
166
|
-
- [ ] support dataclass
|
|
167
165
|
- [ ] group routes by module hierarchy
|
|
166
|
+
- [ ] support dataclass
|
|
167
|
+
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
168
|
+
- [ ] refactor
|
|
169
|
+
- [ ] abstract render module
|
|
170
|
+
- [ ] ui optimization
|
|
171
|
+
- [ ] fixed left/right bar show field information
|
|
172
|
+
- [ ] display standard ER diagram
|
|
173
|
+
- [ ] display potential invalid links
|
|
168
174
|
- [ ] integration with pydantic-resolve
|
|
169
175
|
- [ ] show difference between resolve, post fields
|
|
170
176
|
- [x] strikethrough for excluded fields
|
|
171
177
|
- [ ] display loader as edges
|
|
172
|
-
- [ ]
|
|
178
|
+
- [ ] add tests
|
|
173
179
|
|
|
174
180
|
bugs:
|
|
175
181
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
176
182
|
|
|
183
|
+
## Using with pydantic-resolve
|
|
184
|
+
|
|
185
|
+
pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
|
|
186
|
+
|
|
187
|
+
TODO: ...
|
|
188
|
+
|
|
177
189
|
|
|
178
190
|
## Credits
|
|
179
191
|
|
|
@@ -117,9 +117,9 @@ dot -Tpng router_viz.dot -o router_viz.png
|
|
|
117
117
|
or you can open router_viz.dot with vscode extension `graphviz interactive preview`
|
|
118
118
|
|
|
119
119
|
|
|
120
|
-
## Plan
|
|
120
|
+
## Plan before v1.0
|
|
121
121
|
|
|
122
|
-
features
|
|
122
|
+
features
|
|
123
123
|
- [x] group schemas by module hierarchy
|
|
124
124
|
- [x] module-based coloring via Analytics(module_color={...})
|
|
125
125
|
- [x] view in web browser
|
|
@@ -135,18 +135,30 @@ features:
|
|
|
135
135
|
- [x] alt+click to show field details
|
|
136
136
|
- [x] display source code of routes (including response_model)
|
|
137
137
|
- [x] handle excluded field
|
|
138
|
-
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
139
|
-
- [ ] support dataclass
|
|
140
138
|
- [ ] group routes by module hierarchy
|
|
139
|
+
- [ ] support dataclass
|
|
140
|
+
- [ ] user can generate nodes/edges manually and connect to generated ones
|
|
141
|
+
- [ ] refactor
|
|
142
|
+
- [ ] abstract render module
|
|
143
|
+
- [ ] ui optimization
|
|
144
|
+
- [ ] fixed left/right bar show field information
|
|
145
|
+
- [ ] display standard ER diagram
|
|
146
|
+
- [ ] display potential invalid links
|
|
141
147
|
- [ ] integration with pydantic-resolve
|
|
142
148
|
- [ ] show difference between resolve, post fields
|
|
143
149
|
- [x] strikethrough for excluded fields
|
|
144
150
|
- [ ] display loader as edges
|
|
145
|
-
- [ ]
|
|
151
|
+
- [ ] add tests
|
|
146
152
|
|
|
147
153
|
bugs:
|
|
148
154
|
- [ ] fix duplicated link from class and parent class, it also break clicking highlight
|
|
149
155
|
|
|
156
|
+
## Using with pydantic-resolve
|
|
157
|
+
|
|
158
|
+
pydantic-resolve's @ensure_subset decorator is helpful to pick fields from `source class` in safe.
|
|
159
|
+
|
|
160
|
+
TODO: ...
|
|
161
|
+
|
|
150
162
|
|
|
151
163
|
## Credits
|
|
152
164
|
|
|
@@ -12,7 +12,7 @@ from fastapi_voyager.type_helper import (
|
|
|
12
12
|
update_forward_refs
|
|
13
13
|
)
|
|
14
14
|
from pydantic import BaseModel
|
|
15
|
-
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, ModuleNode
|
|
15
|
+
from fastapi_voyager.type import Route, SchemaNode, Link, Tag, ModuleNode, LinkType
|
|
16
16
|
from fastapi_voyager.module import build_module_tree
|
|
17
17
|
from fastapi_voyager.filter import filter_graph
|
|
18
18
|
|
|
@@ -112,7 +112,6 @@ class Analytics:
|
|
|
112
112
|
# add response_models and create links from route -> response_model
|
|
113
113
|
for schema in get_core_types(route.response_model):
|
|
114
114
|
if schema and issubclass(schema, BaseModel):
|
|
115
|
-
update_forward_refs(schema)
|
|
116
115
|
target_name = full_class_name(schema)
|
|
117
116
|
self.links.append(Link(
|
|
118
117
|
source=route_id,
|
|
@@ -157,7 +156,8 @@ class Analytics:
|
|
|
157
156
|
source_origin: str,
|
|
158
157
|
target: str,
|
|
159
158
|
target_origin: str,
|
|
160
|
-
type:
|
|
159
|
+
type: LinkType
|
|
160
|
+
) -> bool:
|
|
161
161
|
"""
|
|
162
162
|
1. add link to link_set
|
|
163
163
|
2. if duplicated, do nothing, else insert
|
|
@@ -182,6 +182,7 @@ class Analytics:
|
|
|
182
182
|
3. recursively run walk_schema
|
|
183
183
|
"""
|
|
184
184
|
|
|
185
|
+
update_forward_refs(schema)
|
|
185
186
|
self.add_to_node_set(schema)
|
|
186
187
|
|
|
187
188
|
# handle schema inside ensure_subset(schema)
|
|
@@ -262,17 +263,22 @@ class Analytics:
|
|
|
262
263
|
|
|
263
264
|
def generate_dot(self):
|
|
264
265
|
|
|
266
|
+
def generate_link(link: Link):
|
|
267
|
+
if link.type == 'internal':
|
|
268
|
+
return f'''{handle_entry(link.source)}:e -> {handle_entry(link.target)}:w [ {get_link_attributes(link)} ];'''
|
|
269
|
+
else:
|
|
270
|
+
return f'''{handle_entry(link.source)} -> {handle_entry(link.target)} [ {get_link_attributes(link)} ];'''
|
|
271
|
+
|
|
272
|
+
|
|
265
273
|
def get_link_attributes(link: Link):
|
|
266
|
-
if link.type == '
|
|
267
|
-
return 'style = "dashed",
|
|
268
|
-
elif link.type == 'parent':
|
|
269
|
-
return 'style = "solid", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"'
|
|
274
|
+
if link.type == 'parent':
|
|
275
|
+
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< inherit >", color = "purple", tailport="n"'
|
|
270
276
|
elif link.type == 'entry':
|
|
271
|
-
return 'style = "solid", label = "", minlen=3'
|
|
277
|
+
return 'style = "solid", label = "", minlen=3, tailport="e", headport="w"'
|
|
272
278
|
elif link.type == 'subset':
|
|
273
|
-
return 'style = "solid", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"'
|
|
279
|
+
return 'style = "solid, dashed", dir="back", minlen=3, taillabel = "< subset >", color = "orange", tailport="n"'
|
|
274
280
|
|
|
275
|
-
return 'style = "solid", arrowtail="
|
|
281
|
+
return 'style = "solid", arrowtail="odot", dir="back", minlen=3'
|
|
276
282
|
|
|
277
283
|
def render_module(mod: ModuleNode):
|
|
278
284
|
color = self.module_color.get(mod.fullname)
|
|
@@ -296,7 +302,7 @@ class Analytics:
|
|
|
296
302
|
style="rounded"
|
|
297
303
|
label = " {mod.name}"
|
|
298
304
|
labeljust = "l"
|
|
299
|
-
{(f'pencolor = "{color}"' if color else 'pencolor=""')}
|
|
305
|
+
{(f'pencolor = "{color}"' if color else 'pencolor="#ccc"')}
|
|
300
306
|
{(f'penwidth = 3' if color else 'penwidth=""')}
|
|
301
307
|
{inner_nodes_str}
|
|
302
308
|
{child_str}
|
|
@@ -340,9 +346,7 @@ class Analytics:
|
|
|
340
346
|
|
|
341
347
|
modules_str = '\n'.join(render_module(m) for m in _modules)
|
|
342
348
|
|
|
343
|
-
links = [
|
|
344
|
-
f'''{handle_entry(link.source)} -> {handle_entry(link.target)} [ {get_link_attributes(link)} ];''' for link in _links
|
|
345
|
-
]
|
|
349
|
+
links = [ generate_link(link) for link in _links ]
|
|
346
350
|
link_str = '\n'.join(links)
|
|
347
351
|
|
|
348
352
|
template = f'''
|
|
@@ -376,7 +380,7 @@ class Analytics:
|
|
|
376
380
|
color = "#aaa"
|
|
377
381
|
margin=18
|
|
378
382
|
style="dashed"
|
|
379
|
-
label = "
|
|
383
|
+
label = " Routes"
|
|
380
384
|
labeljust = "l"
|
|
381
385
|
fontsize = "20"
|
|
382
386
|
{route_str}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
|
+
@dataclass
|
|
5
|
+
class NodeBase:
|
|
6
|
+
id: str
|
|
7
|
+
name: str
|
|
4
8
|
|
|
5
9
|
@dataclass
|
|
6
10
|
class FieldInfo:
|
|
@@ -11,23 +15,17 @@ class FieldInfo:
|
|
|
11
15
|
is_exclude: bool = False
|
|
12
16
|
|
|
13
17
|
@dataclass
|
|
14
|
-
class Tag:
|
|
15
|
-
id: str
|
|
16
|
-
name: str
|
|
18
|
+
class Tag(NodeBase):
|
|
17
19
|
routes: list['Route'] # route.id
|
|
18
20
|
|
|
19
21
|
@dataclass
|
|
20
|
-
class Route:
|
|
21
|
-
id: str
|
|
22
|
-
name: str
|
|
22
|
+
class Route(NodeBase):
|
|
23
23
|
source_code: str = ''
|
|
24
24
|
vscode_link: str = '' # optional vscode deep link
|
|
25
25
|
|
|
26
26
|
@dataclass
|
|
27
|
-
class SchemaNode:
|
|
28
|
-
id: str
|
|
27
|
+
class SchemaNode(NodeBase):
|
|
29
28
|
module: str
|
|
30
|
-
name: str
|
|
31
29
|
source_code: str = '' # optional for tests / backward compatibility
|
|
32
30
|
vscode_link: str = '' # optional vscode deep link
|
|
33
31
|
fields: list[FieldInfo] = field(default_factory=list)
|
|
@@ -39,10 +37,21 @@ class ModuleNode:
|
|
|
39
37
|
schema_nodes: list[SchemaNode]
|
|
40
38
|
modules: list['ModuleNode']
|
|
41
39
|
|
|
40
|
+
|
|
41
|
+
# type:
|
|
42
|
+
# - entry: tag -> route, route -> response model
|
|
43
|
+
# - subset: schema -> schema (subset)
|
|
44
|
+
# - parent: schema -> schema (inheritance)
|
|
45
|
+
# - internal: schema -> schema (field reference)
|
|
46
|
+
LinkType = Literal['internal', 'parent', 'entry', 'subset']
|
|
47
|
+
|
|
42
48
|
@dataclass
|
|
43
49
|
class Link:
|
|
50
|
+
# node + field level links
|
|
44
51
|
source: str
|
|
45
|
-
source_origin: str # internal relationship
|
|
46
52
|
target: str
|
|
53
|
+
|
|
54
|
+
# node level links
|
|
55
|
+
source_origin: str
|
|
47
56
|
target_origin: str
|
|
48
|
-
type:
|
|
57
|
+
type: LinkType
|
|
@@ -4,6 +4,7 @@ from pydantic import BaseModel
|
|
|
4
4
|
from typing import get_origin, get_args, Union, Annotated, Any, Type
|
|
5
5
|
from fastapi_voyager.type import FieldInfo
|
|
6
6
|
from types import UnionType
|
|
7
|
+
import pydantic_resolve.constant as const
|
|
7
8
|
|
|
8
9
|
# Python <3.12 compatibility: TypeAliasType exists only from 3.12 (PEP 695)
|
|
9
10
|
try: # pragma: no cover - import guard
|
|
@@ -14,24 +15,10 @@ except Exception: # pragma: no cover
|
|
|
14
15
|
TypeAliasType = _DummyTypeAliasType # type: ignore
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
-
origin = get_origin(annotation)
|
|
19
|
-
args = get_args(annotation)
|
|
20
|
-
if origin is Union and type(None) in args:
|
|
21
|
-
return True
|
|
22
|
-
return False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _is_list(annotation):
|
|
18
|
+
def is_list(annotation):
|
|
26
19
|
return getattr(annotation, "__origin__", None) == list
|
|
27
20
|
|
|
28
21
|
|
|
29
|
-
def shelling_type(type):
|
|
30
|
-
while _is_optional(type) or _is_list(type):
|
|
31
|
-
type = type.__args__[0]
|
|
32
|
-
return type
|
|
33
|
-
|
|
34
|
-
|
|
35
22
|
def full_class_name(cls):
|
|
36
23
|
return f"{cls.__module__}.{cls.__qualname__}"
|
|
37
24
|
|
|
@@ -41,12 +28,9 @@ def get_core_types(tp):
|
|
|
41
28
|
- get the core type
|
|
42
29
|
- always return a tuple of core types
|
|
43
30
|
"""
|
|
44
|
-
|
|
45
|
-
return tuple()
|
|
46
|
-
|
|
47
|
-
# Unwrap PEP 695 type aliases (they wrap the actual annotation in __value__)
|
|
48
|
-
# Repeat in case of nested aliasing.
|
|
31
|
+
# Helpers
|
|
49
32
|
def _unwrap_alias(t):
|
|
33
|
+
"""Unwrap PEP 695 type aliases by following __value__ repeatedly."""
|
|
50
34
|
while isinstance(t, TypeAliasType) or (
|
|
51
35
|
t.__class__.__name__ == 'TypeAliasType' and hasattr(t, '__value__')
|
|
52
36
|
):
|
|
@@ -56,47 +40,54 @@ def get_core_types(tp):
|
|
|
56
40
|
break
|
|
57
41
|
return t
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
def _enqueue(items, q):
|
|
44
|
+
for it in items:
|
|
45
|
+
if it is not type(None): # skip None in unions
|
|
46
|
+
q.append(it)
|
|
60
47
|
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
args = getattr(_tp, "__args__", ())
|
|
65
|
-
if args:
|
|
66
|
-
_tp = args[0]
|
|
67
|
-
else:
|
|
68
|
-
break
|
|
69
|
-
return _tp
|
|
70
|
-
|
|
71
|
-
tp = _shell_list(tp)
|
|
48
|
+
# Queue-based shelling to reach concrete core types
|
|
49
|
+
queue: list[object] = [tp]
|
|
50
|
+
result: list[object] = []
|
|
72
51
|
|
|
73
|
-
|
|
74
|
-
|
|
52
|
+
while queue:
|
|
53
|
+
cur = queue.pop(0)
|
|
54
|
+
if cur is type(None):
|
|
55
|
+
continue
|
|
75
56
|
|
|
76
|
-
|
|
77
|
-
return tuple()
|
|
57
|
+
cur = _unwrap_alias(cur)
|
|
78
58
|
|
|
79
|
-
|
|
80
|
-
|
|
59
|
+
# Handle Annotated[T, ...] as a shell
|
|
60
|
+
if get_origin(cur) is Annotated:
|
|
61
|
+
args = get_args(cur)
|
|
62
|
+
if args:
|
|
63
|
+
queue.append(args[0])
|
|
64
|
+
continue
|
|
81
65
|
|
|
66
|
+
# Handle Union / Optional / PEP 604 UnionType
|
|
67
|
+
orig = get_origin(cur)
|
|
82
68
|
if orig in (Union, UnionType):
|
|
83
|
-
args =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
69
|
+
args = get_args(cur)
|
|
70
|
+
# push all non-None members back for further shelling
|
|
71
|
+
_enqueue(args, queue)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Handle list shells
|
|
75
|
+
if is_list(cur):
|
|
76
|
+
args = getattr(cur, "__args__", ())
|
|
77
|
+
if args:
|
|
78
|
+
queue.append(args[0])
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# If still an alias-like wrapper, unwrap again and re-process
|
|
82
|
+
_cur2 = _unwrap_alias(cur)
|
|
83
|
+
if _cur2 is not cur:
|
|
84
|
+
queue.append(_cur2)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Otherwise treat as a concrete core type (could be a class, typing.Final, etc.)
|
|
88
|
+
result.append(cur)
|
|
89
|
+
|
|
90
|
+
return tuple(result)
|
|
100
91
|
|
|
101
92
|
|
|
102
93
|
def get_type_name(anno):
|
|
@@ -149,8 +140,9 @@ def get_type_name(anno):
|
|
|
149
140
|
|
|
150
141
|
return name_of(anno)
|
|
151
142
|
|
|
143
|
+
|
|
152
144
|
def is_inheritance_of_pydantic_base(cls):
|
|
153
|
-
|
|
145
|
+
return issubclass(cls, BaseModel) and cls is not BaseModel
|
|
154
146
|
|
|
155
147
|
|
|
156
148
|
def get_bases_fields(schemas: list[type[BaseModel]]) -> set[str]:
|
|
@@ -240,15 +232,31 @@ def safe_issubclass(kls, classinfo):
|
|
|
240
232
|
|
|
241
233
|
|
|
242
234
|
def update_forward_refs(kls):
|
|
243
|
-
|
|
235
|
+
# TODO: refactor
|
|
236
|
+
def update_pydantic_forward_refs(kls2: Type[BaseModel]):
|
|
244
237
|
"""
|
|
245
238
|
recursively update refs.
|
|
246
239
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
|
|
241
|
+
kls2.model_rebuild()
|
|
242
|
+
setattr(kls2, const.PYDANTIC_FORWARD_REF_UPDATED, True)
|
|
243
|
+
|
|
244
|
+
values = kls2.model_fields.values()
|
|
249
245
|
for field in values:
|
|
250
246
|
update_forward_refs(field.annotation)
|
|
251
247
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
if safe_issubclass(kls, BaseModel):
|
|
249
|
+
kls.model_rebuild()
|
|
250
|
+
setattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, True)
|
|
251
|
+
|
|
252
|
+
for shelled_type in get_core_types(kls):
|
|
253
|
+
if getattr(shelled_type, const.PYDANTIC_FORWARD_REF_UPDATED, False):
|
|
254
|
+
continue
|
|
255
|
+
if safe_issubclass(shelled_type, BaseModel):
|
|
256
|
+
update_pydantic_forward_refs(shelled_type)
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
from tests.demo_anno import PageSprint, PageOverall
|
|
260
|
+
|
|
261
|
+
update_forward_refs(PageOverall)
|
|
262
|
+
update_forward_refs(PageSprint)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.4.
|
|
2
|
+
__version__ = "0.4.4"
|
|
@@ -12,6 +12,11 @@ app = FastAPI(title="Demo API", description="A demo FastAPI application for rout
|
|
|
12
12
|
def get_sprint():
|
|
13
13
|
return []
|
|
14
14
|
|
|
15
|
+
class Tree(BaseModel):
|
|
16
|
+
id: int
|
|
17
|
+
name: str
|
|
18
|
+
children: list[Tree] = []
|
|
19
|
+
|
|
15
20
|
class PageMember(serv.Member):
|
|
16
21
|
fullname: str = ''
|
|
17
22
|
def post_fullname(self):
|
|
@@ -62,4 +67,8 @@ class PageStories(BaseModel):
|
|
|
62
67
|
|
|
63
68
|
@app.get("/page_info/", tags=['for-page'], response_model=PageStories)
|
|
64
69
|
def get_page_stories():
|
|
70
|
+
return {} # no implementation
|
|
71
|
+
|
|
72
|
+
@app.get("/rest-tree/", tags=['for-restapi'], response_model=Tree)
|
|
73
|
+
def get_tree():
|
|
65
74
|
return {} # no implementation
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typing
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from fastapi_voyager.type_helper import get_core_types
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_optional_and_list_core_types():
|
|
9
|
+
class T: ...
|
|
10
|
+
|
|
11
|
+
# Optional[T] -> (T,)
|
|
12
|
+
opt = typing.Optional[T]
|
|
13
|
+
core = get_core_types(opt)
|
|
14
|
+
assert core == (T,)
|
|
15
|
+
|
|
16
|
+
# list[T] -> (T,)
|
|
17
|
+
lst = list[T]
|
|
18
|
+
core2 = get_core_types(lst)
|
|
19
|
+
assert core2 == (T,)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_typing_union_core_types():
|
|
23
|
+
class A: ...
|
|
24
|
+
class B: ...
|
|
25
|
+
|
|
26
|
+
u = typing.Union[A, B]
|
|
27
|
+
core = get_core_types(u)
|
|
28
|
+
# order preserved
|
|
29
|
+
assert core == (A, B)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="PEP 604 union (|) requires Python 3.10+")
|
|
33
|
+
def test_uniontype_pep604_core_types():
|
|
34
|
+
class A: ...
|
|
35
|
+
class B: ...
|
|
36
|
+
|
|
37
|
+
u = A | B
|
|
38
|
+
core = get_core_types(u)
|
|
39
|
+
assert core == (A, B)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_mixed_optional_list():
|
|
43
|
+
class T: ...
|
|
44
|
+
|
|
45
|
+
# Optional[list[T]] -> (T,) (list unwrapped after removing None)
|
|
46
|
+
anno = typing.Optional[list[T]]
|
|
47
|
+
core = get_core_types(anno)
|
|
48
|
+
assert core == (T,)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_nested_union_flattening():
|
|
52
|
+
class A: ...
|
|
53
|
+
class B: ...
|
|
54
|
+
class C: ...
|
|
55
|
+
|
|
56
|
+
anno = typing.Union[A, typing.Union[B, C]]
|
|
57
|
+
core = get_core_types(anno)
|
|
58
|
+
# typing normalizes nested unions -> (A, B, C)
|
|
59
|
+
assert core == (A, B, C)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="PEP 604 union (|) requires Python 3.10+")
|
|
63
|
+
def test_uniontype_with_list_member():
|
|
64
|
+
class A: ...
|
|
65
|
+
class B: ...
|
|
66
|
+
|
|
67
|
+
anno = A | list[B]
|
|
68
|
+
anno2 = A | list[list[B]]
|
|
69
|
+
core = get_core_types(anno)
|
|
70
|
+
core2 = get_core_types(anno2)
|
|
71
|
+
assert core == (A, B)
|
|
72
|
+
assert core2 == (A, B)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Only Python 3.12+ supports the PEP 695 `type` statement producing TypeAliasType
|
|
78
|
+
@pytest.mark.skipif(sys.version_info < (3, 12), reason="PEP 695 type aliases require Python 3.12+")
|
|
79
|
+
def test_union_type_alias_and_list():
|
|
80
|
+
# Dynamically exec a type alias using the new syntax so test file stays valid on <3.12 (even though skipped)
|
|
81
|
+
ns: dict = {}
|
|
82
|
+
code = """
|
|
83
|
+
class A: ...
|
|
84
|
+
class B: ...
|
|
85
|
+
|
|
86
|
+
type MyAlias = A | B
|
|
87
|
+
"""
|
|
88
|
+
exec(code, ns, ns)
|
|
89
|
+
MyAlias = ns['MyAlias']
|
|
90
|
+
A = ns['A']
|
|
91
|
+
B = ns['B']
|
|
92
|
+
|
|
93
|
+
# list[MyAlias] should yield (A, B)
|
|
94
|
+
core = get_core_types(list[MyAlias])
|
|
95
|
+
assert set(core) == {A, B}
|
|
96
|
+
|
|
97
|
+
# Direct alias should also work
|
|
98
|
+
core2 = get_core_types(MyAlias)
|
|
99
|
+
assert set(core2) == {A, B}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import pytest
|
|
3
|
-
from typing import get_args, get_origin
|
|
4
|
-
|
|
5
|
-
from fastapi_voyager.type_helper import get_core_types
|
|
6
|
-
|
|
7
|
-
# Only Python 3.12+ supports the PEP 695 `type` statement producing TypeAliasType
|
|
8
|
-
pytestmark = pytest.mark.skipif(sys.version_info < (3, 12), reason="PEP 695 type aliases require Python 3.12+")
|
|
9
|
-
|
|
10
|
-
def test_union_type_alias_and_list():
|
|
11
|
-
# Dynamically exec a type alias using the new syntax so test file stays valid on <3.12 (even though skipped)
|
|
12
|
-
ns: dict = {}
|
|
13
|
-
code = """
|
|
14
|
-
class A: ...
|
|
15
|
-
class B: ...
|
|
16
|
-
|
|
17
|
-
type MyAlias = A | B
|
|
18
|
-
"""
|
|
19
|
-
exec(code, ns, ns)
|
|
20
|
-
MyAlias = ns['MyAlias']
|
|
21
|
-
A = ns['A']
|
|
22
|
-
B = ns['B']
|
|
23
|
-
|
|
24
|
-
# list[MyAlias] should yield (A, B)
|
|
25
|
-
core = get_core_types(list[MyAlias])
|
|
26
|
-
assert set(core) == {A, B}
|
|
27
|
-
|
|
28
|
-
# Direct alias should also work
|
|
29
|
-
core2 = get_core_types(MyAlias)
|
|
30
|
-
assert set(core2) == {A, B}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/apple-touch-icon.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-16x16.png
RENAMED
|
File without changes
|
{fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-32x32.png
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_voyager-0.4.2 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/site.webmanifest
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|