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.
Files changed (43) hide show
  1. {richapi-0.3.1 → richapi-0.3.2}/PKG-INFO +1 -1
  2. {richapi-0.3.1 → richapi-0.3.2}/pyproject.toml +2 -1
  3. richapi-0.3.2/richapi/__version__.py +1 -0
  4. {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/compiler.py +68 -52
  5. {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/openapi.py +2 -1
  6. {richapi-0.3.1 → richapi-0.3.2}/tests/test_instance_attr_call.py +5 -5
  7. richapi-0.3.1/richapi/__version__.py +0 -1
  8. {richapi-0.3.1 → richapi-0.3.2}/.envrc +0 -0
  9. {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/deploy-docs-on-demand.yml +0 -0
  10. {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/main.yml +0 -0
  11. {richapi-0.3.1 → richapi-0.3.2}/.github/workflows/on-release-main.yml +0 -0
  12. {richapi-0.3.1 → richapi-0.3.2}/.gitignore +0 -0
  13. {richapi-0.3.1 → richapi-0.3.2}/README.md +0 -0
  14. {richapi-0.3.1 → richapi-0.3.2}/deploy_docs.py +0 -0
  15. {richapi-0.3.1 → richapi-0.3.2}/docs/advance.md +0 -0
  16. {richapi-0.3.1 → richapi-0.3.2}/docs/conventions.md +0 -0
  17. {richapi-0.3.1 → richapi-0.3.2}/docs/images/custom-exception.png +0 -0
  18. {richapi-0.3.1 → richapi-0.3.2}/docs/images/with-richapi.png +0 -0
  19. {richapi-0.3.1 → richapi-0.3.2}/docs/images/without-richapi.png +0 -0
  20. {richapi-0.3.1 → richapi-0.3.2}/docs/implementation.md +0 -0
  21. {richapi-0.3.1 → richapi-0.3.2}/docs/index.md +0 -0
  22. {richapi-0.3.1 → richapi-0.3.2}/makefile +0 -0
  23. {richapi-0.3.1 → richapi-0.3.2}/mkdocs.yml +0 -0
  24. {richapi-0.3.1 → richapi-0.3.2}/richapi/__init__.py +0 -0
  25. {richapi-0.3.1 → richapi-0.3.2}/richapi/cli.py +0 -0
  26. {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/__init__.py +0 -0
  27. {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/handler.py +0 -0
  28. {richapi-0.3.1 → richapi-0.3.2}/richapi/exc_parser/protocol.py +0 -0
  29. {richapi-0.3.1 → richapi-0.3.2}/richapi/exceptions.py +0 -0
  30. {richapi-0.3.1 → richapi-0.3.2}/tests/__init__.py +0 -0
  31. {richapi-0.3.1 → richapi-0.3.2}/tests/app/__init__.py +0 -0
  32. {richapi-0.3.1 → richapi-0.3.2}/tests/app/foo1.py +0 -0
  33. {richapi-0.3.1 → richapi-0.3.2}/tests/test_builder_pattern.py +0 -0
  34. {richapi-0.3.1 → richapi-0.3.2}/tests/test_class.py +0 -0
  35. {richapi-0.3.1 → richapi-0.3.2}/tests/test_compiler.py +0 -0
  36. {richapi-0.3.1 → richapi-0.3.2}/tests/test_custom_exc.py +0 -0
  37. {richapi-0.3.1 → richapi-0.3.2}/tests/test_duplicated_function.py +0 -0
  38. {richapi-0.3.1 → richapi-0.3.2}/tests/test_exc_raising_cases.py +0 -0
  39. {richapi-0.3.1 → richapi-0.3.2}/tests/test_exception_edge_cases.py +0 -0
  40. {richapi-0.3.1 → richapi-0.3.2}/tests/test_fastapi_auth_functions.py +0 -0
  41. {richapi-0.3.1 → richapi-0.3.2}/tests/test_import_from_other_module.py +0 -0
  42. {richapi-0.3.1 → richapi-0.3.2}/tox.ini +0 -0
  43. {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.1
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.1"
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
- return _find_explicit_expection_recursively(func, should_search_module_pred)
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
- tree = tree or ast.parse(inspect.getsource(cls))
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.exception(f"Failed to parse source code for {cls_name}.", exc_info=error)
130
+ logger.info(
131
+ f"Failed to parse source code for {cls_name}.__init__", exc_info=error
132
+ )
102
133
  return []
103
134
 
104
- result = _find_explicit_expection_recursively(
105
- cls.__init__, to_filter_predicate, tree
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.__call__, # type: ignore
113
- to_filter_predicate,
114
- tree,
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: Optional[ast.AST] = None,
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
- if parent_part in func.__annotations__:
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.info(f"Analyzing function {func.__name__}")
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 = _find_explicit_expection_recursively(
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.critical(
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 create_inner(self):
47
+ def foo2(self):
48
48
  foo2()
49
49
 
50
- def update_inner(self):
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.create_inner()
61
+ self.service.foo2()
62
62
  self.update_outer()
63
63
 
64
64
  def update_outer(self):
65
- self.service.update_inner()
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