Repository: ramonhagenaars/barentsz

Issue:
Feature request: filtering out abstract classes
While developing a certain machine learning game, I "discovered" ;) that it would be very useful to have the option to automatically filter out abstract classes when using `discover_classes`. I would imagine a parameter `exclude_abstract` with default value `False`.

I might just write a PR for this myself :)

Discussion:

Patch:
diff --git a/barentsz/_discover.py b/barentsz/_discover.py
index c3e915e..4671539 100644
--- a/barentsz/_discover.py
+++ b/barentsz/_discover.py
@@ -25,6 +25,7 @@ from typish import (
 )
 
 from barentsz._attribute import Attribute
+from barentsz._typings import exclude_pred
 from barentsz._here import here
 
 
@@ -165,7 +166,7 @@ def discover_classes(
         include_privates: bool = False,
         in_private_modules: bool = False,
         raise_on_fail: bool = False,
-        exclude: Union[Iterable[type], type] = None
+        exclude: Union[type, exclude_pred, Iterable[Union[type, exclude_pred]]] = None
 ) -> List[type]:
     """
     Discover any classes within the given source and according to the given
@@ -178,8 +179,8 @@ def discover_classes(
         in_private_modules: if True, private modules are explored as well.
         raise_on_fail: if True, raises an ImportError upon the first import
         failure.
-        exclude: a type or multiple types that are to be excluded from the
-        result.
+        exclude: one or more types or predicates that are to be excluded
+        from the result.
 
     Returns: a list of all discovered classes (types).
 
@@ -190,6 +191,9 @@ def discover_classes(
     result = list({cls for cls in elements
                    if (signature is Any or subclass_of(cls, signature))
                    and cls not in exclude_})
+    exclude_predicates = [e for e in exclude_ if inspect.isfunction(e)]
+    for pred in exclude_predicates:
+        result = [cls for cls in result if not pred(cls)]
     result.sort(key=lambda cls: cls.__name__)
     return result
 
diff --git a/barentsz/_typings.py b/barentsz/_typings.py
new file mode 100644
index 0000000..454d94b
--- /dev/null
+++ b/barentsz/_typings.py
@@ -0,0 +1,3 @@
+from typing import Callable
+
+exclude_pred = Callable[[type], bool]


Test patch:
diff --git a/tests/test_discover_classes.py b/tests/test_discover_classes.py
index c2fc32a..86bf9bc 100644
--- a/tests/test_discover_classes.py
+++ b/tests/test_discover_classes.py
@@ -80,7 +80,7 @@ class TestDiscoverClasses(TestCase):
         with self.assertRaises(ValueError):
             discover_classes(123)
 
-    def test_discover_classes_with_exclusions(self):
+    def test_discover_classes_with_type_exclusions(self):
         # SETUP
         path_to_resources = (Path(__file__).parent.parent / 'test_resources'
                              / 'examples_for_tests')
@@ -94,3 +94,19 @@ class TestDiscoverClasses(TestCase):
         self.assertIn(Class1_level2, classes1)
         self.assertEqual(1, len(classes2))
         self.assertIn(Class1_level2, classes2)
+
+    def test_discover_classes_with_predicate_exclusions(self):
+        # SETUP
+        path_to_resources = (Path(__file__).parent.parent / 'test_resources'
+                             / 'examples_for_tests')
+
+        def _name_is_class1(cls: type) -> bool:
+            return cls.__name__.lower() == 'class1'
+
+        # EXECUTE
+        classes1 = discover_classes(path_to_resources, exclude=_name_is_class1)
+        classes2 = discover_classes(path_to_resources, exclude=[_name_is_class1])
+
+        # VERIFY
+        self.assertEqual(0, len(classes1))
+        self.assertEqual(0, len(classes2))