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,6 +15,32 @@ from hapic.processor import RequestParameters
15 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 44
 class ControllerWrapper(object):
19 45
     def before_wrapped_func(
20 46
         self,
@@ -187,17 +213,17 @@ class OutputControllerWrapper(InputOutputControllerWrapper):
187 213
 class DecoratedController(object):
188 214
     def __init__(
189 215
         self,
190
-        token: str,
216
+        reference: ControllerReference,
191 217
         description: ControllerDescription,
192 218
         name: str='',
193 219
     ) -> None:
194
-        self._token = token
220
+        self._reference = reference
195 221
         self._description = description
196 222
         self._name = name
197 223
 
198 224
     @property
199
-    def token(self) -> str:
200
-        return self._token
225
+    def reference(self) -> ControllerReference:
226
+        return self._reference
201 227
 
202 228
     @property
203 229
     def description(self) -> ControllerDescription:

+ 13 - 3
hapic/doc.py View File

@@ -16,17 +16,27 @@ from hapic.description import ControllerDescription
16 16
 BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
17 17
 
18 18
 
19
-def bottle_route_for_view(
19
+def find_bottle_route(
20 20
     decorated_controller: DecoratedController,
21 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 28
     for route in app.routes:
24 29
         route_token = getattr(
25 30
             route.callback,
26 31
             DECORATION_ATTRIBUTE_NAME,
27 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 40
             return route
31 41
     # TODO BS 20171010: specialize exception
32 42
     # TODO BS 20171010: Raise exception or print error ?
@@ -153,7 +163,7 @@ class DocGenerator(object):
153 163
         # with app.test_request_context():
154 164
         paths = {}
155 165
         for controller in controllers:
156
-            bottle_route = bottle_route_for_view(controller, app)
166
+            bottle_route = find_bottle_route(controller, app)
157 167
             swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
158 168
 
159 169
             operations = bottle_generate_operations(

+ 20 - 3
hapic/hapic.py View File

@@ -8,7 +8,9 @@ import marshmallow
8 8
 
9 9
 from hapic.buffer import DecorationBuffer
10 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 14
 from hapic.decorator import ExceptionHandlerControllerWrapper
13 15
 from hapic.decorator import InputBodyControllerWrapper
14 16
 from hapic.decorator import InputHeadersControllerWrapper
@@ -50,7 +52,7 @@ class Hapic(object):
50 52
     def __init__(self):
51 53
         self._buffer = DecorationBuffer()
52 54
         self._controllers = []  # type: typing.List[DecoratedController]
53
-        self._context = None
55
+        self._context = None  # type: ContextInterface
54 56
 
55 57
         # This local function will be pass to different components
56 58
         # who will need context but declared (like with decorator)
@@ -62,6 +64,14 @@ class Hapic(object):
62 64
 
63 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 75
     def with_api_doc(self):
66 76
         def decorator(func):
67 77
 
@@ -72,10 +82,17 @@ class Hapic(object):
72 82
 
73 83
             token = uuid.uuid4().hex
74 84
             setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
85
+            setattr(func, DECORATION_ATTRIBUTE_NAME, token)
86
+
75 87
             description = self._buffer.get_description()
76 88
 
77
-            decorated_controller = DecoratedController(
89
+            reference = ControllerReference(
90
+                wrapper=wrapper,
91
+                wrapped=func,
78 92
                 token=token,
93
+            )
94
+            decorated_controller = DecoratedController(
95
+                reference=reference,
79 96
                 description=description,
80 97
                 name=func.__name__,
81 98
             )

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

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

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

@@ -0,0 +1 @@
1
+# -*- coding: utf-8 -*-

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

@@ -0,0 +1,77 @@
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

@@ -0,0 +1,58 @@
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