Code

Ticket #9877: pythonic_mutation_patch_part_I.2

File pythonic_mutation_patch_part_I.2, 19.3 KB (added by Aryeh Leib Taurog <vim@…>, 6 years ago)

This is an updated patch to Django 1.0.2 release

Line 
1Index: geos/geometries.py
2===================================================================
3--- geos/geometries.py  (revision 2)
4+++ geos/geometries.py  (revision 8)
5@@ -156,10 +156,12 @@
6 
7         # Getting the correct initialization function
8         if kwargs.get('ring', False):
9-            func = create_linearring
10+            self._init_func = create_linearring
11         else:
12-            func = create_linestring
13+            self._init_func = create_linestring
14 
15+        func = self._init_func
16+
17         # If SRID was passed in with the keyword arguments
18         srid = kwargs.get('srid', None)
19       
20@@ -168,13 +170,152 @@
21         super(LineString, self).__init__(func(cs.ptr), srid=srid)
22 
23     def __getitem__(self, index):
24-        "Gets the point at the specified index."
25-        return self._cs[index]
26+        "Gets the coordinates of the point(s) at the specified index/slice."
27+        if isinstance(index, slice):
28+            return [self._cs[i] for i in xrange(*index.indices(len(self._cs)))]
29+        else:
30+            if index < 0:
31+                index += len(self._cs)
32+            return self._cs[index]
33 
34+    def __delitem__(self, index):
35+        "Delete the point(s) at the specified index/slice."
36+        if not isinstance(index, (int, long, slice)):
37+            raise TypeError("%s is not a legal index" % index)
38+
39+        # calculate new length and dimensions
40+        origLen     = len(self._cs)
41+        if isinstance(index, (int, long)):
42+            if index < 0: index += origLen
43+            if index < 0 or origLen <= index:
44+                raise GEOSIndexError('invalid GEOS Geometry index: %d' % index)
45+            indexRange  = [index]
46+        else:
47+            indexRange  = range(*index.indices(origLen))
48+
49+        newLen      = origLen - len(indexRange)
50+        ndim        = self._cs.dims
51+        hasz        = self._cs.hasz # I don't understand why these are different
52+
53+        # create a new coordinate sequence and populate accordingly
54+        cs = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz)
55+        new_i = 0
56+        for orig_i in xrange(origLen):
57+            if orig_i in indexRange: continue
58+            cs[new_i] = self._cs[orig_i]
59+            new_i += 1
60+
61+        ptr = self._init_func(cs.ptr)
62+        if ptr:
63+            destroy_geom(self.ptr)
64+            self._ptr = ptr
65+            self._post_init(self.srid)
66+        else:
67+            # can this happen?
68+            raise GEOSException('Geometry resulting from slice deletion was invalid.')
69+
70     def __setitem__(self, index, value):
71-        "Sets the point at the specified index, e.g., line_str[0] = (1, 2)."
72-        self._cs[index] = value
73+        """Sets the point(s) at the specified index/slice,
74+           e.g., line_str[0] = (1, 2)."""
75+        if isinstance(index, slice):
76+            try:
77+                valueIter = iter(value)
78+            except TypeError:
79+                raise TypeError('can only assign an iterable')
80 
81+            # calculate length and dimensions
82+            origLen     = len(self._cs)
83+            valueList   = list(value)
84+            start, stop, step = index.indices(origLen)
85+            stop = max(0, stop) # stop will be -1 if out-of-bounds
86+                                # negative index is given
87+
88+            # CAREFUL: index.step and step are not the same!
89+            # step will never be None
90+            #
91+            if index.step is None:
92+                # this is a simple slice, can assign slice of any length
93+                # calculate new length
94+                newLen  = origLen - stop + start + len(valueList)
95+                ndim    = self._cs.dims
96+                hasz    = self._cs.hasz # not sure why these are different
97+
98+                # create a new coordinate sequence and populate accordingly
99+                cs      = GEOSCoordSeq(create_cs(newLen, ndim), z=hasz)
100+                new_i   = 0
101+                for orig_i in xrange(origLen + 1):
102+                    if orig_i == start:
103+                        for val in valueList:
104+                            cs[new_i] = val
105+                            new_i += 1
106+
107+                    if orig_i < origLen:
108+                        if orig_i < start or orig_i >= stop:
109+                            cs[new_i] = self._cs[orig_i]
110+                            new_i += 1
111+
112+                ptr = self._init_func(cs.ptr)
113+                # Polygon.__setitem__ doesn't check this, but it seems
114+                # that it can't hurt.
115+                if ptr:
116+                    destroy_geom(self.ptr)
117+                    self._ptr = ptr
118+                    self._post_init(self.srid)
119+                else:
120+                    raise GEOSException('Geometry resulting from slice deletion was invalid.')
121+
122+            else:
123+                indexList   = range(start, stop, step)
124+                # extended slice, only allow assigning slice of same size
125+                if len(valueList) != len(indexList):
126+                    raise ValueError('attempt to assign sequence of size %d '
127+                                     'to extended slice of size %d'
128+                                     % (len(valueList), len(indexList)))
129+
130+                # we're not changing the length of the sequence
131+                # we can just iterate the indices and value and set them
132+                for i, val in zip(indexList , valueList):
133+                    self._cs[i] = val
134+
135+        else:
136+            length  = len(self._cs)
137+            if index < 0: index += length
138+            self._cs[index] = value
139+
140+    def append(self, val):
141+        "Standard list append method"
142+        self[len(self):] = [val]
143+
144+    def extend(self, vals):
145+        "Standard list extend method"
146+        self[len(self):] = vals
147+
148+    def insert(self, index, val):
149+        "Standard list insert method"
150+        if not isinstance(index, (int, long)):
151+            raise TypeError("%s is not a legal index" % index)
152+        self[index:index] = [val]
153+
154+    def pop(self, index=-1):
155+        "Standard list pop method"
156+        result = self[index]
157+        del self[index]
158+        return result
159+
160+    def index(self, val):
161+        "Standard list index method"
162+        for i in xrange(0, len(self)):
163+            if self[i] == val: return i
164+        raise ValueError('%s not in geometry' % str(val))
165+
166+    def remove(self, val):
167+        "Standard list remove method"
168+        del self[self.index(val)]
169+
170+    def count(self):
171+        "Standard list count method"
172+        return len(self)
173+       
174     def __iter__(self):
175         "Allows iteration over this LineString."
176         for i in xrange(len(self)):
177Index: tests/__init__.py
178===================================================================
179--- tests/__init__.py   (revision 2)
180+++ tests/__init__.py   (revision 8)
181@@ -18,6 +18,7 @@
182     # Tests that do not require setting up and tearing down a spatial database.
183     test_suite_names = [
184         'test_geos',
185+        'test_geos_pymutable',
186         'test_measure',
187         ]
188     if HAS_GDAL:
189Index: tests/test_geos_pymutable.py
190===================================================================
191--- tests/test_geos_pymutable.py        (revision 0)
192+++ tests/test_geos_pymutable.py        (revision 8)
193@@ -0,0 +1,199 @@
194+import unittest
195+from pymutable_geometries import *
196+from django.contrib.gis.geos.error import GEOSIndexError
197+   
198+class GEOSPyMutableTest(unittest.TestCase):
199+    '''
200+    Tests Pythonic Mutability of Python GEOS geometry wrappers
201+    get/set/delitem on a slice, normal list methods
202+    '''
203+
204+    def test01_getslice(self):
205+        'Test getting a slice from a geometry'
206+        for f in getslice_functions():
207+            for g in slice_geometries():
208+                self.assertEqual(f(g.coords), f(g.geom), f.__name__)
209+
210+    def test02_getitem(self):
211+        'Test getting a single item from a geometry'
212+        for g in slice_geometries():
213+            for i in seqrange():
214+                self.assertEqual(g.coords[i],   g.geom[i])
215+
216+    def test03_getitem_indexException(self):
217+        'Test get single item with out-of-bounds index'
218+        for g in slice_geometries():
219+            for i in SEQ_OUT_OF_BOUNDS:
220+                self.assertRaises(GEOSIndexError, lambda: g.geom[i])
221+
222+    def test04_delitem_single(self):
223+        'Test delete single item from a geometry'
224+        for i in seqrange():
225+            for g in slice_geometries():
226+                if g.geom.ring and i in SEQ_BOUNDS: continue
227+                del g.coords[i]
228+                del g.geom[i]
229+                self.assertEqual(g.coords , g.geom[:])
230+
231+    def test05_delitem_slice(self):
232+        'Test delete slice from a geometry'
233+        for f in delslice_functions():
234+            for g in slice_geometries():
235+                if g.geom.ring and not f.ring: continue
236+                f(g.coords)
237+                f(g.geom)
238+                self.assertEqual(g.coords , g.geom[:], f.__name__)
239+
240+    def test06_delitem_single_indexException(self):
241+        'Test delete single item with out-of-bounds index'
242+        def func(x, i): del x[i]
243+        for g in slice_geometries():
244+            for i in SEQ_OUT_OF_BOUNDS:
245+                self.assertRaises(GEOSIndexError, func, g.geom, i)
246+
247+    def test07_setitem_single(self):
248+        "Test set single item (make sure we didn't break this)"
249+        for i in seqrange():
250+            for g in slice_geometries():
251+                if g.geom.ring and i in SEQ_BOUNDS: continue
252+                g.coords[i] = (3.14159, 3.14159)
253+                g.geom[i] = (3.14159, 3.14159)
254+                self.assertEqual(g.coords , g.geom[:])
255+
256+    def test08_setslice_simple(self):
257+        'Test setting a simple slice of a geometry'
258+        for f in setslice_simple_functions():
259+            for g in slice_geometries():
260+                if g.geom.ring and not f.ring: continue
261+                f(g.coords)
262+                f(g.geom)
263+                self.assertEqual(g.coords , g.geom[:], f.__name__)
264+
265+    def test09_setslice_extended(self):
266+        'Test setting an extended slice of a geometry'
267+        for f in setslice_extended_functions():
268+            for g in slice_geometries():
269+                if g.geom.ring and not f.ring: continue
270+                f(g.coords)
271+                f(g.geom)
272+                self.assertEqual(g.coords , g.geom[:], f.__name__)
273+               
274+    def test10_setslice_extended_mismatched(self):
275+        'Test setting extended slice with array of mismatched length'
276+        def func(x): x[2:8:2] = [(3.1415, 3.1415)]
277+        for g in slice_geometries():
278+            self.assertRaises(ValueError, func, g.geom)
279+
280+    def test11_setitem_single_indexException(self):
281+        'Test set single item with out-of-bounds index'
282+        def func(x, i): x[i] = (3.1415, 3.1415)
283+        for g in slice_geometries():
284+            for i in SEQ_OUT_OF_BOUNDS:
285+                self.assertRaises(GEOSIndexError, func, g.geom, i)
286+
287+    def test12_append(self):
288+        'Test list method append'
289+        for g in slice_geometries():
290+            if g.geom.ring: continue
291+            g.geom.append((200.0,200.0))
292+            g.coords.append((200.0,200.0))
293+            self.assertEqual(g.coords , g.geom[:])
294+
295+    def test13_extend(self):
296+        'Test list method extend'
297+        for g in slice_geometries():
298+            points = random_coords(5)
299+            if g.geom.ring: points[-1] = g.coords[0]
300+            g.geom.extend(points)
301+            g.coords.extend(points)
302+            self.assertEqual(g.coords , g.geom[:])
303+
304+    def test14_insert(self):
305+        'Test list method insert'
306+        for i in xrange(*SEQ_OUT_OF_BOUNDS):
307+            for g in slice_geometries():
308+                if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS:
309+                    continue
310+                g.geom.insert(i, (3141.5, 3141.5))
311+                g.coords.insert(i, (3141.5, 3141.5))
312+                self.assertEqual(g.coords , g.geom[:])
313+
314+    def test15_insert_typeError(self):
315+        'Test list method insert raises error on invalid index'
316+        for g in slice_geometries():
317+            self.assertRaises(TypeError, g.geom.insert,
318+                                'hi', (3141.5, 3141.5))
319+
320+    def test16_pop(self):
321+        'Test list method pop'
322+        for i in seqrange():
323+            for g in slice_geometries():
324+                if g.geom.ring and i in SEQ_BOUNDS + SEQ_OUT_OF_BOUNDS:
325+                    continue
326+                self.assertEqual(g.coords.pop(i), g.geom.pop(i))
327+
328+    def test16_index(self):
329+        'Test list method index'
330+        for i in xrange(0, SEQ_LENGTH):
331+            for g in slice_geometries():
332+                if g.geom.ring and i in SEQ_BOUNDS: continue
333+                p = (200.0, 200.0)
334+                g.geom[i] = p
335+                self.assertEqual(i, g.geom.index(p))
336+
337+    def test17_index_ValueError(self):
338+        'Test list method raises ValueError if value not found'
339+        for g in slice_geometries():
340+            self.assertRaises(ValueError, g.geom.index, (200.0,200.0))
341+
342+    def test18_remove(self):
343+        'Test list method remove'
344+        for i in xrange(0, SEQ_LENGTH):
345+            for g in slice_geometries():
346+                if g.geom.ring and i in SEQ_BOUNDS: continue
347+                p = (200.0, 200.0)
348+                g.geom[i] = p
349+                g.coords[i] = p
350+                g.geom.remove(p)
351+                g.coords.remove(p)
352+                self.assertEqual(g.coords, g.geom[:])
353+
354+    def test19_count(self):
355+        'Test list method count'
356+        for g in slice_geometries():
357+            self.assertEqual(SEQ_LENGTH, g.geom.count())
358+
359+    def test20_setslice_geos_fcns(self):
360+        'Test geos properties after setting a simple slice of a geometry'
361+        for f in setslice_simple_functions():
362+            for g in slice_geometries():
363+                if g.geom.ring: continue
364+                f(g.coords)
365+                f(g.geom)
366+                if not len(g.coords): continue
367+                for tf in test_geos_functions():
368+                    cg = g.make_geom()
369+                    self.assertEqual(tf(cg) , tf(g.geom))
370+
371+    def test21_delslice_geos_fcns(self):
372+        'Test geos properties after deleting a slice of a geometry'
373+        for f in delslice_functions():
374+            for g in slice_geometries():
375+                if g.geom.ring: continue
376+                f(g.coords)
377+                f(g.geom)
378+                if not len(g.coords): continue
379+                for tf in test_geos_functions():
380+                    cg = g.make_geom()
381+                    self.assertEqual(tf(cg) , tf(g.geom))
382+
383+def suite():
384+    s = unittest.TestSuite()
385+    s.addTest(unittest.makeSuite(GEOSPyMutableTest))
386+    return s
387+
388+def run(verbosity=2):
389+    unittest.TextTestRunner(verbosity=verbosity).run(suite())
390+
391+if __name__ == '__main__':
392+    run()
393
394Property changes on: tests/test_geos_pymutable.py
395___________________________________________________________________
396Name: svn:executable
397   + *
398
399Index: tests/pymutable_geometries.py
400===================================================================
401--- tests/pymutable_geometries.py       (revision 0)
402+++ tests/pymutable_geometries.py       (revision 8)
403@@ -0,0 +1,146 @@
404+from django.contrib.gis.geos import *
405+from random import random
406+
407+SEQ_LENGTH = 10
408+SEQ_RANGE = (-1 * SEQ_LENGTH, SEQ_LENGTH)
409+SEQ_BOUNDS = (-1 * SEQ_LENGTH, -1, 0, SEQ_LENGTH - 1)
410+SEQ_OUT_OF_BOUNDS = (-1 * SEQ_LENGTH -1 , SEQ_LENGTH)
411+
412+def seqrange(): return xrange(*SEQ_RANGE)
413+
414+class PyMutTestGeom:
415+    "The Test Geometry class container."
416+    def __init__(self, geom_type, coords, **kwargs):
417+        self.geom_type  = geom_type
418+        self.coords = coords
419+        self.geom   = self.make_geom()
420+
421+    def make_geom(self):
422+        return self.geom_type(self.coords)
423+
424+def random_coords(length = SEQ_LENGTH,
425+                  dim    = 2,           # coordinate dimensions
426+                  rng    = (-50,50),    # coordinate range
427+                  type   = float,
428+                  ring   = False,
429+                  round_coords = True):
430+
431+    if round_coords:
432+        num = lambda: type(round(random() * (rng[1]-rng[0]) + rng[0]))
433+    else:
434+        num = lambda: type(random() * (rng[1]-rng[0]) + rng[0])
435+
436+    result  = [ tuple( num() for axis in xrange(dim) )
437+                for index in xrange(length) ]
438+    if ring:
439+        result[-1] = result[0]
440+
441+    return result
442+
443+
444+def slice_geometries():
445+    return (
446+        PyMutTestGeom(LineString, random_coords()),
447+        PyMutTestGeom(LinearRing, random_coords(ring=True)),
448+        )
449+
450+def getslice_functions():
451+    return (
452+        lambda x: x[0:4],   
453+        lambda x: x[5:-1], 
454+        lambda x: x[6:2:-1],
455+        lambda x: x[:],     
456+        lambda x: x[:3],   
457+        lambda x: x[::2],   
458+        lambda x: x[::-4], 
459+        lambda x: x[7:7],   
460+        lambda x: x[20:],   
461+        )
462+
463+def delslice_functions():
464+    def ds_01(x): del x[0:4]   
465+    def ds_02(x): del x[5:-1] 
466+    def ds_03(x): del x[6:2:-1]
467+    def ds_04(x): del x[:]     # should this be allowed?
468+    def ds_05(x): del x[:3]   
469+    def ds_06(x): del x[1:9:2]   
470+    def ds_07(x): del x[::-4] 
471+    def ds_08(x): del x[7:7]   
472+    def ds_09(x): del x[-7:-2]
473+
474+    return mark_ring(vars(), 'ds_')
475+
476+def setslice_extended_functions():
477+    a = [(1.0,2.0),(1.0,2.0),(1.0,2.0)]
478+    def sse_00(x): x[:3:1] = a
479+    def sse_01(x): x[0:3:1] = a
480+    def sse_02(x): x[2:5:1] = a
481+    def sse_03(x): x[-3::1] = a
482+    def sse_04(x): x[-4:-1:1] = a
483+    def sse_05(x): x[8:5:-1] = a
484+    def sse_06(x): x[-6:-9:-1] = a
485+    def sse_07(x): x[:8:3] = a
486+    def sse_08(x): x[1::3] = a
487+    def sse_09(x): x[-2::-3] = a
488+    def sse_10(x): x[7:1:-2] = a
489+    def sse_11(x): x[2:8:2] = a
490+
491+    return mark_ring(vars(), 'sse_')
492+
493+def setslice_simple_functions():
494+    a = [(1.0,2.0),(2.0,1.0),(2.0,2.0)]
495+    def ss_00(x): x[:0] = a
496+    def ss_01(x): x[:1] = a
497+    def ss_02(x): x[:2] = a
498+    def ss_03(x): x[:3] = a
499+    def ss_04(x): x[-4:] = a
500+    def ss_05(x): x[-3:] = a
501+    def ss_06(x): x[-2:] = a
502+    def ss_07(x): x[-1:] = a
503+    def ss_08(x): x[5:] = a
504+    def ss_09(x): x[:] = a
505+    def ss_10(x): x[4:4] = a
506+    def ss_11(x): x[4:5] = a
507+    def ss_12(x): x[4:7] = a
508+    def ss_13(x): x[4:8] = a
509+    def ss_14(x): x[20:30] = a
510+    def ss_15(x): x[-13:-8] = a
511+    def ss_16(x): x[-13:-9] = a
512+    def ss_17(x): x[-13:-10] = a
513+    def ss_18(x): x[-13:-11] = a
514+    def ss_19(x): x[10:] = a
515+
516+    return mark_ring(vars(), 'ss_')
517+
518+def test_geos_functions():
519+
520+    return (
521+        lambda x: x.num_coords,
522+        lambda x: x.empty,
523+        lambda x: x.valid,
524+        lambda x: x.simple,
525+        lambda x: x.ring,
526+        lambda x: x.boundary,
527+        lambda x: x.convex_hull,
528+        lambda x: x.extend,
529+        lambda x: x.area,
530+        lambda x: x.length,
531+            )
532+
533+def mark_ring(locals, name_pat, length=SEQ_LENGTH):
534+    '''
535+    Accepts an array of functions which perform slice modifications
536+    and labels each function as to whether or not it preserves ring-ness
537+    '''
538+    func_array = [ val for name, val in locals.items()
539+                    if hasattr(val, '__call__')
540+                    and name.startswith(name_pat) ]
541+
542+    for i in xrange(len(func_array)):
543+        a = range(length)
544+        a[-1] = a[0]
545+        func_array[i](a)
546+        ring = len(a) == 0 or (len(a) > 3 and a[-1] == a[0])
547+        func_array[i].ring = ring
548+
549+    return func_array
550
551Property changes on: tests/pymutable_geometries.py
552___________________________________________________________________
553Name: svn:executable
554   + *
555