Repository: ipython/traitlets
Pull request #380 (2017-03-08 15:59:16)

feat(app): iterate Configurable base-classes for non `app.classes` lists
## Context
The methods `App._classes_inc_parents()` &
`App._classes_with_config_traits()` are really useful when traversing
mro of base-classes in user code (e.g. printing config-value categories,
pre-validation checks).  Currently those work directly on the
:attr:`Application.classes` only, and need to copy their code to emulate them.

## Solution
This change adds an optional `classes` argument to those 2 methods that override the :attr`classes`.
User code can utilize these methods for implementing custom logic on any class-list.


Patch:
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index c5f0ede..45183cf 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -141,14 +141,20 @@ class Application(SingletonConfigurable):
     # be exposed at the command line.
     classes = []
 
-    def _classes_inc_parents(self):
+    def _classes_inc_parents(self, classes=None):
         """Iterate through configurable classes, including configurable parents
 
+        :param classes:
+            The list of classes to iterate; if not set, uses :attr:`classes`.
+
         Children should always be after parents, and each class should only be
         yielded once.
         """
+        if classes is None:
+            classes = self.classes
+
         seen = set()
-        for c in self.classes:
+        for c in classes:
             # We want to sort parents before children, so we reverse the MRO
             for parent in reversed(c.mro()):
                 if issubclass(parent, Configurable) and (parent not in seen):
@@ -658,10 +664,13 @@ class Application(SingletonConfigurable):
         self.update_config(new_config)
 
 
-    def _classes_with_config_traits(self):
+    def _classes_with_config_traits(self, classes=None):
         """
         Yields only classes with own traits, and their subclasses.
 
+        :param classes:
+            The list of classes to iterate; if not set, uses :attr:`classes`.
+
         Thus, produced sample config-file will contain all classes
         on which a trait-value may be overridden:
 
@@ -669,9 +678,12 @@ class Application(SingletonConfigurable):
         - or on its subclasses, even if those subclasses do not define
           any traits themselves.
         """
+        if classes is None:
+            classes = self.classes
+
         cls_to_config = OrderedDict( (cls, bool(cls.class_own_traits(config=True)))
                               for cls
-                              in self._classes_inc_parents())
+                              in self._classes_inc_parents(classes))
 
         def is_any_parent_included(cls):
             return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__)


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index ce301be..7f2757b 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -93,6 +93,10 @@ class MyApp(Application):
         self.bar = Bar(parent=self)
 
 
+def class_to_names(classes):
+    return [klass.__name__ for klass in classes]
+
+
 class TestApplication(TestCase):
 
     def test_log(self):
@@ -113,6 +117,35 @@ class TestApplication(TestCase):
         self.assertEqual(app.classes, [MyApp,Bar,Foo])
         self.assertEqual(app.config_file, u'')
 
+    def test_mro_discovery(self):
+        app = MyApp()
+
+        self.assertSequenceEqual(class_to_names(app._classes_with_config_traits()),
+                                 ['Application', 'MyApp', 'Bar', 'Foo'])
+        self.assertSequenceEqual(class_to_names(app._classes_inc_parents()),
+                                 ['Configurable', 'LoggingConfigurable', 'SingletonConfigurable',
+                                  'Application', 'MyApp', 'Bar', 'Foo'])
+
+        self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Application])),
+                                 ['Application'])
+        self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Application])),
+                                 ['Configurable', 'LoggingConfigurable', 'SingletonConfigurable',
+                                  'Application'])
+
+        self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])),
+                                 ['Foo'])
+        self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Bar])),
+                                 ['Configurable', 'Bar'])
+
+        class MyApp2(Application):  # no defined `classes` attr
+            pass
+
+        self.assertSequenceEqual(class_to_names(app._classes_with_config_traits([Foo])),
+                                 ['Foo'])
+        self.assertSequenceEqual(class_to_names(app._classes_inc_parents([Bar])),
+                                 ['Configurable', 'Bar'])
+
+
     def test_config(self):
         app = MyApp()
         app.parse_command_line(["--i=10","--Foo.j=10","--enabled=False","--log-level=50"])