Repository: Toblerity/Shapely
Pull request #1017 (2020-11-04 10:22:16)

Deprecate the geometry .empty() method
I suppose this is more like an internal method in practice, but it's still publicly exposed, so worth officially deprecating (since it will go away in Shapely 2.0, where geometries are no longer mutable).

coveralls: 
[![Coverage Status](https://coveralls.io/builds/34704309/badge)](https://coveralls.io/builds/34704309)

Coverage increased (+0.01%) to 83.391% when pulling **e926c81eb6da456a4b03765f3f51d671ef23ad30 on jorisvandenbossche:deprecate-empty-method** into **92070f51cc32c8c38bed410997f55b10eff30348 on Toblerity:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/34704309/badge)](https://coveralls.io/builds/34704309)

Coverage increased (+0.01%) to 83.391% when pulling **e926c81eb6da456a4b03765f3f51d671ef23ad30 on jorisvandenbossche:deprecate-empty-method** into **92070f51cc32c8c38bed410997f55b10eff30348 on Toblerity:master**.


Patch:
diff --git a/shapely/coords.py b/shapely/coords.py
index 4d6124a..6a95b3f 100644
--- a/shapely/coords.py
+++ b/shapely/coords.py
@@ -47,7 +47,8 @@ class CoordinateSequence(object):
     def __len__(self):
         self._update()
         cs_len = c_uint(0)
-        lgeos.GEOSCoordSeq_getSize(self._cseq, byref(cs_len))
+        if self._cseq:
+            lgeos.GEOSCoordSeq_getSize(self._cseq, byref(cs_len))
         return cs_len.value
 
     def __iter__(self):
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 5a5746a..18028a2 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -181,6 +181,13 @@ class BaseGeometry(object):
     _lgeos = lgeos
 
     def empty(self, val=EMPTY):
+        warn(
+            "The 'empty()' method is deprecated and will be removed in "
+            "Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2)
+        self._empty(val=val)
+
+    def _empty(self, val=EMPTY):
         if not self._other_owned and self.__geom__ and self.__geom__ != EMPTY:
             try:
                 self._lgeos.GEOSGeom_destroy(self.__geom__)
@@ -198,7 +205,7 @@ class BaseGeometry(object):
         return self.__bool__()
 
     def __del__(self):
-        self.empty(val=None)
+        self._empty(val=None)
         self.__p__ = None
 
     def __str__(self):
@@ -209,7 +216,7 @@ class BaseGeometry(object):
         return (self.__class__, (), self.wkb)
 
     def __setstate__(self, state):
-        self.empty()
+        self._empty()
         self.__geom__ = deserialize_wkb(state)
         self._is_empty = False
         if lgeos.methods['has_z'](self.__geom__):
@@ -223,7 +230,7 @@ class BaseGeometry(object):
 
     @_geom.setter
     def _geom(self, val):
-        self.empty()
+        self._empty()
         self._is_empty = val in [EMPTY, None]
         self.__geom__ = val
 
@@ -302,8 +309,6 @@ class BaseGeometry(object):
 
     def _get_coords(self):
         """Access to geometry's coordinates (CoordinateSequence)"""
-        if self.is_empty:
-            return []
         return CoordinateSequence(self)
 
     def _set_coords(self, ob):
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index fcac541..2c05ca8 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -110,7 +110,7 @@ class LineString(BaseGeometry):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         ret = geos_linestring_from_py(coordinates)
         if ret is not None:
             self._geom, self._ndim = ret
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index 3378705..5a3aa53 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -155,7 +155,7 @@ class Point(BaseGeometry):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         if len(args) == 1:
             self._geom, self._ndim = geos_point_from_py(args[0])
         elif len(args) > 3:
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 7463d2b..e51b74d 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -70,7 +70,7 @@ class LinearRing(LineString):
             "Setting the 'coords' to mutate a Geometry in place is deprecated,"
             " and will not be possible any more in Shapely 2.0",
             ShapelyDeprecationWarning, stacklevel=2)
-        self.empty()
+        self._empty()
         ret = geos_linearring_from_py(coordinates)
         if ret is not None:
             self._geom, self._ndim = ret
@@ -251,7 +251,7 @@ class Polygon(BaseGeometry):
             if ret is not None:
                 self._geom, self._ndim = ret
             else:
-                self.empty()
+                self._empty()
 
     @property
     def exterior(self):
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index cdd22d6..8c38685 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -134,8 +134,8 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
             dz = 0
             if n == 3:
                 dz = cp[sm*i+2*sn]
-                
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, i, dx)
             GEOSCoordSeq_setY_r(handle, cs, i, dy)
@@ -194,8 +194,8 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
                     dz = coords[2]
                 except IndexError:
                     raise ValueError("Inconsistent coordinate dimensionality")
-            
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, i, dx)
             GEOSCoordSeq_setY_r(handle, cs, i, dy)
@@ -301,8 +301,8 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             dz = 0
             if n == 3:
                 dz = cp[sm*i+2*sn]
-        
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, i, dx)
             GEOSCoordSeq_setY_r(handle, cs, i, dy)
@@ -316,14 +316,14 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             dz = 0
             if n == 3:
                 dz = cp[2*sn]
-        
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, M-1, dx)
             GEOSCoordSeq_setY_r(handle, cs, M-1, dy)
             if n == 3:
                 GEOSCoordSeq_setZ_r(handle, cs, M-1, dz)
-            
+
     except AttributeError:
         # Fall back on list
         try:
@@ -366,7 +366,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
                 % update_ndim)
         else:
             cs = GEOSCoordSeq_create_r(handle, M, n)
-        
+
         # add to coordinate sequence
         for i in range(m):
             coords = _coords(ob[i])
@@ -378,8 +378,8 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
                     dz = coords[2]
                 except IndexError:
                     raise ValueError("Inconsistent coordinate dimensionality")
-        
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, i, dx)
             GEOSCoordSeq_setY_r(handle, cs, i, dy)
@@ -397,8 +397,8 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
                     dz = coords[2]
                 except IndexError:
                     raise ValueError("Inconsistent coordinate dimensionality")
-        
-            # Because of a bug in the GEOS C API, 
+
+            # Because of a bug in the GEOS C API,
             # always set X before Y
             GEOSCoordSeq_setX_r(handle, cs, M-1, dx)
             GEOSCoordSeq_setY_r(handle, cs, M-1, dy)
@@ -425,10 +425,11 @@ def coordseq_ctypes(self):
     m = self.__len__()
     array_type = ctypes.c_double * (m * n)
     data = array_type()
-    
-    cs = cast_seq(self._cseq)
+
+    if self._cseq:
+        cs = cast_seq(self._cseq)
     data_p = <double *><uintptr_t>ctypes.addressof(data)
-    
+
     for i in range(m):
         GEOSCoordSeq_getX_r(handle, cs, i, &temp)
         data_p[n*i] = temp
@@ -450,7 +451,8 @@ def coordseq_iter(self):
 
     cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
     cdef GEOSCoordSequence *cs
-    cs = cast_seq(self._cseq)
+    if self._cseq:
+        cs = cast_seq(self._cseq)
 
     has_z = self._ndim == 3
     for i in range(self.__len__()):
@@ -477,7 +479,7 @@ cdef GEOSCoordSequence* transform(GEOSCoordSequence* cs,
                                   double yoff,
                                   double zoff):
     """Performs an affine transformation on a GEOSCoordSequence
-    
+
     Returns the transformed coordinate sequence
     """
     cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
@@ -485,11 +487,11 @@ cdef GEOSCoordSequence* transform(GEOSCoordSequence* cs,
     cdef GEOSCoordSequence *cs_t
     cdef double x, y, z
     cdef double x_t, y_t, z_t
-    
+
     # create a new coordinate sequence with the same size and dimensions
     GEOSCoordSeq_getSize_r(handle, cs, &m)
     cs_t = GEOSCoordSeq_create_r(handle, m, ndim)
-    
+
     # perform the transform
     if ndim == 2:
         for n in range(0, m):
@@ -510,7 +512,7 @@ cdef GEOSCoordSequence* transform(GEOSCoordSequence* cs,
             GEOSCoordSeq_setX_r(handle, cs_t, n, x_t)
             GEOSCoordSeq_setY_r(handle, cs_t, n, y_t)
             GEOSCoordSeq_setZ_r(handle, cs_t, n, z_t)
-    
+
     return cs_t
 
 cpdef affine_transform(geom, matrix):
@@ -533,7 +535,7 @@ cpdef affine_transform(geom, matrix):
             matrix = a, b, d, e, xoff, yoff
     else:
         raise ValueError("'matrix' expects either 6 or 12 coefficients")
-    
+
     cdef GEOSContextHandle_t handle = cast_handle(lgeos.geos_handle)
     cdef GEOSCoordSequence *cs
     cdef GEOSCoordSequence *cs_t
@@ -542,15 +544,15 @@ cpdef affine_transform(geom, matrix):
     cdef int m, n
     cdef double x, y, z
     cdef double x_t, y_t, z_t
-    
+
     # Process coordinates from each supported geometry type
     if geom.type in ('Point', 'LineString', 'LinearRing'):
         the_geom = cast_geom(geom._geom)
         cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, the_geom)
-        
+
         # perform the transformation
         cs_t = transform(cs, ndim, a, b, c, d, e, f, g, h, i, xoff, yoff, zoff)
-        
+
         # create a new geometry from the coordinate sequence
         if geom.type == 'Point':
             the_geom_t = GEOSGeom_createPoint_r(handle, cs_t)
@@ -558,7 +560,7 @@ cpdef affine_transform(geom, matrix):
             the_geom_t = GEOSGeom_createLineString_r(handle, cs_t)
         elif geom.type == 'LinearRing':
             the_geom_t = GEOSGeom_createLinearRing_r(handle, cs_t)
-        
+
         # return the geometry as a Python object
         return geom_factory(<uintptr_t>the_geom_t)
     elif geom.type == 'Polygon':


Test patch:
diff --git a/tests/test_emptiness.py b/tests/test_emptiness.py
index 10bfe72..931ae1e 100644
--- a/tests/test_emptiness.py
+++ b/tests/test_emptiness.py
@@ -1,4 +1,4 @@
-from . import unittest
+from . import unittest, shapely20_deprecated
 
 from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry.base import BaseGeometry, EmptyGeometry
@@ -22,6 +22,7 @@ class EmptinessTestCase(unittest.TestCase):
         g = BaseGeometry()
         self.assertTrue(g._is_empty)
 
+    @shapely20_deprecated
     def test_emptying_point(self):
         p = sgeom.Point(0, 0)
         self.assertFalse(p._is_empty)
diff --git a/tests/test_linestring.py b/tests/test_linestring.py
index 9ad07cb..2b6e7f9 100644
--- a/tests/test_linestring.py
+++ b/tests/test_linestring.py
@@ -1,6 +1,7 @@
 from . import unittest, numpy, shapely20_deprecated
 import pytest
 
+from shapely.coords import CoordinateSequence
 from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry import LineString, asLineString, Point, LinearRing
 
@@ -69,10 +70,12 @@ def test_from_generator():
 def test_from_empty():
     line = LineString()
     assert line.is_empty
+    assert isinstance(line.coords, CoordinateSequence)
     assert line.coords[:] == []
 
     line = LineString([])
     assert line.is_empty
+    assert isinstance(line.coords, CoordinateSequence)
     assert line.coords[:] == []
 
 
@@ -84,6 +87,16 @@ def test_from_numpy():
     assert line.coords[:] == [(1.0, 2.0), (3.0, 4.0)]
 
 
+def test_numpy_empty_linestring_coords():
+    np = pytest.importorskip("numpy")
+
+    # Check empty
+    line = LineString([])
+    la = np.asarray(line.coords)
+
+    assert la.shape == (0,)
+
+
 def test_from_invalid_dim():
     with pytest.raises(ValueError, match="at least 2 coordinate tuples"):
         LineString([(1, 2)])
@@ -105,7 +118,7 @@ def test_from_single_coordinate():
     coords = [[-122.185933073564, 37.3629353839073]]
     with pytest.raises(ValueError):
         ls = LineString(coords)
-        ls.geom_type # caused segfault before fix
+        ls.geom_type  # caused segfault before fix
 
 
 class LineStringTestCase(unittest.TestCase):
@@ -202,15 +215,14 @@ class LineStringTestCase(unittest.TestCase):
         assert_array_equal(la, expected)
 
     @unittest.skipIf(not numpy, 'Numpy required')
-    def test_numpy_coords(self):
-        import numpy as np
+    def test_numpy_linestring_coords(self):
         from numpy.testing import assert_array_equal
 
         line = LineString(((1.0, 2.0), (3.0, 4.0)))
-        expected = np.array([[1.0, 2.0], [3.0, 4.0]])
+        expected = numpy.array([[1.0, 2.0], [3.0, 4.0]])
 
         # Coordinate sequences can be adapted as well
-        la = np.asarray(line.coords)
+        la = numpy.asarray(line.coords)
         assert_array_equal(la, expected)
 
     @shapely20_deprecated
diff --git a/tests/test_point.py b/tests/test_point.py
index a2316bd..264118c 100644
--- a/tests/test_point.py
+++ b/tests/test_point.py
@@ -1,4 +1,5 @@
 from . import unittest, numpy, shapely20_deprecated
+from shapely.coords import CoordinateSequence
 from shapely.geometry import Point, asPoint
 from shapely.errors import DimensionError, ShapelyDeprecationWarning
 
@@ -19,6 +20,7 @@ def test_from_coordinates():
     # empty
     p = Point()
     assert p.is_empty
+    assert isinstance(p.coords, CoordinateSequence)
     assert p.coords[:] == []
 
 
@@ -76,7 +78,7 @@ def test_from_invalid():
         Point(1, 2, 3, 4)
 
 
-class LineStringTestCase(unittest.TestCase):
+class PointTestCase(unittest.TestCase):
 
     def test_point(self):
 
@@ -244,6 +246,7 @@ def test_point_ctypes_deprecated():
     with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
         p.ctypes is not None
 
+
 def test_point_array_interface_deprecated():
     p = Point(3.0, 4.0)
     with pytest.warns(ShapelyDeprecationWarning, match="array_interface"):
@@ -257,3 +260,13 @@ def test_point_array_interface_numpy_deprecated():
     p = Point(3.0, 4.0)
     with pytest.warns(ShapelyDeprecationWarning, match="array interface"):
         np.array(p)
+
+
+def test_numpy_empty_point_coords():
+    np = pytest.importorskip("numpy")
+
+    pe = Point()
+
+    # Access the coords
+    a = np.asarray(pe.coords)
+    assert a.shape == (0,)
diff --git a/tests/test_polygon.py b/tests/test_polygon.py
index 47efcbf..2317a0e 100644
--- a/tests/test_polygon.py
+++ b/tests/test_polygon.py
@@ -5,6 +5,7 @@ from . import unittest, numpy, shapely20_deprecated
 
 import pytest
 
+from shapely.coords import CoordinateSequence
 from shapely.wkb import loads as load_wkb
 from shapely.errors import TopologicalError, ShapelyDeprecationWarning
 from shapely.geos import lgeos
@@ -76,10 +77,12 @@ def test_linearring_from_generator():
 def test_linearring_from_empty():
     ring = LinearRing()
     assert ring.is_empty
+    assert isinstance(ring.coords, CoordinateSequence)
     assert ring.coords[:] == []
 
     ring = LinearRing([])
     assert ring.is_empty
+    assert isinstance(ring.coords, CoordinateSequence)
     assert ring.coords[:] == []
 
 
@@ -88,8 +91,25 @@ def test_linearring_from_numpy():
     np = pytest.importorskip("numpy")
     coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
 
-    line = LineString(np.array(coords))
-    assert line.coords[:] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
+    ring = LinearRing(np.array(coords))
+    assert ring.coords[:] == [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
+
+
+def test_numpy_linearring_coords():
+    np = pytest.importorskip("numpy")
+    from numpy.testing import assert_array_equal
+
+    ring = LinearRing(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0)))
+    ra = np.asarray(ring.coords)
+    expected = np.asarray([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (0.0, 0.0)])
+    assert_array_equal(ra, expected)
+
+
+def test_numpy_empty_linearring_coords():
+    np = pytest.importorskip("numpy")
+
+    ring = LinearRing()
+    assert np.asarray(ring.coords).shape == (0,)
 
 
 def test_polygon_from_coordinate_sequence():
@@ -98,11 +118,11 @@ def test_polygon_from_coordinate_sequence():
     # Construct a polygon, exterior ring only
     polygon = Polygon(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0)))
     assert polygon.exterior.coords[:] == coords
-    assert len(polygon.interiors) ==  0
+    assert len(polygon.interiors) == 0
 
     polygon = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0)])
     assert polygon.exterior.coords[:] == coords
-    assert len(polygon.interiors) ==  0
+    assert len(polygon.interiors) == 0
 
 
 def test_polygon_from_coordinate_sequence_with_holes():
@@ -122,7 +142,7 @@ def test_polygon_from_linearring():
 
     polygon = Polygon(ring)
     assert polygon.exterior.coords[:] == coords
-    assert len(polygon.interiors) ==  0
+    assert len(polygon.interiors) == 0
 
 
 def test_polygon_from_polygon():
@@ -295,7 +315,7 @@ class PolygonTestCase(unittest.TestCase):
     def test_dimensions(self):
 
         # Background: see http://trac.gispython.org/lab/ticket/168
-    # http://lists.gispython.org/pipermail/community/2008-August/001859.html
+        # http://lists.gispython.org/pipermail/community/2008-August/001859.html
 
         coords = ((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0),
                   (1.0, 0.0, 0.0))