richapi 0.3.1__tar.gz → 0.3.2__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.
- {richapi-0.3.1 → richapi-0.3.2}/PKG-INFO +1 -1
- {richapi-0.3.1 → richapi-0.3.2}/pyproject.toml +2 -1
- richapi-0.3.2/richapi/__version__.py +1 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/compiler.py +68 -52
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/openapi.py +2 -1
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_instance_attr_call.py +5 -5
- richapi-0.3.1/richapi/__version__.py +0 -1
- {richapi-0.3.1 → richapi-0.3.2}/.envrc +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/deploy-docs-on-demand.yml +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/main.yml +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/on-release-main.yml +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/.gitignore +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/README.md +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/deploy_docs.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/advance.md +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/conventions.md +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/images/custom-exception.png +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/images/with-richapi.png +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/images/without-richapi.png +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/implementation.md +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/docs/index.md +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/makefile +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/mkdocs.yml +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/cli.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/handler.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/protocol.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/richapi/exceptions.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/app/__init__.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/app/foo1.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_builder_pattern.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_class.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_compiler.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_custom_exc.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_duplicated_function.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_exc_raising_cases.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_exception_edge_cases.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_fastapi_auth_functions.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tests/test_import_from_other_module.py +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/tox.ini +0 -0
- {richapi-0.3.1 → richapi-0.3.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: richapi
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Find HTTPExceptions and turn them into documented responses!
|
|
5
5
|
Project-URL: repository, https://github.com/ManiMozaffar/richapi
|
|
6
6
|
Project-URL: Homepage, https://github.com/ManiMozaffar/richapi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "richapi"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "Find HTTPExceptions and turn them into documented responses!"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9"
|
|
@@ -19,6 +19,7 @@ build-backend = "hatchling.build"
|
|
|
19
19
|
|
|
20
20
|
[tool.pytest.ini_options]
|
|
21
21
|
asyncio_default_fixture_loop_scope = "session"
|
|
22
|
+
addopts = "--log-level=DEBUG -v"
|
|
22
23
|
|
|
23
24
|
[tool.pyright]
|
|
24
25
|
typeCheckingMode = "standard"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2"
|
|
@@ -60,6 +60,22 @@ def is_in_module(module: str, target_modules: list[str]) -> bool:
|
|
|
60
60
|
return False
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def _get_tree(obj: Callable):
|
|
64
|
+
to_search = obj
|
|
65
|
+
if is_absolutely_callable_object(obj):
|
|
66
|
+
to_search: Union[type, Callable] = (
|
|
67
|
+
obj_type if (obj_type := type(obj)) is not type else obj
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
tree = ast.parse(inspect.getsource(to_search))
|
|
72
|
+
return tree
|
|
73
|
+
except Exception as error:
|
|
74
|
+
obj_name = obj.__name__ if hasattr(obj, "__name__") else obj.__str__()
|
|
75
|
+
logger.debug(f"Failed to parse source code for {obj_name}", exc_info=error)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
63
79
|
def find_explicit_exceptions(
|
|
64
80
|
func: Callable,
|
|
65
81
|
target_modules: list[str],
|
|
@@ -82,7 +98,15 @@ def find_explicit_exceptions(
|
|
|
82
98
|
|
|
83
99
|
should_search_module_pred = should_search_module_pred_func
|
|
84
100
|
|
|
85
|
-
|
|
101
|
+
module = inspect.getmodule(func)
|
|
102
|
+
if module is None or _should_be_visited(module, should_search_module_pred) is False:
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
tree = _get_tree(func)
|
|
106
|
+
if tree is None:
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
return _find_explicit_expection_recursively(func, should_search_module_pred, tree)
|
|
86
110
|
|
|
87
111
|
|
|
88
112
|
def get_func_name(func: Callable) -> str:
|
|
@@ -92,29 +116,55 @@ def get_func_name(func: Callable) -> str:
|
|
|
92
116
|
def _find_all_class_exceptions(
|
|
93
117
|
cls: Union[type, Callable],
|
|
94
118
|
to_filter_predicate: Callable[[str], bool],
|
|
95
|
-
tree: Optional[ast.AST] = None,
|
|
96
119
|
):
|
|
120
|
+
cls_module = inspect.getmodule(cls)
|
|
121
|
+
if cls_module and is_stdlib(cls_module.__name__):
|
|
122
|
+
return []
|
|
123
|
+
if cls_module and to_filter_predicate(cls_module.__name__) is False:
|
|
124
|
+
return []
|
|
125
|
+
|
|
97
126
|
try:
|
|
98
|
-
|
|
127
|
+
cls_tree = ast.parse(inspect.getsource(cls))
|
|
99
128
|
except Exception as error:
|
|
100
129
|
cls_name = cls.__name__ if hasattr(cls, "__name__") else cls.__str__()
|
|
101
|
-
logger.
|
|
130
|
+
logger.info(
|
|
131
|
+
f"Failed to parse source code for {cls_name}.__init__", exc_info=error
|
|
132
|
+
)
|
|
102
133
|
return []
|
|
103
134
|
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
init_tree = _find_in_module(
|
|
136
|
+
cls_tree,
|
|
137
|
+
lambda n: (
|
|
138
|
+
isinstance(n, ast.AsyncFunctionDef) or isinstance(n, ast.FunctionDef)
|
|
139
|
+
)
|
|
140
|
+
and n.name == "__init__",
|
|
106
141
|
)
|
|
107
|
-
if hasattr(cls, "__call__"):
|
|
108
|
-
# Not sure if __call__ was called or init, so check for both
|
|
109
142
|
|
|
143
|
+
result: list[tuple[Optional[type[Exception]], ast.Raise]] = []
|
|
144
|
+
if init_tree is not None:
|
|
110
145
|
result.extend(
|
|
111
146
|
_find_explicit_expection_recursively(
|
|
112
|
-
cls.
|
|
113
|
-
|
|
114
|
-
|
|
147
|
+
cls.__init__, to_filter_predicate, init_tree
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if hasattr(cls, "__call__"):
|
|
152
|
+
# Not sure if __call__ was called or init, so check for both
|
|
153
|
+
call_tree = _find_in_module(
|
|
154
|
+
cls_tree,
|
|
155
|
+
lambda n: (
|
|
156
|
+
isinstance(n, ast.AsyncFunctionDef) or isinstance(n, ast.FunctionDef)
|
|
115
157
|
)
|
|
158
|
+
and n.name == "__call__",
|
|
116
159
|
)
|
|
117
160
|
|
|
161
|
+
if call_tree is not None:
|
|
162
|
+
result.extend(
|
|
163
|
+
_find_explicit_expection_recursively(
|
|
164
|
+
getattr(cls, "__call__"), to_filter_predicate, call_tree
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
118
168
|
return result
|
|
119
169
|
|
|
120
170
|
|
|
@@ -129,7 +179,7 @@ def is_absolutely_callable_object(obj: Callable) -> bool:
|
|
|
129
179
|
def _find_explicit_expection_recursively(
|
|
130
180
|
func_obj: Callable,
|
|
131
181
|
to_filter_predicate: Callable[[str], bool],
|
|
132
|
-
tree:
|
|
182
|
+
tree: ast.AST,
|
|
133
183
|
) -> list[tuple[Optional[type[Exception]], ast.Raise]]:
|
|
134
184
|
if func_obj in ExceptionFinder.visited:
|
|
135
185
|
return ExceptionFinder.visited[func_obj]
|
|
@@ -142,33 +192,7 @@ def _find_explicit_expection_recursively(
|
|
|
142
192
|
class_type: Union[type, Callable] = (
|
|
143
193
|
obj_type if (obj_type := type(func_obj)) is not type else func_obj
|
|
144
194
|
)
|
|
145
|
-
|
|
146
|
-
cls_module = inspect.getmodule(class_type)
|
|
147
|
-
if cls_module and is_stdlib(cls_module.__name__):
|
|
148
|
-
return []
|
|
149
|
-
if cls_module and to_filter_predicate(cls_module.__name__) is False:
|
|
150
|
-
return []
|
|
151
|
-
|
|
152
|
-
return _find_all_class_exceptions(class_type, to_filter_predicate, tree)
|
|
153
|
-
|
|
154
|
-
if tree is None:
|
|
155
|
-
try:
|
|
156
|
-
source = inspect.getsource(func_obj)
|
|
157
|
-
except (OSError, TypeError) as error:
|
|
158
|
-
logger.exception(
|
|
159
|
-
f"Failed to get source code for {get_func_name(func_obj)}.",
|
|
160
|
-
exc_info=error,
|
|
161
|
-
)
|
|
162
|
-
return []
|
|
163
|
-
|
|
164
|
-
try:
|
|
165
|
-
tree = ast.parse(source)
|
|
166
|
-
except IndentationError as error:
|
|
167
|
-
logger.exception(
|
|
168
|
-
f"Failed to get source code for {get_func_name(func_obj)}.",
|
|
169
|
-
exc_info=error,
|
|
170
|
-
)
|
|
171
|
-
return []
|
|
195
|
+
return _find_all_class_exceptions(class_type, to_filter_predicate)
|
|
172
196
|
|
|
173
197
|
finder = ExceptionFinder(func_obj, to_filter_predicate)
|
|
174
198
|
try:
|
|
@@ -312,12 +336,8 @@ def _resolve_functions_from_call_node(
|
|
|
312
336
|
node_id = NodeIdentifier(parent_part)
|
|
313
337
|
parent_attr = assignments.get(node_id, None)
|
|
314
338
|
|
|
315
|
-
if parent_attr is None:
|
|
316
|
-
|
|
317
|
-
result = _retrieve_from_annotations(func, parent_part)
|
|
318
|
-
return [] if result is None else [result]
|
|
319
|
-
|
|
320
|
-
parent_attr = getattr(builtins, parent_part, None)
|
|
339
|
+
if parent_attr is None and parent_part in func.__annotations__:
|
|
340
|
+
parent_attr = _retrieve_from_annotations(func, parent_part)
|
|
321
341
|
|
|
322
342
|
if parent_attr is None:
|
|
323
343
|
# use case where you're calling self.foo.do_something(), where self.foo is a class attribute that is a class itself
|
|
@@ -463,7 +483,6 @@ def is_stdlib(module_name: str) -> bool:
|
|
|
463
483
|
|
|
464
484
|
class ExceptionFinder(ast.NodeVisitor):
|
|
465
485
|
visited: dict[Callable, list[tuple[Optional[type[Exception]], ast.Raise]]] = {}
|
|
466
|
-
# cached_assignments: dict[Callable, dict[NodeIdentifier, str]] = {}
|
|
467
486
|
|
|
468
487
|
def __init__(
|
|
469
488
|
self, func: Callable, should_search_module_pred: Callable[[str], bool]
|
|
@@ -473,12 +492,11 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
473
492
|
# self.cached_assignments[func] = self.assignments
|
|
474
493
|
self.func = func
|
|
475
494
|
self.should_search_module_pred = should_search_module_pred
|
|
476
|
-
logger.
|
|
495
|
+
logger.debug(f"Analyzing function {func.__name__}")
|
|
477
496
|
|
|
478
497
|
@classmethod
|
|
479
498
|
def clear_cache(cls):
|
|
480
499
|
cls.visited.clear()
|
|
481
|
-
# cls.cached_assignments.clear()
|
|
482
500
|
|
|
483
501
|
def _should_be_visited(
|
|
484
502
|
self, module: ModuleType
|
|
@@ -616,7 +634,7 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
616
634
|
)
|
|
617
635
|
|
|
618
636
|
elif inspect.isclass(func_obj):
|
|
619
|
-
_excs =
|
|
637
|
+
_excs = _find_all_class_exceptions(
|
|
620
638
|
func_obj, self.should_search_module_pred
|
|
621
639
|
)
|
|
622
640
|
self.exceptions.extend(_excs)
|
|
@@ -636,9 +654,7 @@ class ExceptionFinder(ast.NodeVisitor):
|
|
|
636
654
|
try:
|
|
637
655
|
caller_type = _exctact_type(caller_node_name, self.func.__globals__)
|
|
638
656
|
except Exception:
|
|
639
|
-
logger.
|
|
640
|
-
f"Failed to resolve attribute chain: {caller_node_name}"
|
|
641
|
-
)
|
|
657
|
+
logger.debug(f"Failed to resolve attribute chain: {caller_node_name}")
|
|
642
658
|
self.generic_visit(node)
|
|
643
659
|
return
|
|
644
660
|
|
|
@@ -51,7 +51,8 @@ def enrich_openapi(
|
|
|
51
51
|
if target_module is None or target_module == "__main__":
|
|
52
52
|
raise BaseRichAPIException(
|
|
53
53
|
"Could not determine the module where the FastAPI instance was created.\n"
|
|
54
|
-
"Please provide the module name as a string or list of strings
|
|
54
|
+
"Please provide the module name as a string or list of strings.\n"
|
|
55
|
+
"Example: enrich_openapi(app, target_module='src')\n"
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
target_module = target_module.split(".")[0] # get the top-level module
|
|
@@ -44,10 +44,10 @@ class PaymentService:
|
|
|
44
44
|
def __init__(self):
|
|
45
45
|
foo1()
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def foo2(self):
|
|
48
48
|
foo2()
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def foo3(self):
|
|
51
51
|
foo3()
|
|
52
52
|
|
|
53
53
|
|
|
@@ -58,11 +58,11 @@ class PaymentOrchestrator:
|
|
|
58
58
|
self.service = payment_service
|
|
59
59
|
|
|
60
60
|
def create_outer(self):
|
|
61
|
-
self.service.
|
|
61
|
+
self.service.foo2()
|
|
62
62
|
self.update_outer()
|
|
63
63
|
|
|
64
64
|
def update_outer(self):
|
|
65
|
-
self.service.
|
|
65
|
+
self.service.foo3()
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
@app.post("/payment")
|
|
@@ -78,5 +78,5 @@ def test_class_is_detected():
|
|
|
78
78
|
home_responses = openapi_json["paths"]["/payment"]["post"]["responses"]
|
|
79
79
|
print(home_responses)
|
|
80
80
|
assert "408" in home_responses
|
|
81
|
-
assert "407" in home_responses
|
|
81
|
+
assert "407" not in home_responses
|
|
82
82
|
assert "409" in home_responses
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.1"
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|