From 5146e2cf984a83c2eb9d8102ea73ee0792a9528b Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Tue, 24 Nov 2015 14:21:32 +0500 Subject: [PATCH] Fixed #25662 -- Allowed creation of empty GEOS geometries. --- django/contrib/gis/geos/collections.py | 3 - django/contrib/gis/geos/linestring.py | 10 ++- django/contrib/gis/geos/mutable_list.py | 2 +- django/contrib/gis/geos/point.py | 14 ++-- django/contrib/gis/geos/polygon.py | 6 +- .../contrib/gis/geos/prototypes/__init__.py | 11 +-- django/contrib/gis/geos/prototypes/geom.py | 1 + docs/ref/contrib/gis/geos.txt | 75 ++++++++++++++----- docs/releases/1.10.txt | 2 + tests/gis_tests/geos_tests/test_geos.py | 57 +++++++++++--- .../gis_tests/geos_tests/test_mutable_list.py | 8 +- 11 files changed, 140 insertions(+), 49 deletions(-) diff --git a/django/contrib/gis/geos/collections.py b/django/contrib/gis/geos/collections.py index 0f27d2bcf9..becc6b0ce3 100644 --- a/django/contrib/gis/geos/collections.py +++ b/django/contrib/gis/geos/collections.py @@ -25,9 +25,6 @@ class GeometryCollection(GEOSGeometry): "Initializes a Geometry Collection from a sequence of Geometry objects." # Checking the arguments - if not args: - raise TypeError('Must provide at least one Geometry to initialize %s.' % self.__class__.__name__) - if len(args) == 1: # If only one geometry provided or a list of geometries is provided # in the first argument. diff --git a/django/contrib/gis/geos/linestring.py b/django/contrib/gis/geos/linestring.py index d0edd631ce..cf74aad7fa 100644 --- a/django/contrib/gis/geos/linestring.py +++ b/django/contrib/gis/geos/linestring.py @@ -35,7 +35,14 @@ class LineString(ProjectInterpolateMixin, GEOSGeometry): if not (isinstance(coords, (tuple, list)) or numpy and isinstance(coords, numpy.ndarray)): raise TypeError('Invalid initialization input for LineStrings.') + # If SRID was passed in with the keyword arguments + srid = kwargs.get('srid') + ncoords = len(coords) + if not ncoords: + super(LineString, self).__init__(self._init_func(None), srid=srid) + return + if ncoords < self._minlength: raise ValueError( '%s requires at least %d points, got %s.' % ( @@ -80,9 +87,6 @@ class LineString(ProjectInterpolateMixin, GEOSGeometry): else: cs[i] = coords[i] - # If SRID was passed in with the keyword arguments - srid = kwargs.get('srid') - # Calling the base geometry initialization with the returned pointer # from the function. super(LineString, self).__init__(self._init_func(cs.ptr), srid=srid) diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py index 3fe9a4fbb6..8a613600c0 100644 --- a/django/contrib/gis/geos/mutable_list.py +++ b/django/contrib/gis/geos/mutable_list.py @@ -229,7 +229,7 @@ class ListMixin(object): # ### Private routines ### def _rebuild(self, newLen, newItems): - if newLen < self._minlength: + if newLen and newLen < self._minlength: raise ValueError('Must have at least %d items' % self._minlength) if self._maxlength is not None and newLen > self._maxlength: raise ValueError('Cannot have more than %d items' % self._maxlength) diff --git a/django/contrib/gis/geos/point.py b/django/contrib/gis/geos/point.py index 34a955e924..46972629d8 100644 --- a/django/contrib/gis/geos/point.py +++ b/django/contrib/gis/geos/point.py @@ -14,7 +14,7 @@ class Point(GEOSGeometry): _maxlength = 3 has_cs = True - def __init__(self, x, y=None, z=None, srid=None): + def __init__(self, x=None, y=None, z=None, srid=None): """ The Point object may be initialized with either a tuple, or individual parameters. @@ -23,22 +23,21 @@ class Point(GEOSGeometry): >>> p = Point((5, 23)) # 2D point, passed in as a tuple >>> p = Point(5, 23, 8) # 3D point, passed in with individual parameters """ - if isinstance(x, (tuple, list)): + if x is None: + coords = [] + elif isinstance(x, (tuple, list)): # Here a tuple or list was passed in under the `x` parameter. - ndim = len(x) coords = x elif isinstance(x, six.integer_types + (float,)) and isinstance(y, six.integer_types + (float,)): # Here X, Y, and (optionally) Z were passed in individually, as parameters. if isinstance(z, six.integer_types + (float,)): - ndim = 3 coords = [x, y, z] else: - ndim = 2 coords = [x, y] else: raise TypeError('Invalid parameters given for Point initialization.') - point = self._create_point(ndim, coords) + point = self._create_point(len(coords), coords) # Initializing using the address returned from the GEOS # createPoint factory. @@ -48,6 +47,9 @@ class Point(GEOSGeometry): """ Create a coordinate sequence, set X, Y, [Z], and create point """ + if not ndim: + return capi.create_point(None) + if ndim < 2 or ndim > 3: raise TypeError('Invalid point dimension: %s' % str(ndim)) diff --git a/django/contrib/gis/geos/polygon.py b/django/contrib/gis/geos/polygon.py index fdf7515bed..067730063f 100644 --- a/django/contrib/gis/geos/polygon.py +++ b/django/contrib/gis/geos/polygon.py @@ -29,7 +29,8 @@ class Polygon(GEOSGeometry): ... ((4, 4), (4, 6), (6, 6), (6, 4), (4, 4))) """ if not args: - raise TypeError('Must provide at least one LinearRing, or a tuple, to initialize a Polygon.') + super(Polygon, self).__init__(self._create_polygon(0, None), **kwargs) + return # Getting the ext_ring and init_holes parameters from the argument list ext_ring = args[0] @@ -73,6 +74,9 @@ class Polygon(GEOSGeometry): # _construct_ring will throw a TypeError if a parameter isn't a valid ring # If we cloned the pointers here, we wouldn't be able to clean up # in case of error. + if not length: + return capi.create_empty_polygon() + rings = [] for r in items: if isinstance(r, GEOM_PTR): diff --git a/django/contrib/gis/geos/prototypes/__init__.py b/django/contrib/gis/geos/prototypes/__init__.py index 595555897d..32073749d8 100644 --- a/django/contrib/gis/geos/prototypes/__init__.py +++ b/django/contrib/gis/geos/prototypes/__init__.py @@ -9,11 +9,12 @@ from django.contrib.gis.geos.prototypes.coordseq import ( # NOQA cs_gety, cs_getz, cs_setordinate, cs_setx, cs_sety, cs_setz, get_cs, ) from django.contrib.gis.geos.prototypes.geom import ( # NOQA - create_collection, create_linearring, create_linestring, create_point, - create_polygon, destroy_geom, from_hex, from_wkb, from_wkt, geom_clone, - geos_get_srid, geos_normalize, geos_set_srid, geos_type, geos_typeid, - get_dims, get_extring, get_geomn, get_intring, get_nrings, get_num_coords, - get_num_geoms, to_hex, to_wkb, to_wkt, + create_collection, create_empty_polygon, create_linearring, + create_linestring, create_point, create_polygon, destroy_geom, from_hex, + from_wkb, from_wkt, geom_clone, geos_get_srid, geos_normalize, + geos_set_srid, geos_type, geos_typeid, get_dims, get_extring, get_geomn, + get_intring, get_nrings, get_num_coords, get_num_geoms, to_hex, to_wkb, + to_wkt, ) from django.contrib.gis.geos.prototypes.misc import * # NOQA from django.contrib.gis.geos.prototypes.predicates import ( # NOQA diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index e4bd0b0a51..9e14f214dc 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -94,6 +94,7 @@ create_linearring = GeomOutput('GEOSGeom_createLinearRing', [CS_PTR]) # Polygon and collection creation routines are special and will not # have their argument types defined. create_polygon = GeomOutput('GEOSGeom_createPolygon', None) +create_empty_polygon = GeomOutput('GEOSGeom_createEmptyPolygon', None) create_collection = GeomOutput('GEOSGeom_createCollection', None) # Ring routines diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index e53acbc99c..74fb2e5e54 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -647,7 +647,7 @@ is returned instead. ``Point`` --------- -.. class:: Point(x, y, z=None, srid=None) +.. class:: Point(x=None, y=None, z=None, srid=None) ``Point`` objects are instantiated using arguments that represent the component coordinates of the point or with a single sequence @@ -656,6 +656,16 @@ is returned instead. >>> pnt = Point(5, 23) >>> pnt = Point([5, 23]) + Empty ``Point`` objects may be instantiated by passing no arguments or an + empty sequence. The following are equivalent:: + + >>> pnt = Point() + >>> pnt = Point([]) + + .. versionchanged:: 1.10 + + In previous versions, an empty ``Point`` couldn't be instantiated. + ``LineString`` -------------- @@ -674,6 +684,16 @@ is returned instead. >>> ls = LineString( ((0, 0), (1, 1)) ) >>> ls = LineString( [Point(0, 0), Point(1, 1)] ) + Empty ``LineString`` objects may be instantiated by passing no arguments + or an empty sequence. The following are equivalent:: + + >>> ls = LineString() + >>> ls = LineString([]) + + .. versionchanged:: 1.10 + + In previous versions, an empty ``LineString`` couldn't be instantiated. + ``LinearRing`` -------------- @@ -694,16 +714,20 @@ is returned instead. .. class:: Polygon(*args, **kwargs) - ``Polygon`` objects may be instantiated by passing in one or - more parameters that represent the rings of the polygon. The - parameters must either be :class:`LinearRing` instances, or - a sequence that may be used to construct a :class:`LinearRing`:: + ``Polygon`` objects may be instantiated by passing in parameters that + represent the rings of the polygon. The parameters must either be + :class:`LinearRing` instances, or a sequence that may be used to construct a + :class:`LinearRing`:: >>> ext_coords = ((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)) >>> int_coords = ((0.4, 0.4), (0.4, 0.6), (0.6, 0.6), (0.6, 0.4), (0.4, 0.4)) >>> poly = Polygon(ext_coords, int_coords) >>> poly = Polygon(LinearRing(ext_coords), LinearRing(int_coords)) + .. versionchanged:: 1.10 + + In previous versions, an empty ``Polygon`` couldn't be instantiated. + .. classmethod:: from_bbox(bbox) Returns a polygon object from the given bounding-box, a 4-tuple @@ -732,27 +756,35 @@ Geometry Collections .. class:: MultiPoint(*args, **kwargs) - ``MultiPoint`` objects may be instantiated by passing in one - or more :class:`Point` objects as arguments, or a single - sequence of :class:`Point` objects:: + ``MultiPoint`` objects may be instantiated by passing in :class:`Point` + objects as arguments, or a single sequence of :class:`Point` objects:: >>> mp = MultiPoint(Point(0, 0), Point(1, 1)) >>> mp = MultiPoint( (Point(0, 0), Point(1, 1)) ) + .. versionchanged:: 1.10 + + In previous versions, an empty ``MultiPoint`` couldn't be instantiated. + ``MultiLineString`` ------------------- .. class:: MultiLineString(*args, **kwargs) - ``MultiLineString`` objects may be instantiated by passing in one - or more :class:`LineString` objects as arguments, or a single - sequence of :class:`LineString` objects:: + ``MultiLineString`` objects may be instantiated by passing in + :class:`LineString` objects as arguments, or a single sequence of + :class:`LineString` objects:: >>> ls1 = LineString((0, 0), (1, 1)) >>> ls2 = LineString((2, 2), (3, 3)) >>> mls = MultiLineString(ls1, ls2) >>> mls = MultiLineString([ls1, ls2]) + .. versionchanged:: 1.10 + + In previous versions, an empty ``MultiLineString`` couldn't be + instantiated. + .. attribute:: merged Returns a :class:`LineString` representing the line merge of @@ -764,15 +796,19 @@ Geometry Collections .. class:: MultiPolygon(*args, **kwargs) - ``MultiPolygon`` objects may be instantiated by passing one or - more :class:`Polygon` objects as arguments, or a single sequence - of :class:`Polygon` objects:: + ``MultiPolygon`` objects may be instantiated by passing :class:`Polygon` + objects as arguments, or a single sequence of :class:`Polygon` objects:: >>> p1 = Polygon( ((0, 0), (0, 1), (1, 1), (0, 0)) ) >>> p2 = Polygon( ((1, 1), (1, 2), (2, 2), (1, 1)) ) >>> mp = MultiPolygon(p1, p2) >>> mp = MultiPolygon([p1, p2]) + .. versionchanged:: 1.10 + + In previous versions, an empty ``MultiPolygon`` couldn't be + instantiated. + .. attribute:: cascaded_union .. deprecated:: 1.10 @@ -789,14 +825,19 @@ Geometry Collections .. class:: GeometryCollection(*args, **kwargs) - ``GeometryCollection`` objects may be instantiated by passing in - one or more other :class:`GEOSGeometry` as arguments, or a single - sequence of :class:`GEOSGeometry` objects:: + ``GeometryCollection`` objects may be instantiated by passing in other + :class:`GEOSGeometry` as arguments, or a single sequence of + :class:`GEOSGeometry` objects:: >>> poly = Polygon( ((0, 0), (0, 1), (1, 1), (0, 0)) ) >>> gc = GeometryCollection(Point(0, 0), MultiPoint(Point(0, 0), Point(1, 1)), poly) >>> gc = GeometryCollection((Point(0, 0), MultiPoint(Point(0, 0), Point(1, 1)), poly)) + .. versionchanged:: 1.10 + + In previous versions, an empty ``GeometryCollection`` couldn't be + instantiated. + .. _prepared-geometries: Prepared Geometries diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index d932887a51..853955f936 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -92,6 +92,8 @@ Minor features :class:`~django.contrib.gis.db.models.functions.SymDifference` functions on MySQL. +* Added support for instantiating empty GEOS geometries. + :mod:`django.contrib.messages` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/gis_tests/geos_tests/test_geos.py b/tests/gis_tests/geos_tests/test_geos.py index da9db06c33..fd08e3037a 100644 --- a/tests/gis_tests/geos_tests/test_geos.py +++ b/tests/gis_tests/geos_tests/test_geos.py @@ -793,6 +793,9 @@ class GEOSTest(SimpleTestCase, TestDataMixin): p[:] = (1, 2, 3) self.assertEqual(p, Point(1, 2, 3)) + p[:] = () + self.assertEqual(p.wkt, Point()) + p[:] = (1, 2) self.assertEqual(p.wkt, Point(1, 2)) @@ -804,6 +807,9 @@ class GEOSTest(SimpleTestCase, TestDataMixin): def test_linestring_list_assignment(self): ls = LineString((0, 0), (1, 1)) + ls[:] = () + self.assertEqual(ls, LineString()) + ls[:] = ((0, 0), (1, 1), (2, 2)) self.assertEqual(ls, LineString((0, 0), (1, 1), (2, 2))) @@ -813,12 +819,34 @@ class GEOSTest(SimpleTestCase, TestDataMixin): def test_linearring_list_assignment(self): ls = LinearRing((0, 0), (0, 1), (1, 1), (0, 0)) + ls[:] = () + self.assertEqual(ls, LinearRing()) + ls[:] = ((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)) self.assertEqual(ls, LinearRing((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) with self.assertRaises(ValueError): ls[:] = ((0, 0), (1, 1), (2, 2)) + def test_polygon_list_assignment(self): + pol = Polygon() + + pol[:] = (((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)),) + self.assertEqual(pol, Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)),)) + + pol[:] = () + self.assertEqual(pol, Polygon()) + + def test_geometry_collection_list_assignment(self): + p = Point() + gc = GeometryCollection() + + gc[:] = [p] + self.assertEqual(gc, GeometryCollection(p)) + + gc[:] = () + self.assertEqual(gc, GeometryCollection()) + def test_threed(self): "Testing three-dimensional geometries." # Testing a 3D Point @@ -874,16 +902,27 @@ class GEOSTest(SimpleTestCase, TestDataMixin): def test_emptyCollections(self): "Testing empty geometries and collections." - gc1 = GeometryCollection([]) - gc2 = fromstr('GEOMETRYCOLLECTION EMPTY') - pnt = fromstr('POINT EMPTY') - ls = fromstr('LINESTRING EMPTY') - poly = fromstr('POLYGON EMPTY') - mls = fromstr('MULTILINESTRING EMPTY') - mpoly1 = fromstr('MULTIPOLYGON EMPTY') - mpoly2 = MultiPolygon(()) + geoms = [ + GeometryCollection([]), + fromstr('GEOMETRYCOLLECTION EMPTY'), + GeometryCollection(), + fromstr('POINT EMPTY'), + Point(), + fromstr('LINESTRING EMPTY'), + LineString(), + fromstr('POLYGON EMPTY'), + Polygon(), + fromstr('MULTILINESTRING EMPTY'), + MultiLineString(), + fromstr('MULTIPOLYGON EMPTY'), + MultiPolygon(()), + MultiPolygon(), + ] - for g in [gc1, gc2, pnt, ls, poly, mls, mpoly1, mpoly2]: + if numpy: + geoms.append(LineString(numpy.array([]))) + + for g in geoms: self.assertEqual(True, g.empty) # Testing len() and num_geom. diff --git a/tests/gis_tests/geos_tests/test_mutable_list.py b/tests/gis_tests/geos_tests/test_mutable_list.py index 67338ee865..8f2242ea31 100644 --- a/tests/gis_tests/geos_tests/test_mutable_list.py +++ b/tests/gis_tests/geos_tests/test_mutable_list.py @@ -299,18 +299,18 @@ class ListMixinTest(unittest.TestCase): def test08_min_length(self): 'Length limits' - pl, ul = self.lists_of_len() - ul._minlength = 1 + pl, ul = self.lists_of_len(5) + ul._minlength = 3 def delfcn(x, i): del x[:i] def setfcn(x, i): x[:i] = [] - for i in range(self.limit - ul._minlength + 1, self.limit + 1): + for i in range(len(ul) - ul._minlength + 1, len(ul)): self.assertRaises(ValueError, delfcn, ul, i) self.assertRaises(ValueError, setfcn, ul, i) - del ul[:ul._minlength] + del ul[:len(ul) - ul._minlength] ul._maxlength = 4 for i in range(0, ul._maxlength - len(ul)):