Repository: chaostoolkit/chaostoolkit-lib

Issue:
Do not fail on discovery of module which don't export __all__
Right now the discovery mechanism expects module to have a `__all__` attribute. Do not fail when it is missing.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0af4aa2..c9591c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@
 
 [Unreleased]: https://github.com/chaostoolkit/chaostoolkit-lib/compare/0.13.1...HEAD
 
+### Changed
+
+-   Do not fail the discovery when an extension is missing the `__all__`
+    attribute [#28][28]
+
+[28]: https://github.com/chaostoolkit/chaostoolkit-lib/issues/28
+
 ## [0.13.1][] - 2018-01-30
 
 [0.13.1]: https://github.com/chaostoolkit/chaostoolkit-lib/compare/0.13.0...0.13.1
diff --git a/chaoslib/discovery/discover.py b/chaoslib/discovery/discover.py
index 73fe118..20ad862 100644
--- a/chaoslib/discovery/discover.py
+++ b/chaoslib/discovery/discover.py
@@ -10,6 +10,7 @@ from logzero import logger
 from chaoslib import __version__
 from chaoslib.discovery.package import get_discover_function, install,\
     load_package
+from chaoslib.exceptions import DiscoveryFailed
 from chaoslib.types import Discovery, DiscoveredActivities
 
 
@@ -88,11 +89,18 @@ def discover_activities(extension_mod_name: str,
         mod = importlib.import_module(extension_mod_name)
     except ImportError:
         raise DiscoveryFailed(
-            "could not import Python module '{m}'".format(
+            "could not import extension module '{m}'".format(
                 m=extension_mod_name))
 
     activities = []
-    exported = getattr(mod, "__all__")
+    try:
+        exported = getattr(mod, "__all__")
+    except AttributeError as e:
+        logger.warn("'{m}' does not expose the __all__ attribute. "
+                    "It is required to determine what functions are actually "
+                    "exported as activities.".format(m=extension_mod_name))
+        return activities
+
     funcs = inspect.getmembers(mod, inspect.isfunction)
     for (name, func) in funcs:
         if exported and name not in exported:


Test patch:
diff --git a/tests/fixtures/fakeext.py b/tests/fixtures/fakeext.py
new file mode 100644
index 0000000..ed72c69
--- /dev/null
+++ b/tests/fixtures/fakeext.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+__all__ = ["many_args", "no_args_docstring", "no_args", "one_arg",
+           "one_untyped_arg", "one_arg_with_default",
+           "one_untyped_arg_with_default"]
+
+
+def no_args_docstring():
+    pass
+
+
+def no_args():
+    """
+    No arguments.
+    """
+    pass
+
+
+def one_arg(message: str):
+    """
+    One typed argument.
+    """
+    pass
+
+
+def one_arg_with_default(message: str="hello"):
+    """
+    One typed argument with a default value.
+    """
+    pass
+
+
+def one_untyped_arg(message):
+    """
+    One untyped argument.
+    """
+    pass
+
+
+def one_untyped_arg_with_default(message = "hello"):
+    """
+    One untyped argument with a default value.
+    """
+    pass
+
+
+def many_args(message: str, colour: str="blue"):
+    """
+    Many arguments.
+    """
+    pass
diff --git a/tests/fixtures/keepempty.py b/tests/fixtures/keepempty.py
new file mode 100644
index 0000000..db5dc66
--- /dev/null
+++ b/tests/fixtures/keepempty.py
@@ -0,0 +1,5 @@
+# just keep this as-is
+
+
+def not_an_activity():
+    print("boom")
diff --git a/tests/test_discover.py b/tests/test_discover.py
index 216d379..13bf346 100644
--- a/tests/test_discover.py
+++ b/tests/test_discover.py
@@ -5,5 +5,98 @@ import pytest
 
 from chaoslib.exceptions import DiscoveryFailed
 from chaoslib.discovery import discover, initialize_discovery_result
+from chaoslib.discovery.discover import discover_activities
 from chaoslib.types import Discovery, DiscoveredActivities, \
     DiscoveredSystemInfo
+
+
+def test_fail_discovery_when_module_cannot_be_loaded():
+    with pytest.raises(DiscoveryFailed) as exc:
+        discover_activities("fixtures.burp", "probe")
+    assert "could not import extension module" in str(exc)
+
+
+def test_do_not_fail_when_extension_mod_has_not_all():
+    activities = discover_activities("fixtures.keepempty", "probe")
+    assert len(activities) == 0
+
+
+def test_discover_all_activities():
+    mod = "fixtures.fakeext"
+    activities = discover_activities(mod, "probe")
+    assert len(activities) == 7
+
+    activity = activities[0]
+    assert activity["name"] == "many_args"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "Many arguments."
+    assert activity["arguments"] == [
+        {
+            "name": "message"
+        },
+        {
+            "name": "colour",
+            "default": "blue"
+        }
+    ]
+
+    activity = activities[1]
+    assert activity["name"] == "no_args"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "No arguments."
+    assert activity["arguments"] == []
+
+    activity = activities[2]
+    assert activity["name"] == "no_args_docstring"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == None
+    assert activity["arguments"] == []
+
+    activity = activities[3]
+    assert activity["name"] == "one_arg"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "One typed argument."
+    assert activity["arguments"] == [
+        {
+            "name": "message"
+        }
+    ]
+
+    activity = activities[4]
+    assert activity["name"] == "one_arg_with_default"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "One typed argument with a default value."
+    assert activity["arguments"] == [
+        {
+            "name": "message",
+            "default": "hello"
+        }
+    ]
+
+    activity = activities[5]
+    assert activity["name"] == "one_untyped_arg"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "One untyped argument."
+    assert activity["arguments"] == [
+        {
+            "name": "message"
+        }
+    ]
+
+    activity = activities[6]
+    assert activity["name"] == "one_untyped_arg_with_default"
+    assert activity["type"] == "probe"
+    assert activity["mod"] == mod
+    assert activity["doc"] == "One untyped argument with a default value."
+    assert activity["arguments"] == [
+        {
+            "name": "message",
+            "default": "hello"
+        }
+    ]