Repository: mercadona/rele
Pull request #168 (2020-06-15 16:22:49)

Load class based subscribers
### :tophat: What?

Support class based subscribers

### :thinking: Why?

We have some services using class based subscriptions, as it removes a lot of boilerplate. 

### :link: Related issue

#161 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c4bbe6..2294397 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 Changelog
 =========
 
+`0.13.dev0` (2020-6-16)
+* Traverse all packages to autodiscover all subs.py modules (#167)
+* Auto-discovery of class based subscriptions (#168)
+
 `0.12.0` (2020-6-12)
 * Added `--settings` path option in CLI (#166)
 * Added isort linting (#164)
diff --git a/rele/__init__.py b/rele/__init__.py
index 924b82c..90e304e 100644
--- a/rele/__init__.py
+++ b/rele/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "0.12.0"
+__version__ = "0.13.dev0"
 default_app_config = "rele.apps.ReleConfig"
 
 from .client import Publisher, Subscriber  # noqa
diff --git a/rele/config.py b/rele/config.py
index 171bac2..9f7a1be 100644
--- a/rele/config.py
+++ b/rele/config.py
@@ -56,6 +56,20 @@ def setup(setting=None, **kwargs):
     return config
 
 
+def subscription_from_attribute(attribute):
+    try:
+        if isinstance(attribute, Subscription):
+            subscription = attribute
+        elif issubclass(attribute, Subscription):
+            subscription = attribute()
+        else:
+            return None
+    except TypeError:
+        # If attribute is not a class, TypeError is raised when testing issubclass
+        return None
+    return subscription
+
+
 def load_subscriptions_from_paths(sub_module_paths, sub_prefix=None, filter_by=None):
 
     subscriptions = []
@@ -63,12 +77,15 @@ def load_subscriptions_from_paths(sub_module_paths, sub_prefix=None, filter_by=N
         sub_module = importlib.import_module(sub_module_path)
         for attr_name in dir(sub_module):
             attribute = getattr(sub_module, attr_name)
-            if isinstance(attribute, Subscription):
-                if sub_prefix and not attribute.prefix:
-                    attribute.set_prefix(sub_prefix)
 
-                if filter_by and not attribute.filter_by:
-                    attribute.set_filters(filter_by)
+            subscription = subscription_from_attribute(attribute)
+            if not subscription:
+                continue
+            if sub_prefix and not subscription.prefix:
+                subscription.set_prefix(sub_prefix)
+
+            if filter_by and not subscription.filter_by:
+                subscription.set_filters(filter_by)
 
-                subscriptions.append(attribute)
+            subscriptions.append(subscription)
     return subscriptions


Test patch:
diff --git a/tests/subs.py b/tests/subs.py
index 5df0388..43cd922 100644
--- a/tests/subs.py
+++ b/tests/subs.py
@@ -1,6 +1,25 @@
-from rele import sub
+from rele import Subscription, sub
 
 
 @sub(topic="some-cool-topic", prefix="rele")
 def sub_stub(data, **kwargs):
     return data["id"]
+
+
+class ClassBasedSub(Subscription):
+    topic = "alternative-cool-topic"
+
+    def __init__(self):
+        self._func = self.callback_func
+        super().__init__(self._func, self.topic)
+
+    def callback_func(self, data, **kwargs):
+        return data["id"]
+
+
+class CustomException(Exception):
+    pass
+
+
+def some_other_type():
+    print("Im a function")
diff --git a/tests/test_config.py b/tests/test_config.py
index 7bcc376..942563c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,13 +4,13 @@ from unittest.mock import patch
 
 import pytest
 
-from rele import sub
+from rele import Subscription, sub
 from rele.config import Config, load_subscriptions_from_paths
 
 
 @sub(topic="test-topic", prefix="rele")
-def sub_stub(data, **_kwargs):
-    return data
+def sub_stub(data, **kwargs):
+    return data["id"]
 
 
 class TestLoadSubscriptions:
@@ -19,19 +19,36 @@ class TestLoadSubscriptions:
         return load_subscriptions_from_paths(
             ["tests.test_config"],
             sub_prefix="test",
-            filter_by=lambda attrs: attrs.get("lang") == "en",
+            filter_by=[lambda args: args.get("lang") == "en"],
         )
 
     def test_load_subscriptions_in_a_module(self, subscriptions):
         assert len(subscriptions) == 1
+        func_sub = subscriptions[0]
+        assert isinstance(func_sub, Subscription)
+        assert func_sub.name == "rele-test-topic"
+        assert func_sub({"id": 4}, lang="en") == 4
+
+    def test_loads_subscriptions_when_they_are_class_based(self):
+        subscriptions = load_subscriptions_from_paths(
+            ["tests.subs"],
+            sub_prefix="test",
+            filter_by=[lambda attrs: attrs.get("lang") == "en"],
+        )
+
+        assert len(subscriptions) == 2
+        klass_sub = subscriptions[0]
+        assert isinstance(klass_sub, Subscription)
+        assert klass_sub.name == "test-alternative-cool-topic"
+        assert klass_sub({"id": 4}, lang="en") == 4
 
-    def test_filter_by_applied_to_subscription_returns_true(self, subscriptions):
+    def test_returns_sub_value_when_filtered_value_applied(self, subscriptions):
 
-        assert subscriptions[-1].filter_by({"lang": "en"}) is True
+        assert subscriptions[-1]({"id": 4}, lang="en") == 4
 
-    def test_filter_by_applied_to_subscription_returns_false(self, subscriptions):
+    def test_returns_none_when_filtered_value_does_not_apply(self, subscriptions):
 
-        assert subscriptions[0].filter_by({"lang": "es"}) is False
+        assert subscriptions[0]({"id": 4}, lang="es") is None
 
 
 class TestConfig: