fastapi-voyager 0.4.3__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.
Files changed (43) hide show
  1. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/PKG-INFO +18 -6
  2. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/README.md +17 -5
  3. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/graph.py +18 -14
  4. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/type.py +20 -11
  5. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/type_helper.py +46 -55
  6. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/version.py +1 -1
  7. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/demo_anno.py +4 -2
  8. fastapi_voyager-0.4.4/tests/test_type_helper.py +99 -0
  9. fastapi_voyager-0.4.3/tests/test_type_alias.py +0 -30
  10. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/.gitignore +0 -0
  11. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/LICENSE +0 -0
  12. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/pyproject.toml +0 -0
  13. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/__init__.py +0 -0
  14. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/cli.py +0 -0
  15. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/filter.py +0 -0
  16. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/module.py +0 -0
  17. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/server.py +0 -0
  18. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/route-code-display.js +0 -0
  19. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/schema-code-display.js +0 -0
  20. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/component/schema-field-filter.js +0 -0
  21. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graph-ui.js +0 -0
  22. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graphviz.svg.css +0 -0
  23. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/graphviz.svg.js +0 -0
  24. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/android-chrome-192x192.png +0 -0
  25. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/android-chrome-512x512.png +0 -0
  26. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/apple-touch-icon.png +0 -0
  27. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-16x16.png +0 -0
  28. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon-32x32.png +0 -0
  29. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/favicon.ico +0 -0
  30. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/icon/site.webmanifest +0 -0
  31. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/index.html +0 -0
  32. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/quasar.min.css +0 -0
  33. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/quasar.min.js +0 -0
  34. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/src/fastapi_voyager/web/vue-main.js +0 -0
  35. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/__init__.py +0 -0
  36. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/demo.py +0 -0
  37. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/service/__init__.py +0 -0
  38. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/service/schema.py +0 -0
  39. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/test_analysis.py +0 -0
  40. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/test_import.py +0 -0
  41. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/tests/test_module.py +0 -0
  42. {fastapi_voyager-0.4.3 → fastapi_voyager-0.4.4}/uv.lock +0 -0
  43. {fastapi_voyager-0.4.3 → 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
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
- - [ ] test cases
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
- - [ ] test cases
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
 
@@ -156,7 +156,8 @@ class Analytics:
156
156
  source_origin: str,
157
157
  target: str,
158
158
  target_origin: str,
159
- type: Literal['child', 'parent', 'subset']):
159
+ type: LinkType
160
+ ) -> bool:
160
161
  """
161
162
  1. add link to link_set
162
163
  2. if duplicated, do nothing, else insert
@@ -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 == 'child':
267
- return 'style = "dashed", label = "", minlen=3'
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="odiamond", dir="back", minlen=3'
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 = " Route apis"
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: Literal['child', 'parent', 'entry', 'subset']
57
+ type: LinkType
@@ -15,24 +15,10 @@ except Exception: # pragma: no cover
15
15
  TypeAliasType = _DummyTypeAliasType # type: ignore
16
16
 
17
17
 
18
- def _is_optional(annotation):
19
- origin = get_origin(annotation)
20
- args = get_args(annotation)
21
- if origin is Union and type(None) in args:
22
- return True
23
- return False
24
-
25
-
26
- def _is_list(annotation):
18
+ def is_list(annotation):
27
19
  return getattr(annotation, "__origin__", None) == list
28
20
 
29
21
 
30
- def shelling_type(type):
31
- while _is_optional(type) or _is_list(type):
32
- type = type.__args__[0]
33
- return type
34
-
35
-
36
22
  def full_class_name(cls):
37
23
  return f"{cls.__module__}.{cls.__qualname__}"
38
24
 
@@ -42,12 +28,9 @@ def get_core_types(tp):
42
28
  - get the core type
43
29
  - always return a tuple of core types
44
30
  """
45
- if tp is type(None):
46
- return tuple()
47
-
48
- # Unwrap PEP 695 type aliases (they wrap the actual annotation in __value__)
49
- # Repeat in case of nested aliasing.
31
+ # Helpers
50
32
  def _unwrap_alias(t):
33
+ """Unwrap PEP 695 type aliases by following __value__ repeatedly."""
51
34
  while isinstance(t, TypeAliasType) or (
52
35
  t.__class__.__name__ == 'TypeAliasType' and hasattr(t, '__value__')
53
36
  ):
@@ -57,47 +40,54 @@ def get_core_types(tp):
57
40
  break
58
41
  return t
59
42
 
60
- tp = _unwrap_alias(tp)
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)
61
47
 
62
- # 1. Unwrap list layers
63
- def _shell_list(_tp):
64
- while _is_list(_tp):
65
- args = getattr(_tp, "__args__", ())
66
- if args:
67
- _tp = args[0]
68
- else:
69
- break
70
- return _tp
71
-
72
- tp = _shell_list(tp)
48
+ # Queue-based shelling to reach concrete core types
49
+ queue: list[object] = [tp]
50
+ result: list[object] = []
73
51
 
74
- # Alias could wrap a list element, unwrap again
75
- tp = _unwrap_alias(tp)
52
+ while queue:
53
+ cur = queue.pop(0)
54
+ if cur is type(None):
55
+ continue
76
56
 
77
- if tp is type(None): # check again
78
- return tuple()
57
+ cur = _unwrap_alias(cur)
79
58
 
80
- while True:
81
- orig = get_origin(tp)
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
82
65
 
66
+ # Handle Union / Optional / PEP 604 UnionType
67
+ orig = get_origin(cur)
83
68
  if orig in (Union, UnionType):
84
- args = list(get_args(tp))
85
- non_none = [a for a in args if a is not type(None)] # noqa: E721
86
- has_none = len(non_none) != len(args)
87
- # Optional[T] case -> keep unwrapping (exactly one real type + None)
88
- if has_none and len(non_none) == 1:
89
- tp = non_none[0]
90
- tp = _unwrap_alias(tp)
91
- tp = _shell_list(tp)
92
- continue
93
- # General union: return all non-None members (order preserved)
94
- if non_none:
95
- return tuple(non_none)
96
- return tuple()
97
- break
98
-
99
- # single concrete type
100
- return (tp,)
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)
101
91
 
102
92
 
103
93
  def get_type_name(anno):
@@ -242,6 +232,7 @@ def safe_issubclass(kls, classinfo):
242
232
 
243
233
 
244
234
  def update_forward_refs(kls):
235
+ # TODO: refactor
245
236
  def update_pydantic_forward_refs(kls2: Type[BaseModel]):
246
237
  """
247
238
  recursively update refs.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.4.3"
2
+ __version__ = "0.4.4"
@@ -56,8 +56,6 @@ class PageStory(BaseModel):
56
56
  owner: Optional[PageMember] = None
57
57
  union_tasks: list[TaskUnion] = []
58
58
 
59
- tree: Optional[Tree] = None
60
-
61
59
  @app.get("/page_overall", tags=['for-page'], response_model=PageOverall)
62
60
  async def get_page_info():
63
61
  page_overall = PageOverall(sprints=[]) # focus on schema only
@@ -69,4 +67,8 @@ class PageStories(BaseModel):
69
67
 
70
68
  @app.get("/page_info/", tags=['for-page'], response_model=PageStories)
71
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():
72
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