Repository: edelooff/kalpa

Issue:
Roll child-class spawning methods into one
The `_sprout` and `_sprout_resource` methods can be rolled into one with a variable number of positional arguments. The first one should be the key, the second (optional) argument is the resource class to use, which falls back to the child resource class.

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 1fe1c2f..fe1556a 100644
--- a/README.rst
+++ b/README.rst
@@ -20,10 +20,10 @@ Pyramid's traversal.
     class UserCollection(Branch):
         """User collection, for listings, or loading single users."""
 
-        def __load__(self, path):
+        def __load__(self, key):
             """Return child resource with requested user included."""
-            user = USERS[path]  # Load user or raise KeyError.
-            return self._sprout(key, user=user)
+            user = USERS[key]  # Load user or raise KeyError.
+            return self._child(key, user=user)
 
 
     @UserCollection.child_resource
diff --git a/kalpa/__init__.py b/kalpa/__init__.py
index ef7ddf2..61e6403 100644
--- a/kalpa/__init__.py
+++ b/kalpa/__init__.py
@@ -67,18 +67,29 @@ class Branch(with_metaclass(BranchType, Leaf)):
         """Dynamic resource loader for custom Branch classes.
 
         This method should either raise a KeyError or return an instantiated
-        resource class. Through one of _sprout(), _sprout_resource() or manual
-        instantiation. The response will be cached by the caller, __getitem__.
+        resource class. This can be achieved with the _child() method or manual
+        instantiation. The response will be cached by __getitem__().
         """
         raise KeyError(path)
 
-    def _sprout(self, _name, **attrs):
-        """Returns a child resource of the registered type."""
-        return self._CHILD_CLS(_name, self, **attrs)
+    def _child(self, *args, **attrs):
+        """Returns a newly instantiated child resource.
 
-    def _sprout_resource(self, _resource_cls, _name, **attrs):
-        """Returns a child resource of the provided type, with given name."""
-        return _resource_cls(_name, self, **attrs)
+        Positional arguments are either type+name or just a name. If the type
+        is not given, this defaults to the registered child resource class.
+
+        Additional named parameters are passed verbatim to the new instance.
+        """
+        if len(args) == 1:
+            if self._CHILD_CLS is None:
+                raise TypeError(
+                    'No child resource class is associated with %r. '
+                    'Provide one as the first argument' % self)
+            resource_class = self._CHILD_CLS
+            name, = args
+        else:
+            resource_class, name = args
+        return resource_class(name, self, **attrs)
 
     @classmethod
     def attach(cls, canonical_path, aliases=()):
@@ -92,7 +103,7 @@ class Branch(with_metaclass(BranchType, Leaf)):
 
     @classmethod
     def child_resource(cls, child_cls):
-        """Adds a resource class as the default child resource to sprout."""
+        """Adds a resource class as the default child class to instantiate."""
         cls._CHILD_CLS = child_cls
         return child_cls
 
diff --git a/setup.py b/setup.py
index a40c7f6..8691cf4 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ def contents(filename):
 
 setup(
     name='kalpa',
-    version='0.3',
+    version='0.4',
     packages=find_packages(),
     author='Elmer de Looff',
     author_email='elmer.delooff@gmail.com',


Test patch:
diff --git a/kalpa/tests/test_kalpa.py b/kalpa/tests/test_kalpa.py
index 9f33402..615596c 100644
--- a/kalpa/tests/test_kalpa.py
+++ b/kalpa/tests/test_kalpa.py
@@ -44,12 +44,12 @@ def branching_tree():
     @Root.attach('objects')
     class Collection(Branch):
         def __load__(self, path):
-            return self._sprout(path, fruit='apple', twice=path * 2)
+            return self._child(path, fruit='apple', twice=path * 2)
 
     @Root.attach('people')
     class People(Branch):
         def __load__(self, path):
-            return self._sprout_resource(Person, path, first=path[0].upper())
+            return self._child(Person, path, first=path[0].upper())
 
     @Collection.child_resource
     class Object(Branch):
@@ -85,7 +85,7 @@ def mixed_tree():
 
     class Root(Root):
         def __load__(self, path):
-            return self._sprout(path)
+            return self._child(path)
 
     @Root.attach('spam')
     @Root.attach('eggs')
@@ -103,6 +103,43 @@ def mixed_tree():
         'static_cls': Static}
 
 
+@pytest.fixture
+def alternate_child_tree():
+    """Returns a resource tree where sub-resources can be of multiple types.
+
+    This implicitly tests the following parts of kalpa:
+
+        - Child resource assignment to Branch classes
+        - Child resource creation using the registered class
+    """
+    from kalpa import Root, Leaf
+
+    class Root(Root):
+        def __load__(self, name):
+            if name in ('adam', 'eve'):
+                return self._child(Admin, name)
+            return self._child(name)
+
+    class BadRoot(Root):
+        def __load__(self, name):
+            if name == 'explicit_none':
+                return self._child(None, name)
+            return self._child(name)
+
+    class Admin(Leaf):
+        pass
+
+    @Root.child_resource
+    class User(Leaf):
+        pass
+
+    return {
+        'root': Root(None),
+        'bad_root': BadRoot(None),
+        'admin_cls': Admin,
+        'user_cls': User}
+
+
 class TestBasicResource(object):
     def test_sub_resource_type(self, basic_tree):
         """Leaf is an instance of the Leaf class, for context selection."""
@@ -210,6 +247,33 @@ class TestMixedResource(object):
         assert isinstance(root['bacon'], mixed_tree['dynamic_cls'])
 
 
+class TestAlternateResourceClasses(object):
+    def test_common_child_resource(self, alternate_child_tree):
+        """"Retrieves an instance of the associated child class."""
+        root = alternate_child_tree['root']
+        assert type(root['john']) is alternate_child_tree['user_cls']
+
+    def test_alternate_child_resource(self, alternate_child_tree):
+        """Retrieves an instance of a loader-selected resource class."""
+        root = alternate_child_tree['root']
+        assert type(root['eve']) is alternate_child_tree['admin_cls']
+
+    def test_missing_child_resource(self, alternate_child_tree):
+        root = alternate_child_tree['bad_root']
+        with pytest.raises(TypeError) as excinfo:
+            root['adam']
+        # Check for the more informative message for the bad-config case
+        assert 'No child resource class is associated' in str(excinfo.value)
+        assert 'not callable' not in str(excinfo.value)
+
+    def test_bad_child_resource_class(self, alternate_child_tree):
+        root = alternate_child_tree['bad_root']
+        with pytest.raises(TypeError) as excinfo:
+            root['explicit_none']
+        # Check for the default Python non-callable error message
+        assert 'not callable' in str(excinfo.value)
+
+
 class TestAttributeAccess(object):
     def test_simple_attribute_access(self, branching_tree):
         """Keywords given for object creation are available as attributes."""