Browse Source

error message

Bastien Sevajol 6 years ago
parent
commit
91d7786682
5 changed files with 151 additions and 23 deletions
  1. 6 1
      example_a.py
  2. 1 0
      hapic/__init__.py
  3. 70 19
      hapic/decorator.py
  4. 20 0
      hapic/hapic.py
  5. 54 3
      tests/unit/test_decorator.py

+ 6 - 1
example_a.py View File

@@ -1,4 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2
+from http import HTTPStatus
3
+
2 4
 import bottle
3 5
 import hapic
4 6
 from example import HelloResponseSchema, HelloPathSchema, HelloJsonSchema, \
@@ -16,10 +18,13 @@ def bob(f):
16 18
 
17 19
 @hapic.with_api_doc()
18 20
 # @hapic.ext.bottle.bottle_context()
19
-# @hapic.error_schema(ErrorResponseSchema())
21
+@hapic.handle_exception(ZeroDivisionError, http_code=HTTPStatus.BAD_REQUEST)
20 22
 @hapic.input_path(HelloPathSchema())
21 23
 @hapic.output_body(HelloResponseSchema())
22 24
 def hello(name: str, hapic_data: HapicData):
25
+    if name == 'zero':
26
+        raise ZeroDivisionError('Don\'t call him zero !')
27
+
23 28
     return {
24 29
         'sentence': 'Hello !',
25 30
         'name': name,

+ 1 - 0
hapic/__init__.py View File

@@ -13,6 +13,7 @@ output_headers = _hapic_default.output_headers
13 13
 output_body = _hapic_default.output_body
14 14
 # with_api_doc_bis = _hapic_default.with_api_doc_bis
15 15
 generate_doc = _hapic_default.generate_doc
16
+handle_exception = _hapic_default.handle_exception
16 17
 
17 18
 # from hapic.hapic import with_api_doc
18 19
 # from hapic.hapic import with_api_doc_bis

+ 70 - 19
hapic/decorator.py View File

@@ -11,18 +11,6 @@ from hapic.processor import RequestParameters
11 11
 
12 12
 
13 13
 class ControllerWrapper(object):
14
-    def __init__(
15
-        self,
16
-        context: ContextInterface,
17
-        processor: ProcessorInterface,
18
-        error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
19
-        default_http_code: HTTPStatus=HTTPStatus.OK,
20
-    ) -> None:
21
-        self.context = context
22
-        self.processor = processor
23
-        self.error_http_code = error_http_code
24
-        self.default_http_code = default_http_code
25
-
26 14
     def before_wrapped_func(
27 15
         self,
28 16
         func_args: typing.Tuple[typing.Any, ...],
@@ -44,13 +32,35 @@ class ControllerWrapper(object):
44 32
             if replacement_response:
45 33
                 return replacement_response
46 34
 
47
-            response = func(*args, **kwargs)
35
+            response = self._execute_wrapped_function(func, args, kwargs)
48 36
             new_response = self.after_wrapped_function(response)
49 37
             return new_response
50 38
         return wrapper
51 39
 
40
+    def _execute_wrapped_function(
41
+        self,
42
+        func,
43
+        func_args,
44
+        func_kwargs,
45
+    ) -> typing.Any:
46
+        return func(*func_args, **func_kwargs)
47
+
52 48
 
53
-class InputControllerWrapper(ControllerWrapper):
49
+class InputOutputControllerWrapper(ControllerWrapper):
50
+    def __init__(
51
+        self,
52
+        context: ContextInterface,
53
+        processor: ProcessorInterface,
54
+        error_http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
55
+        default_http_code: HTTPStatus=HTTPStatus.OK,
56
+    ) -> None:
57
+        self.context = context
58
+        self.processor = processor
59
+        self.error_http_code = error_http_code
60
+        self.default_http_code = default_http_code
61
+
62
+
63
+class InputControllerWrapper(InputOutputControllerWrapper):
54 64
     def before_wrapped_func(
55 65
         self,
56 66
         func_args: typing.Tuple[typing.Any, ...],
@@ -122,7 +132,7 @@ class InputControllerWrapper(ControllerWrapper):
122 132
         return error_response
123 133
 
124 134
 
125
-class OutputControllerWrapper(ControllerWrapper):
135
+class OutputControllerWrapper(InputOutputControllerWrapper):
126 136
     def __init__(
127 137
         self,
128 138
         context: ContextInterface,
@@ -130,10 +140,12 @@ class OutputControllerWrapper(ControllerWrapper):
130 140
         error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
131 141
         default_http_code: HTTPStatus=HTTPStatus.OK,
132 142
     ) -> None:
133
-        self.context = context
134
-        self.processor = processor
135
-        self.error_http_code = error_http_code
136
-        self.default_http_code = default_http_code
143
+        super().__init__(
144
+            context,
145
+            processor,
146
+            error_http_code,
147
+            default_http_code,
148
+        )
137 149
 
138 150
     def get_error_response(
139 151
         self,
@@ -271,3 +283,42 @@ class InputFormsControllerWrapper(InputControllerWrapper):
271 283
             request_parameters.form_parameters,
272 284
         )
273 285
         return processed_data
286
+
287
+
288
+class ExceptionHandlerControllerWrapper(ControllerWrapper):
289
+    def __init__(
290
+        self,
291
+        handled_exception_class: typing.Type[Exception],
292
+        context: ContextInterface,
293
+        http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
294
+    ) -> None:
295
+        self.handled_exception_class = handled_exception_class
296
+        self.context = context
297
+        self.http_code = http_code
298
+
299
+    def _execute_wrapped_function(
300
+        self,
301
+        func,
302
+        func_args,
303
+        func_kwargs,
304
+    ) -> typing.Any:
305
+        try:
306
+            return super()._execute_wrapped_function(
307
+                func,
308
+                func_args,
309
+                func_kwargs,
310
+            )
311
+        except self.handled_exception_class as exc:
312
+            # TODO: error_dict configurable name
313
+            # TODO: Who assume error structure ? We have to rethink it
314
+            error_dict = {
315
+                'error_message': str(exc),
316
+            }
317
+            if hasattr(exc, 'error_dict'):
318
+                error_dict.update(exc.error_dict)
319
+
320
+            error_response = self.context.get_response(
321
+                error_dict,
322
+                self.http_code,
323
+            )
324
+            return error_response

+ 20 - 0
hapic/hapic.py View File

@@ -11,6 +11,7 @@ import marshmallow
11 11
 from hapic.buffer import DecorationBuffer
12 12
 from hapic.context import ContextInterface, BottleContext
13 13
 from hapic.decorator import DecoratedController
14
+from hapic.decorator import ExceptionHandlerControllerWrapper
14 15
 from hapic.decorator import InputBodyControllerWrapper
15 16
 from hapic.decorator import InputHeadersControllerWrapper
16 17
 from hapic.decorator import InputPathControllerWrapper
@@ -234,6 +235,25 @@ class Hapic(object):
234 235
             return decoration.get_wrapper(func)
235 236
         return decorator
236 237
 
238
+    def handle_exception(
239
+        self,
240
+        handled_exception_class: typing.Type[Exception],
241
+        http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
242
+        context: ContextInterface = None,
243
+    ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
244
+        context = context or _default_global_context
245
+
246
+        decoration = ExceptionHandlerControllerWrapper(
247
+            handled_exception_class,
248
+            context,
249
+            http_code,
250
+        )
251
+
252
+        def decorator(func):
253
+            self._buffer.input_forms = InputFormsDescription(decoration)
254
+            return decoration.get_wrapper(func)
255
+        return decorator
256
+
237 257
     def generate_doc(self, app=None):
238 258
         # TODO @Damien bottle specific code !
239 259
         app = app or bottle.default_app()

+ 54 - 3
tests/unit/test_decorator.py View File

@@ -6,7 +6,8 @@ import marshmallow
6 6
 
7 7
 from hapic.context import ContextInterface
8 8
 from hapic.data import HapicData
9
-from hapic.decorator import ControllerWrapper
9
+from hapic.decorator import InputOutputControllerWrapper
10
+from hapic.decorator import ExceptionHandlerControllerWrapper
10 11
 from hapic.decorator import InputControllerWrapper
11 12
 from hapic.decorator import OutputControllerWrapper
12 13
 from hapic.processor import RequestParameters
@@ -63,7 +64,7 @@ class MyProcessor(ProcessorInterface):
63 64
         )
64 65
 
65 66
 
66
-class MyControllerWrapper(ControllerWrapper):
67
+class MyControllerWrapper(InputOutputControllerWrapper):
67 68
     def before_wrapped_func(
68 69
         self,
69 70
         func_args: typing.Tuple[typing.Any, ...],
@@ -103,7 +104,7 @@ class TestControllerWrapper(Base):
103 104
     def test_unit__base_controller_wrapper__ok__no_behaviour(self):
104 105
         context = MyContext()
105 106
         processor = MyProcessor()
106
-        wrapper = ControllerWrapper(context, processor)
107
+        wrapper = InputOutputControllerWrapper(context, processor)
107 108
 
108 109
         @wrapper.get_wrapper
109 110
         def func(foo):
@@ -198,3 +199,53 @@ class TestOutputControllerWrapper(Base):
198 199
         assert result['original_error'].error_details == {
199 200
             'name': ['Missing data for required field.']
200 201
         }
202
+
203
+
204
+class TestExceptionHandlerControllerWrapper(Base):
205
+    def test_unit__exception_handled__ok__nominal_case(self):
206
+        context = MyContext()
207
+        wrapper = ExceptionHandlerControllerWrapper(
208
+            ZeroDivisionError,
209
+            context,
210
+            http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
211
+        )
212
+
213
+        @wrapper.get_wrapper
214
+        def func(foo):
215
+            raise ZeroDivisionError('We are testing')
216
+
217
+        response = func(42)
218
+        assert 'http_code' in response
219
+        assert response['http_code'] == HTTPStatus.INTERNAL_SERVER_ERROR
220
+        assert 'original_response' in response
221
+        assert response['original_response'] == {
222
+            'error_message': 'We are testing',
223
+        }
224
+
225
+    def test_unit__exception_handled__ok__exception_error_dict(self):
226
+        class MyException(Exception):
227
+            def __init__(self, *args, **kwargs):
228
+                super().__init__(*args, **kwargs)
229
+                self.error_dict = {}
230
+
231
+        context = MyContext()
232
+        wrapper = ExceptionHandlerControllerWrapper(
233
+            MyException,
234
+            context,
235
+            http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
236
+        )
237
+
238
+        @wrapper.get_wrapper
239
+        def func(foo):
240
+            exc = MyException('We are testing')
241
+            exc.error_dict = {'foo': 'bar'}
242
+            raise exc
243
+
244
+        response = func(42)
245
+        assert 'http_code' in response
246
+        assert response['http_code'] == HTTPStatus.INTERNAL_SERVER_ERROR
247
+        assert 'original_response' in response
248
+        assert response['original_response'] == {
249
+            'error_message': 'We are testing',
250
+            'foo': 'bar',
251
+        }