Repository: 4degrees/clique

Issue:
collection.format hits maximum recursion depth for collections with lots of holes.
The following code gives an example.

```python
paths = ["name.{0:04d}.jpg".format(x) for x in range(2000)[::2]]
collection = clique.assemble(paths)[0][0]
collection.format("{head}####{tail}")
```

Discussion:

Patch:
diff --git a/source/clique/collection.py b/source/clique/collection.py
index 0c3b296..db9276c 100644
--- a/source/clique/collection.py
+++ b/source/clique/collection.py
@@ -251,15 +251,25 @@ class Collection(object):
         else:
             data['padding'] = '%d'
 
-        if self.indexes:
+        if '{holes}' in pattern:
             data['holes'] = self.holes().format('{ranges}')
 
+        if '{range}' in pattern or '{ranges}' in pattern:
             indexes = list(self.indexes)
-            if len(indexes) == 1:
+            indexes_count = len(indexes)
+
+            if indexes_count == 0:
+                data['range'] = ''
+
+            elif indexes_count == 1:
                 data['range'] = '{0}'.format(indexes[0])
+
             else:
-                data['range'] = '{0}-{1}'.format(indexes[0], indexes[-1])
+                data['range'] = '{0}-{1}'.format(
+                    indexes[0], indexes[-1]
+                )
 
+        if '{ranges}' in pattern:
             separated = self.separate()
             if len(separated) > 1:
                 ranges = [collection.format('{range}')
@@ -270,11 +280,6 @@ class Collection(object):
 
             data['ranges'] = ', '.join(ranges)
 
-        else:
-            data['holes'] = ''
-            data['range'] = ''
-            data['ranges'] = ''
-
         return pattern.format(**data)
 
     def is_contiguous(self):


Test patch:
diff --git a/test/unit/test_collection.py b/test/unit/test_collection.py
index ce4daa7..11cb01e 100644
--- a/test/unit/test_collection.py
+++ b/test/unit/test_collection.py
@@ -2,6 +2,7 @@
 # :copyright: Copyright (c) 2013 Martin Pengelly-Phillips
 # :license: See LICENSE.txt.
 
+import sys
 import inspect
 
 import pytest
@@ -242,7 +243,6 @@ def test_remove_non_member():
     (PaddedCollection, '{range}', '1-12'),
     (PaddedCollection, '{ranges}', '1-3, 7, 9-12'),
     (PaddedCollection, '{holes}', '4-6, 8'),
-
 ])
 def test_format(CollectionCls, pattern, expected):
     '''Format collection according to pattern.'''
@@ -250,6 +250,25 @@ def test_format(CollectionCls, pattern, expected):
     assert collection.format(pattern) == expected
 
 
+def test_format_sparse_collection():
+    '''Format sparse collection without recursion error.'''
+    recursion_limit = sys.getrecursionlimit()
+    recursion_error_occurred = False
+
+    try:
+        collection = PaddedCollection(
+            indexes=set(range(0, recursion_limit * 2, 2))
+        )
+        collection.format()
+    except RuntimeError as error:
+        if 'maximum recursion depth exceeded' in str(error):
+            recursion_error_occurred = True
+        else:
+            raise
+
+    assert not recursion_error_occurred
+
+
 @pytest.mark.parametrize(('collection', 'expected'), [
     (PaddedCollection(indexes=set([])), True),
     (PaddedCollection(indexes=set([1])), True),