Repository: Toblerity/Shapely
Pull request #843 (2020-02-18 20:37:16)

Problems with VScode users and multiples environments
## Virtual Environment VScode problem

I have multiples different environments and diferents IDEs. When I was in VScode without conda, I had the following error:

```

from MTSPTW.algorithm.clustering import Clustering...
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\prueba_test.py in 
----> 1 from MTSPTW.algorithm.clustering import Clustering

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\MTSPTW\algorithm\clustering.py in 
      2 from pandas import concat, DataFrame
      3 from sklearn.cluster import KMeans
----> 4 from geopandas import datasets, read_file
      5 from shapely.geometry import Point
      6 import matplotlib.cm as color_maps

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\geopandas\__init__.py in 
----> 1 from geopandas.geoseries import GeoSeries  # noqa
      2 from geopandas.geodataframe import GeoDataFrame  # noqa
      3 from geopandas.array import _points_from_xy as points_from_xy  # noqa
      4 
      5 from geopandas.io.file import read_file  # noqa

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\geopandas\geoseries.py in 
      8 
      9 from pyproj import CRS, Transformer
---> 10 from shapely.geometry.base import BaseGeometry
     11 from shapely.ops import transform
     12 

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\shapely\geometry\__init__.py in 
      2 """
      3 
----> 4 from .base import CAP_STYLE, JOIN_STYLE
      5 from .geo import box, shape, asShape, mapping
      6 from .point import Point, asPoint

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\shapely\geometry\base.py in 
     16 
     17 from shapely.affinity import affine_transform
---> 18 from shapely.coords import CoordinateSequence
     19 from shapely.errors import WKBReadingError, WKTReadingError
     20 from shapely.geos import WKBWriter, WKTWriter

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\shapely\coords.py in 
      6 from ctypes import byref, c_double, c_uint
      7 
----> 8 from shapely.geos import lgeos
      9 from shapely.topology import Validating
     10 

c:\Users\alberto.rubiales\Desktop\projects\MTSPTW\venv-MTSPTW\lib\site-packages\shapely\geos.py in 
    143     if os.getenv('CONDA_PREFIX', ''):
    144         # conda package.
--> 145         _lgeos = CDLL(os.path.join(sys.prefix, 'Library', 'bin', 'geos_c.dll'))
    146     else:
    147         try:

~\AppData\Local\Programs\Python\Python37\lib\ctypes\__init__.py in __init__(self, name, mode, handle, use_errno, use_last_error)
    362 
    363         if handle is None:
--> 364             self._handle = _dlopen(self._name, mode)
    365         else:
    366             self._handle = handle

OSError: [WinError 126] The specified module could not be found
```

In VScode without Conda when you execute code in jupyter interactive console with a virtual environment and you have other environments with conda  ```os.getenv('CONDA_PREFIX', '')``` is True, and the code give and error, because your actual environment is not in conda, so you need to change the doble condition.

**I have been researching and I think that this behaviour occurs because the Interactive Jupyter Python console of VScode works internally with Conda.**  But don't have conda-meta folder.

I have tested my solution in Virtual Environments with and without conda, and both work.

my current setup is:

* Windows: 10 64bits win32
* VScode: 1.42
* Python: 3.7.6
* Conda versions: 3.5.2 and 4.8.1

Yes, I have tested in both Conda versions.

any help or additional information you need, please let me know!

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

Coverage remained the same at 80.937% when pulling **4bbd2c26b07d4e6a5700df2ff5a07a76ca4c6d1d on arubiales:master** into **fdbc34ee375ebc08183e45a4d4c0c32bf7cea166 on Toblerity:master**.

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

Coverage remained the same at 80.937% when pulling **4bbd2c26b07d4e6a5700df2ff5a07a76ca4c6d1d on arubiales:master** into **fdbc34ee375ebc08183e45a4d4c0c32bf7cea166 on Toblerity:master**.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 9f071fe..775d9bb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,21 +8,6 @@ cache:
 
 matrix:
   include:
-    - python: "2.7"
-      env:
-        GEOSVERSION="3.4.3"
-        SPEEDUPS=1
-        NUMPY=1
-    - python: "2.7"
-      env:
-        GEOSVERSION="3.4.3"
-        SPEEDUPS=0
-        NUMPY=1
-    - python: "2.7"
-      env:
-        GEOSVERSION="3.4.3"
-        SPEEDUPS=0
-        NUMPY=0
     - python: "3.5"
       env:
         GEOSVERSION="3.5.2"
@@ -70,17 +55,17 @@ matrix:
         NUMPY=0
     - python: "3.8"
       env:
-        GEOSVERSION="3.8.0"
+        GEOSVERSION="3.8.1"
         SPEEDUPS=1
         NUMPY=1
     - python: "3.8"
       env:
-        GEOSVERSION="3.8.0"
+        GEOSVERSION="3.8.1"
         SPEEDUPS=0
         NUMPY=1
     - python: "3.8"
       env:
-        GEOSVERSION="3.8.0"
+        GEOSVERSION="3.8.1"
         SPEEDUPS=0
         NUMPY=0
     - python: "3.8-dev"
@@ -111,6 +96,9 @@ install:
   - pip install -v -e .[all]
 
 script:
+  - export LD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOSVERSION/lib
+  - export DYLD_LIBRARY_PATH=$HOME/geosinstall/geos-$GEOSVERSION/lib
+  - python -c "from shapely.geos import geos_version; print(geos_version)"
   - python -m pytest --cov shapely --cov-report term-missing "${SPEEDUPS_FLAG}"
 
 after_success:
diff --git a/CHANGES.txt b/CHANGES.txt
index 7321d93..ae11305 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,21 @@
 Changes
 =======
 
+1.8.0 (TBD)
+-----------
+
+New features:
+
+- shapely.ops.voronoi_diagram() computes the Voronoi Diagram of a geometry or geometry collection (#833, #851).
+
+1.7.1 (TBD)
+-----------
+
+- 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
+  (#852).
+
 1.7.0 (2020-01-28)
 ------------------
 
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/_vendor/packaging/_compat.py b/_vendor/packaging/_compat.py
index 210bb80..3247969 100644
--- a/_vendor/packaging/_compat.py
+++ b/_vendor/packaging/_compat.py
@@ -1,21 +1,9 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
-
-import sys
-
-
-PY2 = sys.version_info[0] == 2
-PY3 = sys.version_info[0] == 3
 
 # flake8: noqa
 
-if PY3:
-    string_types = str,
-else:
-    string_types = basestring,
-
 
 def with_metaclass(meta, *bases):
     """
diff --git a/_vendor/packaging/markers.py b/_vendor/packaging/markers.py
index 52fcee1..d1f3d78 100644
--- a/_vendor/packaging/markers.py
+++ b/_vendor/packaging/markers.py
@@ -12,7 +12,6 @@ from pyparsing import ParseException, ParseResults, stringStart, stringEnd
 from pyparsing import ZeroOrMore, Group, Forward, QuotedString
 from pyparsing import Literal as L  # noqa
 
-from ._compat import string_types
 from .specifiers import Specifier, InvalidSpecifier
 
 
@@ -132,7 +131,7 @@ def _coerce_parse_result(results):
 
 
 def _format_marker(marker, first=True):
-    assert isinstance(marker, (list, tuple, string_types))
+    assert isinstance(marker, (list, tuple, str))
 
     # Sometimes we have a structure like [[...]] which is a single item list
     # where the single item is itself it's own list. In that case we want skip
@@ -201,7 +200,7 @@ def _evaluate_markers(markers, environment):
     groups = [[]]
 
     for marker in markers:
-        assert isinstance(marker, (list, tuple, string_types))
+        assert isinstance(marker, (list, tuple, str))
 
         if isinstance(marker, list):
             groups[-1].append(_evaluate_markers(marker, environment))
diff --git a/_vendor/packaging/specifiers.py b/_vendor/packaging/specifiers.py
index 08f3e5f..902f6de 100644
--- a/_vendor/packaging/specifiers.py
+++ b/_vendor/packaging/specifiers.py
@@ -8,7 +8,7 @@ import functools
 import itertools
 import re
 
-from ._compat import string_types, with_metaclass
+from ._compat import with_metaclass
 from .version import Version, LegacyVersion, parse
 
 
@@ -112,7 +112,7 @@ class _IndividualSpecifier(BaseSpecifier):
         return hash(self._spec)
 
     def __eq__(self, other):
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             try:
                 other = self.__class__(other)
             except InvalidSpecifier:
@@ -123,7 +123,7 @@ class _IndividualSpecifier(BaseSpecifier):
         return self._spec == other._spec
 
     def __ne__(self, other):
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             try:
                 other = self.__class__(other)
             except InvalidSpecifier:
@@ -625,7 +625,7 @@ class SpecifierSet(BaseSpecifier):
         return hash(self._specs)
 
     def __and__(self, other):
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             other = SpecifierSet(other)
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -648,7 +648,7 @@ class SpecifierSet(BaseSpecifier):
         return specifier
 
     def __eq__(self, other):
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             other = SpecifierSet(other)
         elif isinstance(other, _IndividualSpecifier):
             other = SpecifierSet(str(other))
@@ -658,7 +658,7 @@ class SpecifierSet(BaseSpecifier):
         return self._specs == other._specs
 
     def __ne__(self, other):
-        if isinstance(other, string_types):
+        if isinstance(other, str):
             other = SpecifierSet(other)
         elif isinstance(other, _IndividualSpecifier):
             other = SpecifierSet(str(other))
diff --git a/appveyor.yml b/appveyor.yml
index dd3816d..54f53a5 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -2,107 +2,69 @@ image: Visual Studio 2017
 platform: x64
 
 environment:
-  global:
-    GEOSVERSION: "3.8.0"
-
   matrix:
-#    - PYTHON: "C:\\Python27"
-#      ARCH: x86
-#      COMPILER: msvc90
-#      GEOSVERSION: "3.5.2"
-#      image: Visual Studio 2015
-#    - PYTHON: "C:\\Python27-x64"
-#      ARCH: x64
-#      COMPILER: msvc90
-#      GEOSVERSION: "3.5.2"
-#      image: Visual Studio 2015
+    # Interleave PYTHON, ARCH and GEOSVERSION
     - PYTHON: "C:\\Python35"
       ARCH: x86
       COMPILER: msvc2017
-    - PYTHON: "C:\\Python35-x64"
-      ARCH: x64
-      COMPILER: msvc2017
-    - PYTHON: "C:\\Python36"
-      ARCH: x86
-      COMPILER: msvc2017
+      GEOSVERSION: "3.5.2"
     - PYTHON: "C:\\Python36-x64"
       ARCH: x64
       COMPILER: msvc2017
+      GEOSVERSION: "3.6.4"
     - PYTHON: "C:\\Python37"
       ARCH: x86
       COMPILER: msvc2017
-    - PYTHON: "C:\\Python37-x64"
-      ARCH: x64
-      COMPILER: msvc2017
-    - PYTHON: "C:\\Python38"
-      ARCH: x86
-      COMPILER: msvc2017
+      GEOSVERSION: "3.7.3"
     - PYTHON: "C:\\Python38-x64"
       ARCH: x64
       COMPILER: msvc2017
+      GEOSVERSION: "3.8.1"
 
-build_script:
-  - set _INITPATH=%PATH%
 
-  - ps: 'Write-Host "Configuring PATH with $env:PYTHON" -ForegroundColor Magenta'
-  - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%
+install:
+  # If there is a newer build queued for the same PR, cancel this one.
+  # The AppVeyor 'rollout builds' option is supposed to serve the same
+  # purpose but it is problematic because it tends to cancel builds pushed
+  # directly to master instead of just PR builds (or the converse).
+  # credits: JuliaLang developers.
+  - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod `
+        https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | `
+        Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { `
+          throw "There are newer queued builds for this pull request, failing early." }
+
+
+build_script:
+  - set GEOSINSTALL=C:\projects\deps\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
+  - set CYTHONPF=C:\\projects\\deps\\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
+  - set GEOS_LIBRARY_PATH=%GEOSINSTALL%\bin\geos_c.dll
 
   - ps: 'Write-Host "Configuring MSVC compiler $env:COMPILER" -ForegroundColor Magenta'
-  - if %COMPILER%==msvc90 ( call "C:\Users\appveyor\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\vcvarsall.bat" %ARCH% )
   - if %COMPILER%==msvc2017 ( call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" %ARCH% )
 
-  - ps: 'Write-Host "Cached dependency folder" -ForegroundColor Magenta'
-  - if not exist C:\projects\deps mkdir C:\projects\deps
-  - dir C:\projects\deps
+  - ps: 'Write-Host "Configuring PATH with $env:PYTHON and $env:GEOSINSTALL\bin" -ForegroundColor Magenta'
+  - set PATH=%PYTHON%;%PYTHON%\Scripts;%GEOSINSTALL%\bin;%PATH%
+  - python -m pip install --disable-pip-version-check --upgrade pip
 
-  - set GEOSINSTALL=C:\projects\deps\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
-  - set CYTHONPF=C:\\projects\\deps\\geos-%GEOSVERSION%-%COMPILER%-%ARCH%
   - ps: 'Write-Host "Checking GEOS build $env:GEOSINSTALL" -ForegroundColor Magenta'
-  - call c:\projects\shapely\ci\appveyor\build_geos.cmd
-
-  # - ps: 'Push-AppveyorArtifact $env:GEOSINSTALL\bin\geos.dll'
-  # - ps: 'Push-AppveyorArtifact $env:GEOSINSTALL\bin\geos_c.dll'
+  - if not exist C:\projects\deps mkdir C:\projects\deps
+  - call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\build_geos.cmd
 
-  - ps: 'Write-Host "Copying and checking geos_c.dll" -ForegroundColor Magenta'
-  - cd C:\projects\shapely
-  - dir
-  - if %ARCH%==x86 ( set CPDIR=DLLs_x86_VC9 )
-  - if %ARCH%==x64 ( set CPDIR=DLLs_AMD64_VC9 )
-  - xcopy %GEOSINSTALL%\bin\geos*.dll %CPDIR%
-  - cd %CPDIR%
-  - dir
-  - dumpbin /dependents geos_c.dll
-  - dumpbin /dependents %PYTHON%\python.exe
+  - ps: 'Write-Host "Checking geos_c.dll" -ForegroundColor Magenta'
   - python -c "import sys; print('Python ' + sys.version)"
-  - python -c "import ctypes; print(ctypes.CDLL('./geos_c.dll'))"
-  - cd ..
+  - python -c "import ctypes, os; print(ctypes.CDLL(os.environ['GEOS_LIBRARY_PATH']))"
 
-  - ps: 'Write-Host "Building wheel" -ForegroundColor Magenta'
+  - ps: 'Write-Host "Building extension" -ForegroundColor Magenta'
+  - cd %APPVEYOR_BUILD_FOLDER%
   - pip install -r requirements-dev.txt
-  - set GEOS_LIBRARY_PATH=%GEOSINSTALL%\bin\geos_c.dll
-  - set PATH=%GEOSINSTALL%\bin;%PATH%
-  - python setup.py build_ext -I%CYTHONPF%\\include -lgeos_c -L%CYTHONPF%\\lib bdist_wheel
+  - python setup.py build_ext --inplace -I%CYTHONPF%\\include -lgeos_c -L%CYTHONPF%\\lib
 
-  - ps: 'Write-Host "Checking pip install in a new environment" -ForegroundColor Magenta'
-  - set PATH=%_INITPATH%
-  - set GEOS_LIBRARY_PATH=
-  - '%PYTHON%\python.exe -m venv C:\projects\test'
-  - call C:\projects\test\Scripts\activate.bat
-  - pip install -r requirements-dev.txt
-  - cd dist
-  - pip install --no-index --find-links . --no-cache-dir shapely
+  - ps: 'Write-Host "Running pytest" -ForegroundColor Magenta'
+  - pytest
   - python -c "import shapely; print(shapely.__version__)"
   - python -c "from shapely.geos import geos_version_string; print(geos_version_string)"
   - python -c "from shapely import speedups; assert speedups.enabled"
 
-  - ps: 'Write-Host "Running pytest" -ForegroundColor Magenta'
-  - xcopy ..\tests tests\
-  - pytest
-
 
 cache:
-  - C:\projects\deps
-
-artifacts:
-  - path: dist\*.whl
-    name: wheel
+  - C:\projects\deps -> %APPVEYOR_BUILD_FOLDER%\ci\appveyor\build_geos.cmd
diff --git a/ci/appveyor/build_geos.cmd b/ci/appveyor/build_geos.cmd
index 05cda63..26a7c81 100644
--- a/ci/appveyor/build_geos.cmd
+++ b/ci/appveyor/build_geos.cmd
@@ -10,17 +10,16 @@ if exist %GEOSINSTALL% (
   curl -fsSO http://download.osgeo.org/geos/geos-%GEOSVERSION%.tar.bz2
   7z x geos-%GEOSVERSION%.tar.bz2
   7z x geos-%GEOSVERSION%.tar
-  cd geos-%GEOSVERSION%
+  cd geos-%GEOSVERSION% || exit /B 5
 
   pip install ninja
   cmake --version
 
   mkdir build
   cd build
-  cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOSINSTALL% ..
-  cmake --build . --config Release -j 2
-  ctest . --config Release
-  cmake --install . --config Release
+  cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=%GEOSINSTALL% .. || exit /B 1
+  cmake --build . --config Release || exit /B 2
+  ctest . --config Release || exit /B 3
+  cmake --install . --config Release || exit /B 4
   cd ..
 )
-
diff --git a/ci/travis/install_geos.sh b/ci/travis/install_geos.sh
index 852224a..424d028 100755
--- a/ci/travis/install_geos.sh
+++ b/ci/travis/install_geos.sh
@@ -13,17 +13,9 @@ function build_geos {
     echo "Building geos-$GEOSVERSION"
     mkdir build
     cd build
-    # disable building tests to speed up compilation
-    if [ "$GEOSVERSION" = "master" ] || dpkg --compare-versions "$GEOSVERSION" "gt" "3.8.0"; then
-        BUILD_TESTING=BUILD_TESTING
-    else
-        # cmake variable used for older GEOS releases
-        BUILD_TESTING=GEOS_ENABLE_TESTS
-        # note that this intentially also catches GEOS "3.8.0"
-        # see https://trac.osgeo.org/geos/ticket/1006
-    fi
-    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$GEOSINSTVERSION -D$BUILD_TESTING=OFF ..
+    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$GEOSINSTVERSION ..
     make -j 2
+    ctest .
     make install
 }
 
diff --git a/docs/code/voronoi_diagram.py b/docs/code/voronoi_diagram.py
new file mode 100644
index 0000000..09d2c41
--- /dev/null
+++ b/docs/code/voronoi_diagram.py
@@ -0,0 +1,24 @@
+from shapely.geometry import MultiPoint
+from shapely.ops import voronoi_diagram
+
+from matplotlib import pyplot
+from descartes.patch import PolygonPatch
+from figures import SIZE, BLUE, GRAY, set_limits
+
+points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)])
+regions = voronoi_diagram(points)
+
+fig = pyplot.figure(1, figsize=SIZE, dpi=90)
+fig.set_frameon(True)
+ax = fig.add_subplot(111)
+
+for region in regions:
+    patch = PolygonPatch(region, facecolor=BLUE, edgecolor=BLUE, alpha=0.5, zorder=2)
+    ax.add_patch(patch)
+
+for point in points:
+    pyplot.plot(point.x, point.y, 'o', color=GRAY)
+
+set_limits(ax, -1, 4, -1, 3)
+
+pyplot.show()
diff --git a/docs/manual.rst b/docs/manual.rst
index 6f3ba75..fd4bf13 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
@@ -1008,7 +1024,7 @@ A ring with an undesired orientation can be reversed like this:
 
 .. attribute:: object.is_ring
 
-  Returns ``True`` if the feature is closed. A closed feature's `boundary`
+  Returns ``True`` if the feature is a closed and simple ``LineString``. A closed feature's `boundary`
   coincides with the empty set.
 
 .. code-block:: pycon
@@ -1676,23 +1692,35 @@ linestring feature (right).
   Returns a LineString or MultiLineString geometry at a distance from the
   object on its right or its left side.
 
-  Distance must be a positive float value. The side parameter may be 'left' or
-  'right'. Left and right is determined by following the direction of given 
-  geometric points of the LineString. The resolution of the offset around 
-  each vertex of the object is parameterized as in the buffer method.
+  The `distance` parameter must be a positive float value.
 
-  The join style is for outside corners between line segments. Accepted integer
+  The `side` parameter may be 'left' or 'right'. Left and right are determined
+  by following the direction of the given geometric points of the LineString.
+  Right hand offsets are returned in the reverse direction of the original
+  LineString or LineRing, while left side offsets flow in the same direction.
+
+  The `resolution` of the offset around each vertex of the object is
+  parameterized as in the :meth:`buffer` method.
+
+  The `join_style` is for outside corners between line segments. Accepted integer
   values are 1 (round), 2 (mitre), and 3 (bevel). See also
   :data:`shapely.geometry.JOIN_STYLE`.
 
-  Severely mitered corners can be controlled by the mitre_limit parameter
-  (spelled in British English, en-gb). The ratio of the distance from the
-  corner to the end of the mitred offset corner is the miter ratio. Corners
-  with a ratio which exceed the limit will be beveled.
+  Severely mitered corners can be controlled by the `mitre_limit` parameter
+  (spelled in British English, en-gb). The corners of a parallel line will
+  be further from the original than most places with the mitre join style. The
+  ratio of this further distance to the specified `distance` is the miter ratio.
+  Corners with a ratio which exceed the limit will be beveled.
 
-.. note::
+  .. note::
 
-  This method is only available for `LinearRing` and `LineString`  objects.
+    This method may sometimes return a `MultiLineString` where a simple
+    `LineString` was expected; for example, an offset to a slightly
+    curved LineString.
+
+  .. note::
+
+    This method is only available for `LinearRing` and `LineString`  objects.
 
 .. plot:: code/parallel_offset.py
 
@@ -1963,10 +1991,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
@@ -1979,30 +2014,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
+
+    wgs84_pt = Point(-72.2495, 43.886)
 
-    project = partial(
-        pyproj.transform,
-        proj_in,
-        proj_out)
+    wgs84 = pyproj.CRS('EPSG:4326')
+    utm = pyproj.CRS('EPSG:32618')
+
+    project = pyproj.Transformer.from_crs(wgs84, utm, always_xy=True).transform
+    utm_point = transform(project, wgs84_pt)
 
-    g2 = transform(project, g1)
+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.0` a more performant method would be
+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
 
@@ -2183,6 +2233,51 @@ Delaunay triangulation from a collection of points.
    'POLYGON ((3 1, 1 1, 1 0, 3 1))',
    'POLYGON ((1 0, 1 1, 0 0, 1 0))']
 
+Voronoi Diagram
+---------------
+
+The :func:`~shapely.ops.voronoi_diagram` function in `shapely.ops` constructs a
+Voronoi diagram from a collection points, or the vertices of any geometry.
+
+.. plot:: code/voronoi_diagram.py
+
+.. function:: shapely.ops.voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False)
+
+   Constructs a Voronoi diagram from the vertices of the input geometry.
+
+   The source may be any geometry type. All vertices of the geometry will be
+   used as the input points to the diagram.
+
+   The `envelope` keyword argument provides an envelope to use to clip the
+   resulting diagram. If `None`, it will be calculated automatically.
+   The diagram will be clipped to the *larger* of the provided envelope
+   or an envelope surrounding the sites.
+
+   The `tolerance` keyword argument sets the snapping tolerance used to improve
+   the robustness of the computation. A tolerance of 0.0 specifies
+   that no snapping will take place. The tolerance `argument` can be
+   finicky and is known to cause the algorithm to fail in several cases.
+   If you're using `tolerance` and getting a failure, try removing it.
+   The test cases in `tests/test_voronoi_diagram.py` show more details.
+
+   If the `edges` keyword argument is `False` a list of `Polygon`s
+   will be returned. Otherwise a list of `LineString` edges is returned.
+
+
+.. code-block:: pycon
+
+  >>> from shapely.ops import voronoi_diagram
+  >>> points = MultiPoint([(0, 0), (1, 1), (0, 2), (2, 2), (3, 1), (1, 0)])
+  >>> regions = voronoi_diagram(points)
+  >>> pprint([region.wkt for region in regions])
+  ['POLYGON ((2 1, 2 0.5, 0.5 0.5, 0 1, 1 2, 2 1))',
+   'POLYGON ((6 5, 6 -3, 3.75 -3, 2 0.5, 2 1, 6 5))',
+   'POLYGON ((0.5 -3, -3 -3, -3 1, 0 1, 0.5 0.5, 0.5 -3))',
+   'POLYGON ((3.75 -3, 0.5 -3, 0.5 0.5, 2 0.5, 3.75 -3))',
+   'POLYGON ((-3 1, -3 5, 1 5, 1 2, 0 1, -3 1))',
+   'POLYGON ((1 5, 6 5, 2 1, 1 2, 1 5))']
+
+
 Nearest points
 --------------
 
@@ -2300,9 +2395,9 @@ The :func:`~shapely.ops.substring` function in `shapely.ops` returns a line segm
     Negative distance values are taken as measured in the reverse
     direction from the end of the geometry. Out-of-range index
     values are handled by clamping them to the valid range of values.
-    
+
     If the start distance equals the end distance, a point is being returned.
-    
+
     If the normalized arg is True, the distance will be interpreted as a
     fraction of the geometry's length.
 
@@ -2409,37 +2504,61 @@ STR-packed R-tree
 
 Shapely provides an interface to the query-only GEOS R-tree packed using the
 Sort-Tile-Recursive algorithm. Pass a list of geometry objects to the STRtree
-constructor to create an R-tree that you can query with another geometric object.
+constructor to create a spatial index that you can query with another geometric
+object. Query-only means that once created, the `STRtree` is immutable. You
+cannot add or remove geometries.
 
 .. class:: strtree.STRtree(geometries)
 
   The `STRtree` constructor takes a sequence of geometric objects.
 
-  These are copied and stored in the R-tree.
+  References to these geometric objects are kept and stored in the R-tree.
 
   `New in version 1.4.0`.
 
-The `query` method on `STRtree` returns a list of all geometries in the tree that
-intersect the provided geometry argument. If you want to match geometries of a
-more specific spatial relationship (eg. crosses, contains, overlaps), consider
-performing the query on the R-tree, followed by a manual search through the
-returned subset using the desired binary predicate.
+  .. method:: strtree.query(geom)
 
-Query-only means that once created, the R-tree is immutable. You cannot 
-add or remove geometries.
+    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
+    subset using the desired binary predicate (eg. intersects, crosses, contains,
+    overlaps) may be necessary to further filter the results according to their
+    specific spatial relationships.
 
-.. code-block:: pycon
+    .. code-block:: pycon
 
-  >>> from shapely.geometry import Point
-  >>> from shapely.strtree import STRtree
-  >>> points = [Point(i, i) for i in range(10)]
-  >>> tree = STRtree(points)
-  >>> tree.query(Point(2,2).buffer(0.99))
-  >>> [o.wkt for o in tree.query(Point(2,2).buffer(0.99))]
-  ['POINT (2 2)']
-  >>> [o.wkt for o in tree.query(Point(2,2).buffer(1.0))]
-  ['POINT (1 1)', 'POINT (2 2)', 'POINT (3 3)']
+      >>> from shapely.strtree import STRtree
+      >>> points = [Point(i, i) for i in range(10)]
+      >>> tree = STRtree(points)
+      >>> query_geom = Point(2,2).buffer(0.99)
+      >>> [o.wkt for o in tree.query(query_geom)]
+      ['POINT (2 2)']
+      >>> query_geom = Point(2, 2).buffer(1.0)
+      >>> [o.wkt for o in tree.query(query_geom)]
+      ['POINT (1 1)', 'POINT (2 2)', 'POINT (3 3)']
+      >>> [o.wkt for o in tree.query(query_geom) if o.intersects(query_geom)]
+      ['POINT (2 2)']
+
+    .. note::
+      To get the original indexes of the query results, create an auxiliary
+      dictionary. But use the geometry `ids` as keys since the shapely geometries
+      themselves are not hashable.
+
+      .. code-block:: pycon
+
+        >>> index_by_id = dict((id(pt), i) for i, pt in enumerate(points))
+        >>> [(index_by_id[id(pt)], pt.wkt) for pt in tree.query(Point(2,2).buffer(1.0))]
+        [(1, 'POINT (1 1)'), (2, 'POINT (2 2)'), (3, 'POINT (3 3)')]
+
+
+  .. method:: strtree.nearest(geom)
+
+    Returns the nearest geometry in `strtree` to `geom`.
+
+    .. code-block:: pycon
 
+      >>> tree = STRtree([Point(i, i) for i in range(10)])
+      >>> tree.nearest(Point(2.2, 2.2)).wkt
+      'Point (2 2)'
 
 Interoperation
 ==============
diff --git a/environment.yml b/environment.yml
index 1715107..18982df 100644
--- a/environment.yml
+++ b/environment.yml
@@ -1,11 +1,8 @@
 name: _shapely
 channels:
 - defaults
-- conda-forge
 dependencies:
-- python>=3.5
-- cython
-- descartes
-- geos>=3.3
+- python==3.6.*
+- geos==3.8.*
 - matplotlib
-- numpy>=1.9
+- descartes
diff --git a/setup.py b/setup.py
index 42d21cc..232c968 100755
--- a/setup.py
+++ b/setup.py
@@ -61,25 +61,14 @@ import re
 import shutil
 import subprocess
 import sys
-try:
-    # If possible, use setuptools
-    from setuptools import setup
-    from setuptools.extension import Extension
-    from setuptools.command.build_ext import build_ext as distutils_build_ext
-except ImportError:
-    from distutils.core import setup
-    from distutils.extension import Extension
-    from distutils.command.build_ext import build_ext as distutils_build_ext
+from setuptools import setup
+from setuptools.extension import Extension
+from setuptools.command.build_ext import build_ext as distutils_build_ext
 from distutils.errors import CCompilerError, DistutilsExecError, \
     DistutilsPlatformError
 
 from _vendor.packaging.version import Version
 
-# Ensure minimum version of Python is running
-py_version = sys.version_info[0:2]
-if not (py_version == (2, 7) or py_version >= (3, 4)):
-    raise RuntimeError('Shapely requires Python 2.7, >=3.4')
-
 # Get geos_version from GEOS dynamic library, which depends on
 # GEOS_LIBRARY_PATH and/or GEOS_CONFIG environment variables
 from shapely._buildcfg import geos_version_string, geos_version, \
@@ -110,10 +99,7 @@ class GEOSConfig(object):
             raise OSError("Could not find geos-config script")
         if stderr and not stdout:
             raise ValueError(stderr.strip())
-        if sys.version_info[0] >= 3:
-            result = stdout.decode('ascii').strip()
-        else:
-            result = stdout.strip()
+        result = stdout.decode('ascii').strip()
         log.debug('%s %s: %r', self.cmd, option, result)
         return result
 
@@ -163,21 +149,16 @@ if geos_config and not os.environ.get('NO_GEOS_CHECK') or sys.platform == 'win32
             "Installation continuing. GEOS version will be "
             "checked on import of shapely.", exc)
 
-# Handle UTF-8 encoding of certain text files.
-open_kwds = {}
-if sys.version_info >= (3,):
-    open_kwds['encoding'] = 'utf-8'
-
-with open('VERSION.txt', 'w', **open_kwds) as fp:
+with open('VERSION.txt', 'w') as fp:
     fp.write(str(shapely_version))
 
-with open('README.rst', 'r', **open_kwds) as fp:
+with open('README.rst', 'r') as fp:
     readme = fp.read()
 
-with open('CREDITS.txt', 'r', **open_kwds) as fp:
+with open('CREDITS.txt', 'r', encoding='utf-8') as fp:
     credits = fp.read()
 
-with open('CHANGES.txt', 'r', **open_kwds) as fp:
+with open('CHANGES.txt', 'r') as fp:
     changes = fp.read()
 
 long_description = readme + '\n\n' + credits + '\n\n' + changes
@@ -185,7 +166,6 @@ long_description = readme + '\n\n' + credits + '\n\n' + changes
 extra_reqs = {
     'test': ['pytest', 'pytest-cov'],
     'vectorized': ['numpy']}
-
 extra_reqs['all'] = list(it.chain.from_iterable(extra_reqs.values()))
 
 # Make a dict of setup arguments. Some items will be updated as
@@ -193,7 +173,6 @@ extra_reqs['all'] = list(it.chain.from_iterable(extra_reqs.values()))
 setup_args = dict(
     name                = 'Shapely',
     version             = str(shapely_version),
-    requires            = ['Python (>=2.7)', 'libgeos_c (>=3.3)'],
     description         = 'Geometric objects, predicates, and operations',
     license             = 'BSD',
     keywords            = 'geometry topology gis',
@@ -217,8 +196,6 @@ setup_args = dict(
         'Intended Audience :: Science/Research',
         'License :: OSI Approved :: BSD License',
         'Operating System :: OS Independent',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
@@ -227,6 +204,7 @@ setup_args = dict(
         'Topic :: Scientific/Engineering :: GIS',
     ],
     cmdclass           = {},
+    python_requires    = '>=3.5',
     extras_require     = extra_reqs,
     package_data={
         'shapely': ['shapely/_geos.pxi']},
diff --git a/shapely/__init__.py b/shapely/__init__.py
index 14d9d2f..9c6de1a 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.7.0"
+__version__ = "1.8dev"
diff --git a/shapely/_buildcfg.py b/shapely/_buildcfg.py
index 79752cf..086539b 100644
--- a/shapely/_buildcfg.py
+++ b/shapely/_buildcfg.py
@@ -76,10 +76,7 @@ def get_geos_config(option):
             'Could not find geos-config %r: %s' % (geos_config, ex))
     if stderr and not stdout:
         raise ValueError(stderr.strip())
-    if sys.version_info[0] >= 3:
-        result = stdout.decode('ascii').strip()
-    else:
-        result = stdout.strip()
+    result = stdout.decode('ascii').strip()
     log.debug('%s %s: %r', geos_config, option, result)
     return result
 
@@ -239,9 +236,7 @@ def _geos_version():
     GEOSversion.restype = c_char_p
     GEOSversion.argtypes = []
     # #define GEOS_CAPI_VERSION "@VERSION@-CAPI-@CAPI_VERSION@"
-    geos_version_string = GEOSversion()
-    if sys.version_info[0] >= 3:
-        geos_version_string = geos_version_string.decode('ascii')
+    geos_version_string = GEOSversion().decode('ascii')
 
     res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_version_string)
     geos_version = tuple(int(x) for x in res[0])
diff --git a/shapely/algorithms/polylabel.py b/shapely/algorithms/polylabel.py
index 08382f1..b4e7f4b 100644
--- a/shapely/algorithms/polylabel.py
+++ b/shapely/algorithms/polylabel.py
@@ -47,7 +47,9 @@ class Cell(object):
         boundary.
         """
         inside = polygon.contains(self.centroid)
-        distance = self.centroid.distance(LineString(polygon.exterior.coords))
+        distance = self.centroid.distance(polygon.exterior)
+        for interior in polygon.interiors:
+            distance = min(distance, self.centroid.distance(interior))
         if inside:
             return distance
         return -distance
diff --git a/shapely/coords.py b/shapely/coords.py
index 427d171..05746e1 100644
--- a/shapely/coords.py
+++ b/shapely/coords.py
@@ -8,9 +8,6 @@ from ctypes import byref, c_double, c_uint
 from shapely.geos import lgeos
 from shapely.topology import Validating
 
-if sys.version_info[0] < 3:
-    range = xrange
-
 
 class CoordinateSequence(object):
     """
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index 7ecfb3a..be6271f 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -248,6 +248,10 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSDelaunayTriangulation.restype = c_void_p
         lgeos.GEOSDelaunayTriangulation.argtypes = [c_void_p, c_double, c_int]
 
+    if geos_version >= (3, 5, 0):
+        lgeos.GEOSVoronoiDiagram.restype = c_void_p
+        lgeos.GEOSVoronoiDiagram.argtypes = [c_void_p, c_void_p, c_double, c_int]
+
     lgeos.GEOSLineMerge.restype = c_void_p
     lgeos.GEOSLineMerge.argtypes = [c_void_p]
 
@@ -538,3 +542,6 @@ 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
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index fcfeabd..a4e0d6a 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -22,18 +22,11 @@ from shapely.geos import lgeos
 from shapely.impl import DefaultImplementation, delegated
 
 
-if sys.version_info[0] < 3:
-    range = xrange
-    integer_types = (int, long)
-else:
-    integer_types = (int,)
-
-
 try:
     import numpy as np
-    integer_types = integer_types + (np.integer,)
+    integer_types = (int, np.integer)
 except ImportError:
-    pass
+    integer_types = (int,)
 
 
 GEOMETRY_TYPES = [
@@ -97,8 +90,7 @@ def geom_factory(g, parent=None):
 def geom_from_wkt(data):
     warn("`geom_from_wkt` is deprecated. Use `geos.wkt_reader.read(data)`.",
          DeprecationWarning)
-    if sys.version_info[0] >= 3:
-        data = data.encode('ascii')
+    data = data.encode('ascii')
     geom = lgeos.GEOSGeomFromWKT(c_char_p(data))
     if not geom:
         raise WKTReadingError(
@@ -450,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
     # ----------------------
 
@@ -538,56 +535,75 @@ class BaseGeometry(object):
     def buffer(self, distance, resolution=16, quadsegs=None,
                cap_style=CAP_STYLE.round, join_style=JOIN_STYLE.round,
                mitre_limit=5.0, single_sided=False):
-        """Returns a geometry with an envelope at a distance from the object's
-        envelope
+        """Get a geometry that represents all points within a distance
+        of this geometry.
+
+        A positive distance produces a dilation, a negative distance an
+        erosion. A very small or zero distance may sometimes be used to
+        "tidy" a polygon.
 
         Parameters
-        ==========
-        distance: float
-            The distance to buffer around the object. A negative distance has a "shrink" effect.
-            A zero distance may be used to "tidy" a polygon.
-        resolution: int, optional
-            The resolution of the buffer around each vertex of the object.
-        quadsegs: int, optional
-            Sets the number of line segments used to approximate an angle fillet.
-            Note: the use of a `quadsegs` parameter is deprecated and will be gone from
-            the next major release.
-        cap_style: int, optional
-            The styles of caps are: CAP_STYLE.round (1), CAP_STYLE.flat (2), and
-            CAP_STYLE.square (3).
-        join_style: int, optional
-            The styles of joins between offset segments are: JOIN_STYLE.round (1),
-            JOIN_STYLE.mitre (2), and JOIN_STYLE.bevel (3).
-        mitre_limit: float, optional
-            The mitre limit ratio is used for very sharp corners. The mitre ratio
-            is the ratio of the distance from the corner to the end of the mitred
-            offset corner. When two line segments meet at a sharp angle, a miter
-            join will extend the original geometry. To prevent unreasonable
-            geometry, the mitre limit allows controlling the maximum length of the
-            join corner. Corners with a ratio which exceed the limit will be
-            beveled.
-        single_side: bool, optional
-            The side used is determined by the sign of the buffer distance:
+        ----------
+        distance : float
+            The distance to buffer around the object.
+        resolution : int, optional
+            The resolution of the buffer around each vertex of the
+            object.
+        quadsegs : int, optional
+            Sets the number of line segments used to approximate an
+            angle fillet.  Note: the use of a `quadsegs` parameter is
+            deprecated and will be gone from the next major release.
+        cap_style : int, optional
+            The styles of caps are: CAP_STYLE.round (1), CAP_STYLE.flat
+            (2), and CAP_STYLE.square (3).
+        join_style : int, optional
+            The styles of joins between offset segments are:
+            JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and
+            JOIN_STYLE.bevel (3).
+        mitre_limit : float, optional
+            The mitre limit ratio is used for very sharp corners. The
+            mitre ratio is the ratio of the distance from the corner to
+            the end of the mitred offset corner. When two line segments
+            meet at a sharp angle, a miter join will extend the original
+            geometry. To prevent unreasonable geometry, the mitre limit
+            allows controlling the maximum length of the join corner.
+            Corners with a ratio which exceed the limit will be beveled.
+        single_side : bool, optional
+            The side used is determined by the sign of the buffer
+            distance:
+
                 a positive distance indicates the left-hand side
                 a negative distance indicates the right-hand side
-            The single-sided buffer of point geometries is the same as the regular buffer.
-            The End Cap Style for single-sided buffers is always ignored, and forced to the
-            equivalent of CAP_FLAT.
-
-        Example:
-
-          >>> from shapely.wkt import loads
-          >>> g = loads('POINT (0.0 0.0)')
-          >>> g.buffer(1.0).area        # 16-gon approx of a unit radius circle
-          3.1365484905459389
-          >>> g.buffer(1.0, 128).area   # 128-gon approximation
-          3.1415138011443009
-          >>> g.buffer(1.0, 3).area     # triangle approximation
-          3.0
-          >>> list(g.buffer(1.0, cap_style=CAP_STYLE.square).exterior.coords)
-          [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)]
-          >>> g.buffer(1.0, cap_style=CAP_STYLE.square).area
-          4.0
+
+            The single-sided buffer of point geometries is the same as
+            the regular buffer.  The End Cap Style for single-sided
+            buffers is always ignored, and forced to the equivalent of
+            CAP_FLAT.
+
+        Returns
+        -------
+        Geometry
+
+        Notes
+        -----
+        The return value is a strictly two-dimensional geometry. All
+        Z coordinates of the original geometry will be ignored.
+
+        Examples
+        --------
+        >>> from shapely.wkt import loads
+        >>> g = loads('POINT (0.0 0.0)')
+        >>> g.buffer(1.0).area        # 16-gon approx of a unit radius circle
+        3.1365484905459389
+        >>> g.buffer(1.0, 128).area   # 128-gon approximation
+        3.1415138011443009
+        >>> g.buffer(1.0, 3).area     # triangle approximation
+        3.0
+        >>> list(g.buffer(1.0, cap_style=CAP_STYLE.square).exterior.coords)
+        [(1.0, 1.0), (1.0, -1.0), (-1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)]
+        >>> g.buffer(1.0, cap_style=CAP_STYLE.square).area
+        4.0
+
         """
         if quadsegs is not None:
             warn(
@@ -596,6 +612,7 @@ class BaseGeometry(object):
             res = quadsegs
         else:
             res = resolution
+
         if mitre_limit == 0.0:
             raise ValueError(
                 'Cannot compute offset from zero-length line segment')
diff --git a/shapely/geometry/geo.py b/shapely/geometry/geo.py
index ce2a602..11969b7 100644
--- a/shapely/geometry/geo.py
+++ b/shapely/geometry/geo.py
@@ -1,7 +1,6 @@
 """
 Geometry factories based on the geo interface
 """
-
 from .point import Point, asPoint
 from .linestring import LineString, asLineString
 from .polygon import Polygon, asPolygon
@@ -10,6 +9,47 @@ from .multilinestring import MultiLineString, asMultiLineString
 from .multipolygon import MultiPolygon, MultiPolygonAdapter
 from .collection import GeometryCollection
 
+# numpy is an optional dependency
+try:
+    import numpy as np
+except ImportError:
+    _has_numpy = False
+else:
+    _has_numpy = True
+
+
+def _is_coordinates_empty(coordinates):
+    """Helper to identify if coordinates or subset of coordinates are empty"""
+
+    if coordinates is None:
+        return True
+
+    is_numpy_array = _has_numpy and isinstance(coordinates, np.ndarray)
+    if isinstance(coordinates, (list, tuple)) or is_numpy_array:
+        if len(coordinates) == 0:
+            return True
+        return all(map(_is_coordinates_empty, coordinates))
+    else:
+        return False
+
+
+def _empty_shape_for_no_coordinates(geom_type):
+    """Return empty counterpart for geom_type"""
+    if geom_type == 'point':
+        return Point()
+    elif geom_type == 'multipoint':
+        return MultiPoint()
+    elif geom_type == 'linestring':
+        return LineString()
+    elif geom_type == 'multilinestring':
+        return MultiLineString()
+    elif geom_type == 'polygon':
+        return Polygon()
+    elif geom_type == 'multipolygon':
+        return MultiPolygon()
+    else:
+        raise ValueError("Unknown geometry type: %s" % geom_type)
+
 
 def box(minx, miny, maxx, maxy, ccw=True):
     """Returns a rectangular polygon with configurable normal vector"""
@@ -18,24 +58,51 @@ def box(minx, miny, maxx, maxy, ccw=True):
         coords = coords[::-1]
     return Polygon(coords)
 
+
 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__
     else:
         ob = context
     geom_type = ob.get("type").lower()
-    if geom_type == "point":
+    if 'coordinates' in ob and _is_coordinates_empty(ob['coordinates']):
+        return _empty_shape_for_no_coordinates(geom_type)
+    elif geom_type == "point":
         return Point(ob["coordinates"])
     elif geom_type == "linestring":
         return LineString(ob["coordinates"])
     elif geom_type == "polygon":
-        if not ob["coordinates"]:
-            return Polygon()
-        else:
-            return Polygon(ob["coordinates"][0], ob["coordinates"][1:])
+        return Polygon(ob["coordinates"][0], ob["coordinates"][1:])
     elif geom_type == "multipoint":
         return MultiPoint(ob["coordinates"])
     elif geom_type == "multilinestring":
@@ -48,9 +115,43 @@ def shape(context):
     else:
         raise ValueError("Unknown geometry type: %s" % geom_type)
 
+
 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__
@@ -80,6 +181,25 @@ def asShape(context):
     else:
         raise ValueError("Unknown geometry type: %s" % geom_type)
 
+
 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 41041f5..66e5d70 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -1,11 +1,6 @@
 """Line strings and related utilities
 """
 
-import sys
-
-if sys.version_info[0] < 3:
-    range = xrange
-
 from ctypes import c_double
 
 from shapely.geos import lgeos, TopologicalError
@@ -204,7 +199,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
 
     try:
         m = len(ob)
-    except TypeError:  # Iterators, e.g. Python 3 zip
+    except TypeError:  # generators
         ob = list(ob)
         m = len(ob)
 
diff --git a/shapely/geometry/multilinestring.py b/shapely/geometry/multilinestring.py
index ca432df..4821d44 100644
--- a/shapely/geometry/multilinestring.py
+++ b/shapely/geometry/multilinestring.py
@@ -1,11 +1,6 @@
 """Collections of linestrings and related utilities
 """
 
-import sys
-
-if sys.version_info[0] < 3:
-    range = xrange
-
 from ctypes import c_void_p, cast
 
 from shapely.geos import lgeos
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index 538b75b..ca25cfe 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -1,11 +1,6 @@
 """Collections of points and related utilities
 """
 
-import sys
-
-if sys.version_info[0] < 3:
-    range = xrange
-
 from ctypes import byref, c_double, c_void_p, cast
 
 from shapely.geos import lgeos
diff --git a/shapely/geometry/multipolygon.py b/shapely/geometry/multipolygon.py
index 3acc4f3..8188ff4 100644
--- a/shapely/geometry/multipolygon.py
+++ b/shapely/geometry/multipolygon.py
@@ -1,11 +1,6 @@
 """Collections of polygons and related utilities
 """
 
-import sys
-
-if sys.version_info[0] < 3:
-    range = xrange
-
 from ctypes import c_void_p, cast
 
 from shapely.geos import lgeos
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index 7f646ff..3bc6fc1 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -203,7 +203,7 @@ def geos_point_from_py(ob, update_geom=None, update_ndim=0):
         return geos_geom_from_py(ob)
 
     # Accept either (x, y) or [(x, y)]
-    if not hasattr(ob, '__getitem__'):  # Iterators, e.g. Python 3 zip
+    if not hasattr(ob, '__getitem__'):  # generators
         ob = list(ob)
 
     if isinstance(ob[0], tuple):
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 58cb46f..c3f3b5f 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -3,9 +3,6 @@
 
 import sys
 
-if sys.version_info[0] < 3:
-    range = xrange
-
 from ctypes import c_void_p, cast, POINTER
 import weakref
 
@@ -146,9 +143,6 @@ class InteriorRingSequence(object):
         else:
             raise StopIteration
 
-    if sys.version_info[0] < 3:
-        next = __next__
-
     def __len__(self):
         return lgeos.GEOSGetNumInteriorRings(self._geom)
 
@@ -425,7 +419,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
     try:
         m = len(ob)
-    except TypeError:  # Iterators, e.g. Python 3 zip
+    except TypeError:  # generators
         ob = list(ob)
         m = len(ob)
 
diff --git a/shapely/geos.py b/shapely/geos.py
index d5a67d2..41e1b33 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -21,12 +21,6 @@ from .errors import WKBReadingError, WKTReadingError, TopologicalError, Predicat
 # Add message handler to this module's logger
 LOG = logging.getLogger(__name__)
 
-if sys.version_info[0] >= 3:
-    text_types = str
-else:
-    text_types = (str, unicode)
-
-
 # Find and load the GEOS and C libraries
 # If this ever gets any longer, we'll break it into separate modules
 
@@ -62,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
@@ -76,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:
@@ -107,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:
@@ -135,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:
@@ -184,9 +186,7 @@ def _geos_version():
     GEOSversion = _lgeos.GEOSversion
     GEOSversion.restype = c_char_p
     GEOSversion.argtypes = []
-    geos_version_string = GEOSversion()
-    if sys.version_info[0] >= 3:
-        geos_version_string = geos_version_string.decode('ascii')
+    geos_version_string = GEOSversion().decode('ascii')
     res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_version_string)
     assert len(res) == 2, res
     geos_version = tuple(int(x) for x in res[0])
@@ -221,7 +221,7 @@ if geos_version >= (3, 1, 0):
                 # finishGEOS.
                 new_func.argtypes = [c_void_p]
             else:
-                new_func.argtypes = [c_void_p] + old_func.argtypes
+                new_func.argtypes = [c_void_p] + list(old_func.argtypes)
             if old_func.errcheck is not None:
                 new_func.errcheck = old_func.errcheck
 
@@ -275,10 +275,9 @@ class WKTReader(object):
 
     def read(self, text):
         """Returns geometry from WKT"""
-        if not isinstance(text, text_types):
+        if not isinstance(text, str):
             raise TypeError("Only str is accepted.")
-        if sys.version_info[0] >= 3:
-            text = text.encode()
+        text = text.encode()
         c_string = c_char_p(text)
         geom = self._lgeos.GEOSWKTReader_read(self._reader, c_string)
         if not geom:
@@ -395,10 +394,7 @@ class WKTWriter(object):
         result = self._lgeos.GEOSWKTWriter_write(self._writer, geom._geom)
         text = string_at(result)
         lgeos.GEOSFree(result)
-        if sys.version_info[0] >= 3:
-            return text.decode('ascii')
-        else:
-            return text
+        return text.decode('ascii')
 
 
 class WKBReader(object):
@@ -432,8 +428,7 @@ class WKBReader(object):
 
     def read_hex(self, data):
         """Returns geometry from WKB hex"""
-        if sys.version_info[0] >= 3:
-            data = data.encode('ascii')
+        data = data.encode('ascii')
         geom = self._lgeos.GEOSWKBReader_readHEX(
             self._reader, c_char_p(data), c_size_t(len(data)))
         if not geom:
@@ -532,10 +527,7 @@ class WKBWriter(object):
             self._writer, geom._geom, pointer(size))
         data = string_at(result, size.value)
         lgeos.GEOSFree(result)
-        if sys.version_info[0] >= 3:
-            return data.decode('ascii')
-        else:
-            return data
+        return data.decode('ascii')
 
 
 # Errcheck functions for ctypes
@@ -555,10 +547,7 @@ def errcheck_just_free(result, func, argtuple):
     """Returns string from a C pointer"""
     retval = string_at(result)
     lgeos.GEOSFree(result)
-    if sys.version_info[0] >= 3:
-        return retval.decode('ascii')
-    else:
-        return retval
+    return retval.decode('ascii')
 
 
 def errcheck_null_exception(result, func, argtuple):
@@ -862,13 +851,29 @@ 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)
         self.methods['clip_by_rect'] = self.GEOSClipByRect
+        self.methods['voronoi_diagram'] = self.GEOSVoronoiDiagram
+
+
+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
 
 
-if geos_version >= (3, 5, 0):
+if 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 eff851f..e91a461 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -1,15 +1,9 @@
 """Support for various GEOS geometry operations
 """
 
-import sys
-
-if sys.version_info[0] < 3:
-    from itertools import izip
-else:
-    izip = zip
-
 from ctypes import byref, c_void_p, c_double
 
+from shapely.prepared import prep
 from shapely.geos import lgeos
 from shapely.geometry.base import geom_factory, BaseGeometry, BaseMultipartGeometry
 from shapely.geometry import asShape, asLineString, asMultiLineString, Point, MultiPoint,\
@@ -20,7 +14,7 @@ from shapely.algorithms.polylabel import polylabel
 
 __all__ = ['cascaded_union', 'linemerge', 'operator', 'polygonize',
            'polygonize_full', 'transform', 'unary_union', 'triangulate',
-           'split', 'section']
+           'voronoi_diagram', 'split', 'substring']
 
 
 class CollectionOperator(object):
@@ -178,6 +172,64 @@ def triangulate(geom, tolerance=0.0, edges=False):
     gc = geom_factory(func(geom._geom, tolerance, int(edges)))
     return [g for g in gc.geoms]
 
+
+def voronoi_diagram(geom, envelope=None, tolerance=0.0, edges=False):
+    """
+    Constructs a Voronoi Diagram [1] from the given geometry.
+    Returns a list of geometries.
+
+    Parameters
+    ----------
+    geom: geometry
+        the input geometry whose vertices will be used to calculate
+        the final diagram.
+    envelope: geometry, None
+        clipping envelope for the returned diagram, automatically
+        determined if None. The diagram will be clipped to the larger
+        of this envelope or an envelope surrounding the sites.
+    tolerance: float, 0.0
+        sets the snapping tolerance used to improve the robustness
+        of the computation. A tolerance of 0.0 specifies that no
+        snapping will take place.
+    edges: bool, False
+        If False, return regions as polygons. Else, return only
+        edges e.g. LineStrings.
+
+    GEOS documentation can be found at [2]
+
+    Returns
+    -------
+    GeometryCollection
+        geometries representing the Voronoi regions.
+
+    Notes
+    -----
+    The tolerance `argument` can be finicky and is known to cause the
+    algorithm to fail in several cases. If you're using `tolerance`
+    and getting a failure, try removing it. The test cases in
+    tests/test_voronoi_diagram.py show more details.
+
+
+    References
+    ----------
+    [1] https://en.wikipedia.org/wiki/Voronoi_diagram
+    [2] https://geos.osgeo.org/doxygen/geos__c_8h_source.html  (line 730)
+    """
+    func = lgeos.methods['voronoi_diagram']
+    envelope = envelope._geom if envelope else None
+    try:
+        result = geom_factory(func(geom._geom, envelope, tolerance, int(edges)))
+    except ValueError:
+        errstr = "Could not create Voronoi Diagram with the specified inputs."
+        if tolerance:
+            errstr += " Try running again with default tolerance value."
+        raise ValueError(errstr)
+
+    if result.type != 'GeometryCollection':
+        return GeometryCollection([result])
+    return result
+
+
 class ValidateOp(object):
     def __call__(self, this):
         return lgeos.GEOSisValidReason(this._geom)
@@ -202,19 +254,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)
@@ -231,11 +284,11 @@ def transform(func, geom):
         # extra cost.
         try:
             if geom.type in ('Point', 'LineString', 'LinearRing'):
-                return type(geom)(zip(*func(*izip(*geom.coords))))
+                return type(geom)(zip(*func(*zip(*geom.coords))))
             elif geom.type == 'Polygon':
                 shell = type(geom.exterior)(
-                    zip(*func(*izip(*geom.exterior.coords))))
-                holes = list(type(ring)(zip(*func(*izip(*ring.coords))))
+                    zip(*func(*zip(*geom.exterior.coords))))
+                holes = list(type(ring)(zip(*func(*zip(*ring.coords))))
                              for ring in geom.interiors)
                 return type(geom)(shell, holes)
 
@@ -340,6 +393,11 @@ class SplitOp(object):
 
         union = poly.boundary.union(splitter)
 
+        # greatly improves split performance for big geometries with many
+        # holes (the following contains checks) with minimal overhead
+        # for common cases
+        poly = prep(poly)
+
         # some polygonized geometries may be holes, we do not want them
         # that's why we test if the original polygon (poly) contains
         # an inner point of polygonized geometry (pg)
@@ -498,39 +556,43 @@ def substring(geom, start_dist, end_dist, normalized=False):
     """
 
     assert(isinstance(geom, LineString))
-    
+
     # Filter out cases in which to return a point
     if start_dist == end_dist:
         return geom.interpolate(start_dist, normalized)
     elif not normalized and start_dist >= geom.length and end_dist >= geom.length:
         return geom.interpolate(geom.length, normalized)
     elif not normalized and -start_dist >= geom.length and -end_dist >= geom.length:
-        return geom.interpolate(0, normalized)                    
+        return geom.interpolate(0, normalized)
     elif normalized and start_dist >= 1 and end_dist >= 1:
-        return geom.interpolate(1, normalized)  
+        return geom.interpolate(1, normalized)
     elif normalized and -start_dist >= 1 and -end_dist >= 1:
         return geom.interpolate(0, normalized)
 
     start_point = geom.interpolate(start_dist, normalized)
     end_point = geom.interpolate(end_dist, normalized)
-    
+
     min_dist = min(start_dist, end_dist)
     max_dist = max(start_dist, end_dist)
     if normalized:
         min_dist *= geom.length
         max_dist *= geom.length
-    
+
     if start_dist < end_dist:
         vertex_list = [(start_point.x, start_point.y)]
     else:
         vertex_list = [(end_point.x, end_point.y)]
+
     coords = list(geom.coords)
-    for p in coords:
-        pd = geom.project(Point(p))
-        if min_dist < pd < max_dist:
-            vertex_list.append(p)
-        elif pd >= max_dist:
+    current_distance = 0
+    for p1, p2 in zip(coords, coords[1:]):
+        if min_dist < current_distance < max_dist:
+            vertex_list.append(p1)
+        elif current_distance >= max_dist:
             break
+
+        current_distance += ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5
+
     if start_dist < end_dist:
         vertex_list.append((end_point.x, end_point.y))
     else:
@@ -573,6 +635,25 @@ def clip_by_rect(geom, xmin, ymin, xmax, ymax):
 
 
 def orient(geom, sign=1.0):
+    """A properly oriented copy of the given geometry.
+
+    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 rings will
+    be oriented counter-clockwise.
+
+    Parameters
+    ----------
+    geom : Geometry
+        The original geometry. May be a Polygon, MultiPolygon, or
+        GeometryCollection.
+    sign : float, optional.
+        The sign of the result's signed area.
+
+    Returns
+    -------
+    Geometry
+
+    """
     if isinstance(geom, BaseMultipartGeometry):
         return geom.__class__(
             list(
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index a66ef9b..e669b2e 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -1,3 +1,5 @@
+# cython: language_level=3
+
 # geos_linestring_from_py was transcribed from shapely.geometry.linestring
 # geos_linearring_from_py was transcribed from shapely.geometry.polygon
 # coordseq_ctypes was transcribed from shapely.coords.CoordinateSequence.ctypes
@@ -126,7 +128,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
             cs = GEOSCoordSeq_create_r(handle, <int>m, <int>n)
 
         # add to coordinate sequence
-        for i in xrange(m):
+        for i in range(m):
             dx = cp[sm*i]
             dy = cp[sm*i+sn]
             dz = 0
@@ -144,7 +146,7 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
         # Fall back on list
         try:
             m = len(ob)
-        except TypeError:  # Iterators, e.g. Python 3 zip
+        except TypeError:  # generators
             ob = list(ob)
             m = len(ob)
 
@@ -182,15 +184,16 @@ def geos_linestring_from_py(ob, update_geom=None, update_ndim=0):
             cs = GEOSCoordSeq_create_r(handle, <int>m, <int>n)
 
         # add to coordinate sequence
-        for i in xrange(m):
+        for i in range(m):
             coords = _coords(ob[i])
             dx = coords[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
@@ -289,7 +292,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             cs = GEOSCoordSeq_create_r(handle, M, n)
 
         # add to coordinate sequence
-        for i in xrange(m):
+        for i in range(m):
             dx = cp[sm*i]
             dy = cp[sm*i+sn]
             dz = 0
@@ -322,7 +325,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         # Fall back on list
         try:
             m = len(ob)
-        except TypeError:  # Iterators, e.g. Python 3 zip
+        except TypeError:  # generators
             ob = list(ob)
             m = len(ob)
 
@@ -362,13 +365,16 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
             cs = GEOSCoordSeq_create_r(handle, M, n)
         
         # add to coordinate sequence
-        for i in xrange(m):
+        for i in range(m):
             coords = _coords(ob[i])
             dx = coords[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
@@ -384,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
@@ -417,7 +426,7 @@ def coordseq_ctypes(self):
     cs = cast_seq(self._cseq)
     data_p = <double *><uintptr_t>ctypes.addressof(data)
     
-    for i in xrange(m):
+    for i in range(m):
         GEOSCoordSeq_getX_r(handle, cs, i, &temp)
         data_p[n*i] = temp
         GEOSCoordSeq_getY_r(handle, cs, i, &temp)
diff --git a/shapely/strtree.py b/shapely/strtree.py
index 733b0c4..e0f9611 100644
--- a/shapely/strtree.py
+++ b/shapely/strtree.py
@@ -1,33 +1,74 @@
+"""
+strtree
+=======
+
+Index geometry objects for efficient lookup of nearby or
+nearest neighbors. Home of the `STRtree` class which is
+an interface to the query-only GEOS R-tree packed using
+the Sort-Tile-Recursive algorithm [1]_.
+
+.. autoclass:: STRtree
+    :members:
+
+References
+----------
+  .. [1]  Leutenegger, Scott & Lopez, Mario & Edgington, Jeffrey. (1997).
+     "STR: A Simple and Efficient Algorithm for R-Tree Packing." Proc.
+     VLDB Conf. 497-506. 10.1109/ICDE.1997.582015.
+     https://www.cs.odu.edu/~mln/ltrs-pdfs/icase-1997-14.pdf
+"""
+
 from shapely.geos import lgeos
 import ctypes
 
 class STRtree:
     """
-    STRtree is an R-tree that is created using the Sort-Tile-Recursive
-    algorithm. STRtree takes a sequence of geometry objects as initialization
-    parameter. After initialization the query method can be used to make a
-    spatial query over those objects.
-
-    >>> from shapely.geometry import Polygon
-    >>> polys = [ Polygon(((0, 0), (1, 0), (1, 1))), Polygon(((0, 1), (0, 0), (1, 0))), Polygon(((100, 100), (101, 100), (101, 101))) ]
-    >>> s = STRtree(polys)
+    An STRtree is a spatial index; specifically, an R-tree created
+    using the Sort-Tile-Recursive algorithm.
+
+    Pass a list of geometry objects to the `STRtree` constructor to
+    create a spatial index. References to these indexed objects are
+    kept and stored in the R-tree. You can query them with another
+    geometric object.
+
+    The `STRtree` is query-only, meaning that once created
+    you cannot add or remove geometries.
+
+    *New in version 1.4.0*.
+
+    Parameters
+    ----------
+    geoms : sequence of geometry objects
+        geometry objects to be indexed
+
+    Examples
+    --------
+
+    Creating an index of pologons:
+
+    >>> from shapely.strtree import STRtree
+    >>> from shapely.geometry import Polygon, Point
+    >>>
+    >>> polys = [Polygon(((0, 0), (1, 0), (1, 1))),
+    ...          Polygon(((0, 1), (0, 0), (1, 0))),
+    ...          Polygon(((100, 100), (101, 100), (101, 101)))]
+    >>> tree = STRtree(polys)
     >>> query_geom = Polygon(((-1, -1), (2, 0), (2, 2), (-1, 2)))
-    >>> result = s.query(query_geom)
+    >>> result = tree.query(query_geom)
     >>> polys[0] in result
     True
     >>> polys[1] in result
     True
     >>> polys[2] in result
     False
-    >>> # Test empty tree
-    >>> s = STRtree([])
-    >>> s.query(query_geom)
+
+    Behavior if an `STRtree` is created empty:
+
+    >>> tree = STRtree([])
+    >>> tree.query(Point(0, 0))
     []
-    >>> # Test tree with one object
-    >>> s = STRtree([polys[0]])
-    >>> result = s.query(query_geom)
-    >>> polys[0] in result
-    True
+    >>> print(tree.nearest(Point(0, 0)))
+    None
     """
 
     def __init__(self, geoms):
@@ -44,10 +85,67 @@ 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):
+        """
+        Search the index for geometry objects whose extents
+        intersect the extent of the given object.
+
+        Parameters
+        ----------
+        geom : geometry object
+            The query geometry
+
+        Returns
+        -------
+        list of geometry objects
+            All the geometry objects in the index whose extents
+            intersect the extent of `geom`.
+
+        Note
+        ----
+        A geometry object's "extent" is its the minimum xy bounding
+        rectangle.
+
+        Examples
+        --------
+
+        A buffer around a point can be used to control the extent
+        of the query.
+
+        >>> from shapely.strtree import STRtree
+        >>> from shapely.geometry import Point
+        >>> points = [Point(i, i) for i in range(10)]
+        >>> tree = STRtree(points)
+        >>> query_geom = Point(2,2).buffer(0.99)
+        >>> [o.wkt for o in tree.query(query_geom)]
+        ['POINT (2 2)']
+        >>> query_geom = Point(2, 2).buffer(1.0)
+        >>> [o.wkt for o in tree.query(query_geom)]
+        ['POINT (1 1)', 'POINT (2 2)', 'POINT (3 3)']
+
+        A subsequent search through the returned subset using the
+        desired binary predicate (eg. intersects, crosses, contains,
+        overlaps) may be necessary to further filter the results
+        according to their specific spatial relationships.
+
+        >>> [o.wkt for o in tree.query(query_geom) if o.intersects(query_geom)]
+        ['POINT (2 2)']
+
+        To get the original indices of the returned objects, create an
+        auxiliary dictionary. But use the geometry *ids* as keys since
+        the shapely geometry objects themselves are not hashable.
+
+        >>> index_by_id = dict((id(pt), i) for i, pt in enumerate(points))
+        >>> [(index_by_id[id(pt)], pt.wkt) for pt in tree.query(Point(2,2).buffer(1.0))]
+        [(1, 'POINT (1 1)'), (2, 'POINT (2 2)'), (3, 'POINT (3 3)')]
+        """
         if self._n_geoms == 0:
             return []
 
@@ -62,9 +160,41 @@ class STRtree:
         return result
 
     def nearest(self, geom):
+        """
+        Get the nearest object in the index to a geometry object.
+
+        Parameters
+        ----------
+        geom : geometry object
+            The query geometry
+
+        Returns
+        -------
+        geometry object
+            The nearest geometry object in the index to `geom`.
+
+            Will always only return *one* object even if several
+            in the index are the minimum distance away.
+
+            `None` if the index is empty.
+
+        Examples
+        --------
+        >>> from shapely.strtree import STRtree
+        >>> from shapely.geometry import Point
+        >>> tree = STRtree([Point(i, i) for i in range(10)])
+        >>> tree.nearest(Point(2.2, 2.2)).wkt
+        'POINT (2 2)'
+
+        Will only return one object:
+
+        >>> tree = STRtree ([Point(0, 0), Point(0, 0)])
+        >>> tree.nearest(Point(0, 0)).wkt
+        'POINT (0 0)'
+        """
         if self._n_geoms == 0:
             return None
-        
+
         envelope = geom.envelope
 
         def callback(item1, item2, distance, userdata):
@@ -75,12 +205,12 @@ class STRtree:
                 lgeos.GEOSDistance(geom1._geom, geom2._geom, dist)
                 return 1
             except:
-                return 0 
-        
+                return 0
+
         item = lgeos.GEOSSTRtree_nearest_generic(self._tree_handle, ctypes.py_object(geom), envelope._geom, \
             lgeos.GEOSDistanceCallback(callback), None)
         result = ctypes.cast(item, ctypes.py_object).value
-        
+
         return result
 
 if __name__ == "__main__":
diff --git a/shapely/vectorized/_vectorized.pyx b/shapely/vectorized/_vectorized.pyx
index f9991ca..8060242 100644
--- a/shapely/vectorized/_vectorized.pyx
+++ b/shapely/vectorized/_vectorized.pyx
@@ -1,3 +1,5 @@
+# cython: language_level=3
+
 import cython
 cimport cpython.array
 
@@ -105,7 +107,7 @@ cdef _predicated_1d(geometry, np.double_t[:] x, np.double_t[:] y, predicate fn):
     geos_geom = geos_from_prepared(geometry)
 
     with nogil:
-        for idx in xrange(n):
+        for idx in range(n):
             # Construct a coordinate sequence with our x, y values.
             cs = GEOSCoordSeq_create_r(geos_h, 1, 2)
             GEOSCoordSeq_setX_r(geos_h, cs, 0, x[idx])
diff --git a/tools/get_appveyor_artifacts.py b/tools/get_appveyor_artifacts.py
deleted file mode 100755
index 3f1d361..0000000
--- a/tools/get_appveyor_artifacts.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Download AppVeyor artifacts for shapely
-
-See https://www.appveyor.com/docs/api/projects-builds/
-
-Created on Tue Jan 21 12:06:02 2020
-
-@author: Mike Taves
-"""
-
-import os
-import requests
-import textwrap
-
-account = 'frsci'
-slug = 'shapely'
-
-api = 'https://ci.appveyor.com/api'
-
-
-def download_job_artifacts(job_id):
-    resp = requests.get(f'{api}/buildjobs/{job_id}/artifacts')
-    for artifact in resp.json():
-        filename = artifact['fileName']
-        print(f'downloading: {filename}')
-        resp = requests.get(f'{api}/buildjobs/{job_id}/artifacts/{filename}')
-        save_path = os.path.split(filename)[-1]
-        with open(save_path, 'wb') as f:
-            f.write(resp.content)
-
-
-def main(branch='master'):
-    print(f"Gathering last build information for branch '{branch}'")
-    resp = requests.get(f'{api}/projects/{account}/{slug}/branch/{branch}')
-    data = resp.json()
-    if 'build' not in data:
-        raise ValueError(data)
-    build = data['build']
-    jobs = build.get('jobs', [])
-    if not jobs:
-        raise ValueError('no jobs found')
-    print('Found {0} jobs for last build for {1}'.format(len(jobs), branch))
-    print('Commit id: ' + str(build['commitId']))
-    print(textwrap.indent(build['message'], '> '))
-    # Check if all jobs were built successfully with one artifact
-    job_ids = []
-    for job in jobs:
-        if job.get('status') != 'success':
-            raise ValueError('not all jobs were built successfully')
-        elif job.get('artifactsCount', 0) == 0:
-            raise ValueError('not all jobs produced artifacts')
-        job_ids.append(job['jobId'])
-    for job_id in job_ids:
-        download_job_artifacts(job_id)
-
-
-if __name__ == '__main__':
-    import argparse
-    parser = argparse.ArgumentParser()
-    parser.add_argument(
-        '--branch', default='master',
-        help='branch to download artifacts from (default: %(default)s)')
-    args = parser.parse_args()
-    main(**vars(args))


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9cf4ac0..5a6094d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,12 @@ import sys
 
 import pytest
 
+from shapely.geos import geos_version
+
+
+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_collection.py b/tests/test_collection.py
index 1e6f0a6..c87fd7a 100644
--- a/tests/test_collection.py
+++ b/tests/test_collection.py
@@ -63,6 +63,19 @@ class CollectionTestCase(unittest.TestCase):
         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_suite():
     return unittest.TestLoader().loadTestsFromTestCase(CollectionTestCase)
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_geos_err_handler.py b/tests/test_geos_err_handler.py
index 29ff874..b7e687b 100644
--- a/tests/test_geos_err_handler.py
+++ b/tests/test_geos_err_handler.py
@@ -1,5 +1,4 @@
 import logging
-import sys
 
 import pytest
 
@@ -43,7 +42,6 @@ def test_error_handler_wrong_type():
         loads(1)
 
 
-@pytest.mark.skipif(sys.version_info[0] < 3, reason='accept str and unicode for python2 and str for python3')
 def test_error_handler_for_bytes():
     with pytest.raises(TypeError):
         loads(b'POINT (10 10)')
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_ndarrays.py b/tests/test_ndarrays.py
index f8db1dd..26e1843 100644
--- a/tests/test_ndarrays.py
+++ b/tests/test_ndarrays.py
@@ -2,10 +2,7 @@
 # https://github.com/sgillies/shapely/issues/26 for discussion.
 # Requires numpy.
 
-import sys
-
-if sys.version_info[0] >= 3:
-    from functools import reduce
+from functools import reduce
 
 from . import unittest
 from shapely import geometry
diff --git a/tests/test_persist.py b/tests/test_persist.py
index a1b9bdc..c67df30 100644
--- a/tests/test_persist.py
+++ b/tests/test_persist.py
@@ -9,14 +9,6 @@ import struct
 
 class PersistTestCase(unittest.TestCase):
 
-    @staticmethod
-    def _byte(i):
-        """Convert an integer in the range [0, 256) to a byte."""
-        if bytes == str: # Python 2
-            return chr(i)
-        else: # Python 3
-            return int(i)
-
     def test_pickle(self):
 
         p = Point(0.0, 0.0)
@@ -44,8 +36,8 @@ class PersistTestCase(unittest.TestCase):
         # Simple Features Specification for SQL, revision 1.1, the
         # first byte of a WKB representation indicates byte order.
         # Big-endian is 0, little-endian is 1.
-        self.assertEqual(wkb_big_endian[0], self._byte(0))
-        self.assertEqual(wkb_little_endian[0], self._byte(1))
+        self.assertEqual(wkb_big_endian[0], 0)
+        self.assertEqual(wkb_little_endian[0], 1)
         # Check that the doubles (0.5, 2.0) are in correct byte order
         double_size = struct.calcsize('d')
         self.assertEqual(
diff --git a/tests/test_pickle.py b/tests/test_pickle.py
index 182ee93..d8243d5 100644
--- a/tests/test_pickle.py
+++ b/tests/test_pickle.py
@@ -1,11 +1,7 @@
 import pytest
 from shapely.geometry import Point, LineString, LinearRing, Polygon, MultiPoint
 
-import sys
-if sys.version_info[0] >= 3:
-    from pickle import dumps, loads, HIGHEST_PROTOCOL
-else:
-    from cPickle import dumps, loads, HIGHEST_PROTOCOL
+from pickle import dumps, loads, HIGHEST_PROTOCOL
 
 TEST_DATA = {
     "point2d": (Point, [(1.0, 2.0)]),
diff --git a/tests/test_polylabel.py b/tests/test_polylabel.py
index 84eab12..fd89f66 100644
--- a/tests/test_polylabel.py
+++ b/tests/test_polylabel.py
@@ -70,6 +70,19 @@ class PolylabelTestCase(unittest.TestCase):
         label = polylabel(polygon)
         self.assertEqual(label.coords[:], [(32.722025, -117.201875)])
 
+    def test_polygon_with_hole(self):
+        """
+        Finds pole of inaccessibility for a polygon with a hole
+        https://github.com/Toblerity/Shapely/issues/817
+        """
+        polygon = Polygon(
+            shell=[(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)],
+            holes=[[(2, 2), (6, 2), (6, 6), (2, 6), (2, 2)]],
+        )
+        label = polylabel(polygon, 0.05)
+        self.assertAlmostEqual(label.x, 7.65625)
+        self.assertAlmostEqual(label.y, 7.65625)
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PolylabelTestCase)
diff --git a/tests/test_shape.py b/tests/test_shape.py
new file mode 100644
index 0000000..6b7f3aa
--- /dev/null
+++ b/tests/test_shape.py
@@ -0,0 +1,30 @@
+import pytest
+
+from shapely.geometry import shape, Polygon
+
+
+@pytest.mark.parametrize(
+    "geom",
+    [{"type": "Polygon", "coordinates": None}, {"type": "Polygon", "coordinates": []}],
+)
+def test_polygon_no_coords(geom):
+    assert shape(geom) == Polygon()
+
+
+def test_polygon_empty_np_array():
+    np = pytest.importorskip("numpy")
+    geom = {"type": "Polygon", "coordinates": np.array([])}
+    assert shape(geom) == Polygon()
+
+
+def test_polygon_with_coords_list():
+    geom = {"type": "Polygon", "coordinates": [[[5, 10], [10, 10], [10, 5]]]}
+    obj = shape(geom)
+    assert obj == Polygon([(5, 10), (10, 10), (10, 5)])
+
+
+def test_polygon_not_empty_np_array():
+    np = pytest.importorskip("numpy")
+    geom = {"type": "Polygon", "coordinates": np.array([[[5, 10], [10, 10], [10, 5]]])}
+    obj = shape(geom)
+    assert obj == Polygon([(5, 10), (10, 10), (10, 5)])
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
diff --git a/tests/test_substring.py b/tests/test_substring.py
index 8917753..8634594 100644
--- a/tests/test_substring.py
+++ b/tests/test_substring.py
@@ -1,7 +1,9 @@
+import json
+
 from . import unittest
 from shapely.ops import substring
 from shapely.geos import geos_version
-from shapely.geometry import Point, LineString
+from shapely.geometry import Point, LineString, shape
 
 
 class SubstringTestCase(unittest.TestCase):
@@ -10,27 +12,27 @@ class SubstringTestCase(unittest.TestCase):
         self.point = Point(1, 1)
         self.line1 = LineString(([0, 0], [2, 0]))
         self.line2 = LineString(([3, 0], [3, 6], [4.5, 6]))
-  
+
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startpoint(self):
         self.assertTrue(substring(self.line1, -500, -600).equals(Point(0, 0)))
         self.assertTrue(substring(self.line1, -500, -500).equals(Point(0, 0)))
         self.assertTrue(substring(self.line1, -1, -1.1, True).equals(Point(0, 0)))
-        self.assertTrue(substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))               
+        self.assertTrue(substring(self.line1, -1.1, -1.1, True).equals(Point(0, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_endpoint(self):
         self.assertTrue(substring(self.line1, 500, 600).equals(Point(2, 0)))
         self.assertTrue(substring(self.line1, 500, 500).equals(Point(2, 0)))
-        self.assertTrue(substring(self.line1, 1, 1.1, True).equals(Point(2, 0)))   
+        self.assertTrue(substring(self.line1, 1, 1.1, True).equals(Point(2, 0)))
         self.assertTrue(substring(self.line1, 1.1, 1.1, True).equals(Point(2, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_midpoint(self):
-        self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0)))        
+        self.assertTrue(substring(self.line1, 0.5, 0.5).equals(Point(0.5, 0)))
         self.assertTrue(substring(self.line1, -0.5, -0.5).equals(Point(1.5, 0)))
-        self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)))        
-        self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))       
+        self.assertTrue(substring(self.line1, 0.5, 0.5, True).equals(Point(1, 0)))
+        self.assertTrue(substring(self.line1, -0.5, -0.5, True).equals(Point(1, 0)))
 
     @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
     def test_return_startsubstring(self):
@@ -75,7 +77,15 @@ class SubstringTestCase(unittest.TestCase):
     def test_return_substring_issue682(self):
         assert list(substring(self.line2, 0.1, 0).coords) == [(3.0, 0.1), (3.0, 0.0)]
 
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_return_substring_issue848(self):
+        line = shape(json.loads(data_issue_848))
+        cut_line = substring(line, 0.7, 0.8, normalized=True)
+        assert len(cut_line.coords) == 53
 
 def test_suite():
     loader = unittest.TestLoader()
     return loader.loadTestsFromTestCase(SubstringTestCase)
+
+
+data_issue_848 = '{"type": "LineString", "coordinates": [[-87.71314, 41.96793], [-87.71312, 41.96667], [-87.71311, 41.96643], [-87.7131, 41.96635], [-87.71309, 41.9663], [-87.71303, 41.96621], [-87.71298, 41.96615], [-87.71292, 41.96611], [-87.7128, 41.96607], [-87.71268, 41.96605], [-87.71255, 41.96605], [-87.7124, 41.96605], [-87.71219, 41.96605], [-87.71173, 41.96606], [-87.71108, 41.96607], [-87.71027, 41.96607], [-87.70884, 41.96609], [-87.70763, 41.96611], [-87.70645, 41.96612], [-87.70399, 41.96613], [-87.70267, 41.96614], [-87.70166, 41.96615], [-87.70075, 41.96615], [-87.69954, 41.96615], [-87.69873, 41.96616], [-87.69789, 41.96618], [-87.69675, 41.9662], [-87.69502, 41.96621], [-87.69411, 41.96621], [-87.69145, 41.96623], [-87.69026, 41.96624], [-87.68946, 41.96625], [-87.6885, 41.96625], [-87.68718, 41.96628], [-87.68545, 41.96631], [-87.68399, 41.96632], [-87.68271, 41.96635], [-87.68159, 41.96636], [-87.68034, 41.96638], [-87.67863, 41.96641], [-87.67766, 41.96642], [-87.67741, 41.96641], [-87.67722, 41.9664], [-87.67695, 41.96638], [-87.67665, 41.96632], [-87.67638, 41.96623], [-87.67613, 41.96612], [-87.67589, 41.96596], [-87.6757, 41.96579], [-87.67557, 41.96565], [-87.67544, 41.96547], [-87.67539, 41.96536], [-87.6753, 41.96519], [-87.67524, 41.96503], [-87.67523, 41.96491], [-87.67522, 41.96477], [-87.67521, 41.96457], [-87.6752, 41.96434], [-87.67519, 41.96371], [-87.67517, 41.96175], [-87.67513, 41.96077], [-87.67505, 41.95798], [-87.67501, 41.95666], [-87.67497, 41.95513], [-87.67496, 41.95452], [-87.67491, 41.95392], [-87.67487, 41.95302], [-87.67485, 41.95202], [-87.67484, 41.95101], [-87.67479, 41.94959], [-87.67476, 41.94859], [-87.67474, 41.94703], [-87.67468, 41.94596], [-87.67466, 41.94513], [-87.67463, 41.94494], [-87.67457, 41.94474], [-87.6745, 41.94455], [-87.67442, 41.94438], [-87.6743, 41.94424], [-87.67419, 41.94414], [-87.67405, 41.94404], [-87.67386, 41.94393], [-87.67367, 41.94386], [-87.67348, 41.9438], [-87.67334, 41.94376], [-87.67311, 41.94373], [-87.67289, 41.9437], [-87.67263, 41.94369], [-87.67234, 41.94369], [-87.6715, 41.9437], [-87.67088, 41.94371], [-87.66938, 41.94373], [-87.66749, 41.94377], [-87.66585, 41.94378], [-87.66508, 41.94379], [-87.66361, 41.94381], [-87.6591, 41.94391], [-87.65767, 41.94391], [-87.65608, 41.94393], [-87.6555, 41.94394], [-87.65521, 41.94394], [-87.65503, 41.94393], [-87.65488, 41.9439], [-87.6547, 41.94386], [-87.65454, 41.9438], [-87.65441, 41.94375], [-87.65425, 41.94364], [-87.6541, 41.94351], [-87.654, 41.94342], [-87.65392, 41.94331], [-87.65382, 41.94319], [-87.65375, 41.94306], [-87.65367, 41.94292], [-87.65361, 41.9428], [-87.65355, 41.94269], [-87.65351, 41.94257], [-87.65347, 41.94238], [-87.65345, 41.94218], [-87.65338, 41.93975], [-87.65337, 41.93939], [-87.65337, 41.93893], [-87.65336, 41.93865], [-87.65333, 41.93763], [-87.65331, 41.93717], [-87.65328, 41.93627], [-87.65327, 41.93603], [-87.65323, 41.93532], [-87.65322, 41.93491], [-87.6532, 41.93445], [-87.65314, 41.93312], [-87.65313, 41.93273], [-87.6531, 41.93218], [-87.65307, 41.93151], [-87.65305, 41.9309], [-87.65302, 41.9303], [-87.65299, 41.92951], [-87.65296, 41.9287], [-87.65295, 41.92842], [-87.65294, 41.92768], [-87.65292, 41.92715], [-87.65289, 41.92599], [-87.65288, 41.92537], [-87.65287, 41.92505], [-87.65282, 41.92352], [-87.65276, 41.92172], [-87.65274, 41.92113], [-87.65264, 41.91822], [-87.65264, 41.91808], [-87.65262, 41.91763], [-87.65261, 41.91718], [-87.65255, 41.91563], [-87.6525, 41.91406], [-87.65242, 41.91377], [-87.65234, 41.91362], [-87.65223, 41.91351], [-87.65208, 41.91339], [-87.65183, 41.91322], [-87.65093, 41.9126], [-87.65017, 41.91203], [-87.64985, 41.9118], [-87.64971, 41.91171], [-87.64957, 41.91164], [-87.64948, 41.9116], [-87.64939, 41.91158], [-87.6492, 41.91153], [-87.649, 41.9115], [-87.64883, 41.9115], [-87.64863, 41.9115], [-87.64792, 41.91151], [-87.64781, 41.9115], [-87.64768, 41.91146], [-87.64756, 41.91139], [-87.64745, 41.91122], [-87.6474, 41.91112], [-87.64739, 41.91101], [-87.64738, 41.91086], [-87.64736, 41.91071], [-87.64734, 41.91061], [-87.64728, 41.91051], [-87.64718, 41.91044], [-87.64709, 41.9104], [-87.64697, 41.91036], [-87.64682, 41.91034], [-87.64664, 41.91033], [-87.64646, 41.91033], [-87.6458, 41.91034], [-87.64523, 41.91034], [-87.64348, 41.91036], [-87.64255, 41.91039], [-87.641, 41.9104], [-87.64038, 41.9104], [-87.63975, 41.9104], [-87.6393, 41.91041], [-87.63814, 41.91042], [-87.63798, 41.91041], [-87.63787, 41.91039], [-87.63771, 41.91034], [-87.63757, 41.91027], [-87.63746, 41.91021], [-87.63736, 41.91011], [-87.6373, 41.90999], [-87.63727, 41.90986], [-87.63726, 41.90973], [-87.63725, 41.90951], [-87.63723, 41.90874], [-87.63718, 41.90758], [-87.63713, 41.90607], [-87.63711, 41.90543], [-87.63702, 41.90381], [-87.63702, 41.90368], [-87.63701, 41.90334], [-87.63699, 41.90322], [-87.63694, 41.90312], [-87.63688, 41.90299], [-87.63682, 41.90292], [-87.63671, 41.90279], [-87.63659, 41.90265], [-87.63653, 41.90255], [-87.63649, 41.90245], [-87.63646, 41.90235], [-87.63647, 41.90221], [-87.63647, 41.90211], [-87.6365, 41.90202], [-87.63653, 41.9019], [-87.63659, 41.90177], [-87.63666, 41.90156], [-87.63669, 41.90143], [-87.6367, 41.90131], [-87.6367, 41.90119], [-87.63664, 41.90029], [-87.63664, 41.90008], [-87.63662, 41.89975], [-87.63658, 41.89892], [-87.63657, 41.89867], [-87.63654, 41.89761], [-87.63654, 41.89738], [-87.63649, 41.89726], [-87.63641, 41.89715], [-87.63634, 41.89708], [-87.63623, 41.89699], [-87.63595, 41.89677], [-87.63583, 41.89667], [-87.63574, 41.89654], [-87.63569, 41.89645], [-87.63568, 41.89633], [-87.63565, 41.89542], [-87.63563, 41.89434], [-87.6356, 41.89327], [-87.63558, 41.89261], [-87.63554, 41.89147], [-87.63553, 41.89051], [-87.63548, 41.8903], [-87.6354, 41.89021], [-87.63533, 41.89012], [-87.63524, 41.89007], [-87.63508, 41.89001], [-87.63493, 41.88997], [-87.63475, 41.88994], [-87.63462, 41.88991], [-87.63447, 41.88989], [-87.63436, 41.88984], [-87.63425, 41.88979], [-87.63414, 41.8897], [-87.63407, 41.88962], [-87.63402, 41.88952], [-87.63399, 41.88943], [-87.63397, 41.88897], [-87.63396, 41.88707], [-87.63391, 41.88572], [-87.63389, 41.88441], [-87.63385, 41.8827], [-87.63384, 41.88144], [-87.63378, 41.88014], [-87.63374, 41.87872], [-87.63369, 41.87726], [-87.63369, 41.87706], [-87.63365, 41.87695], [-87.63359, 41.87691], [-87.63353, 41.87688], [-87.63345, 41.87686], [-87.63338, 41.87685], [-87.63263, 41.87685], [-87.63173, 41.87686], [-87.62925, 41.87689], [-87.62821, 41.87691], [-87.62757, 41.87693], [-87.6265, 41.87696], [-87.62635, 41.87696], [-87.62603, 41.87697], [-87.62605, 41.87831], [-87.6261, 41.87951], [-87.62616, 41.88203], [-87.62619, 41.88322], [-87.62622, 41.88443], [-87.62626, 41.88534], [-87.62625, 41.88552], [-87.62625, 41.88557], [-87.62627, 41.88562], [-87.6263, 41.88566], [-87.62635, 41.88569], [-87.62642, 41.88572], [-87.6265, 41.88573], [-87.62655, 41.88574], [-87.62661, 41.88574], [-87.62683, 41.88574], [-87.62784, 41.88574], [-87.62887, 41.88574], [-87.62948, 41.88574], [-87.62982, 41.88574], [-87.62992, 41.88574], [-87.63011, 41.88574], [-87.6302, 41.88574], [-87.63089, 41.88574], [-87.63204, 41.88574], [-87.63285, 41.88573], [-87.63391, 41.88572], [-87.63396, 41.88707], [-87.63397, 41.88897], [-87.63399, 41.88943], [-87.63402, 41.88952], [-87.63407, 41.88962], [-87.63414, 41.8897], [-87.63425, 41.88979], [-87.63436, 41.88984], [-87.63447, 41.88989], [-87.63462, 41.88991], [-87.63475, 41.88994], [-87.63493, 41.88997], [-87.63508, 41.89001], [-87.63524, 41.89007], [-87.63533, 41.89012], [-87.6354, 41.89021], [-87.63548, 41.8903], [-87.63553, 41.89051], [-87.63554, 41.89147], [-87.63558, 41.89261], [-87.6356, 41.89327], [-87.63563, 41.89434], [-87.63565, 41.89542], [-87.63568, 41.89633], [-87.63569, 41.89645], [-87.63574, 41.89654], [-87.63583, 41.89667], [-87.63595, 41.89677], [-87.63623, 41.89699], [-87.63634, 41.89708], [-87.63641, 41.89715], [-87.63649, 41.89726], [-87.63654, 41.89738], [-87.63654, 41.89761], [-87.63657, 41.89867], [-87.63658, 41.89892], [-87.63662, 41.89975], [-87.63664, 41.90008], [-87.63664, 41.90029], [-87.6367, 41.90119], [-87.6367, 41.90131], [-87.63669, 41.90143], [-87.63666, 41.90156], [-87.63659, 41.90177], [-87.63653, 41.9019], [-87.6365, 41.90202], [-87.63647, 41.90211], [-87.63647, 41.90221], [-87.63646, 41.90235], [-87.63649, 41.90245], [-87.63653, 41.90255], [-87.63659, 41.90265], [-87.63671, 41.90279], [-87.63682, 41.90292], [-87.63688, 41.90299], [-87.63694, 41.90312], [-87.63699, 41.90322], [-87.63701, 41.90334], [-87.63702, 41.90368], [-87.63702, 41.90381], [-87.63711, 41.90543], [-87.63713, 41.90607], [-87.63718, 41.90758], [-87.63723, 41.90874], [-87.63725, 41.90951], [-87.63726, 41.90973], [-87.63727, 41.90986], [-87.6373, 41.90999], [-87.63736, 41.91011], [-87.63746, 41.91021], [-87.63757, 41.91027], [-87.63771, 41.91034], [-87.63787, 41.91039], [-87.63798, 41.91041], [-87.63814, 41.91042], [-87.6393, 41.91041], [-87.63975, 41.9104], [-87.64038, 41.9104], [-87.641, 41.9104], [-87.64255, 41.91039], [-87.64348, 41.91036], [-87.64523, 41.91034], [-87.6458, 41.91034], [-87.64646, 41.91033], [-87.64664, 41.91033], [-87.64682, 41.91034], [-87.64697, 41.91036], [-87.64709, 41.9104], [-87.64718, 41.91044], [-87.64728, 41.91051], [-87.64734, 41.91061], [-87.64736, 41.91071], [-87.64738, 41.91086], [-87.64739, 41.91101], [-87.6474, 41.91112], [-87.64745, 41.91122], [-87.64756, 41.91139], [-87.64768, 41.91146], [-87.64781, 41.9115], [-87.64792, 41.91151], [-87.64863, 41.9115], [-87.64883, 41.9115], [-87.649, 41.9115], [-87.6492, 41.91153], [-87.64939, 41.91158], [-87.64948, 41.9116], [-87.64957, 41.91164], [-87.64971, 41.91171], [-87.64985, 41.9118], [-87.65017, 41.91203], [-87.65093, 41.9126], [-87.65183, 41.91322], [-87.65208, 41.91339], [-87.65223, 41.91351], [-87.65234, 41.91362], [-87.65242, 41.91377], [-87.6525, 41.91406], [-87.65255, 41.91563], [-87.65261, 41.91718], [-87.65262, 41.91763], [-87.65264, 41.91808], [-87.65264, 41.91822], [-87.65274, 41.92113], [-87.65276, 41.92172], [-87.65282, 41.92352], [-87.65287, 41.92505], [-87.65288, 41.92537], [-87.65289, 41.92599], [-87.65292, 41.92715], [-87.65294, 41.92768], [-87.65295, 41.92842], [-87.65296, 41.9287], [-87.65299, 41.92951], [-87.65302, 41.9303], [-87.65305, 41.9309], [-87.65307, 41.93151], [-87.6531, 41.93218], [-87.65313, 41.93273], [-87.65314, 41.93312], [-87.6532, 41.93445], [-87.65322, 41.93491], [-87.65323, 41.93532], [-87.65327, 41.93603], [-87.65328, 41.93627], [-87.65331, 41.93717], [-87.65333, 41.93763], [-87.65336, 41.93865], [-87.65337, 41.93893], [-87.65337, 41.93939], [-87.65338, 41.93975], [-87.65345, 41.94218], [-87.65347, 41.94238], [-87.65351, 41.94257], [-87.65355, 41.94269], [-87.65361, 41.9428], [-87.65367, 41.94292], [-87.65375, 41.94306], [-87.65382, 41.94319], [-87.65392, 41.94331], [-87.654, 41.94342], [-87.6541, 41.94351], [-87.65425, 41.94364], [-87.65441, 41.94375], [-87.65454, 41.9438], [-87.6547, 41.94386], [-87.65488, 41.9439], [-87.65503, 41.94393], [-87.65521, 41.94394], [-87.6555, 41.94394], [-87.65608, 41.94393], [-87.65767, 41.94391], [-87.6591, 41.94391], [-87.66361, 41.94381], [-87.66508, 41.94379], [-87.66585, 41.94378], [-87.66749, 41.94377], [-87.66938, 41.94373], [-87.67088, 41.94371], [-87.6715, 41.9437], [-87.67234, 41.94369], [-87.67263, 41.94369], [-87.67289, 41.9437], [-87.67311, 41.94373], [-87.67334, 41.94376], [-87.67348, 41.9438], [-87.67367, 41.94386], [-87.67386, 41.94393], [-87.67405, 41.94404], [-87.67419, 41.94414], [-87.6743, 41.94424], [-87.67442, 41.94438], [-87.6745, 41.94455], [-87.67457, 41.94474], [-87.67463, 41.94494], [-87.67466, 41.94513], [-87.67468, 41.94596], [-87.67474, 41.94703], [-87.67476, 41.94859], [-87.67479, 41.94959], [-87.67484, 41.95101], [-87.67485, 41.95202], [-87.67487, 41.95302], [-87.67491, 41.95392], [-87.67496, 41.95452], [-87.67497, 41.95513], [-87.67501, 41.95666], [-87.67505, 41.95798], [-87.67513, 41.96077], [-87.67517, 41.96175], [-87.67519, 41.96371], [-87.6752, 41.96434], [-87.67521, 41.96457], [-87.67522, 41.96477], [-87.67523, 41.96491], [-87.67524, 41.96503], [-87.6753, 41.96519], [-87.67539, 41.96536], [-87.67544, 41.96547], [-87.67557, 41.96565], [-87.6757, 41.96579], [-87.67589, 41.96596], [-87.67613, 41.96612], [-87.67638, 41.96623], [-87.67665, 41.96632], [-87.67695, 41.96638], [-87.67722, 41.9664], [-87.67741, 41.96641], [-87.67766, 41.96642], [-87.67863, 41.96641], [-87.68034, 41.96638], [-87.68159, 41.96636], [-87.68271, 41.96635], [-87.68399, 41.96632], [-87.68545, 41.96631], [-87.68718, 41.96628], [-87.6885, 41.96625], [-87.68946, 41.96625], [-87.69026, 41.96624], [-87.69145, 41.96623], [-87.69411, 41.96621], [-87.69502, 41.96621], [-87.69675, 41.9662], [-87.69789, 41.96618], [-87.69873, 41.96616], [-87.69954, 41.96615], [-87.70075, 41.96615], [-87.70166, 41.96615], [-87.70267, 41.96614], [-87.70399, 41.96613], [-87.70645, 41.96612], [-87.70763, 41.96611], [-87.70884, 41.96609], [-87.71027, 41.96607], [-87.71108, 41.96607], [-87.71173, 41.96606], [-87.71219, 41.96605], [-87.7124, 41.96605], [-87.71255, 41.96605], [-87.71268, 41.96605], [-87.7128, 41.96607], [-87.71292, 41.96611], [-87.71298, 41.96615], [-87.71303, 41.96621], [-87.71309, 41.9663], [-87.7131, 41.96635], [-87.71311, 41.96643], [-87.71312, 41.96667], [-87.71314, 41.96793]]}'
diff --git a/tests/test_voronoi_diagram.py b/tests/test_voronoi_diagram.py
new file mode 100644
index 0000000..d3fd902
--- /dev/null
+++ b/tests/test_voronoi_diagram.py
@@ -0,0 +1,135 @@
+"""
+Test cases for Voronoi Diagram creation.
+
+Overall, I'm trying less to test the correctness of the result
+and more to cover input cases and behavior, making sure
+that we return a sane result without error or raise a useful one.
+"""
+
+import pytest
+
+from shapely.geos import geos_version
+from shapely.geometry import MultiPoint
+from shapely.wkt import loads as load_wkt
+
+from shapely.ops import voronoi_diagram
+
+requires_geos_35 = pytest.mark.skipif(geos_version < (3, 5, 0), reason='GEOS >= 3.5.0 is required.')
+
+
+@requires_geos_35
+def test_no_regions():
+    mp = MultiPoint(points=[(0.5, 0.5)])
+    regions = voronoi_diagram(mp)
+
+    assert len(regions) == 0
+
+
+@requires_geos_35
+def test_two_regions():
+    mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
+    regions = voronoi_diagram(mp)
+
+    assert len(regions) == 2
+
+
+@requires_geos_35
+def test_edges():
+    mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
+    regions = voronoi_diagram(mp, edges=True)
+
+    assert len(regions) == 1
+    assert all(r.type == 'LineString' for r in regions)
+
+
+@requires_geos_35
+def test_smaller_envelope():
+    mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
+    poly = load_wkt('POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))')
+
+    regions = voronoi_diagram(mp, envelope=poly)
+
+    assert len(regions) == 2
+    assert sum(r.area for r in regions) > poly.area
+
+
+@requires_geos_35
+def test_larger_envelope():
+    """When the envelope we specify is larger than the
+    area of the input feature, the created regions should
+    expand to fill that area."""
+    mp = MultiPoint(points=[(0.5, 0.5), (1.0, 1.0)])
+    poly = load_wkt('POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))')
+
+    regions = voronoi_diagram(mp, envelope=poly)
+
+    assert len(regions) == 2
+    assert sum(r.area for r in regions) == poly.area
+
+
+@requires_geos_35
+def test_from_polygon():
+    poly = load_wkt('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
+    regions = voronoi_diagram(poly)
+
+    assert len(regions) == 4
+
+
+@requires_geos_35
+def test_from_polygon_with_enough_tolerance():
+    poly = load_wkt('POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))')
+    regions = voronoi_diagram(poly, tolerance=1.0)
+
+    assert len(regions) == 2
+
+
+@requires_geos_35
+def test_from_polygon_without_enough_tolerance():
+    poly = load_wkt('POLYGON ((0 0, 0.5 0, 0.5 0.5, 0 0.5, 0 0))')
+    with pytest.raises(ValueError) as exc:
+        voronoi_diagram(poly, tolerance=0.6)
+
+    assert exc.match("Could not create Voronoi Diagram with the specified inputs. "
+                     "Try running again with default tolerance value.")
+
+
+@requires_geos_35
+def test_from_polygon_without_floating_point_coordinates():
+    poly = load_wkt('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
+    with pytest.raises(ValueError) as exc:
+        voronoi_diagram(poly, tolerance=0.1)
+
+    assert exc.match("Could not create Voronoi Diagram with the specified inputs. "
+                     "Try running again with default tolerance value")
+
+
+@requires_geos_35
+def test_from_multipoint_without_floating_point_coordinates():
+    """A Multipoint with the same "shape" as the above Polygon raises the same error..."""
+    mp = load_wkt('MULTIPOINT (0 0, 1 0, 1 1, 0 1)')
+
+    with pytest.raises(ValueError) as exc:
+        voronoi_diagram(mp, tolerance=0.1)
+
+    assert exc.match("Could not create Voronoi Diagram with the specified inputs. "
+                     "Try running again with default tolerance value")
+
+
+@requires_geos_35
+def test_from_multipoint_with_tolerace_without_floating_point_coordinates():
+    """This multipoint will not work with a tolerance value."""
+    mp = load_wkt('MULTIPOINT (0 0, 1 0, 1 2, 0 1)')
+    with pytest.raises(ValueError) as exc:
+        voronoi_diagram(mp, tolerance=0.1)
+
+    assert exc.match("Could not create Voronoi Diagram with the specified inputs. "
+                     "Try running again with default tolerance value")
+
+
+@requires_geos_35
+def test_from_multipoint_without_tolerace_without_floating_point_coordinates():
+    """But it's fine without it."""
+    mp = load_wkt('MULTIPOINT (0 0, 1 0, 1 2, 0 1)')
+    regions = voronoi_diagram(mp)
+    assert len(regions) == 4
+