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

+ 1 - 0
hapic/__init__.py View File

13
 output_body = _hapic_default.output_body
13
 output_body = _hapic_default.output_body
14
 # with_api_doc_bis = _hapic_default.with_api_doc_bis
14
 # with_api_doc_bis = _hapic_default.with_api_doc_bis
15
 generate_doc = _hapic_default.generate_doc
15
 generate_doc = _hapic_default.generate_doc
16
+handle_exception = _hapic_default.handle_exception
16
 
17
 
17
 # from hapic.hapic import with_api_doc
18
 # from hapic.hapic import with_api_doc
18
 # from hapic.hapic import with_api_doc_bis
19
 # from hapic.hapic import with_api_doc_bis

+ 70 - 19
hapic/decorator.py View File

11
 
11
 
12
 
12
 
13
 class ControllerWrapper(object):
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
     def before_wrapped_func(
14
     def before_wrapped_func(
27
         self,
15
         self,
28
         func_args: typing.Tuple[typing.Any, ...],
16
         func_args: typing.Tuple[typing.Any, ...],
44
             if replacement_response:
32
             if replacement_response:
45
                 return replacement_response
33
                 return replacement_response
46
 
34
 
47
-            response = func(*args, **kwargs)
35
+            response = self._execute_wrapped_function(func, args, kwargs)
48
             new_response = self.after_wrapped_function(response)
36
             new_response = self.after_wrapped_function(response)
49
             return new_response
37
             return new_response
50
         return wrapper
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
     def before_wrapped_func(
64
     def before_wrapped_func(
55
         self,
65
         self,
56
         func_args: typing.Tuple[typing.Any, ...],
66
         func_args: typing.Tuple[typing.Any, ...],
122
         return error_response
132
         return error_response
123
 
133
 
124
 
134
 
125
-class OutputControllerWrapper(ControllerWrapper):
135
+class OutputControllerWrapper(InputOutputControllerWrapper):
126
     def __init__(
136
     def __init__(
127
         self,
137
         self,
128
         context: ContextInterface,
138
         context: ContextInterface,
130
         error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
140
         error_http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
131
         default_http_code: HTTPStatus=HTTPStatus.OK,
141
         default_http_code: HTTPStatus=HTTPStatus.OK,
132
     ) -> None:
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
     def get_error_response(
150
     def get_error_response(
139
         self,
151
         self,
271
             request_parameters.form_parameters,
283
             request_parameters.form_parameters,
272
         )
284
         )
273
         return processed_data
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
 from hapic.buffer import DecorationBuffer
11
 from hapic.buffer import DecorationBuffer
12
 from hapic.context import ContextInterface, BottleContext
12
 from hapic.context import ContextInterface, BottleContext
13
 from hapic.decorator import DecoratedController
13
 from hapic.decorator import DecoratedController
14
+from hapic.decorator import ExceptionHandlerControllerWrapper
14
 from hapic.decorator import InputBodyControllerWrapper
15
 from hapic.decorator import InputBodyControllerWrapper
15
 from hapic.decorator import InputHeadersControllerWrapper
16
 from hapic.decorator import InputHeadersControllerWrapper
16
 from hapic.decorator import InputPathControllerWrapper
17
 from hapic.decorator import InputPathControllerWrapper
234
             return decoration.get_wrapper(func)
235
             return decoration.get_wrapper(func)
235
         return decorator
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
     def generate_doc(self, app=None):
257
     def generate_doc(self, app=None):
238
         # TODO @Damien bottle specific code !
258
         # TODO @Damien bottle specific code !
239
         app = app or bottle.default_app()
259
         app = app or bottle.default_app()

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

6
 
6
 
7
 from hapic.context import ContextInterface
7
 from hapic.context import ContextInterface
8
 from hapic.data import HapicData
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
 from hapic.decorator import InputControllerWrapper
11
 from hapic.decorator import InputControllerWrapper
11
 from hapic.decorator import OutputControllerWrapper
12
 from hapic.decorator import OutputControllerWrapper
12
 from hapic.processor import RequestParameters
13
 from hapic.processor import RequestParameters
63
         )
64
         )
64
 
65
 
65
 
66
 
66
-class MyControllerWrapper(ControllerWrapper):
67
+class MyControllerWrapper(InputOutputControllerWrapper):
67
     def before_wrapped_func(
68
     def before_wrapped_func(
68
         self,
69
         self,
69
         func_args: typing.Tuple[typing.Any, ...],
70
         func_args: typing.Tuple[typing.Any, ...],
103
     def test_unit__base_controller_wrapper__ok__no_behaviour(self):
104
     def test_unit__base_controller_wrapper__ok__no_behaviour(self):
104
         context = MyContext()
105
         context = MyContext()
105
         processor = MyProcessor()
106
         processor = MyProcessor()
106
-        wrapper = ControllerWrapper(context, processor)
107
+        wrapper = InputOutputControllerWrapper(context, processor)
107
 
108
 
108
         @wrapper.get_wrapper
109
         @wrapper.get_wrapper
109
         def func(foo):
110
         def func(foo):
198
         assert result['original_error'].error_details == {
199
         assert result['original_error'].error_details == {
199
             'name': ['Missing data for required field.']
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
+        }