Repository: Toblerity/Shapely
Pull request #900 (2020-04-22 07:49:11)

Deprecate mutability of Point, LineString, LinearRing geometries
xref https://github.com/Toblerity/Shapely/issues/782/ (Shapely 2.0).

Example of deprecating mutability this for Point:

- Raise a warning in the `coords` setter
- Splitted the tests to more easily isolate the parts that are deprecated (to now filter the warnings, and later to remove in Shapely 2.0)
- Added one test that actually asserts a warning is raised


If that workflow looks OK, I can do the same for the other geometry types. 


Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index ae11305..39112a3 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -7,6 +7,7 @@ Changes
 New features:
 
 - shapely.ops.voronoi_diagram() computes the Voronoi Diagram of a geometry or geometry collection (#833, #851).
+- shapely.validation.make_valid() fixes invalid geometries (#883)
 
 1.7.1 (TBD)
 -----------
diff --git a/CREDITS.txt b/CREDITS.txt
index c4d0e51..ff72820 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -76,6 +76,7 @@ Shapely is written by:
 * Taro Matsuzawa aka. btm <btm@tech.email.ne.jp>
 * Thibault Deutsch <thibault.deutsch@gmail.com>
 * Thomas Kluyver <takowl@gmail.com>
+* Tom Caruso <carusot42@gmail.com>
 * Tobias Sauerwein <tobias.sauerwein@camptocamp.com>
 * Tom Clancy <17627475+clncy@users.noreply.github.com>
 * WANG Aiyong <gepcelway@gmail.com>
diff --git a/docs/code/make_valid_geometrycollection.py b/docs/code/make_valid_geometrycollection.py
new file mode 100644
index 0000000..17c2f1b
--- /dev/null
+++ b/docs/code/make_valid_geometrycollection.py
@@ -0,0 +1,33 @@
+from shapely.geometry import Polygon
+from shapely.validation import make_valid
+
+from matplotlib import pyplot
+from descartes.patch import PolygonPatch
+from figures import SIZE, BLUE, RED, set_limits, plot_line
+
+
+invalid_poly = Polygon([(0, 2), (0, 1), (2, 0), (0, 0), (0, 2)])
+valid_poly = make_valid(invalid_poly)
+
+fig = pyplot.figure(1, figsize=SIZE, dpi=90)
+fig.set_frameon(True)
+
+
+invalid_ax = fig.add_subplot(121)
+
+patch = PolygonPatch(invalid_poly, facecolor=BLUE, edgecolor=BLUE, alpha=0.5, zorder=2)
+invalid_ax.add_patch(patch)
+
+set_limits(invalid_ax, -1, 3, -1, 3)
+
+
+valid_ax = fig.add_subplot(122)
+
+patch = PolygonPatch(valid_poly[0], facecolor=BLUE, edgecolor=BLUE, alpha=0.5, zorder=2)
+valid_ax.add_patch(patch)
+
+plot_line(valid_ax, valid_poly[1], color=RED, linewidth=1)
+
+set_limits(valid_ax, -1, 3, -1, 3)
+
+pyplot.show()
\ No newline at end of file
diff --git a/docs/code/make_valid_multipolygon.py b/docs/code/make_valid_multipolygon.py
new file mode 100644
index 0000000..b156e46
--- /dev/null
+++ b/docs/code/make_valid_multipolygon.py
@@ -0,0 +1,33 @@
+from shapely.geometry import Polygon
+from shapely.validation import make_valid
+
+from matplotlib import pyplot
+from descartes.patch import PolygonPatch
+from figures import SIZE, BLUE, RED, set_limits
+
+invalid_poly = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)])
+valid_poly = make_valid(invalid_poly)
+
+fig = pyplot.figure(1, figsize=SIZE, dpi=90)
+fig.set_frameon(True)
+
+
+invalid_ax = fig.add_subplot(121)
+
+patch = PolygonPatch(invalid_poly, facecolor=BLUE, edgecolor=BLUE, alpha=0.5, zorder=2)
+invalid_ax.add_patch(patch)
+
+set_limits(invalid_ax, -1, 3, -1, 3)
+
+
+valid_ax = fig.add_subplot(122)
+
+patch = PolygonPatch(valid_poly[0], facecolor=BLUE, edgecolor=BLUE, alpha=0.5, zorder=2)
+valid_ax.add_patch(patch)
+
+patch = PolygonPatch(valid_poly[1], facecolor=RED, edgecolor=RED, alpha=0.5, zorder=2)
+valid_ax.add_patch(patch)
+
+set_limits(valid_ax, -1, 3, -1, 3)
+
+pyplot.show()
\ No newline at end of file
diff --git a/docs/manual.rst b/docs/manual.rst
index 8c18dd2..eaf140f 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -200,6 +200,22 @@ General Attributes and Methods
 
   Returns the length (``float``) of the object.
 
+.. attribute:: object.minimum_clearance
+
+  Returns the smallest distance by which a node could be moved to produce an invalid geometry.
+
+  This can be thought of as a measure of the robustness of a geometry, where larger values of
+  minimum clearance indicate a more robust geometry. If no minimum clearance exists for a geometry,
+  such as a point, this will return `math.infinity`.
+
+  Requires GEOS 3.6 or higher.
+
+.. code-block:: pycon
+
+  >>> from shapely.geometry import Polygon
+  >>> Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]).minimum_clearance
+  1.0
+
 .. attribute:: object.geom_type
 
   Returns a string specifying the `Geometry Type` of the object in accordance
@@ -551,7 +567,8 @@ To obtain a polygon with a known orientation, use
 
   Returns a properly oriented copy of the given polygon. The signed area of the
   result will have the given sign. A sign of 1.0 means that the coordinates of
-  the product's exterior ring will be oriented counter-clockwise.
+  the product's exterior ring will be oriented counter-clockwise and the interior 
+  rings (holes) will be oriented clockwise.
 
   `New in version 1.2.10`.
 
@@ -1975,10 +1992,17 @@ Shapely supports map projections and other arbitrary transformations of geometri
   geometry of the same type from the transformed coordinates.
 
   `func` maps x, y, and optionally z to output xp, yp, zp. The input
-  parameters may iterable types like lists or arrays or single values.
+  parameters may be iterable types like lists or arrays or single values.
   The output shall be of the same type: scalars in, scalars out;
   lists in, lists out.
 
+  `transform` tries to determine which kind of function was passed in
+  by calling `func` first with n iterables of coordinates, where n
+  is the dimensionality of the input geometry. If `func` raises
+  a `TypeError` when called with iterables as arguments,
+  then it will instead call `func` on each individual coordinate
+  in the geometry.
+
   `New in version 1.2.18`.
 
 For example, here is an identity function applicable to both types of input
@@ -1991,30 +2015,45 @@ For example, here is an identity function applicable to both types of input
 
     g2 = transform(id_func, g1)
 
-A partially applied transform function from pyproj satisfies the requirements
-for `func`.
+
+If using `pyproj>=2.1.0`, the preferred method to project geometries is:
 
 .. code-block:: python
 
-    from shapely.ops import transform
-    from functools import partial
     import pyproj
 
-    proj_in = pyproj.Proj(init='epsg:4326')
-    proj_out = pyproj.Proj(init='epsg:26913')
+    from shapely.geometry import Point
+    from shapely.ops import transform
 
-    project = partial(
-        pyproj.transform,
-        proj_in,
-        proj_out)
+    wgs84_pt = Point(-72.2495, 43.886)
 
-    g2 = transform(project, g1)
+    wgs84 = pyproj.CRS('EPSG:4326')
+    utm = pyproj.CRS('EPSG:32618')
 
-If using `pyproj>=2.1.0` a more performant method would be
+    project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform
+    utm_point = transform(project, wgs84_pt)
+
+It is important to note that in the example above, the `always_xy` kwarg is required as Shapely only supports coordinates in X,Y
+order, and in PROJ 6 the WGS84 CRS uses the EPSG-defined Lat/Lon coordinate order instead of the expected Lon/Lat.
+
+If using `pyproj < 2.1`, then the canonical example is:
 
 .. code-block:: python
 
-    project = pyproj.Transformer.from_proj(proj_in, proj_out).transform
+    from functools import partial
+    import pyproj
+
+    from shapely.ops import transform
+
+    wgs84 = pyproj.Proj(init='epsg:4326')
+    utm = pyproj.Proj(init='epsg:32618')
+
+    project = partial(
+        pyproj.transform,
+        wgs84,
+        utm)
+
+    utm_point = transform(project, wgs84_pt)
 
 Lambda expressions such as the one in
 
@@ -2421,6 +2460,47 @@ be parsed out.
   >>> explain_validity(p)
   'Ring Self-intersection[1 1]'
 
+.. function:: validation.make_valid(ob)
+
+  Returns a valid representation of the geometry, if it is invalid.
+  If it is valid, the input geometry will be returned.
+
+  In many cases, in order to create a valid geometry, the input geometry
+  must be split into multiple parts or multiple geometries. If the geometry
+  must be split into multiple parts of the same geometry type, then a multi-part
+  geometry (e.g. a MultiPolygon) will be returned. if the geometry must be split
+  into multiple parts of different types, then a GeometryCollection will be returned.
+
+  For example, this operation on a geometry with a bow-tie structure:
+
+.. code-block:: pycon
+
+  >>> from shapely.validation import make_valid
+  >>> coords = [(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)]
+  >>> p = Polygon(coords)
+  >>> str(make_valid(p))
+  'MULTIPOLYGON (((0 0, 0 2, 1 1, 0 0)), ((1 1, 2 2, 2 0, 1 1)))'
+
+  Yields a MultiPolygon with two parts:
+
+.. plot:: code/make_valid_multipolygon.py
+
+  While this operation:
+
+.. code-block:: pycon
+
+  >>> from shapely.validation import make_valid
+  >>> coords = [(0, 2), (0, 1), (2, 0), (0, 0), (0, 2)]
+  >>> p = Polygon(coords)
+  >>> str(make_valid(p))
+
+  Yields a GeometryCollection with a Polygon and a LineString:
+
+.. plot:: code/make_valid_geometrycollection.py
+
+  `New in version 1.8`
+  `Requires GEOS > 3.8`
+
 The Shapely version, GEOS library version, and GEOS C API version are
 accessible via :attr:`shapely.__version__`,
 :attr:`shapely.geos.geos_version_string`, and
@@ -2478,7 +2558,7 @@ cannot add or remove geometries.
 
   `New in version 1.4.0`.
 
-  .. method:: strtree.query(goem)
+  .. method:: strtree.query(geom)
 
     Returns a list of all geometries in the `strtree` whose extents intersect the
     extent of `geom`. This means that a subsequent search through the returned
@@ -2593,16 +2673,13 @@ adapted to Numpy arrays.
 
 .. code-block:: pycon
 
-  >>> from numpy import array
-  >>> array(Point(0, 0))
+  >>> from numpy import asarray
+  >>> asarray(Point(0, 0))
   array([ 0.,  0.])
-  >>> array(LineString([(0, 0), (1, 1)]))
+  >>> asarray(LineString([(0, 0), (1, 1)]))
   array([[ 0.,  0.],
          [ 1.,  1.]])
 
-The :func:`numpy.asarray` function does not copy coordinate values – at the
-price of slower Numpy access to the coordinates of Shapely objects.
-
 .. note::
 
   The Numpy array interface is provided without a dependency on Numpy itself.
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 0b686d5..4ee040a 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,6 @@
 cython==0.29.14
 descartes==1.0.1
+matplotlib
 numpy>=1.4.1
 pytest
 pytest-cov
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index eb6c803..f9d199a 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -102,7 +102,7 @@ def prototype(lgeos, geos_version):
     lgeos.GEOSCoordSeq_getDimensions.restype = c_int
     lgeos.GEOSCoordSeq_getDimensions.argtypes = [c_void_p, c_void_p]
 
-    # Linear refeferencing
+    # Linear referencing
 
     if geos_version >= (3, 2, 0):
 
@@ -542,3 +542,11 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSSTRtree_nearest_generic.argtypes = [
             c_void_p, py_object, c_void_p, lgeos.GEOSDistanceCallback, py_object]
         lgeos.GEOSSTRtree_nearest_generic.restype = c_void_p
+
+        lgeos.GEOSMinimumClearance.argtypes = [c_void_p]
+        lgeos.GEOSMinimumClearance.restype = c_double
+
+    if geos_version > (3, 8, 0):
+        lgeos.GEOSMakeValid.restype = c_void_p
+        lgeos.GEOSMakeValid.argtypes = [c_void_p]
+
diff --git a/shapely/errors.py b/shapely/errors.py
index 8166f5a..095c4a3 100644
--- a/shapely/errors.py
+++ b/shapely/errors.py
@@ -31,3 +31,10 @@ class TopologicalError(ShapelyError):
 
 class PredicateError(ShapelyError):
     """A geometric predicate has failed to return True/False."""
+
+
+class ShapelyDeprecationWarning(FutureWarning):
+    """
+    Warning for features that will be removed or behaviour that will be
+    changed in a future release.
+    """
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 61adaf9..a4e0d6a 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -442,6 +442,11 @@ class BaseGeometry(object):
         """Unitless length of the geometry (float)"""
         return self.impl['length'](self)
 
+    @property
+    def minimum_clearance(self):
+        """Unitless distance by which a node could be moved to produce an invalid geometry (float)"""
+        return self.impl['minimum_clearance'](self)
+
     # Topological properties
     # ----------------------
 
diff --git a/shapely/geometry/geo.py b/shapely/geometry/geo.py
index c78d355..11969b7 100644
--- a/shapely/geometry/geo.py
+++ b/shapely/geometry/geo.py
@@ -60,8 +60,35 @@ def box(minx, miny, maxx, maxy, ccw=True):
 
 
 def shape(context):
-    """Returns a new, independent geometry with coordinates *copied* from the
-    context.
+    """
+    Returns a new, independent geometry with coordinates *copied* from the
+    context. Changes to the original context will not be reflected in the
+    geometry object.
+
+    Parameters
+    ----------
+    context :
+        a GeoJSON-like dict, which provides a "type" member describing the type
+        of the geometry and "coordinates" member providing a list of coordinates,
+        or an object which implements __geo_interface__.
+
+    Returns
+    -------
+    Geometry object
+
+    Example
+    -------
+    Create a Point from GeoJSON, and then create a copy using __geo_interface__.
+
+    >>> context = {'type': 'Point', 'coordinates': [0, 1]}
+    >>> geom = shape(context)
+    >>> geom.type == 'Point'
+    True
+    >>> geom.wkt
+    'POINT (0 1)'
+    >>> geom2 = shape(geom)
+    >>> geom == geom2
+    True
     """
     if hasattr(context, "__geo_interface__"):
         ob = context.__geo_interface__
@@ -90,8 +117,41 @@ def shape(context):
 
 
 def asShape(context):
-    """Adapts the context to a geometry interface. The coordinates remain
-    stored in the context.
+    """
+    Adapts the context to a geometry interface. The coordinates remain
+    stored in the context, and changes to them will be reflected in the
+    returned geometry object.
+
+    Parameters
+    ----------
+    context :
+        a GeoJSON-like dict, which provides a "type" member describing the type
+        of the geometry and "coordinates" member providing a list of coordinates,
+        or an object which implements __geo_interface__.
+
+    Returns
+    -------
+    Geometry object
+
+    Notes
+    -----
+    The Adapter classes returned by this function trade performance for
+    reduced storage of coordinate values. In general, the shape() function
+    should be used instead.
+
+    Example
+    -------
+    Create a Point and Polygon from GeoJSON, change the coordinates of the Point's
+    context and show that the corresponding geometry is changed, as well.
+
+    >>> point_context = {'type': 'Point', 'coordinates': [0.5, 0.5]}
+    >>> poly_context = {'type': 'Polygon', 'coordinates': [[[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]]}
+    >>> point, poly = asShape(point_context), asShape(poly_context)
+    >>> poly.intersects(point)
+    True
+    >>> point_context['coordinates'][0] = 1.5
+    >>> poly.intersects(point)
+    False
     """
     if hasattr(context, "__geo_interface__"):
         ob = context.__geo_interface__
@@ -123,5 +183,23 @@ def asShape(context):
 
 
 def mapping(ob):
-    """Returns a GeoJSON-like mapping"""
+    """
+    Returns a GeoJSON-like mapping from a Geometry or any
+    object which implements __geo_interface__
+
+    Parameters
+    ----------
+    ob :
+        An object which implements __geo_interface__.
+
+    Returns
+    -------
+    dict
+
+    Example
+    -------
+    >>> pt = Point(0, 0)
+    >>> mapping(p)
+    {'type': 'Point', 'coordinates': (0.0, 0.0)}
+    """
     return ob.__geo_interface__
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index 66e5d70..c25cc7a 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -2,7 +2,9 @@
 """
 
 from ctypes import c_double
+import warnings
 
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geos import lgeos, TopologicalError
 from shapely.geometry.base import (
     BaseGeometry, geom_factory, JOIN_STYLE, geos_geom_from_py
@@ -40,7 +42,9 @@ class LineString(BaseGeometry):
         """
         BaseGeometry.__init__(self)
         if coordinates is not None:
-            self._set_coords(coordinates)
+            ret = geos_linestring_from_py(coordinates)
+            if ret is not None:
+                self._geom, self._ndim = ret
 
     @property
     def __geo_interface__(self):
@@ -88,6 +92,10 @@ class LineString(BaseGeometry):
 
     # Coordinate access
     def _set_coords(self, coordinates):
+        warnings.warn(
+            "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()
         ret = geos_linestring_from_py(coordinates)
         if ret is not None:
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index 3bc6fc1..8deddee 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -2,8 +2,9 @@
 """
 
 from ctypes import c_double
+import warnings
 
-from shapely.errors import DimensionError
+from shapely.errors import DimensionError, ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.geometry.base import BaseGeometry, geos_geom_from_py
 from shapely.geometry.proxy import CachingGeometryProxy
@@ -45,7 +46,14 @@ class Point(BaseGeometry):
         """
         BaseGeometry.__init__(self)
         if len(args) > 0:
-            self._set_coords(*args)
+            if len(args) == 1:
+                self._geom, self._ndim = geos_point_from_py(args[0])
+            elif len(args) > 3:
+                raise TypeError(
+                    "Point() takes at most 3 arguments ({} given)".format(len(args))
+                )
+            else:
+                self._geom, self._ndim = geos_point_from_py(tuple(args))
 
     # Coordinate getters and setters
 
@@ -128,6 +136,10 @@ class Point(BaseGeometry):
     # Coordinate access
 
     def _set_coords(self, *args):
+        warnings.warn(
+            "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()
         if len(args) == 1:
             self._geom, self._ndim = geos_point_from_py(args[0])
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index c3f3b5f..731267c 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -2,6 +2,7 @@
 """
 
 import sys
+import warnings
 
 from ctypes import c_void_p, cast, POINTER
 import weakref
@@ -12,7 +13,7 @@ from shapely.geometry.base import BaseGeometry, geos_geom_from_py
 from shapely.geometry.linestring import LineString, LineStringAdapter
 from shapely.geometry.point import Point
 from shapely.geometry.proxy import PolygonProxy
-from shapely.errors import TopologicalError
+from shapely.errors import TopologicalError, ShapelyDeprecationWarning
 
 
 __all__ = ['Polygon', 'asPolygon', 'LinearRing', 'asLinearRing']
@@ -49,7 +50,9 @@ class LinearRing(LineString):
         """
         BaseGeometry.__init__(self)
         if coordinates is not None:
-            self._set_coords(coordinates)
+            ret = geos_linearring_from_py(coordinates)
+            if ret is not None:
+                self._geom, self._ndim = ret
 
     @property
     def __geo_interface__(self):
@@ -63,6 +66,10 @@ class LinearRing(LineString):
     _get_coords = BaseGeometry._get_coords
 
     def _set_coords(self, coordinates):
+        warnings.warn(
+            "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()
         ret = geos_linearring_from_py(coordinates)
         if ret is not None:
diff --git a/shapely/geos.py b/shapely/geos.py
index 17ebb22..791fda8 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -56,6 +56,10 @@ def load_dll(libname, fallbacks=None, mode=DEFAULT_MODE):
                 libname, fallbacks or []))
 
 _lgeos = None
+def exists_conda_env():
+    """Does this module exist in a conda environment?"""
+    return os.path.exists(os.path.join(sys.prefix, 'conda-meta'))
+
 
 if sys.platform.startswith('linux'):
     # Test to see if we have a wheel repaired by 'auditwheel' containing its
@@ -70,7 +74,7 @@ if sys.platform.startswith('linux'):
         if len(geos_pyinstaller_so) == 1:
             _lgeos = CDLL(geos_pyinstaller_so[0])
             LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
-    elif os.getenv('CONDA_PREFIX', ''):
+    elif exists_conda_env():
         # conda package.
         _lgeos = CDLL(os.path.join(sys.prefix, 'lib', 'libgeos_c.so'))
     else:
@@ -101,7 +105,7 @@ elif sys.platform == 'darwin':
             _lgeos = CDLL(geos_whl_dylib)
             LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
 
-    elif os.getenv('CONDA_PREFIX', ''):
+    elif exists_conda_env():
         # conda package.
         _lgeos = CDLL(os.path.join(sys.prefix, 'lib', 'libgeos_c.dylib'))
     else:
@@ -129,12 +133,16 @@ elif sys.platform == 'darwin':
             ]
         _lgeos = load_dll('geos_c', fallbacks=alt_paths)
 
-    free = load_dll('c').free
+    # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
+    # manpage says, "If filename is NULL, then the returned handle is for the
+    # main program". This way we can let the linker do the work to figure out
+    # which libc Python is actually using.
+    free = CDLL(None).free
     free.argtypes = [c_void_p]
     free.restype = None
 
 elif sys.platform == 'win32':
-    if os.getenv('CONDA_PREFIX', ''):
+    if exists_conda_env():
         # conda package.
         _lgeos = CDLL(os.path.join(sys.prefix, 'Library', 'bin', 'geos_c.dll'))
     else:
@@ -843,6 +851,8 @@ class LGEOS340(LGEOS330):
 class LGEOS350(LGEOS340):
     """Proxy for GEOS 3.5.0-CAPI-1.9.0
     """
+    geos_version = (3, 5, 0)
+    geos_capi_version = (1, 9, 0)
 
     def __init__(self, dll):
         super(LGEOS350, self).__init__(dll)
@@ -850,7 +860,33 @@ class LGEOS350(LGEOS340):
         self.methods['voronoi_diagram'] = self.GEOSVoronoiDiagram
 
 
-if geos_version >= (3, 5, 0):
+class LGEOS360(LGEOS350):
+    """Proxy for GEOS 3.6.0-CAPI-1.10.0
+    """
+    geos_version = (3, 6, 0)
+    geos_capi_version = (1, 10, 0)
+
+    def __init__(self, dll):
+        super(LGEOS360, self).__init__(dll)
+        self.methods['minimum_clearance'] = self.GEOSMinimumClearance
+
+
+class LGEOS380(LGEOS360):
+    """Proxy for GEOS 3.8.0-CAPI-1.13.0"""
+
+    geos_version = (3, 8, 0)
+    geos_capi_version = (1, 13, 0)
+
+    def __init__(self, dll):
+        super(LGEOS380, self).__init__(dll)
+        self.methods['make_valid'] = self.GEOSMakeValid
+
+
+if geos_version >= (3, 8, 0):
+    L = LGEOS380
+elif geos_version >= (3, 6, 0):
+    L = LGEOS360
+elif geos_version >= (3, 5, 0):
     L = LGEOS350
 elif geos_version >= (3, 4, 0):
     L = LGEOS340
diff --git a/shapely/impl.py b/shapely/impl.py
index cc6550a..dbc9bc4 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -148,6 +148,9 @@ IMPL330 = {
     'buffer_with_params': (UnaryTopologicalOp, 'buffer_with_params'),
 }
 
+IMPL360 = {
+    'minimum_clearance': (UnaryRealProperty, 'minimum_clearance')
+}
 
 def impl_items(defs):
     return [(k, v[0](v[1])) for k, v in list(defs.items())]
@@ -161,5 +164,7 @@ if lgeos.geos_version >= (3, 2, 0):
     imp.update(impl_items(IMPL320))
 if lgeos.geos_version >= (3, 3, 0):
     imp.update(impl_items(IMPL330))
+if lgeos.geos_version >= (3, 6, 0):
+    imp.update(impl_items(IMPL360))
 
 DefaultImplementation = imp
diff --git a/shapely/ops.py b/shapely/ops.py
index 104089e..8571404 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -14,7 +14,8 @@ from shapely.algorithms.polylabel import polylabel
 
 __all__ = ['cascaded_union', 'linemerge', 'operator', 'polygonize',
            'polygonize_full', 'transform', 'unary_union', 'triangulate',
-           'voronoi_diagram', 'split', 'section']
+           'voronoi_diagram', 'split', 'nearest_points', 'validate', 'snap',
+           'shared_paths', 'clip_by_rect', 'orient', 'substring']
 
 
 class CollectionOperator(object):
@@ -254,19 +255,20 @@ def transform(func, geom):
 
       g2 = transform(id_func, g1)
 
-    A partially applied transform function from pyproj satisfies the
-    requirements for `func`.
+    Using pyproj >= 2.1, this example will accurately project Shapely geometries:
 
-      from functools import partial
       import pyproj
 
-      project = partial(
-          pyproj.transform,
-          pyproj.Proj(init='epsg:4326'),
-          pyproj.Proj(init='epsg:26913'))
+      wgs84 = pyproj.CRS('EPSG:4326')
+      utm = pyproj.CRS('EPSG:32618')
+
+      project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform
 
       g2 = transform(project, g1)
 
+    Note that the always_xy kwarg is required here as Shapely geometries only support
+    X,Y coordinate ordering.
+
     Lambda expressions such as the one in
 
       g2 = transform(lambda x, y, z=None: (x+1.0, y+1.0), g1)
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index 149526e..e669b2e 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -190,9 +190,10 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
             dy = coords[1]
             dz = 0
             if n == 3:
-                if len(coords) != 3:
+                try:
+                    dz = coords[2]
+                except IndexError:
                     raise ValueError("Inconsistent coordinate dimensionality")
-                dz = coords[2]
             
             # Because of a bug in the GEOS C API, 
             # always set X before Y
@@ -370,7 +371,10 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             dy = coords[1]
             dz = 0
             if n == 3:
-                dz = coords[2]
+                try:
+                    dz = coords[2]
+                except IndexError:
+                    raise ValueError("Inconsistent coordinate dimensionality")
         
             # Because of a bug in the GEOS C API, 
             # always set X before Y
@@ -386,7 +390,10 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             dy = coords[1]
             dz = 0
             if n == 3:
-                dz = coords[2]
+                try:
+                    dz = coords[2]
+                except IndexError:
+                    raise ValueError("Inconsistent coordinate dimensionality")
         
             # Because of a bug in the GEOS C API, 
             # always set X before Y
diff --git a/shapely/strtree.py b/shapely/strtree.py
index 4b2f482..e0f9611 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -85,7 +85,11 @@ class STRtree:
 
     def __del__(self):
         if self._tree_handle is not None:
-            lgeos.GEOSSTRtree_destroy(self._tree_handle)
+            try:
+                lgeos.GEOSSTRtree_destroy(self._tree_handle)
+            except AttributeError:
+                pass  # lgeos might be empty on shutdown.
+
             self._tree_handle = None
 
     def query(self, geom):
diff --git a/shapely/validation.py b/shapely/validation.py
index 541a779..6ef1f7a 100644
--- a/shapely/validation.py
+++ b/shapely/validation.py
@@ -1,6 +1,55 @@
 # TODO: allow for implementations using other than GEOS
-
+from shapely.geometry.base import geom_factory
 from shapely.geos import lgeos
 
+__all__ = ['explain_validity', 'make_valid']
+
+
 def explain_validity(ob):
+    """
+    Explain the validity of the input geometry, if it is invalid.
+    This will describe why the geometry is invalid, and might
+    include a location if there is a self-intersection or a
+    ring self-intersection.
+
+    Parameters
+    ----------
+    ob: Geometry
+        A shapely geometry object
+
+    Returns
+    -------
+    str
+        A string describing the reason the geometry is invalid.
+
+    """
     return lgeos.GEOSisValidReason(ob._geom)
+
+
+def make_valid(ob):
+    """
+    Make the input geometry valid according to the GEOS MakeValid algorithm.
+
+    If the input geometry is already valid, then it will be returned.
+
+    If the geometry must be split into multiple parts of the same type to be made valid,
+    then a multi-part geometry will be returned.
+
+    If the geometry must be split into multiple parts of different types to be made valid,
+    then a GeometryCollection will be returned.
+
+    Parameters
+    ----------
+    ob : Geometry
+        A shapely geometry object which should be made valid. If the object is already valid,
+        it will be returned as-is.
+
+    Returns
+    -------
+    Geometry
+        The input geometry, made valid according to the GEOS MakeValid algorithm.
+
+    """
+    if ob.is_valid:
+        return ob
+    return geom_factory(lgeos.GEOSMakeValid(ob._geom))


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index f6e2433..9ea07db 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,6 +4,8 @@ import unittest
 from shapely.geos import geos_version_string, lgeos, WKTWriter
 from shapely import speedups
 
+import pytest
+
 
 test_int_types = [int]
 
@@ -24,3 +26,8 @@ print('Cython speedups: ' + str(speedups.available))
 if lgeos.geos_version >= (3, 3, 0):
     # Remove any WKT writer defaults to pass tests for all versions of GEOS
     WKTWriter.defaults = {}
+
+
+shapely20_deprecated = pytest.mark.filterwarnings(
+    "ignore::shapely.errors.ShapelyDeprecationWarning"
+)
diff --git a/tests/conftest.py b/tests/conftest.py
index 9cf4ac0..6319788 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,13 @@ import sys
 
 import pytest
 
+from shapely.geos import geos_version
+
+
+requires_geos_38 = pytest.mark.skipif(geos_version < (3, 8, 0), reason='GEOS >= 3.8.0 is required.')
+requires_geos_342 = pytest.mark.skipif(geos_version < (3, 4, 2), reason="GEOS > 3.4.2 is required.")
+
+
 def pytest_addoption(parser):
     parser.addoption("--with-speedups", action="store_true", default=False,
                      help="Run tests with speedups.")
diff --git a/tests/test_create_inconsistent_dimensionality.py b/tests/test_create_inconsistent_dimensionality.py
new file mode 100644
index 0000000..14b31cc
--- /dev/null
+++ b/tests/test_create_inconsistent_dimensionality.py
@@ -0,0 +1,54 @@
+"""
+When a "context" passed to shape/asShape has a coordinate
+which is missing a dimension we should raise a descriptive error.
+
+When we use mixed dimensions in a WKT geometry, the parser strips
+any dimension which is not present in every coordinate.
+"""
+
+
+import pytest
+
+from shapely import wkt
+from shapely.geometry import asShape, LineString, Polygon
+
+
+geojson_cases = [
+    {"type": "LineString", "coordinates": [[1, 1, 1], [2, 2]]},
+    # Specific test case from #869
+    {"type": "Polygon", "coordinates": [[[55.12916764533149, 24.980385694214384, 2.5], [55.13098248044217, 24.979828079961905], [55.13966519231666, 24.97801442415322], [55.13966563924936, 24.97801442415322], [55.14139286840762, 24.982307444496097], [55.14169331277646, 24.983717465495562], [55.14203489144224, 24.985419446276566, 2.5], [55.14180327151276, 24.98428602667792, 2.5], [55.14170091915952, 24.984242720177235, 2.5], [55.14122966992623, 24.984954809433702, 2.5], [55.14134021791831, 24.985473928648396, 2.5], [55.141405876161286, 24.986090184809793, 2.5], [55.141361358941225, 24.986138101357326, 2.5], [55.14093322994411, 24.986218753894093, 2.5], [55.140897653420964, 24.986214283545635, 2.5], [55.14095492976058, 24.9863027591922, 2.5], [55.140900447388745, 24.98628436557094, 2.5], [55.140867059473706, 24.98628869622101, 2.5], [55.14089155325796, 24.986402364143782, 2.5], [55.14090938808566, 24.986479011993385, 2.5], [55.140943893587824, 24.986471188883584, 2.5], [55.1410161176551, 24.9864174050037, 2.5], [55.140996932409635, 24.986521806266644, 2.5], [55.14163554031332, 24.986910400619593, 2.5], [55.14095781686062, 24.987033474900578, 2.5], [55.14058258698692, 24.98693261266349, 2.5], [55.14032624044253, 24.98747538747211, 2.5], [55.14007240846915, 24.988001119077232, 2.5], [55.14013122149105, 24.98831115636925, 2.5], [55.13991827457961, 24.98834356639557, 2.5], [55.139779460946755, 24.988254625087706, 2.5], [55.13974742344948, 24.988261377176524, 2.5], [55.139515198160304, 24.98841811876934, 2.5], [55.13903617238334, 24.98817914139135, 2.5], [55.1391330764994, 24.988660542040925, 2.5], [55.13914369357698, 24.989438289540374, 2.5], [55.136431216517785, 24.98966711550207, 2.0], [55.13659028641709, 24.99041706302204, 2.0], [55.1355852030721, 24.990933481401207, 2.5], [55.13535549235394, 24.99110470506038, 2.5], [55.13512578163577, 24.99127592871955, 2.5], [55.129969653784556, 24.991440074326995, 2.5], [55.130221623112746, 24.988070688875112, 2.5], [55.130451333830905, 24.98789946521594, 2.5], [55.13089208224919, 24.98742639990359, 2.5], [55.132177586827666, 24.989003408454433, 2.5], [55.13238862452779, 24.988701566801254, 2.5], [55.132482594977674, 24.988501518707757, 2.5], [55.132525994610624, 24.988048802794115, 2.5], [55.13249018525683, 24.987180623870653, 2.5], [55.13253358488978, 24.986727907957015, 2.5], [55.1322761673244, 24.985827132742713, 2.5], [55.13163341503516, 24.98503862846729, 2.5], [55.131514764536504, 24.984469124700183, 2.5], [55.131275600894, 24.983796337257242, 2.0], [55.13066865795855, 24.98387601190528, 2.0], [55.13026930682963, 24.981537228037503, 2.0], [55.130260412698846, 24.981495691049748, 2.0], [55.13025151856806, 24.981454154061993, 2.0], [55.13022925995803, 24.98096497686874, 2.5], [55.12984453059386, 24.9804285816199, 2.5], [55.129998291954365, 24.98021419115843, 2.5], [55.12916764533149, 24.980385694214384, 2.5]]]},
+]
+
+
+direct_cases = [
+    (LineString, [[[0, 0, 0], [1, 1]]]),
+    (Polygon, [[[0, 0, 0], [1, 1, 0], [1, 1], [0, 1, 0], [0, 0, 0]]]),
+    # Specific test case from #869
+    (Polygon, [[[55.12916764533149, 24.980385694214384, 2.5], [55.13098248044217, 24.979828079961905], [55.13966519231666, 24.97801442415322], [55.13966563924936, 24.97801442415322], [55.14139286840762, 24.982307444496097], [55.14169331277646, 24.983717465495562], [55.14203489144224, 24.985419446276566, 2.5], [55.14180327151276, 24.98428602667792, 2.5], [55.14170091915952, 24.984242720177235, 2.5], [55.14122966992623, 24.984954809433702, 2.5], [55.14134021791831, 24.985473928648396, 2.5], [55.141405876161286, 24.986090184809793, 2.5], [55.141361358941225, 24.986138101357326, 2.5], [55.14093322994411, 24.986218753894093, 2.5], [55.140897653420964, 24.986214283545635, 2.5], [55.14095492976058, 24.9863027591922, 2.5], [55.140900447388745, 24.98628436557094, 2.5], [55.140867059473706, 24.98628869622101, 2.5], [55.14089155325796, 24.986402364143782, 2.5], [55.14090938808566, 24.986479011993385, 2.5], [55.140943893587824, 24.986471188883584, 2.5], [55.1410161176551, 24.9864174050037, 2.5], [55.140996932409635, 24.986521806266644, 2.5], [55.14163554031332, 24.986910400619593, 2.5], [55.14095781686062, 24.987033474900578, 2.5], [55.14058258698692, 24.98693261266349, 2.5], [55.14032624044253, 24.98747538747211, 2.5], [55.14007240846915, 24.988001119077232, 2.5], [55.14013122149105, 24.98831115636925, 2.5], [55.13991827457961, 24.98834356639557, 2.5], [55.139779460946755, 24.988254625087706, 2.5], [55.13974742344948, 24.988261377176524, 2.5], [55.139515198160304, 24.98841811876934, 2.5], [55.13903617238334, 24.98817914139135, 2.5], [55.1391330764994, 24.988660542040925, 2.5], [55.13914369357698, 24.989438289540374, 2.5], [55.136431216517785, 24.98966711550207, 2.0], [55.13659028641709, 24.99041706302204, 2.0], [55.1355852030721, 24.990933481401207, 2.5], [55.13535549235394, 24.99110470506038, 2.5], [55.13512578163577, 24.99127592871955, 2.5], [55.129969653784556, 24.991440074326995, 2.5], [55.130221623112746, 24.988070688875112, 2.5], [55.130451333830905, 24.98789946521594, 2.5], [55.13089208224919, 24.98742639990359, 2.5], [55.132177586827666, 24.989003408454433, 2.5], [55.13238862452779, 24.988701566801254, 2.5], [55.132482594977674, 24.988501518707757, 2.5], [55.132525994610624, 24.988048802794115, 2.5], [55.13249018525683, 24.987180623870653, 2.5], [55.13253358488978, 24.986727907957015, 2.5], [55.1322761673244, 24.985827132742713, 2.5], [55.13163341503516, 24.98503862846729, 2.5], [55.131514764536504, 24.984469124700183, 2.5], [55.131275600894, 24.983796337257242, 2.0], [55.13066865795855, 24.98387601190528, 2.0], [55.13026930682963, 24.981537228037503, 2.0], [55.130260412698846, 24.981495691049748, 2.0], [55.13025151856806, 24.981454154061993, 2.0], [55.13022925995803, 24.98096497686874, 2.5], [55.12984453059386, 24.9804285816199, 2.5], [55.129998291954365, 24.98021419115843, 2.5], [55.12916764533149, 24.980385694214384, 2.5]]]),
+]
+
+
+wkt_cases = [
+    ('LINESTRING (1 1 1, 2 2)', 'LINESTRING (1.0000000000000000 1.0000000000000000, 2.0000000000000000 2.0000000000000000)'),
+    ('POLYGON ((0 0 0, 1 0 0, 1 1, 0 1 0, 0 0 0))', 'POLYGON ((0.0000000000000000 0.0000000000000000, 1.0000000000000000 0.0000000000000000, 1.0000000000000000 1.0000000000000000, 0.0000000000000000 1.0000000000000000, 0.0000000000000000 0.0000000000000000))')
+]
+
+
+@pytest.mark.parametrize('geojson', geojson_cases)
+def test_create_from_geojson(geojson):
+    with pytest.raises(ValueError) as exc:
+        wkt = asShape(geojson).wkt
+    assert exc.match("Inconsistent coordinate dimensionality")
+
+
+@pytest.mark.parametrize('constructor, args', direct_cases)
+def test_create_directly(constructor, args):
+    with pytest.raises(ValueError) as exc:
+        geom = constructor(*args)
+    assert exc.match("Inconsistent coordinate dimensionality")
+
+
+@pytest.mark.parametrize('wkt_geom,expected', wkt_cases)
+def test_create_from_wkt(wkt_geom, expected):
+    geom = wkt.loads(wkt_geom)
+    assert geom.wkt == expected
diff --git a/tests/test_linestring.py b/tests/test_linestring.py
index 6fd9093..51bd7c7 100644
--- a/tests/test_linestring.py
+++ b/tests/test_linestring.py
@@ -1,5 +1,7 @@
-from . import unittest, numpy
+from . import unittest, numpy, shapely20_deprecated
 import pytest
+
+from shapely.errors import ShapelyDeprecationWarning
 from shapely.geos import lgeos
 from shapely.geometry import LineString, asLineString, Point, LinearRing
 
@@ -38,23 +40,32 @@ class LineStringTestCase(unittest.TestCase):
                          {'type': 'LineString',
                           'coordinates': ((1.0, 2.0), (3.0, 4.0))})
 
+    @shapely20_deprecated
+    def test_linestring_mutate(self):
+        line = LineString(((1.0, 2.0), (3.0, 4.0)))
+
         # Coordinate modification
         line.coords = ((-1.0, -1.0), (1.0, 1.0))
         self.assertEqual(line.__geo_interface__,
                          {'type': 'LineString',
                           'coordinates': ((-1.0, -1.0), (1.0, 1.0))})
 
+    def test_linestring_adapter(self):
         # Adapt a coordinate list to a line string
         coords = [[5.0, 6.0], [7.0, 8.0]]
         la = asLineString(coords)
         self.assertEqual(la.coords[:], [(5.0, 6.0), (7.0, 8.0)])
 
+    def test_linestring_empty(self):
         # Test Non-operability of Null geometry
         l_null = LineString()
         self.assertEqual(l_null.wkt, 'GEOMETRYCOLLECTION EMPTY')
         self.assertEqual(l_null.length, 0.0)
 
+    @shapely20_deprecated
+    def test_linestring_empty_mutate(self):
         # Check that we can set coordinates of a null geometry
+        l_null = LineString()
         l_null.coords = [(0, 0), (1, 1)]
         self.assertAlmostEqual(l_null.length, 1.4142135623730951)
 
@@ -168,5 +179,12 @@ class LineStringTestCase(unittest.TestCase):
         self.assertEqual(a.shape[0], 0)
 
 
+def test_linestring_mutability_deprecated():
+    line = LineString(((1.0, 2.0), (3.0, 4.0)))
+    with pytest.warns(ShapelyDeprecationWarning, match="Setting"):
+        line.coords = ((-1.0, -1.0), (1.0, 1.0))
+
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(LineStringTestCase)
diff --git a/tests/test_make_valid.py b/tests/test_make_valid.py
new file mode 100644
index 0000000..3d1108c
--- /dev/null
+++ b/tests/test_make_valid.py
@@ -0,0 +1,20 @@
+from shapely.geometry import Polygon
+from shapely.validation import make_valid
+
+from tests.conftest import requires_geos_38
+
+
+@requires_geos_38
+def test_make_valid_invalid_input():
+    geom = Polygon([(0, 0), (0, 2), (1, 1), (2, 2), (2, 0), (1, 1), (0, 0)])
+    valid = make_valid(geom)
+    assert len(valid) == 2
+    assert all(geom.type == 'Polygon' for geom in valid)
+
+
+@requires_geos_38
+def test_make_valid_input():
+    geom = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
+    valid = make_valid(geom)
+    assert id(valid) == id(geom)
+
diff --git a/tests/test_minimum_clearance.py b/tests/test_minimum_clearance.py
new file mode 100644
index 0000000..be7f1fd
--- /dev/null
+++ b/tests/test_minimum_clearance.py
@@ -0,0 +1,36 @@
+"""
+Tests for the minimum clearance property.
+"""
+
+import math
+
+import pytest
+
+from shapely.wkt import loads as load_wkt
+from shapely.geos import geos_version
+
+requires_geos_36 = pytest.mark.skipif(geos_version < (3, 6, 0), reason="GEOS > 3.6.0 is required.")
+
+
+@requires_geos_36
+def test_point():
+    point = load_wkt("POINT (0 0)")
+    assert point.minimum_clearance == math.inf
+
+
+@requires_geos_36
+def test_linestring():
+    line = load_wkt('LINESTRING (0 0, 1 1, 2 2)')
+    assert round(line.minimum_clearance, 6) == 1.414214
+
+
+@requires_geos_36
+def test_simple_polygon():
+    poly = load_wkt('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
+    assert poly.minimum_clearance == 1.0
+
+
+@requires_geos_36
+def test_more_complicated_polygon():
+    poly = load_wkt('POLYGON ((20 20, 34 124, 70 140, 130 130, 70 100, 110 70, 170 20, 90 10, 20 20))')
+    assert round(poly.minimum_clearance, 6) == 35.777088
diff --git a/tests/test_point.py b/tests/test_point.py
index c959a3f..599a78f 100644
--- a/tests/test_point.py
+++ b/tests/test_point.py
@@ -1,6 +1,8 @@
-from . import unittest, numpy
+from . import unittest, numpy, shapely20_deprecated
 from shapely.geometry import Point, asPoint
-from shapely.errors import DimensionError
+from shapely.errors import DimensionError, ShapelyDeprecationWarning
+
+import pytest
 
 
 class LineStringTestCase(unittest.TestCase):
@@ -47,7 +49,10 @@ class LineStringTestCase(unittest.TestCase):
         self.assertEqual(p.__geo_interface__,
                          {'type': 'Point', 'coordinates': (3.0, 4.0)})
 
+    @shapely20_deprecated
+    def test_point_mutate(self):
         # Modify coordinates
+        p = Point(3.0, 4.0)
         p.coords = (2.0, 1.0)
         self.assertEqual(p.__geo_interface__,
                          {'type': 'Point', 'coordinates': (2.0, 1.0)})
@@ -57,6 +62,8 @@ class LineStringTestCase(unittest.TestCase):
         self.assertEqual(p.__geo_interface__,
                          {'type': 'Point', 'coordinates': (0.0, 0.0)})
 
+    def test_point_adapter(self):
+        p = Point(0.0, 0.0)
         # Adapt a coordinate list to a point
         coords = [3.0, 4.0]
         pa = asPoint(coords)
@@ -68,13 +75,17 @@ class LineStringTestCase(unittest.TestCase):
         self.assertEqual(pa.coords[0], (1.0, 4.0))
         self.assertAlmostEqual(pa.distance(p), 4.123105625617661)
 
+    def test_point_empty(self):
         # Test Non-operability of Null geometry
         p_null = Point()
         self.assertEqual(p_null.wkt, 'GEOMETRYCOLLECTION EMPTY')
         self.assertEqual(p_null.coords[:], [])
         self.assertEqual(p_null.area, 0.0)
 
+    @shapely20_deprecated
+    def test_point_empty_mutate(self):
         # Check that we can set coordinates of a null geometry
+        p_null = Point()
         p_null.coords = (1, 2)
         self.assertEqual(p_null.coords[:], [(1.0, 2.0)])
 
@@ -150,5 +161,11 @@ def test_empty_point_bounds():
     assert p.bounds == ()
 
 
+def test_point_mutability_deprecated():
+    p = Point(3.0, 4.0)
+    with pytest.warns(ShapelyDeprecationWarning, match="Setting"):
+        p.coords = (2.0, 1.0)
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(LineStringTestCase)
diff --git a/tests/test_polygon.py b/tests/test_polygon.py
index a00b7ed..e11fdee 100644
--- a/tests/test_polygon.py
+++ b/tests/test_polygon.py
@@ -1,7 +1,7 @@
 """Polygons and Linear Rings
 """
 
-from . import unittest, numpy
+from . import unittest, numpy, shapely20_deprecated
 from shapely.wkb import loads as load_wkb
 from shapely.errors import TopologicalError
 from shapely.geos import lgeos
@@ -12,7 +12,7 @@ from shapely.geometry.base import dump_coords
 
 class PolygonTestCase(unittest.TestCase):
 
-    def test_polygon(self):
+    def test_linearring(self):
 
         # Initialization
         # Linear rings won't usually be created by users, but by polygons
@@ -26,6 +26,11 @@ class PolygonTestCase(unittest.TestCase):
         # Ring from sequence of Points
         self.assertEqual(LinearRing((map(Point, coords))), ring)
 
+    @shapely20_deprecated
+    def test_linearring_mutate(self):
+        coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
+        ring = LinearRing(coords)
+
         # Coordinate modification
         ring.coords = ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0))
         self.assertEqual(
@@ -34,6 +39,7 @@ class PolygonTestCase(unittest.TestCase):
              'coordinates': ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0),
                              (0.0, 0.0))})
 
+    def test_linearring_adapter(self):
         # Test ring adapter
         coords = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]
         ra = asLinearRing(coords)
@@ -46,6 +52,9 @@ class PolygonTestCase(unittest.TestCase):
                          [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0),
                           (0.0, 0.0)])
 
+    def test_polygon(self):
+        coords = ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
+
         # Construct a polygon, exterior ring only
         polygon = Polygon(coords)
         self.assertEqual(len(polygon.exterior.coords), 5)
@@ -95,7 +104,7 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual(
             polygon.__geo_interface__,
             {'type': 'Polygon',
-             'coordinates': (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (2.0, -1.0),
+             'coordinates': (((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0),
                              (0.0, 0.0)), ((0.25, 0.25), (0.25, 0.5),
                              (0.5, 0.5), (0.5, 0.25), (0.25, 0.25)))})
 
@@ -106,21 +115,24 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual(len(pa.interiors), 1)
         self.assertEqual(len(pa.interiors[0].coords), 5)
 
+        # Error handling
+        with self.assertRaises(ValueError):
+            # A LinearRing must have at least 3 coordinate tuples
+            Polygon([[1, 2], [2, 3]])
+
+    def test_linearring_empty(self):
         # Test Non-operability of Null rings
         r_null = LinearRing()
         self.assertEqual(r_null.wkt, 'GEOMETRYCOLLECTION EMPTY')
         self.assertEqual(r_null.length, 0.0)
 
+    @shapely20_deprecated
+    def test_linearring_empty_mutate(self):
         # Check that we can set coordinates of a null geometry
+        r_null = LinearRing()
         r_null.coords = [(0, 0), (1, 1), (1, 0)]
         self.assertAlmostEqual(r_null.length, 3.414213562373095)
 
-        # Error handling
-        with self.assertRaises(ValueError):
-            # A LinearRing must have at least 3 coordinate tuples
-            Polygon([[1, 2], [2, 3]])
-
-
     def test_linearring_from_closed_linestring(self):
         coords = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)]
         line = LineString(coords)
diff --git a/tests/test_strtree.py b/tests/test_strtree.py
index 8eb5dcf..e046cbe 100644
--- a/tests/test_strtree.py
+++ b/tests/test_strtree.py
@@ -1,69 +1,81 @@
 import gc
 
-from . import unittest
-
 from shapely.strtree import STRtree
 from shapely.geometry import Point, Polygon
-from shapely.geos import geos_version
-
-
-@unittest.skipIf(geos_version < (3, 4, 2), 'GEOS 3.4.2 required')
-class STRTestCase(unittest.TestCase):
-    def test_query(self):
-        points = [Point(i, i) for i in range(10)]
-        tree = STRtree(points)
-        results = tree.query(Point(2,2).buffer(0.99))
-        self.assertEqual(len(results), 1)
-        results = tree.query(Point(2,2).buffer(1.0))
-        self.assertEqual(len(results), 3)
-
-    def test_insert_empty_geometry(self):
-        """
-        Passing nothing but empty geometries results in an empty strtree.
-        The query segfaults if the empty geometry was actually inserted.
-        """
-        empty = Polygon()
-        geoms = [empty]
-        tree = STRtree(geoms)
-        assert(tree._n_geoms == 0)
-        query = Polygon([(0,0),(1,1),(2,0),(0,0)])
-        results = tree.query(query)
-
-    def test_query_empty_geometry(self):
-        """
-        Empty geometries should be filtered out.
-        The query segfaults if the empty geometry was actually inserted.
-        """
-        empty = Polygon()
-        point = Point(1, 0.5)
-        geoms = [empty, point]
-        tree = STRtree(geoms)
-        assert(tree._n_geoms == 1)
-        query = Polygon([(0,0),(1,1),(2,0),(0,0)])
-        results = tree.query(query)
-        self.assertEqual(len(results), 1)
-        self.assertEqual(results[0], point)
-
-    def test_references(self):
-        """Don't crash due to dangling references"""
-        empty = Polygon()
-        point = Point(1, 0.5)
-        geoms = [empty, point]
-        tree = STRtree(geoms)
-        assert(tree._n_geoms == 1)
-
-        empty = None
-        point = None
-        gc.collect()
-
-        query = Polygon([(0,0),(1,1),(2,0),(0,0)])
-        results = tree.query(query)
-        self.assertEqual(len(results), 1)
-        self.assertEqual(results[0], Point(1, 0.5))
-
-
-def test_suite():
-    return unittest.TestLoader().loadTestsFromTestCase(STRTestCase)
-
-if __name__ == '__main__':
-    unittest.main()
+
+from shapely import strtree
+
+from tests.conftest import requires_geos_342
+
+
+@requires_geos_342
+def test_query():
+    points = [Point(i, i) for i in range(10)]
+    tree = STRtree(points)
+    results = tree.query(Point(2, 2).buffer(0.99))
+    assert len(results) == 1
+    results = tree.query(Point(2, 2).buffer(1.0))
+    assert len(results) == 3
+
+
+@requires_geos_342
+def test_insert_empty_geometry():
+    """
+    Passing nothing but empty geometries results in an empty strtree.
+    The query segfaults if the empty geometry was actually inserted.
+    """
+    empty = Polygon()
+    geoms = [empty]
+    tree = STRtree(geoms)
+    assert tree._n_geoms == 0
+    query = Polygon([(0, 0), (1, 1), (2, 0), (0, 0)])
+    results = tree.query(query)
+    assert len(results) == 0
+
+
+@requires_geos_342
+def test_query_empty_geometry():
+    """
+    Empty geometries should be filtered out.
+    The query segfaults if the empty geometry was actually inserted.
+    """
+    empty = Polygon()
+    point = Point(1, 0.5)
+    geoms = [empty, point]
+    tree = STRtree(geoms)
+    assert tree._n_geoms == 1
+    query = Polygon([(0, 0), (1, 1), (2, 0), (0, 0)])
+    results = tree.query(query)
+    assert len(results) == 1
+    assert results[0] == point
+
+
+@requires_geos_342
+def test_references():
+    """Don't crash due to dangling references"""
+    empty = Polygon()
+    point = Point(1, 0.5)
+    geoms = [empty, point]
+    tree = STRtree(geoms)
+    assert tree._n_geoms == 1
+
+    empty = None
+    point = None
+    gc.collect()
+
+    query = Polygon([(0, 0), (1, 1), (2, 0), (0, 0)])
+    results = tree.query(query)
+    assert len(results) == 1
+    assert results[0] == Point(1, 0.5)
+
+
+@requires_geos_342
+def test_safe_delete():
+    tree = STRtree([])
+
+    _lgeos = strtree.lgeos
+    strtree.lgeos = None
+
+    del tree
+
+    strtree.lgeos = _lgeos