Repository: Toblerity/Shapely
Pull request #955 (2020-08-05 15:10:18)

Deprecate ctypes attribute and array interface
Deprecating the ctypes attribute and the array_interface.

See https://github.com/shapely/shapely-rfc/pull/1/files#diff-554e526a1f0fe104169383c013e37a7cR360 for the rationale

The array interface is kept intact for the CoordinateSequence, so occurence of `np.array(geom)` can be replaced with `np.array(geom.coords)`. 
(we should eventually implement this differently (not using ctypes), but that can be left for later or even for Shapely 2.0)


Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 39112a3..ecd9732 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,6 +12,12 @@ New features:
 1.7.1 (TBD)
 -----------
 
+- Get free() not from libc but from the processes global symbols (#891),
+  fixing a bug that manifests on OS X 10.15 and 10.16.
+- Extracting substrings from complex lines has been made more correct (#848,
+  #849).
+- Splitting of complex geometries has been sped up by preparing the input
+  geometry (#871).
 - Fix bug in concatenation of function argtypes (#866).
 - Improved documentation of STRtree usage (#857).
 - Improved handling for empty list or list of lists in GeoJSON coordinates
diff --git a/docs/manual.rst b/docs/manual.rst
index ed89b0a..895debe 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -1228,8 +1228,9 @@ interior point in common.
 
 .. method:: object.overlaps(other)
 
-  Returns ``True`` if the objects intersect (see above) but neither contains
-  the other.
+  Returns ``True`` if the geometries have more than one but not all points in common,
+  have the same dimension, and the intersection of the interiors of the geometries
+  has the same dimension as the geometries themselves.
 
 .. method:: object.touches(other)
 
@@ -1531,6 +1532,28 @@ boundaries.
   of many objects. See :func:`shapely.ops.unary_union` for a more effective
   method.
 
+Several of these set-theoretic methods can be invoked using overloaded operators:
+
+- `intersection` can be accessed with and, `&`
+- `union` can be accessed with or, `|`
+- `difference` can be accessed with minus, `-`
+- `symmetric_difference` can be accessed with xor, `^`
+
+
+.. code-block:: pycon
+>>> from shapely import wkt
+>>> p1 = wkt.loads('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))')
+>>> p2 = wkt.loads('POLYGON((0.5 0, 1.5 0, 1.5 1, 0.5 1, 0.5 0))')
+>>> (p1 & p2).wkt
+'POLYGON ((1 0, 0.5 0, 0.5 1, 1 1, 1 0))'
+>>> (p1 | p2).wkt
+'POLYGON ((0.5 0, 0 0, 0 1, 0.5 1, 1 1, 1.5 1, 1.5 0, 1 0, 0.5 0))'
+>>> (p1 - p2).wkt
+'POLYGON ((0.5 0, 0 0, 0 1, 0.5 1, 0.5 0))'
+>>> (p1 ^ p2).wkt
+'MULTIPOLYGON (((0.5 0, 0 0, 0 1, 0.5 1, 0.5 0)), ((1 0, 1 1, 1.5 1, 1.5 0, 1 0)))'
+
+
 Constructive Methods
 --------------------
 
@@ -2632,6 +2655,11 @@ counterparts. To serialize a geometric object to a binary or text string, use
 ``dumps()``. To deserialize a string and get a new geometric object of the
 appropriate type, use ``loads()``.
 
+The default settings for the wkt attribute and `shapely.wkt.dumps()` function
+are different. By default, the attribute's value is trimmed of excess decimals,
+while this is not the case for `dumps()`, though it can be replicated by setting
+`trim=True`.
+
 .. function:: shapely.wkb.dumps(ob)
 
   Returns a WKB representation of `ob`.
@@ -2642,18 +2670,23 @@ appropriate type, use ``loads()``.
 
 .. code-block:: pycon
 
-  >> from shapely.wkb import dumps, loads
-  >>> wkb = dumps(Point(0, 0))
-  >>> wkb.encode('hex')
+  >>> from shapely import wkb
+  >>> pt = Point(0, 0)
+  >>> wkb.dumps(pt)
+  b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+  >>> pt.wkb
+  b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+  >>> pt.wkb_hex
   '010100000000000000000000000000000000000000'
-  >>> loads(wkb).wkt
-  'POINT (0.0000000000000000 0.0000000000000000)'
+  >>> wkb.loads(pt.wkb).wkt
+  'POINT (0 0)'
 
 All of Shapely's geometry types are supported by these functions.
 
 .. function:: shapely.wkt.dumps(ob)
 
-  Returns a WKT representation of `ob`.
+  Returns a WKT representation of `ob`. Several keyword arguments are available
+  to alter the WKT which is returned; see the docstrings for more details.
 
 .. function:: shapely.wkt.loads(wkt)
 
@@ -2661,11 +2694,14 @@ All of Shapely's geometry types are supported by these functions.
 
 .. code-block:: pycon
 
-  >> from shapely.wkt import dumps, loads
-  >> wkt = dumps(Point(0, 0))
-  >>> wkt
+  >>> from shapely import wkt
+  >>> pt = Point(0, 0)
+  >>> thewkt = wkt.dumps(pt)
+  >>> thewkt
   'POINT (0.0000000000000000 0.0000000000000000)'
-  >>> loads(wkt).wkt
+  >>> pt.wkt
+  'POINT (0 0)'
+  >>> wkt.dumps(pt, trim=True)
   'POINT (0 0)'
 
 Numpy and Python Arrays
diff --git a/shapely/coords.py b/shapely/coords.py
index 05746e1..4d6124a 100644
--- a/shapely/coords.py
+++ b/shapely/coords.py
@@ -4,7 +4,9 @@
 import sys
 from array import array
 from ctypes import byref, c_double, c_uint
+import warnings
 
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.topology import Validating
 
@@ -100,7 +102,7 @@ class CoordinateSequence(object):
             raise TypeError("key must be an index or slice")
 
     @property
-    def ctypes(self):
+    def _ctypes(self):
         self._update()
         has_z = self._ndim == 3
         n = self._ndim
@@ -118,6 +120,14 @@ class CoordinateSequence(object):
                 data[n*i+2] = temp.value
         return data
 
+    @property
+    def ctypes(self):
+        warnings.warn(
+            "Accessing the 'ctypes' attribute is deprecated,"
+            " and will not be possible any more in Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._ctypes
+
     def array_interface(self):
         """Provide the Numpy array protocol."""
         if sys.byteorder == 'little':
@@ -130,13 +140,13 @@ class CoordinateSequence(object):
         ai = {
             'version': 3,
             'typestr': typestr,
-            'data': self.ctypes,
+            'data': self._ctypes,
             }
         ai.update({'shape': (len(self), self._ndim)})
         return ai
-    
+
     __array_interface__ = property(array_interface)
-    
+
     @property
     def xy(self):
         """X and Y arrays"""
diff --git a/shapely/errors.py b/shapely/errors.py
index 095c4a3..a7c293b 100644
--- a/shapely/errors.py
+++ b/shapely/errors.py
@@ -38,3 +38,6 @@ class ShapelyDeprecationWarning(FutureWarning):
     Warning for features that will be removed or behaviour that will be
     changed in a future release.
     """
+
+class EmptyPartError(ShapelyError):
+    """An error signifying an empty part was encountered when creating a multi-part."""
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index a5c85d7..c08b526 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -17,6 +17,7 @@ from functools import wraps
 from shapely.affinity import affine_transform
 from shapely.coords import CoordinateSequence
 from shapely.errors import WKBReadingError, WKTReadingError
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geos import WKBWriter, WKTWriter
 from shapely.geos import lgeos
 from shapely.impl import DefaultImplementation, delegated
@@ -287,13 +288,21 @@ class BaseGeometry(object):
     # Array and ctypes interfaces
     # ---------------------------
 
+    @property
+    def _ctypes(self):
+        raise NotImplementedError
+
     @property
     def ctypes(self):
         """Return ctypes buffer"""
-        raise NotImplementedError
+        warn(
+            "Accessing the 'ctypes' attribute is deprecated,"
+            " and will not be possible any more in Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._ctypes
 
     @property
-    def array_interface_base(self):
+    def _array_interface_base(self):
         if sys.byteorder == 'little':
             typestr = '<f8'
         elif sys.byteorder == 'big':
@@ -304,9 +313,17 @@ class BaseGeometry(object):
         return {
             'version': 3,
             'typestr': typestr,
-            'data': self.ctypes,
+            'data': self._ctypes,
             }
 
+    @property
+    def array_interface_base(self):
+        warn(
+            "The 'array_interface_base' property is deprecated and will be "
+            "removed in Shapely 2.0.",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._array_interface_base()
+
     @property
     def __array_interface__(self):
         """Provide the Numpy array protocol."""
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index 47b7f71..fcac541 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -75,12 +75,12 @@ class LineString(BaseGeometry):
             ).format(pnt_format, 2. * scale_factor, stroke_color)
 
     @property
-    def ctypes(self):
+    def _ctypes(self):
         if not self._ctypes_data:
-            self._ctypes_data = self.coords.ctypes
+            self._ctypes_data = self.coords._ctypes
         return self._ctypes_data
 
-    def array_interface(self):
+    def _array_interface(self):
         """Provide the Numpy array protocol."""
         if self.is_empty:
             ai = {'version': 3, 'typestr': '<f8', 'shape': (0,), 'data': (c_double * 0)()}
@@ -88,7 +88,21 @@ class LineString(BaseGeometry):
             ai = self.coords.array_interface()
         return ai
 
-    __array_interface__ = property(array_interface)
+    def array_interface(self):
+        """Provide the Numpy array protocol."""
+        warnings.warn(
+            "The 'array_interface' method is deprecated and will be removed "
+            "in Shapely 2.0.",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._array_interface()
+
+    @property
+    def __array_interface__(self):
+        warnings.warn(
+            "The array interface is deprecated and will no longer work in "
+            "Shapely 2.0. Convert the '.coords' to a numpy array instead.",
+            ShapelyDeprecationWarning, stacklevel=3)
+        return self._array_interface()
 
     # Coordinate access
     def _set_coords(self, coordinates):
diff --git a/shapely/geometry/multilinestring.py b/shapely/geometry/multilinestring.py
index 3b7baf2..1235470 100644
--- a/shapely/geometry/multilinestring.py
+++ b/shapely/geometry/multilinestring.py
@@ -4,7 +4,7 @@
 from ctypes import c_void_p, cast
 import warnings
 
-from shapely.errors import ShapelyDeprecationWarning
+from shapely.errors import EmptyPartError, ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.geometry.base import BaseMultipartGeometry, geos_geom_from_py
 from shapely.geometry import linestring
@@ -135,6 +135,10 @@ def geos_multilinestring_from_py(ob):
     # add to coordinate sequence
     for l in range(L):
         geom, ndims = linestring.geos_linestring_from_py(obs[l])
+
+        if lgeos.GEOSisEmpty(geom):
+            raise EmptyPartError("Can't create MultiLineString with empty component")
+
         subs[l] = cast(geom, c_void_p)
             
     return (lgeos.GEOSGeom_createCollection(5, subs, L), N)
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index f57c73e..16cd04b 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -4,7 +4,7 @@
 from ctypes import byref, c_double, c_void_p, cast
 import warnings
 
-from shapely.errors import ShapelyDeprecationWarning
+from shapely.errors import EmptyPartError, ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.geometry.base import (
     BaseMultipartGeometry, exceptNull, geos_geom_from_py)
@@ -84,7 +84,7 @@ class MultiPoint(BaseMultipartGeometry):
 
     @property
     @exceptNull
-    def ctypes(self):
+    def _ctypes(self):
         if not self._ctypes_data:
             temp = c_double()
             n = self._ndim
@@ -104,13 +104,36 @@ class MultiPoint(BaseMultipartGeometry):
             self._ctypes_data = data
         return self._ctypes_data
 
+    @property
+    def ctypes(self):
+        warnings.warn(
+            "Accessing the 'ctypes' attribute is deprecated,"
+            " and will not be possible any more in Shapely 2.0",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._ctypes
+
     @exceptNull
-    def array_interface(self):
+    def _array_interface(self):
         """Provide the Numpy array protocol."""
-        ai = self.array_interface_base
+        ai = self._array_interface_base
         ai.update({'shape': (len(self.geoms), self._ndim)})
         return ai
-    __array_interface__ = property(array_interface)
+
+    def array_interface(self):
+        """Provide the Numpy array protocol."""
+        warnings.warn(
+            "The 'array_interface' method is deprecated and will be removed "
+            "in Shapely 2.0.",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._array_interface()
+
+    @property
+    def __array_interface__(self):
+        warnings.warn(
+            "The array interface is deprecated and will no longer work in "
+            "Shapely 2.0. Convert the '.coords' to a numpy array instead.",
+            ShapelyDeprecationWarning, stacklevel=3)
+        return self._array_interface()
 
 
 class MultiPointAdapter(CachingGeometryProxy, MultiPoint):
@@ -172,6 +195,10 @@ def geos_multipoint_from_py(ob):
     for i in range(m):
         coords = ob[i]
         geom, ndims = point.geos_point_from_py(coords)
+
+        if lgeos.GEOSisEmpty(geom):
+            raise EmptyPartError("Can't create MultiPoint with empty component")
+
         subs[i] = cast(geom, c_void_p)
 
     return lgeos.GEOSGeom_createCollection(4, subs, m), n
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index fef3cc7..ad33ed7 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -102,7 +102,7 @@ class Point(BaseGeometry):
             ).format(self, 3. * scale_factor, 1. * scale_factor, fill_color)
 
     @property
-    def ctypes(self):
+    def _ctypes(self):
         if not self._ctypes_data:
             array_type = c_double * self._ndim
             array = array_type()
@@ -114,15 +114,30 @@ class Point(BaseGeometry):
             self._ctypes_data = array
         return self._ctypes_data
 
-    def array_interface(self):
+    def _array_interface(self):
         """Provide the Numpy array protocol."""
         if self.is_empty:
             ai = {'version': 3, 'typestr': '<f8', 'shape': (0,), 'data': (c_double * 0)()}
         else:
-            ai = self.array_interface_base
+            ai = self._array_interface_base
             ai.update({'shape': (self._ndim,)})
         return ai
-    __array_interface__ = property(array_interface)
+
+    def array_interface(self):
+        """Provide the Numpy array protocol."""
+        warnings.warn(
+            "The 'array_interface' method is deprecated and will be removed "
+            "in Shapely 2.0.",
+            ShapelyDeprecationWarning, stacklevel=2)
+        return self._array_interface()
+
+    @property
+    def __array_interface__(self):
+        warnings.warn(
+            "The array interface is deprecated and will no longer work in "
+            "Shapely 2.0. Convert the '.coords' to a numpy array instead.",
+            ShapelyDeprecationWarning, stacklevel=3)
+        return self._array_interface()
 
     @property
     def bounds(self):
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 66edb16..7463d2b 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -297,9 +297,9 @@ class Polygon(BaseGeometry):
     __hash__ = None
 
     @property
-    def ctypes(self):
+    def _ctypes(self):
         if not self._ctypes_data:
-            self._ctypes_data = self.exterior.ctypes
+            self._ctypes_data = self.exterior._ctypes
         return self._ctypes_data
 
     @property
diff --git a/shapely/speedups/__init__.py b/shapely/speedups/__init__.py
index c294a20..1bbb04c 100644
--- a/shapely/speedups/__init__.py
+++ b/shapely/speedups/__init__.py
@@ -53,8 +53,8 @@ def enable():
     if _orig:
         return
 
-    _orig['CoordinateSequence.ctypes'] = coords.CoordinateSequence.ctypes
-    coords.CoordinateSequence.ctypes = property(_speedups.coordseq_ctypes)
+    _orig['CoordinateSequence._ctypes'] = coords.CoordinateSequence._ctypes
+    coords.CoordinateSequence._ctypes = property(_speedups.coordseq_ctypes)
 
     _orig['CoordinateSequence.__iter__'] = coords.CoordinateSequence.__iter__
     coords.CoordinateSequence.__iter__ = method_wrapper(
@@ -84,7 +84,7 @@ def disable():
     if not _orig:
         return
 
-    coords.CoordinateSequence.ctypes = _orig['CoordinateSequence.ctypes']
+    coords.CoordinateSequence._ctypes = _orig['CoordinateSequence._ctypes']
     coords.CoordinateSequence.__iter__ = _orig['CoordinateSequence.__iter__']
     linestring.geos_linestring_from_py = _orig['geos_linestring_from_py']
     polygon.geos_linearring_from_py = _orig['geos_linearring_from_py']
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index e669b2e..cdd22d6 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -239,7 +239,10 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             elif GEOSisClosed_r(handle, g) and m >= 4:
                 cs = GEOSCoordSeq_clone_r(handle, cs)
                 return <uintptr_t>GEOSGeom_createLinearRing_r(handle, cs), n
-            # else continue below.
+            else:
+                # else continue below.
+                # (and extract coords to avoid array interface of LineString)
+                ob = ob.coords
 
     # If numpy is present, we use numpy.require to ensure that we have a
     # C-continguous array that owns its data. View data will be copied.
diff --git a/shapely/strtree.py b/shapely/strtree.py
index e0f9611..40da40b 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -75,13 +75,26 @@ class STRtree:
         # filter empty geometries out of the input
         geoms = [geom for geom in geoms if not geom.is_empty]
         self._n_geoms = len(geoms)
+
+        self._init_tree_handle(geoms)
+
+        # Keep references to geoms.
+        self._geoms = list(geoms)
+
+    def _init_tree_handle(self, geoms):
         # GEOS STRtree capacity has to be > 1
         self._tree_handle = lgeos.GEOSSTRtree_create(max(2, len(geoms)))
         for geom in geoms:
             lgeos.GEOSSTRtree_insert(self._tree_handle, geom._geom, ctypes.py_object(geom))
 
-        # Keep references to geoms.
-        self._geoms = list(geoms)
+    def __getstate__(self):
+        state = self.__dict__.copy()
+        del state["_tree_handle"]
+        return state
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        self._init_tree_handle(self._geoms)
 
     def __del__(self):
         if self._tree_handle is not None:
diff --git a/shapely/wkt.py b/shapely/wkt.py
index a6f3a46..cf98671 100644
--- a/shapely/wkt.py
+++ b/shapely/wkt.py
@@ -5,22 +5,83 @@ from shapely import geos
 
 # Pickle-like convenience functions
 
+
 def loads(data):
-    """Load a geometry from a WKT string."""
+    """
+    Load a geometry from a WKT string.
+
+    Parameters
+    ----------
+    data : str
+        A WKT string
+
+    Returns
+    -------
+    Shapely geometry object
+    """
     return geos.WKTReader(geos.lgeos).read(data)
 
+
 def load(fp):
-    """Load a geometry from an open file."""
+    """
+    Load a geometry from an open file.
+
+    Parameters
+    ----------
+    fp :
+        A file-like object which implements a `read` method.
+
+    Returns
+    -------
+    Shapely geometry object
+    """
     data = fp.read()
     return loads(data)
 
+
 def dumps(ob, trim=False, **kw):
-    """Dump a WKT representation of a geometry to a string.
+    """
+    Dump a WKT representation of a geometry to a string.
 
-    See available keyword output settings in ``shapely.geos.WKTWriter``.
+    Parameters
+    ----------
+    ob :
+        A geometry object of any type to be dumped to WKT.
+    trim : bool, default False
+        Remove excess decimals from the WKT.
+    rounding_precision (GEOS 3.3+) : int
+        Round output to the specified number of digits.
+        Default behavior returns full precision.
+    output_dimension (GEOS 3.3+): int, default 3
+        Force removal of dimensions above the one specified.
+
+    Returns
+    -------
+    input geometry as WKT string
     """
     return geos.WKTWriter(geos.lgeos, trim=trim, **kw).write(ob)
 
+
 def dump(ob, fp, **settings):
-    """Dump a geometry to an open file."""
+    """
+    Dump a geometry to an open file.
+
+    Parameters
+    ----------
+    ob :
+        A geometry object of any type to be dumped to WKT.
+    fp :
+        A file-like object which implements a `write` method.
+    trim : bool, default False
+        Remove excess decimals from the WKT.
+    rounding_precision (GEOS 3.3+) : int
+        Round output to the specified number of digits.
+        Default behavior returns full precision.
+    output_dimension (GEOS 3.3+): int, default 3
+        Force removal of dimensions above the one specified.
+
+    Returns
+    -------
+    None
+    """
     fp.write(dumps(ob, **settings))


Test patch:
diff --git a/tests/test_collection.py b/tests/test_collection.py
index 60c48cd..e707507 100644
--- a/tests/test_collection.py
+++ b/tests/test_collection.py
@@ -1,4 +1,5 @@
-from . import unittest, shapely20_deprecated
+from shapely import wkt
+from . import shapely20_deprecated
 
 from shapely.errors import ShapelyDeprecationWarning
 from shapely.geometry import LineString
@@ -9,101 +10,88 @@ from shapely.geometry import asShape
 import pytest
 
 
-class CollectionTestCase(unittest.TestCase):
-
-    def test_array_interface(self):
-        m = GeometryCollection()
-        self.assertEqual(len(m), 0)
-        self.assertEqual(m.geoms, [])
-
-    def test_child_with_deleted_parent(self):
-        # test that we can remove a collection while having
-        # childs around
-        a = LineString([(0, 0), (1, 1), (1, 2), (2, 2)])
-        b = LineString([(0, 0), (1, 1), (2, 1), (2, 2)])
-        collection = a.intersection(b)
-
-        child = collection.geoms[0]
-        # delete parent of child
-        del collection
-
-        # access geometry, this should not seg fault as 1.2.15 did
-        self.assertIsNotNone(child.wkt)
-
-    @shapely20_deprecated
-    def test_geointerface_adapter(self):
-        d = {"type": "GeometryCollection","geometries": [
-                {"type": "Point", "coordinates": (0, 3)},
-                {"type": "LineString", "coordinates": ((2, 0), (1, 0))}
-            ]}
-
-        # asShape
-        m = asShape(d)
-        self.assertEqual(m.geom_type, "GeometryCollection")
-        self.assertEqual(len(m), 2)
-        geom_types = [g.geom_type for g in m.geoms]
-        self.assertIn("Point", geom_types)
-        self.assertIn("LineString", geom_types)
-
-    def test_geointerface(self):
-        d = {"type": "GeometryCollection","geometries": [
-                {"type": "Point", "coordinates": (0, 3)},
-                {"type": "LineString", "coordinates": ((2, 0), (1, 0))}
-            ]}
-
-        # shape
-        m = shape(d)
-        self.assertEqual(m.geom_type, "GeometryCollection")
-        self.assertEqual(len(m), 2)
-        geom_types = [g.geom_type for g in m.geoms]
-        self.assertIn("Point", geom_types)
-        self.assertIn("LineString", geom_types)
-
-    @shapely20_deprecated
-    def test_empty_geointerface_adapter(self):
-        d = {"type": "GeometryCollection", "geometries": []}
-
-        # asShape
-        m = asShape(d)
-        self.assertEqual(m.geom_type, "GeometryCollection")
-        self.assertEqual(len(m), 0)
-        self.assertEqual(m.geoms, [])
-
-    def test_empty_geointerface(self):
-        d = {"type": "GeometryCollection", "geometries": []}
-
-        # shape
-        m = shape(d)
-        self.assertEqual(m.geom_type, "GeometryCollection")
-        self.assertEqual(len(m), 0)
-        self.assertEqual(m.geoms, [])
-
-    def test_empty_coordinates(self):
-
-        d = {"type": "GeometryCollection", "geometries": [
-            {"type": "Point", "coordinates": ()},
-            {"type": "LineString", "coordinates": (())}
-        ]}
-
-        # shape
-        m = shape(d)
-        self.assertEqual(m.geom_type, "GeometryCollection")
-        self.assertEqual(len(m), 0)
-        self.assertEqual(m.geoms, [])
-
-
-def test_geometrycollection_adapter_deprecated():
-    d = {"type": "GeometryCollection","geometries": [
-            {"type": "Point", "coordinates": (0, 3)},
-            {"type": "LineString", "coordinates": ((2, 0), (1, 0))}
+@pytest.fixture()
+def geometrycollection_geojson():
+    return {"type": "GeometryCollection", "geometries": [
+        {"type": "Point", "coordinates": (0, 3, 0)},
+        {"type": "LineString", "coordinates": ((2, 0), (1, 0))}
     ]}
+
+
+@pytest.mark.parametrize('geom', [
+    GeometryCollection(),
+    shape({"type": "GeometryCollection", "geometries": []}),
+    shape({"type": "GeometryCollection", "geometries": [
+        {"type": "Point", "coordinates": ()},
+        {"type": "LineString", "coordinates": (())}
+    ]}),
+    wkt.loads('GEOMETRYCOLLECTION EMPTY'),
+])
+def test_empty(geom):
+    assert geom.type == "GeometryCollection"
+    assert geom.type == geom.geom_type
+    assert geom.is_empty
+    assert len(geom) == 0
+    assert geom.geoms == []
+
+
+def test_child_with_deleted_parent():
+    # test that we can remove a collection while keeping
+    # children around
+    a = LineString([(0, 0), (1, 1), (1, 2), (2, 2)])
+    b = LineString([(0, 0), (1, 1), (2, 1), (2, 2)])
+    collection = a.intersection(b)
+
+    child = collection.geoms[0]
+    # delete parent of child
+    del collection
+
+    # access geometry, this should not seg fault as 1.2.15 did
+    assert child.wkt is not None
+
+
+def test_from_geojson(geometrycollection_geojson):
+    geom = shape(geometrycollection_geojson)
+    assert geom.geom_type == "GeometryCollection"
+    assert len(geom) == 2
+
+    geom_types = [g.geom_type for g in geom.geoms]
+    assert "Point" in geom_types
+    assert "LineString" in geom_types
+
+
+def test_geointerface(geometrycollection_geojson):
+    geom = shape(geometrycollection_geojson)
+    assert geom.__geo_interface__ == geometrycollection_geojson
+
+
+@shapely20_deprecated
+def test_geointerface_adapter(geometrycollection_geojson):
+    geom = asShape(geometrycollection_geojson)
+    assert geom.geom_type == "GeometryCollection"
+    assert len(geom) == 2
+
+    geom_types = [g.geom_type for g in geom.geoms]
+    assert "Point" in geom_types
+    assert "LineString" in geom_types
+
+
+@shapely20_deprecated
+def test_empty_geointerface_adapter():
+    d = {"type": "GeometryCollection", "geometries": []}
+
+    geom = asShape(d)
+    assert geom.geom_type == "GeometryCollection"
+    assert geom.is_empty
+    assert len(geom) == 0
+    assert geom.geoms == []
+
+
+def test_geometrycollection_adapter_deprecated(geometrycollection_geojson):
     with pytest.warns(ShapelyDeprecationWarning):
-        asShape(d)
+        asShape(geometrycollection_geojson)
 
     d = {"type": "GeometryCollection", "geometries": []}
     with pytest.warns(ShapelyDeprecationWarning):
         asShape(d)
 
-
-def test_suite():
-    return unittest.TestLoader().loadTestsFromTestCase(CollectionTestCase)
diff --git a/tests/test_coords.py b/tests/test_coords.py
index d4c15a8..a1fd73e 100644
--- a/tests/test_coords.py
+++ b/tests/test_coords.py
@@ -1,6 +1,9 @@
 from . import unittest, numpy
-from shapely import geometry
 
+import pytest
+
+from shapely import geometry
+from shapely.errors import ShapelyDeprecationWarning
 
 class CoordsTestCase(unittest.TestCase):
     """
@@ -36,5 +39,15 @@ class CoordsTestCase(unittest.TestCase):
         )
 
 
+def test_coords_ctypes_deprecated():
+    """
+    Test that the .ctypes attribute of a CoordinateSequence raises
+    a deprecation warning.
+    """
+    coords = geometry.LineString([[12, 34], [56, 78]]).coords
+    with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
+        coords.ctypes
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(CoordsTestCase)
diff --git a/tests/test_geomseq.py b/tests/test_geomseq.py
index 95132fb..7e7e5aa 100644
--- a/tests/test_geomseq.py
+++ b/tests/test_geomseq.py
@@ -1,8 +1,11 @@
-from . import unittest
+from . import unittest, shapely20_deprecated
+
 from shapely import geometry
 
 
 class MultiLineTestCase(unittest.TestCase):
+
+    @shapely20_deprecated
     def test_array_interface(self):
         m = geometry.MultiLineString([((0, 0), (1, 1)), ((2, 2), (3, 3))])
         ai = m.geoms[0].__array_interface__
diff --git a/tests/test_linestring.py b/tests/test_linestring.py
index 98bc51e..ee3d4fc 100644
--- a/tests/test_linestring.py
+++ b/tests/test_linestring.py
@@ -128,7 +128,7 @@ class LineStringTestCase(unittest.TestCase):
             ls = LineString(coords)
             ls.geom_type # caused segfault before fix
 
-
+    @shapely20_deprecated
     @unittest.skipIf(not numpy, 'Numpy required')
     def test_numpy(self):
 
@@ -145,8 +145,16 @@ class LineStringTestCase(unittest.TestCase):
         expected = array([[1.0, 2.0], [3.0, 4.0]])
         assert_array_equal(la, expected)
 
+    @unittest.skipIf(not numpy, 'Numpy required')
+    def test_numpy_coords(self):
+        import numpy as np
+        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]])
+
         # Coordinate sequences can be adapted as well
-        la = asarray(line.coords)
+        la = np.asarray(line.coords)
         assert_array_equal(la, expected)
 
     @shapely20_deprecated
@@ -168,6 +176,7 @@ class LineStringTestCase(unittest.TestCase):
         pas = asarray(la)
         assert_array_equal(pas, array([[1.0, 2.0], [3.0, 4.0]]))
 
+    @shapely20_deprecated
     @unittest.skipIf(not numpy, 'Numpy required')
     def test_numpy_asarray(self):
         from numpy import array, asarray
@@ -185,6 +194,7 @@ class LineStringTestCase(unittest.TestCase):
         b = asarray(line)
         assert_array_equal(b, array([[0., 0.], [2., 2.], [1., 1.]]))
 
+    @shapely20_deprecated
     @unittest.skipIf(not numpy, 'Numpy required')
     def test_numpy_empty(self):
         from numpy import array, asarray
@@ -208,5 +218,26 @@ def test_linestring_adapter_deprecated():
         asLineString(coords)
 
 
+def test_linestring_ctypes_deprecated():
+    line = LineString(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
+        line.ctypes
+
+
+def test_linestring_array_interface_deprecated():
+    line = LineString(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="array_interface"):
+        line.array_interface()
+
+
+@unittest.skipIf(not numpy, 'Numpy required')
+def test_linestring_array_interface_numpy_deprecated():
+    import numpy as np
+
+    line = LineString(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="array interface"):
+        np.array(line)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(LineStringTestCase)
diff --git a/tests/test_multilinestring.py b/tests/test_multilinestring.py
index e69cd4f..8cc3971 100644
--- a/tests/test_multilinestring.py
+++ b/tests/test_multilinestring.py
@@ -3,7 +3,7 @@ from .test_multi import MultiGeometryTestCase
 
 import pytest
 
-from shapely.errors import ShapelyDeprecationWarning
+from shapely.errors import EmptyPartError, ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.geometry import LineString, MultiLineString, asMultiLineString
 from shapely.geometry.base import dump_coords
@@ -90,6 +90,15 @@ class MultiLineStringTestCase(MultiGeometryTestCase):
         line1 = LineString([(4.0, 5.0), (6.0, 7.0)])
         self.subgeom_access_test(MultiLineString, [line0, line1])
 
+    def test_create_multi_with_empty_component(self):
+        with self.assertRaises(EmptyPartError) as exc:
+            wkt = MultiLineString([
+                LineString([(0, 0), (1, 1), (2, 2)]),
+                LineString()
+            ]).wkt
+
+        self.assertEqual(str(exc.exception), "Can't create MultiLineString with empty component")
+
 
 def test_multilinestring_adapter_deprecated():
     coords = [[[5.0, 6.0], [7.0, 8.0]]]
diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py
index 4728fab..55f4e9e 100644
--- a/tests/test_multipoint.py
+++ b/tests/test_multipoint.py
@@ -3,7 +3,7 @@ from .test_multi import MultiGeometryTestCase
 
 import pytest
 
-from shapely.errors import ShapelyDeprecationWarning
+from shapely.errors import EmptyPartError, ShapelyDeprecationWarning
 from shapely.geometry import Point, MultiPoint, asMultiPoint
 from shapely.geometry.base import dump_coords
 
@@ -47,7 +47,7 @@ class MultiPointTestCase(MultiGeometryTestCase):
         self.assertEqual(dump_coords(geoma), [[(5.0, 6.0)], [(7.0, 8.0)]])
 
     @unittest.skipIf(not numpy, 'Numpy required')
-    def test_numpy(self):
+    def test_multipoint_from_numpy(self):
 
         from numpy import array, asarray
         from numpy.testing import assert_array_equal
@@ -58,6 +58,13 @@ class MultiPointTestCase(MultiGeometryTestCase):
         self.assertEqual(len(geom.geoms), 2)
         self.assertEqual(dump_coords(geom), [[(0.0, 0.0)], [(1.0, 2.0)]])
 
+    @shapely20_deprecated
+    @unittest.skipIf(not numpy, 'Numpy required')
+    def test_numpy(self):
+
+        from numpy import array, asarray
+        from numpy.testing import assert_array_equal
+
         # Geo interface (cont.)
         geom = MultiPoint((Point(1.0, 2.0), Point(3.0, 4.0)))
         assert_array_equal(array(geom), array([[1., 2.], [3., 4.]]))
@@ -87,6 +94,12 @@ class MultiPointTestCase(MultiGeometryTestCase):
         p1 = Point(3.0, 4.0)
         self.subgeom_access_test(MultiPoint, [p0, p1])
 
+    def test_create_multi_with_empty_component(self):
+        with self.assertRaises(EmptyPartError) as exc:
+            wkt = MultiPoint([Point(0, 0), Point()]).wkt
+
+        self.assertEqual(str(exc.exception), "Can't create MultiPoint with empty component")
+
 
 def test_multipoint_adapter_deprecated():
     coords = [[5.0, 6.0], [7.0, 8.0]]
@@ -94,5 +107,26 @@ def test_multipoint_adapter_deprecated():
         asMultiPoint(coords)
 
 
+def test_multipoint_ctypes_deprecated():
+    geom = MultiPoint(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
+        geom.ctypes
+
+
+def test_multipoint_array_interface_deprecated():
+    geom = MultiPoint(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="array_interface"):
+        geom.array_interface()
+
+
+@unittest.skipIf(not numpy, 'Numpy required')
+def test_multipoint_array_interface_numpy_deprecated():
+    import numpy as np
+
+    geom = MultiPoint(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="array interface"):
+        np.array(geom)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiPointTestCase)
diff --git a/tests/test_point.py b/tests/test_point.py
index b147e24..033c277 100644
--- a/tests/test_point.py
+++ b/tests/test_point.py
@@ -133,6 +133,7 @@ class LineStringTestCase(unittest.TestCase):
         pas = asarray(pa)
         assert_array_equal(pas, array([1.0, 4.0]))
 
+    @shapely20_deprecated
     @unittest.skipIf(not numpy, 'Numpy required')
     def test_numpy_asarray(self):
         from numpy import array, asarray
@@ -142,7 +143,6 @@ class LineStringTestCase(unittest.TestCase):
         p = Point(0.0, 0.0, 1.0)
         coords = p.coords[0]
         self.assertEqual(coords, (0.0, 0.0, 1.0))
-        self.assertIsNotNone(p.ctypes)
 
         # Convert to Numpy array, passing through Python sequence
         a = asarray(coords)
@@ -185,5 +185,25 @@ def test_point_adapter_deprecated():
         asPoint(coords)
 
 
+def test_point_ctypes_deprecated():
+    p = Point(3.0, 4.0)
+    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"):
+        p.array_interface()
+
+
+@unittest.skipIf(not numpy, 'Numpy required')
+def test_point_array_interface_numpy_deprecated():
+    import numpy as np
+
+    p = Point(3.0, 4.0)
+    with pytest.warns(ShapelyDeprecationWarning, match="array interface"):
+        np.array(p)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(LineStringTestCase)
diff --git a/tests/test_polygon.py b/tests/test_polygon.py
index ebae799..3f1a2ca 100644
--- a/tests/test_polygon.py
+++ b/tests/test_polygon.py
@@ -175,7 +175,7 @@ class PolygonTestCase(unittest.TestCase):
             LinearRing(line)
 
     @unittest.skipIf(not numpy, 'Numpy required')
-    def test_numpy(self):
+    def test_polygon_from_numpy(self):
 
         from numpy import array, asarray
         from numpy.testing import assert_array_equal
@@ -186,6 +186,17 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual(dump_coords(polygon.exterior),
                          [(0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)])
         self.assertEqual(len(polygon.interiors), 0)
+
+    @shapely20_deprecated
+    @unittest.skipIf(not numpy, 'Numpy required')
+    def test_polygon_exterior_array_interface(self):
+
+        from numpy import array, asarray
+        from numpy.testing import assert_array_equal
+
+        a = asarray(((0., 0.), (0., 1.), (1., 1.), (1., 0.), (0., 0.)))
+        polygon = Polygon(a)
+
         b = asarray(polygon.exterior)
         self.assertEqual(b.shape, (5, 2))
         assert_array_equal(
@@ -293,5 +304,18 @@ def test_polygon_adapter_deprecated():
         asPolygon(coords, hole_coords)
 
 
+def test_ctypes_deprecated():
+    coords = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]
+    hole_coords = [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))]
+    ring = LinearRing(coords)
+    polygon = Polygon(coords, hole_coords)
+
+    with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
+        ring.ctypes
+
+    with pytest.warns(ShapelyDeprecationWarning, match="ctypes"):
+        polygon.ctypes
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PolygonTestCase)
diff --git a/tests/test_shape.py b/tests/test_shape.py
index 6b7f3aa..63e59e2 100644
--- a/tests/test_shape.py
+++ b/tests/test_shape.py
@@ -1,6 +1,7 @@
 import pytest
 
-from shapely.geometry import shape, Polygon
+from shapely.geometry import shape, Polygon, MultiLineString
+from shapely.geometry.geo import _is_coordinates_empty
 
 
 @pytest.mark.parametrize(
@@ -28,3 +29,23 @@ def test_polygon_not_empty_np_array():
     geom = {"type": "Polygon", "coordinates": np.array([[[5, 10], [10, 10], [10, 5]]])}
     obj = shape(geom)
     assert obj == Polygon([(5, 10), (10, 10), (10, 5)])
+
+
+@pytest.mark.parametrize("geom", [
+    {'type': "MultiLineString", "coordinates": []},
+    {'type': 'MultiLineString', 'coordinates': [[]]},
+    {'type': 'MultiLineString', 'coordinates': None}
+])
+def test_multilinestring_empty(geom):
+    assert shape(geom) == MultiLineString()
+
+
+@pytest.mark.parametrize("coords", [
+    [],
+    [[]],
+    [[], []],
+    None,
+    [[[]]]
+])
+def test_is_coordinates_empty(coords):
+    assert _is_coordinates_empty(coords)
diff --git a/tests/test_strtree.py b/tests/test_strtree.py
index e046cbe..4cca258 100644
--- a/tests/test_strtree.py
+++ b/tests/test_strtree.py
@@ -1,4 +1,8 @@
 import gc
+import os
+import pickle
+import subprocess
+import sys
 
 from shapely.strtree import STRtree
 from shapely.geometry import Point, Polygon
@@ -79,3 +83,20 @@ def test_safe_delete():
     del tree
 
     strtree.lgeos = _lgeos
+
+
+@requires_geos_342
+def test_pickle_persistence():
+    """
+    Don't crash trying to use unpickled GEOS handle.
+    """
+    tree = STRtree([Point(i, i).buffer(0.1) for i in range(3)])
+    pickled_strtree = pickle.dumps(tree)
+    unpickle_script_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "unpickle-strtree.py")
+    proc = subprocess.Popen(
+        [sys.executable, str(unpickle_script_file_path)],
+        stdin=subprocess.PIPE,
+    )
+    proc.communicate(input=pickled_strtree)
+    proc.wait()
+    assert proc.returncode == 0
diff --git a/tests/unpickle-strtree.py b/tests/unpickle-strtree.py
new file mode 100644
index 0000000..4566025
--- /dev/null
+++ b/tests/unpickle-strtree.py
@@ -0,0 +1,29 @@
+# See test_strtree.py::test_pickle_persistence
+
+import sys
+import os
+sys.path.append(os.getcwd())
+
+import pickle
+from shapely.geometry import Point
+from shapely.geos import geos_version
+
+
+if __name__ == "__main__":
+    try:
+        pickled_strtree = sys.stdin.buffer.read()
+    except AttributeError:
+        # Python 2.7
+        pickled_strtree = sys.stdin.read()
+
+    print("received pickled strtree:", repr(pickled_strtree))
+    strtree = pickle.loads(pickled_strtree)
+    # Exercise API.
+    print("calling \"query()\"...")
+    strtree.query(Point(0, 0))
+    if geos_version >= (3, 6, 0):
+        print("calling \"nearest()\"...")
+        strtree.nearest(Point(0, 0))
+    else:
+        print("skipping \"nearest()\"")
+    print("done")