Index: geos/geometries.py
===================================================================
--- geos/geometries.py	(revision 2)
+++ geos/geometries.py	(revision 7)
@@ -156,10 +156,12 @@
 
         # Getting the correct initialization function
         if kwargs.get('ring', False):
-            func = create_linearring
+            self._init_func = create_linearring
         else:
-            func = create_linestring
+            self._init_func = create_linestring
 
+        func = self._init_func
+
         # If SRID was passed in with the keyword arguments
         srid = kwargs.get('srid', None)
        
@@ -168,13 +170,152 @@
         super(LineString, self).__init__(func(cs.ptr), srid=srid)
 
     def __getitem__(self, index):
-        "Gets the point at the specified index."
-        return self._cs[index]
+        "Gets the coordinates of the point(s) at the specified index/slice."
+        if isinstance(index, slice):
+            return [self._cs[i] for i in xrange(*index.indices(len(self._cs)))]
+        else:
+            if index < 0:
+                index += len(self._cs)
+            return self._cs[index]
 
+    def __delitem__(self, index):
+        "Delete the point(s) at the specified index/slice."
+        if not isinstance(index, (int, long, slice)):
+            raise TypeError("%s is not a legal index" % index)
+
+        # calculate new length and dimensions
+        currLen     = len(self._cs)
+        if isinstance(index, (int, long)):
+            if index < 0: index += currLen
+            if index < 0 or currLen <= index:
+                raise GEOSIndexError('invalid GEOS Geometry index: %d' % index)
+            indexRange  = [index]
+        else:
+            indexRange  = range(*index.indices(currLen))
+
+        newLen      = currLen - len(indexRange)
+        ndim        = self._cs.dims
+        hasz        = self._cs.hasz # I don't understand why these are different
+
+        # create a new coordinate sequence and populate accordingly
+        cs = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz)
+        new_i = 0
+        for old_i in xrange(currLen):
+            if old_i in indexRange: continue
+            cs[new_i] = self._cs[old_i]
+            new_i += 1
+
+        ptr = self._init_func(cs.ptr)
+        if ptr:
+            destroy_geom(self.ptr)
+            self._ptr = ptr
+            self._post_init(self.srid)
+        else:
+            # can this happen?
+            raise GEOSException('Geometry resulting from slice deletion was invalid.')
+
     def __setitem__(self, index, value):
-        "Sets the point at the specified index, e.g., line_str[0] = (1, 2)."
-        self._cs[index] = value
+        """Sets the point(s) at the specified index/slice,
+           e.g., line_str[0] = (1, 2)."""
+        if isinstance(index, slice):
+            try:
+                valueIter = iter(value)
+            except TypeError:
+                raise TypeError('can only assign an iterable')
 
+            # calculate length and dimensions
+            currLen     = len(self._cs)
+            valueList   = list(value)
+            start, stop, step = index.indices(currLen)
+            stop = max(0, stop) # stop will be -1 if out-of-bounds
+                                # negative index is given
+
+            # CAREFUL: index.step and step are not the same!
+            # step will never be None
+            #
+            if index.step is None:
+                # this is a simple slice, can assign slice of any length
+                # calculate new length
+                newLen  = currLen - stop + start + len(valueList)
+                ndim    = self._cs.dims
+                hasz    = self._cs.hasz # not sure why these are different
+
+                # create a new coordinate sequence and populate accordingly
+                cs      = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz)
+                new_i   = 0
+                for old_i in xrange(currLen + 1):
+                    if old_i == start:
+                        for val in valueList:
+                            cs[new_i] = val
+                            new_i += 1
+
+                    if old_i < currLen:
+                        if old_i < start or old_i >= stop:
+                            cs[new_i] = self._cs[old_i]
+                            new_i += 1
+
+                ptr = self._init_func(cs.ptr)
+                # Polygon.__setitem__ doesn't check this, but it seems 
+                # that it can't hurt.
+                if ptr:
+                    destroy_geom(self.ptr)
+                    self._ptr = ptr
+                    self._post_init(self.srid)
+                else:
+                    raise GEOSException('Geometry resulting from slice deletion was invalid.')
+
+            else:
+                indexList   = range(start, stop, step)
+                # extended slice, only allow assigning slice of same size
+                if len(valueList) != len(indexList):
+                    raise ValueError('attempt to assign sequence of size %d '
+                                     'to extended slice of size %d'
+                                     % (len(valueList), len(indexList)))
+
+                # we're not changing the length of the sequence
+                # we can just iterate the indices and value and set them
+                for i, val in zip(indexList , valueList):
+                    self._cs[i] = val
+
+        else:
+            length  = len(self._cs)
+            if index < 0: index += length
+            self._cs[index] = value
+
+    def append(self, val):
+        "Standard list append method"
+        self[len(self):] = [val]
+
+    def extend(self, vals):
+        "Standard list extend method"
+        self[len(self):] = vals
+
+    def insert(self, index, val):
+        "Standard list insert method"
+        if not isinstance(index, (int, long)):
+            raise TypeError("%s is not a legal index" % index)
+        self[index:index] = [val]
+
+    def pop(self, index=-1):
+        "Standard list pop method"
+        result = self[index]
+        del self[index]
+        return result
+
+    def index(self, val):
+        "Standard list index method"
+        for i in xrange(0, len(self)):
+            if self[i] == val: return i
+        raise ValueError('%s not in geometry' % str(val))
+
+    def remove(self, val):
+        "Standard list remove method"
+        del self[self.index(val)]
+
+    def count(self):
+        "Standard list count method"
+        return len(self)
+        
     def __iter__(self):
         "Allows iteration over this LineString."
         for i in xrange(len(self)):
Index: tests/__init__.py
===================================================================
--- tests/__init__.py	(revision 2)
+++ tests/__init__.py	(revision 7)
@@ -18,6 +18,7 @@
     # Tests that do not require setting up and tearing down a spatial database.
     test_suite_names = [
         'test_geos',
+        'test_geos_pymutable',
         'test_measure',
         ]
     if HAS_GDAL:
Index: tests/test_geos_pymutable.py
===================================================================
--- tests/test_geos_pymutable.py	(revision 0)
+++ tests/test_geos_pymutable.py	(revision 7)
@@ -0,0 +1,174 @@
+import unittest
+from pymutable_geometries import *
+from django.contrib.gis.geos.error import GEOSIndexError
+    
+class GEOSPyMutableTest(unittest.TestCase):
+    '''
+    Tests Pythonic Mutability of Python GEOS geometry wrappers
+    get/set/delitem on a slice, normal list methods
+    '''
+
+    def test01_getslice(self):
+        'Test getting a slice from a geometry'
+        for f in getslice_functions():
+            for g in slice_geometries():
+                self.assertEqual(f(g.coords), f(g.geom), f.__name__)
+
+    def test02_getitem(self):
+        'Test getting a single item from a geometry'
+        for g in slice_geometries():
+            for i in seqrange():
+                self.assertEqual(g.coords[i],   g.geom[i])
+
+    def test03_getitem_indexException(self):
+        'Test get single item with out-of-bounds index'
+        for g in slice_geometries():
+            for i in SEQ_OUT_OF_BOUNDS:
+                self.assertRaises(GEOSIndexError, lambda: g.geom[i])
+
+    def test04_delitem_single(self):
+        'Test delete single item from a geometry'
+        for i in seqrange():
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS: continue
+                del g.coords[i]
+                del g.geom[i]
+                self.assertEqual(g.coords , g.geom[:])
+
+    def test05_delitem_slice(self):
+        'Test delete slice from a geometry'
+        for f in delslice_functions():
+            for g in slice_geometries():
+                if g.geom.ring and not f.ring: continue
+                f(g.coords)
+                f(g.geom)
+                self.assertEqual(g.coords , g.geom[:], f.__name__)
+
+    def test06_delitem_single_indexException(self):
+        'Test delete single item with out-of-bounds index'
+        def func(x, i): del x[i]
+        for g in slice_geometries():
+            for i in SEQ_OUT_OF_BOUNDS:
+                self.assertRaises(GEOSIndexError, func, g.geom, i)
+
+    def test07_setitem_single(self):
+        "Test set single item (make sure we didn't break this)"
+        for i in seqrange():
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS: continue
+                g.coords[i] = (3.14159, 3.14159)
+                g.geom[i] = (3.14159, 3.14159)
+                self.assertEqual(g.coords , g.geom[:])
+
+    def test08_setslice_simple(self):
+        'Test setting a simple slice of a geometry'
+        for f in setslice_simple_functions():
+            for g in slice_geometries():
+                if g.geom.ring and not f.ring: continue
+                f(g.coords)
+                f(g.geom)
+                self.assertEqual(g.coords , g.geom[:], f.__name__)
+
+    def test09_setslice_extended(self):
+        'Test setting an extended slice of a geometry'
+        for f in setslice_extended_functions():
+            for g in slice_geometries():
+                if g.geom.ring and not f.ring: continue
+                f(g.coords)
+                f(g.geom)
+                self.assertEqual(g.coords , g.geom[:], f.__name__)
+                
+    def test10_setslice_extended_mismatched(self):
+        'Test setting extended slice with array of mismatched length'
+        def func(x): x[2:8:2] = [(3.1415, 3.1415)]
+        for g in slice_geometries():
+            self.assertRaises(ValueError, func, g.geom)
+
+    def test11_setitem_single_indexException(self):
+        'Test set single item with out-of-bounds index'
+        def func(x, i): x[i] = (3.1415, 3.1415)
+        for g in slice_geometries():
+            for i in SEQ_OUT_OF_BOUNDS:
+                self.assertRaises(GEOSIndexError, func, g.geom, i)
+
+    def test12_append(self):
+        'Test list method append'
+        for g in slice_geometries():
+            if g.geom.ring: continue
+            g.geom.append((200.0,200.0))
+            g.coords.append((200.0,200.0))
+            self.assertEqual(g.coords , g.geom[:])
+
+    def test13_extend(self):
+        'Test list method extend'
+        for g in slice_geometries():
+            points = random_coords(5)
+            if g.geom.ring: points[-1] = g.coords[0]
+            g.geom.extend(points)
+            g.coords.extend(points)
+            self.assertEqual(g.coords , g.geom[:])
+
+    def test14_insert(self):
+        'Test list method insert'
+        for i in xrange(*SEQ_OUT_OF_BOUNDS):
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS:
+                    continue
+                g.geom.insert(i, (3141.5, 3141.5))
+                g.coords.insert(i, (3141.5, 3141.5))
+                self.assertEqual(g.coords , g.geom[:])
+
+    def test15_insert_typeError(self):
+        'Test list method insert raises error on invalid index'
+        for g in slice_geometries():
+            self.assertRaises(TypeError, g.geom.insert,
+                                'hi', (3141.5, 3141.5))
+
+    def test16_pop(self):
+        'Test list method pop'
+        for i in seqrange():
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS:
+                    continue
+                self.assertEqual(g.coords.pop(i), g.geom.pop(i))
+
+    def test16_index(self):
+        'Test list method index'
+        for i in xrange(0, SEQ_LENGTH):
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS: continue
+                p = (200.0, 200.0)
+                g.geom[i] = p
+                self.assertEqual(i, g.geom.index(p))
+
+    def test17_index_ValueError(self):
+        'Test list method raises ValueError if value not found'
+        for g in slice_geometries():
+            self.assertRaises(ValueError, g.geom.index, (200.0,200.0))
+
+    def test18_remove(self):
+        'Test list method remove'
+        for i in xrange(0, SEQ_LENGTH):
+            for g in slice_geometries():
+                if g.geom.ring and i in SEQ_BOUNDS: continue
+                p = (200.0, 200.0)
+                g.geom[i] = p
+                g.coords[i] = p
+                g.geom.remove(p)
+                g.coords.remove(p)
+                self.assertEqual(g.coords, g.geom[:])
+
+    def test19_count(self):
+        'Test list method count'
+        for g in slice_geometries():
+            self.assertEqual(SEQ_LENGTH, g.geom.count())
+def suite():
+    s = unittest.TestSuite()
+    s.addTest(unittest.makeSuite(GEOSPyMutableTest))
+    return s
+
+def run(verbosity=2):
+    unittest.TextTestRunner(verbosity=verbosity).run(suite())
+
+if __name__ == '__main__':
+    run()

Property changes on: tests/test_geos_pymutable.py
___________________________________________________________________
Name: svn:executable
   + *

Index: tests/pymutable_geometries.py
===================================================================
--- tests/pymutable_geometries.py	(revision 0)
+++ tests/pymutable_geometries.py	(revision 7)
@@ -0,0 +1,125 @@
+from django.contrib.gis.geos import *
+from random import random
+
+SEQ_LENGTH = 10
+SEQ_RANGE = (-1 * SEQ_LENGTH, SEQ_LENGTH)
+SEQ_BOUNDS = (-1 * SEQ_LENGTH, -1, 0, SEQ_LENGTH - 1)
+SEQ_OUT_OF_BOUNDS = (-1 * SEQ_LENGTH -1 , SEQ_LENGTH)
+
+def seqrange(): return xrange(*SEQ_RANGE)
+
+class PyMutTestGeom:
+    "The Test Geometry class container."
+    def __init__(self, geom_type, coords, **kwargs):
+        self.coords = coords
+        self.geom   = geom_type(coords)
+
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+def random_coords(length=SEQ_LENGTH,    dim=2,      rng=(-50,50),   type=float,
+                  ring=False,   round_coords=True):
+    if round_coords:
+        num = lambda: type(round(random() * (rng[1]-rng[0]) + rng[0]))
+    else:
+        num = lambda: type(random() * (rng[1]-rng[0]) + rng[0])
+
+    result  = [ tuple( num() for axis in xrange(dim) )
+                for index in xrange(length) ]
+    if ring:
+        result[-1] = result[0]
+
+    return result
+
+
+def slice_geometries(): 
+    return (
+        PyMutTestGeom(LineString, random_coords()),
+        PyMutTestGeom(LinearRing, random_coords(ring=True)),
+        )
+
+def getslice_functions(): 
+    return (
+        lambda x: x[0:4],   
+        lambda x: x[5:-1],  
+        lambda x: x[6:2:-1],
+        lambda x: x[:],     
+        lambda x: x[:3],    
+        lambda x: x[::2],   
+        lambda x: x[::-4],  
+        lambda x: x[7:7],   
+        lambda x: x[20:],   
+        )
+
+def delslice_functions():
+    def ds_01(x): del x[0:4]   
+    def ds_02(x): del x[5:-1]  
+    def ds_03(x): del x[6:2:-1]
+    def ds_04(x): del x[:]     # should this be allowed?
+    def ds_05(x): del x[:3]    
+    def ds_06(x): del x[1:9:2]   
+    def ds_07(x): del x[::-4]  
+    def ds_08(x): del x[7:7]   
+    def ds_09(x): del x[-7:-2]
+
+    return mark_ring(vars(), 'ds_')
+
+def setslice_extended_functions():
+    a = [(1.0,2.0),(1.0,2.0),(1.0,2.0)]
+    def sse_00(x): x[:3:1] = a
+    def sse_01(x): x[0:3:1] = a
+    def sse_02(x): x[2:5:1] = a
+    def sse_03(x): x[-3::1] = a
+    def sse_04(x): x[-4:-1:1] = a
+    def sse_05(x): x[8:5:-1] = a
+    def sse_06(x): x[-6:-9:-1] = a
+    def sse_07(x): x[:8:3] = a
+    def sse_08(x): x[1::3] = a
+    def sse_09(x): x[-2::-3] = a
+    def sse_10(x): x[7:1:-2] = a
+    def sse_11(x): x[2:8:2] = a
+
+    return mark_ring(vars(), 'sse_')
+
+def setslice_simple_functions():
+    a = [(1.0,2.0),(2.0,1.0),(2.0,2.0)]
+    def ss_00(x): x[:0] = a
+    def ss_01(x): x[:1] = a
+    def ss_02(x): x[:2] = a
+    def ss_03(x): x[:3] = a
+    def ss_04(x): x[-4:] = a
+    def ss_05(x): x[-3:] = a
+    def ss_06(x): x[-2:] = a
+    def ss_07(x): x[-1:] = a
+    def ss_08(x): x[5:] = a
+    def ss_09(x): x[:] = a
+    def ss_10(x): x[4:4] = a
+    def ss_11(x): x[4:5] = a
+    def ss_12(x): x[4:7] = a
+    def ss_13(x): x[4:8] = a
+    def ss_14(x): x[20:30] = a
+    def ss_15(x): x[-13:-8] = a
+    def ss_16(x): x[-13:-9] = a
+    def ss_17(x): x[-13:-10] = a
+    def ss_18(x): x[-13:-11] = a
+    def ss_19(x): x[10:] = a
+
+    return mark_ring(vars(), 'ss_')
+
+def mark_ring(locals, name_pat, length=SEQ_LENGTH):
+    '''
+    Accepts an array of functions which perform slice modifications
+    and labels each function as to whether or not it preserves ring-ness
+    '''
+    func_array = [ val for name, val in locals.items()
+                    if hasattr(val, '__call__')
+                    and name.startswith(name_pat) ]
+
+    for i in xrange(len(func_array)):
+        a = range(length)
+        a[-1] = a[0]
+        func_array[i](a)
+        ring = len(a) == 0 or (len(a) > 3 and a[-1] == a[0])
+        func_array[i].ring = ring
+
+    return func_array

Property changes on: tests/pymutable_geometries.py
___________________________________________________________________
Name: svn:executable
   + *

