Browse Source

Enhance controller references strategy and write tests

Bastien Sevajol 6 years ago
parent
commit
268c659b30
7 changed files with 200 additions and 10 deletions
  1. 30 4
      hapic/decorator.py
  2. 13 3
      hapic/doc.py
  3. 20 3
      hapic/hapic.py
  4. 1 0
      tests/ext/__init__.py
  5. 1 0
      tests/ext/unit/__init__.py
  6. 77 0
      tests/ext/unit/test_bottle.py
  7. 58 0
      tests/unit/test_hapic.py

+ 30 - 4
hapic/decorator.py View File

15
 DECORATION_ATTRIBUTE_NAME = '_hapic_decoration_token'
15
 DECORATION_ATTRIBUTE_NAME = '_hapic_decoration_token'
16
 
16
 
17
 
17
 
18
+class ControllerReference(object):
19
+    def __init__(
20
+        self,
21
+        wrapper: typing.Callable[..., typing.Any],
22
+        wrapped: typing.Callable[..., typing.Any],
23
+        token: str,
24
+    ) -> None:
25
+        """
26
+        This class is a centralization of different ways to match
27
+        final controller with decorated function:
28
+          - wrapper will match if final controller is the hapic returned
29
+            wrapper
30
+          - wrapped will match if final controller is the controller itself
31
+          - token will match if only apposed token still exist: This case
32
+            happen when hapic decoration is make on class function and final
33
+            controller is the same function but as instance function.
34
+
35
+        :param wrapper: Wrapper returned by decorator
36
+        :param wrapped: Function wrapped by decorator
37
+        :param token: String token set on these both functions
38
+        """
39
+        self.wrapper = wrapper
40
+        self.wrapped = wrapped
41
+        self.token = token
42
+
43
+
18
 class ControllerWrapper(object):
44
 class ControllerWrapper(object):
19
     def before_wrapped_func(
45
     def before_wrapped_func(
20
         self,
46
         self,
187
 class DecoratedController(object):
213
 class DecoratedController(object):
188
     def __init__(
214
     def __init__(
189
         self,
215
         self,
190
-        token: str,
216
+        reference: ControllerReference,
191
         description: ControllerDescription,
217
         description: ControllerDescription,
192
         name: str='',
218
         name: str='',
193
     ) -> None:
219
     ) -> None:
194
-        self._token = token
220
+        self._reference = reference
195
         self._description = description
221
         self._description = description
196
         self._name = name
222
         self._name = name
197
 
223
 
198
     @property
224
     @property
199
-    def token(self) -> str:
200
-        return self._token
225
+    def reference(self) -> ControllerReference:
226
+        return self._reference
201
 
227
 
202
     @property
228
     @property
203
     def description(self) -> ControllerDescription:
229
     def description(self) -> ControllerDescription:

+ 13 - 3
hapic/doc.py View File

16
 BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
16
 BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
17
 
17
 
18
 
18
 
19
-def bottle_route_for_view(
19
+def find_bottle_route(
20
     decorated_controller: DecoratedController,
20
     decorated_controller: DecoratedController,
21
     app: bottle.Bottle,
21
     app: bottle.Bottle,
22
 ):
22
 ):
23
+    if not app.routes:
24
+        # TODO BS 20171010: specialize exception
25
+        raise Exception('There is no routes in yout bottle app')
26
+
27
+    reference = decorated_controller.reference
23
     for route in app.routes:
28
     for route in app.routes:
24
         route_token = getattr(
29
         route_token = getattr(
25
             route.callback,
30
             route.callback,
26
             DECORATION_ATTRIBUTE_NAME,
31
             DECORATION_ATTRIBUTE_NAME,
27
             None,
32
             None,
28
         )
33
         )
29
-        if route_token == decorated_controller.token:
34
+
35
+        match_with_wrapper = route.callback == reference.wrapper
36
+        match_with_wrapped = route.callback == reference.wrapped
37
+        match_with_token = route_token == reference.token
38
+
39
+        if match_with_wrapper or match_with_wrapped or match_with_token:
30
             return route
40
             return route
31
     # TODO BS 20171010: specialize exception
41
     # TODO BS 20171010: specialize exception
32
     # TODO BS 20171010: Raise exception or print error ?
42
     # TODO BS 20171010: Raise exception or print error ?
153
         # with app.test_request_context():
163
         # with app.test_request_context():
154
         paths = {}
164
         paths = {}
155
         for controller in controllers:
165
         for controller in controllers:
156
-            bottle_route = bottle_route_for_view(controller, app)
166
+            bottle_route = find_bottle_route(controller, app)
157
             swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
167
             swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
158
 
168
 
159
             operations = bottle_generate_operations(
169
             operations = bottle_generate_operations(

+ 20 - 3
hapic/hapic.py View File

8
 
8
 
9
 from hapic.buffer import DecorationBuffer
9
 from hapic.buffer import DecorationBuffer
10
 from hapic.context import ContextInterface
10
 from hapic.context import ContextInterface
11
-from hapic.decorator import DecoratedController, DECORATION_ATTRIBUTE_NAME
11
+from hapic.decorator import DecoratedController
12
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
13
+from hapic.decorator import ControllerReference
12
 from hapic.decorator import ExceptionHandlerControllerWrapper
14
 from hapic.decorator import ExceptionHandlerControllerWrapper
13
 from hapic.decorator import InputBodyControllerWrapper
15
 from hapic.decorator import InputBodyControllerWrapper
14
 from hapic.decorator import InputHeadersControllerWrapper
16
 from hapic.decorator import InputHeadersControllerWrapper
50
     def __init__(self):
52
     def __init__(self):
51
         self._buffer = DecorationBuffer()
53
         self._buffer = DecorationBuffer()
52
         self._controllers = []  # type: typing.List[DecoratedController]
54
         self._controllers = []  # type: typing.List[DecoratedController]
53
-        self._context = None
55
+        self._context = None  # type: ContextInterface
54
 
56
 
55
         # This local function will be pass to different components
57
         # This local function will be pass to different components
56
         # who will need context but declared (like with decorator)
58
         # who will need context but declared (like with decorator)
62
 
64
 
63
         # TODO: Permettre la surcharge des classes utilisés ci-dessous
65
         # TODO: Permettre la surcharge des classes utilisés ci-dessous
64
 
66
 
67
+    @property
68
+    def controllers(self) -> typing.List[DecoratedController]:
69
+        return self._controllers
70
+
71
+    @property
72
+    def context(self) -> ContextInterface:
73
+        return self._context
74
+
65
     def with_api_doc(self):
75
     def with_api_doc(self):
66
         def decorator(func):
76
         def decorator(func):
67
 
77
 
72
 
82
 
73
             token = uuid.uuid4().hex
83
             token = uuid.uuid4().hex
74
             setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
84
             setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
85
+            setattr(func, DECORATION_ATTRIBUTE_NAME, token)
86
+
75
             description = self._buffer.get_description()
87
             description = self._buffer.get_description()
76
 
88
 
77
-            decorated_controller = DecoratedController(
89
+            reference = ControllerReference(
90
+                wrapper=wrapper,
91
+                wrapped=func,
78
                 token=token,
92
                 token=token,
93
+            )
94
+            decorated_controller = DecoratedController(
95
+                reference=reference,
79
                 description=description,
96
                 description=description,
80
                 name=func.__name__,
97
                 name=func.__name__,
81
             )
98
             )

+ 1 - 0
tests/ext/__init__.py View File

1
+# -*- coding: utf-8 -*-

+ 1 - 0
tests/ext/unit/__init__.py View File

1
+# -*- coding: utf-8 -*-

+ 77 - 0
tests/ext/unit/test_bottle.py View File

1
+# -*- coding: utf-8 -*-
2
+import bottle
3
+
4
+import hapic
5
+from hapic.doc import find_bottle_route
6
+from tests.base import Base
7
+
8
+
9
+class TestBottleExt(Base):
10
+    def test_unit__map_binding__ok__decorated_function(self):
11
+        hapic_ = hapic.Hapic()
12
+        hapic_.set_context(hapic.ext.bottle.bottle_context)
13
+
14
+        app = bottle.Bottle()
15
+
16
+        @hapic_.with_api_doc()
17
+        @app.route('/')
18
+        def controller_a():
19
+            pass
20
+
21
+        assert hapic_.controllers
22
+        decoration = hapic_.controllers[0]
23
+        route = find_bottle_route(decoration, app)
24
+
25
+        assert route
26
+        assert route.callback != controller_a
27
+        assert route.callback == decoration.reference.wrapped
28
+        assert route.callback != decoration.reference.wrapper
29
+
30
+    def test_unit__map_binding__ok__mapped_function(self):
31
+        hapic_ = hapic.Hapic()
32
+        hapic_.set_context(hapic.ext.bottle.bottle_context)
33
+
34
+        app = bottle.Bottle()
35
+
36
+        @hapic_.with_api_doc()
37
+        def controller_a():
38
+            pass
39
+
40
+        app.route('/', callback=controller_a)
41
+
42
+        assert hapic_.controllers
43
+        decoration = hapic_.controllers[0]
44
+        route = find_bottle_route(decoration, app)
45
+
46
+        assert route
47
+        assert route.callback == controller_a
48
+        assert route.callback == decoration.reference.wrapper
49
+        assert route.callback != decoration.reference.wrapped
50
+
51
+    def test_unit__map_binding__ok__mapped_method(self):
52
+        hapic_ = hapic.Hapic()
53
+        hapic_.set_context(hapic.ext.bottle.bottle_context)
54
+
55
+        app = bottle.Bottle()
56
+
57
+        class MyControllers(object):
58
+            def bind(self, app):
59
+                app.route('/', callback=self.controller_a)
60
+
61
+            @hapic_.with_api_doc()
62
+            def controller_a(self):
63
+                pass
64
+
65
+        my_controllers = MyControllers()
66
+        my_controllers.bind(app)
67
+
68
+        assert hapic_.controllers
69
+        decoration = hapic_.controllers[0]
70
+        route = find_bottle_route(decoration, app)
71
+
72
+        assert route
73
+        # Important note: instance controller_a method is
74
+        # not class controller_a, so no matches with callbacks
75
+        assert route.callback != MyControllers.controller_a
76
+        assert route.callback != decoration.reference.wrapped
77
+        assert route.callback != decoration.reference.wrapper

+ 58 - 0
tests/unit/test_hapic.py View File

1
+# -*- coding: utf-8 -*-
2
+from hapic import Hapic
3
+from hapic.decorator import DECORATION_ATTRIBUTE_NAME
4
+from tests.base import Base
5
+
6
+
7
+class TestHapic(Base):
8
+    def test_unit__decoration__ok__simple_function(self):
9
+        hapic = Hapic()
10
+
11
+        @hapic.with_api_doc()
12
+        def controller_a():
13
+            pass
14
+
15
+        token = getattr(controller_a, DECORATION_ATTRIBUTE_NAME, None)
16
+        assert token
17
+
18
+        assert hapic.controllers
19
+        assert 1 == len(hapic.controllers)
20
+        reference = hapic.controllers[0].reference
21
+
22
+        assert token == reference.token
23
+        assert controller_a == reference.wrapper
24
+        assert controller_a != reference.wrapped
25
+
26
+    def test_unit__decoration__ok__method(self):
27
+        hapic = Hapic()
28
+
29
+        class MyControllers(object):
30
+            @hapic.with_api_doc()
31
+            def controller_a(self):
32
+                pass
33
+
34
+        my_controllers = MyControllers()
35
+        class_method_token = getattr(
36
+            MyControllers.controller_a,
37
+            DECORATION_ATTRIBUTE_NAME,
38
+            None,
39
+        )
40
+        assert class_method_token
41
+        instance_method_token = getattr(
42
+            my_controllers.controller_a,
43
+            DECORATION_ATTRIBUTE_NAME,
44
+            None,
45
+        )
46
+        assert instance_method_token
47
+
48
+        assert hapic.controllers
49
+        assert 1 == len(hapic.controllers)
50
+        reference = hapic.controllers[0].reference
51
+
52
+        assert class_method_token == reference.token
53
+        assert instance_method_token == reference.token
54
+
55
+        assert MyControllers.controller_a == reference.wrapper
56
+        assert MyControllers.controller_a != reference.wrapped
57
+        assert my_controllers.controller_a != reference.wrapper
58
+        assert my_controllers.controller_a != reference.wrapped