Repository: Toblerity/Shapely
Pull request #682 (2019-03-01 17:20:30)

Fix misorderded points in substrings
When start_dist > end_dist, the first/last points where disordered

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

Coverage increased (+0.02%) to 80.718% when pulling **4a920b20f15cdefa1f0ea665e04bd0bca0d8d275 on cquest:master** into **ddf140779eefc95069bfec70070ea821880dd3ed on Toblerity:master**.

sgillies: @cquest would you be willing to add a test for this to our test suite? tests/test_substring.py would be the file.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 2d34dc2..9f071fe 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,79 +1,114 @@
 language: python
+dist: xenial
+
 cache:
-  - pip
-  - apt
+  directories:
+    - $HOME/geosinstall
+    - ~/.cache/pip
 
 matrix:
   include:
     - python: "2.7"
       env:
-        - SPEEDUPS=1
-        - NUMPY=1
+        GEOSVERSION="3.4.3"
+        SPEEDUPS=1
+        NUMPY=1
     - python: "2.7"
       env:
-        - SPEEDUPS=0
-        - NUMPY=1
+        GEOSVERSION="3.4.3"
+        SPEEDUPS=0
+        NUMPY=1
     - python: "2.7"
       env:
-        - SPEEDUPS=0
-        - NUMPY=0
+        GEOSVERSION="3.4.3"
+        SPEEDUPS=0
+        NUMPY=0
+    - python: "3.5"
+      env:
+        GEOSVERSION="3.5.2"
+        SPEEDUPS=1
+        NUMPY=1
     - python: "3.5"
       env:
-        - SPEEDUPS=1
-        - NUMPY=1
+        GEOSVERSION="3.5.2"
+        SPEEDUPS=0
+        NUMPY=1
     - python: "3.5"
       env:
-        - SPEEDUPS=0
-        - NUMPY=1
+        GEOSVERSION="3.5.2"
+        SPEEDUPS=0
+        NUMPY=0
     - python: "3.6"
       env:
-        - SPEEDUPS=1
-        - NUMPY=1
+        GEOSVERSION="3.6.4"
+        SPEEDUPS=1
+        NUMPY=1
     - python: "3.6"
       env:
-        - SPEEDUPS=0
-        - NUMPY=1
+        GEOSVERSION="3.6.4"
+        SPEEDUPS=0
+        NUMPY=1
     - python: "3.6"
       env:
-        - SPEEDUPS=0
-        - NUMPY=0
+        GEOSVERSION="3.6.4"
+        SPEEDUPS=0
+        NUMPY=0
     - python: "3.7"
-      sudo: required
-      dist: xenial
       env:
-        - SPEEDUPS=1
-        - NUMPY=1
+        GEOSVERSION="3.7.3"
+        SPEEDUPS=1
+        NUMPY=1
     - python: "3.7"
-      sudo: required
-      dist: xenial
       env:
-        - SPEEDUPS=0
-        - NUMPY=1
+        GEOSVERSION="3.7.3"
+        SPEEDUPS=0
+        NUMPY=1
     - python: "3.7"
-      sudo: required
-      dist: xenial
       env:
-        - SPEEDUPS=0
-        - NUMPY=0
-
-addons:
-  apt:
-    packages:
-      - libgeos-dev
+        GEOSVERSION="3.7.3"
+        SPEEDUPS=0
+        NUMPY=0
+    - python: "3.8"
+      env:
+        GEOSVERSION="3.8.0"
+        SPEEDUPS=1
+        NUMPY=1
+    - python: "3.8"
+      env:
+        GEOSVERSION="3.8.0"
+        SPEEDUPS=0
+        NUMPY=1
+    - python: "3.8"
+      env:
+        GEOSVERSION="3.8.0"
+        SPEEDUPS=0
+        NUMPY=0
+    - python: "3.8-dev"
+      env:
+        GEOSVERSION="master"
+        SPEEDUPS=1
+        NUMPY=1
+  allow_failures:
+    - python: "3.8-dev"
+      env:
+        GEOSVERSION="master"
+        SPEEDUPS=1
+        NUMPY=1
 
 before_install:
-  - python -m pip install -U pip
-  - python -m pip --version
+  - ./ci/travis/install_geos.sh
+  - pip install --upgrade pip
   # if building with speedups install cython
   - if [ "$SPEEDUPS" == "1" ]; then pip install --install-option="--no-cython-compile" cython; fi
   # if testing without numpy explicitly remove it
   - if [ "$NUMPY" == "0" ]; then pip uninstall --yes numpy; fi
   # convert SPEEDUPS to --with-speedups/--without-speedups
   - if [ "$SPEEDUPS" == "1" ]; then SPEEDUPS_FLAG=--with-speedups; else SPEEDUPS_FLAG=--without-speedups; fi
+  - pip install --upgrade coveralls pytest-cov pytest>=3.8
 
 install:
-  - python -m pip install --upgrade coveralls pytest-cov pytest>=3.8
-  - python -m pip install -e .[all]
+  - export GEOS_CONFIG=$HOME/geosinstall/geos-$GEOSVERSION/bin/geos-config
+  - pip install -v -e .[all]
 
 script:
   - python -m pytest --cov shapely --cov-report term-missing "${SPEEDUPS_FLAG}"
diff --git a/CHANGES.txt b/CHANGES.txt
index 8e3c38a..5909d78 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,9 +1,18 @@
 Changes
 =======
 
-Master
-------
+1.7a3 (2019-12-30)
+------------------
+
+- Implement ``__bool__()`` for geometry base classes so that ``bool(geom)``
+  returns the logical complement of ``geom.is_empty`` (#754).
+- Remove assertion on the number of version-like strings found in the GEOS
+  version string. It could be 2 or 3.
+
+1.7a2 (2019-06-21)
+------------------
 
+- Nearest neighbor search has been added to STRtree (#668).
 - Disallow sequences of MultiPolygons as arguments to the MultiPolygon
   constructor, resolving #588.
 - Removed vendorized `functools` functions previously used to support
@@ -11,6 +20,26 @@ Master
 
 Bug fixes:
 
+- Avoid reloading the GEOS shared library when using an installed binary wheel
+  on OS X (#735), resolving issue #553.
+- The shapely.ops.orient function can now orient multi polygons and geometry
+  collections as well as polygons (#733).
+- Polygons can now be constructed from sequences of point objects as well as
+  sequences of x, y sequences (#732).
+- The exterior of an empty polygon is now equal to an empty linear ring (#731).
+- The bounds property of an empty point object now returns an empty tuple,
+  consistent with other geometry types (#723).
+- Segmentation faults when non-string values are passed to the WKT loader are
+  avoided by #700.
+- Failure of ops.substring when the sub linestring coincides with the beginning
+  of the linestring has been fixed (#658).
+- Segmentation faults from interpolating on an empty linestring are prevented
+  by #655.
+- A missing special case for rectangular polygons has been added to the
+  polylabel algorithm (#644).
+- LinearRing can be created from a LineString (#638).
+- The prepared geoemtry validation condition has been tightened in #632 to fix
+  the bug reported in #631.
 - Attempting to interpolate an empty geometry no longer results in a
   segmentation fault, raising `ValueError` instead (#653).
 
diff --git a/CREDITS.txt b/CREDITS.txt
index fa96dc3..506275a 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -6,12 +6,12 @@ Shapely is written by:
 * Sean Gillies <sean.gillies@gmail.com>
 * Oliver Tonnhofer <olt@bogosoft.com>
 * Joshua Arnott <josh@snorfalorpagus.net>
-* Mike Toews <mwtoews@gmail.com>
+* Mike Taves <mwtoews@gmail.com>
 * Jacob Wasserman <jwasserman@gmail.com>
 * Aron Bierbaum <aronbierbaum@gmail.com>
 * Allan Adair <allan@rfspot.com>
 * Johannes Schönberger <jschoenberger@demuc.de>
-* georgeouzou <geothrock@gmail.com>
+* George Ouzounoudis <geothrock@gmail.com>
 * Phil Elson <pelson.pub@gmail.com>
 * Howard Butler <hobu.inc@gmail.com>
 * Kelsey Jordahl <kjordahl@enthought.com>
@@ -20,7 +20,7 @@ Shapely is written by:
 * Gabi Davar <grizzly.nyo@gmail.com>
 * Thibault Deutsch <thibault.deutsch@gmail.com>
 * Dave Collins <dave@hopest.net>
-* fredj <frederic.junod@camptocamp.com>
+* Frédéric Junod <frederic.junod@camptocamp.com>
 * Brad Hards <bradh@frogmouth.net>
 * David Baumgold <david@davidbaumgold.com>
 * Henry Walshaw <henry.walshaw@gmail.com>
@@ -49,10 +49,15 @@ Shapely is written by:
 * giumas <gmasetti@ccom.unh.edu>
 * Leandro Lima <leandro@limaesilva.com.br>
 * Maarten Vermeyen <maarten.vermeyen@rwo.vlaanderen.be>
-* joelostblom <joelostblom@users.noreply.github.com>
+* Joel Ostblom <joelostblom@users.noreply.github.com>
 * Marco De Nadai <me@marcodena.it>
 * Johan Euphrosine <proppy@aminche.com>
 * James Gaboardi <jgaboardi@gmail.com>
+* Denis Rykov <rykovd@gmail.com>
+* Zachary Ware <zachary.ware@gmail.com>
+* Pierre PACI <villerupt@gmail.com>
+* Hugo van Kemenade (@hugovk)
+* Juan Luis Cano Rodríguez <juanlu@satellogic.com>
 
 See also: https://github.com/Toblerity/Shapely/graphs/contributors.
 
@@ -63,6 +68,6 @@ Additional help from:
 * Sandro Santilli, Mateusz Loskot, Paul Ramsey, et al (GEOS Project)
 
 Major portions of this work were supported by a grant (for Pleiades_) from the
-U.S. National Endowment for the Humanities (http://www.neh.gov).
+U.S. National Endowment for the Humanities (https://www.neh.gov).
 
-.. _Pleiades: http://pleiades.stoa.org
+.. _Pleiades: https://pleiades.stoa.org
diff --git a/FAQ.rst b/FAQ.rst
new file mode 100644
index 0000000..2f73cda
--- /dev/null
+++ b/FAQ.rst
@@ -0,0 +1,11 @@
+Frequently asked questions and answers
+======================================
+
+Are there references for the algorithms used by shapely?
+--------------------------------------------------------
+
+Generally speaking, shapely's predicates and operations are derived from
+methods of the same name from the `JTS Topology Suite
+<https://locationtech.github.io/jts/>`__.  See the JTS FAQ for `references
+describing the JTS algorithms
+<https://locationtech.github.io/jts/jts-faq.html#E1>`__.
diff --git a/README.rst b/README.rst
index a305482..3c835ea 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,7 @@ Manipulation and analysis of geometric objects in the Cartesian plane.
 
 Shapely is a BSD-licensed Python package for manipulation and analysis of
 planar geometric objects. It is based on the widely deployed `GEOS
-<http://trac.osgeo.org/geos/>`__ (the engine of `PostGIS
+<https://trac.osgeo.org/geos/>`__ (the engine of `PostGIS
 <http://postgis.org>`__) and `JTS
 <https://locationtech.github.io/jts/>`__ (from which GEOS is ported)
 libraries. Shapely is not concerned with data formats or coordinate systems,
@@ -43,8 +43,8 @@ Built distributions
 -------------------
 
 Windows users have two good installation options: the wheels at
-http://www.lfd.uci.edu/~gohlke/pythonlibs/#shapely and the 
-Anaconda platform's `conda-forge <https://conda-forge.github.io/>`__
+https://www.lfd.uci.edu/~gohlke/pythonlibs/#shapely and the 
+Anaconda platform's `conda-forge <https://conda-forge.org/>`__
 channel.
 
 OS X and Linux users can get Shapely wheels with GEOS included from the 
@@ -155,7 +155,7 @@ Support
 =======
 
 Questions about using Shapely may be asked on the `GIS StackExchange 
-<http://gis.stackexchange.com/questions/tagged/shapely>`__ using the "shapely"
+<https://gis.stackexchange.com/questions/tagged/shapely>`__ using the "shapely"
 tag.
 
 Bugs may be reported at https://github.com/Toblerity/Shapely/issues.
diff --git a/appveyor.yml b/appveyor.yml
index a9b81ff..1afd72f 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -10,7 +10,7 @@ environment:
         # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
         # /E:ON and /V:ON options are not enabled in the batch script interpreter
         # See: http://stackoverflow.com/a/13751649/163740
-        CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd"
+        CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\appveyor\\run_with_env.cmd"
         GDAL_HOME: "C:\\gdal"
 
     matrix:
@@ -38,7 +38,7 @@ install:
 
   # Install Python (from the official .msi of http://python.org) and pip when
   # not already installed.
-  # - ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
+  # - ps: if (-not(Test-Path($env:PYTHON))) { & ci\appveyor\install.ps1 }
 
   # Prepend newly installed Python to the PATH of this build (this cannot be
   # done from inside the powershell script as it would require to restart
diff --git a/appveyor/install.ps1 b/ci/appveyor/install.ps1
similarity index 100%
rename from appveyor/install.ps1
rename to ci/appveyor/install.ps1
diff --git a/appveyor/run_with_env.cmd b/ci/appveyor/run_with_env.cmd
similarity index 100%
rename from appveyor/run_with_env.cmd
rename to ci/appveyor/run_with_env.cmd
diff --git a/ci/travis/install_geos.sh b/ci/travis/install_geos.sh
new file mode 100755
index 0000000..852224a
--- /dev/null
+++ b/ci/travis/install_geos.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+# based on contribution by @rbuffat to Toblerity/Fiona
+set -e
+
+# Cache install directory from .travis.yml
+CACHEGEOSINST=$HOME/geosinstall
+
+# Create directories, if they don't exit
+GEOSINSTVERSION=$CACHEGEOSINST/geos-$GEOSVERSION
+mkdir -p $GEOSINSTVERSION
+
+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 ..
+    make -j 2
+    make install
+}
+
+if [ "$GEOSVERSION" = "master" ]; then
+    # use GitHub mirror
+    git clone --depth 1 https://github.com/libgeos/geos.git geos-$GEOSVERSION
+    cd geos-$GEOSVERSION
+    git rev-parse HEAD > newrev.txt
+    BUILD=no
+    # Only build if nothing cached or if the GEOS revision changed
+    if test ! -f $GEOSINSTVERSION/rev.txt; then
+        BUILD=yes
+    elif ! diff newrev.txt $GEOSINSTVERSION/rev.txt >/dev/null; then
+        BUILD=yes
+    fi
+    if test "$BUILD" = "no"; then
+        echo "Using cached install $GEOSINSTVERSION"
+    else
+        cp newrev.txt $GEOSINSTVERSION/rev.txt
+        build_geos
+    fi
+else
+    if [ -d "$GEOSINSTVERSION/include/geos" ]; then
+        echo "Using cached install $GEOSINSTVERSION"
+    else
+        wget -q http://download.osgeo.org/geos/geos-$GEOSVERSION.tar.bz2
+        tar xfj geos-$GEOSVERSION.tar.bz2
+        cd geos-$GEOSVERSION
+        build_geos
+    fi
+fi
diff --git a/docs/code/cascaded_union.py b/docs/code/unary_union.py
similarity index 90%
rename from docs/code/cascaded_union.py
rename to docs/code/unary_union.py
index 877737f..df4e2b7 100644
--- a/docs/code/cascaded_union.py
+++ b/docs/code/unary_union.py
@@ -1,6 +1,6 @@
 from matplotlib import pyplot
 from shapely.geometry import Point
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 from descartes import PolygonPatch
 
 from figures import SIZE, BLUE, GRAY, set_limits
@@ -23,7 +23,7 @@ set_limits(ax, -2, 6, -2, 2)
 #2
 ax = fig.add_subplot(122)
 
-u = cascaded_union(polygons)
+u = unary_union(polygons)
 patch2b = PolygonPatch(u, fc=BLUE, ec=BLUE, alpha=0.5, zorder=2)
 ax.add_patch(patch2b)
 
@@ -32,4 +32,3 @@ ax.set_title('b) union')
 set_limits(ax, -2, 6, -2, 2)
 
 pyplot.show()
-
diff --git a/docs/conf.py b/docs/conf.py
index 0d575a5..52e7ae9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -32,7 +32,6 @@ from shapely.ops import cascaded_union
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
-    'matplotlib.sphinxext.only_directives',
     'matplotlib.sphinxext.plot_directive',
     'sphinx.ext.autodoc',
     #'sphinx.ext.pngmath', # <----- pick one, not both
@@ -121,7 +120,7 @@ html_theme_path = ['themes']
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-html_title = "Shapely 1.6 documentation"
+# html_title = "Shapely 1.6 documentation"
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
 #html_short_title = None
diff --git a/docs/manual.rst b/docs/manual.rst
index aa35b65..7fb87a2 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -118,7 +118,7 @@ Relationships
 The spatial data model is accompanied by a group of natural language
 relationships between geometric objects – `contains`, `intersects`, `overlaps`,
 `touches`, etc. – and a theoretical framework for understanding them using the
-3x3 matrix of the mutual intersections of their component point sets [2]_: the
+3x3 matrix of the mutual intersections of their component point sets [3]_: the
 DE-9IM. A comprehensive review of the relationships in terms of the DE-9IM is
 found in [4]_ and will not be reiterated in this manual.
 
@@ -207,8 +207,8 @@ General Attributes and Methods
 
 .. code-block:: pycon
 
-  >>> print Point(0, 0).geom_type
-  Point
+  >>> Point(0, 0).geom_type
+  'Point'
 
 .. method:: object.distance(other)
 
@@ -222,8 +222,9 @@ General Attributes and Methods
 .. method:: object.hausdorff_distance(other)
 
   Returns the Hausdorff distance (``float``) to the `other` geometric object.
-  The Hausdorff distance is the furthest distance from any point on the first
-  geometry to any point on the second geometry.
+  The Hausdorff distance between two geometries is the furthest distance that
+  a point on either geometry can be from the nearest point to it on the other
+  geometry.
 
   `New in Shapely 1.6.0`
 
@@ -1676,8 +1677,9 @@ linestring feature (right).
   object on its right or its left side.
 
   Distance must be a positive float value. The side parameter may be 'left' or
-  'right'. The resolution of the offset around each vertex of the object is
-  parameterized as in the buffer method.
+  '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 join style is for outside corners between line segments. Accepted integer
   values are 1 (round), 2 (mitre), and 3 (bevel). See also
@@ -1766,13 +1768,21 @@ preserved or supported by 3D affine transformations.
   which represents the augmented matrix:
 
   .. math::
-    \begin{bmatrix} x' & y' & 1 \end{bmatrix} =
-    \begin{bmatrix} x  & y  & 1 \end{bmatrix}
+    \begin{bmatrix}
+      x' \\
+      y' \\
+      1
+    \end{bmatrix} =
     \begin{bmatrix}
       a & b & x_\mathrm{off} \\
       d & e & y_\mathrm{off} \\
       0 & 0 & 1
     \end{bmatrix}
+    \begin{bmatrix}
+      x \\
+      y \\
+      1
+    \end{bmatrix}
 
   or the equations for the transformed coordinates:
 
@@ -1787,14 +1797,24 @@ preserved or supported by 3D affine transformations.
   which represents the augmented matrix:
 
   .. math::
-    \begin{bmatrix} x' & y' & z' & 1 \end{bmatrix} =
-    \begin{bmatrix} x  & y  & z  & 1 \end{bmatrix}
+    \begin{bmatrix}
+      x' \\
+      y' \\
+      z' \\
+      1
+    \end{bmatrix} =
     \begin{bmatrix}
       a & b & c & x_\mathrm{off} \\
       d & e & f & y_\mathrm{off} \\
       g & h & i & z_\mathrm{off} \\
       0 & 0 & 0 & 1
     \end{bmatrix}
+    \begin{bmatrix}
+      x \\
+      y \\
+      z \\
+      1
+    \end{bmatrix}
 
   or the equations for the transformed coordinates:
 
@@ -1968,13 +1988,22 @@ for `func`.
     from functools import partial
     import pyproj
 
+    proj_in = pyproj.Proj(init='epsg:4326')
+    proj_out = pyproj.Proj(init='epsg:26913')
+
     project = partial(
         pyproj.transform,
-        pyproj.Proj(init='epsg:4326'),
-        pyproj.Proj(init='epsg:26913'))
+        proj_in,
+        proj_out)
 
     g2 = transform(project, g1)
 
+If using `pyproj>=2.1.0` a more performant method would be
+
+.. code-block:: python
+
+    project = pyproj.Transformer.from_proj(proj_in, proj_out).transform
+
 Lambda expressions such as the one in
 
 .. code-block:: python
@@ -2069,18 +2098,21 @@ using functions in the :mod:`shapely.ops` module.
      <shapely.geometry.linestring.LineString object at 0x...>,
      <shapely.geometry.linestring.LineString object at 0x...>]
 
-Cascading Unions
+Efficient Unions
 ----------------
 
 The :func:`~shapely.ops.unary_union` function in `shapely.ops` is more
 efficient than accumulating with :meth:`union`.
 
-.. plot:: code/cascaded_union.py
+.. plot:: code/unary_union.py
 
 .. function:: shapely.ops.unary_union(geoms)
 
   Returns a representation of the union of the given geometric objects.
 
+  Areas of overlapping `Polygons` will get merged. `LineStrings` will
+  get fully dissolved and noded. Duplicate `Points` will get merged.
+
   .. code-block:: pycon
 
     >>> from shapely.ops import unary_union
@@ -2088,15 +2120,21 @@ efficient than accumulating with :meth:`union`.
     >>> unary_union(polygons)
     <shapely.geometry.polygon.Polygon object at 0x...>
 
-  The function is particularly useful in dissolving `MultiPolygons`.
+  Because the union merges the areas of overlapping `Polygons` it can be
+  used in an attempt to fix invalid `MultiPolygons`. As with the zero
+  distance :meth:`buffer` trick, your mileage may vary when using this.
 
   .. code-block:: pycon
 
     >>> m = MultiPolygon(polygons)
     >>> m.area
     7.6845438018375516
+    >>> m.is_valid
+    False
     >>> unary_union(m).area
     6.6103013551167971
+    >>> unary_union(m).is_valid
+    True
 
 .. function:: shapely.ops.cascaded_union(geoms)
 
@@ -2104,10 +2142,10 @@ efficient than accumulating with :meth:`union`.
 
   .. note::
 
-     Since 1.2.16 :func:`shapely.ops.cascaded_union` is superceded by
-     :func:`shapely.ops.unary_union` if GEOS 3.2+ is used. The unary union
+     In 1.2.16 :func:`shapely.ops.cascaded_union` was transparently superseded by
+     :func:`shapely.ops.unary_union` if GEOS 3.3+ is used. The unary union
      function can operate on different geometry types, not only polygons as is
-     the case for the older cascaded unions.
+     the case for the older cascaded union.
 
 Delaunay triangulation
 ----------------------
@@ -2119,7 +2157,7 @@ Delaunay triangulation from a collection of points.
 
 .. function:: shapely.ops.triangulate(geom, tolerance=0.0, edges=False)
 
-   Returns a Delaunary triangulation of the vertices of the input geometry.
+   Returns a Delaunay triangulation of the vertices of the input geometry.
 
    The source may be any geometry type. All vertices of the geometry will be
    used as the points of the triangulation.
@@ -2342,6 +2380,29 @@ accessible via :attr:`shapely.__version__`,
   >>> shapely.geos.geos_version_string
   '3.3.0-CAPI-1.7.0'
 
+Polylabel
+---------
+
+.. function:: shapely.ops.polylabel(polygon, tolerance)
+
+  Finds the approximate location of the pole of inaccessibility for a given
+  polygon. Based on Vladimir Agafonkin's polylabel_.
+
+  `New in version 1.6.0`
+
+.. note::
+
+  Prior to 1.7 `polylabel` must be imported from `shapely.algorithms.polylabel`
+  instead of `shapely.ops`.
+
+.. code-block:: pycon
+
+  >>> from shapely.ops import polylabel
+  >>> polygon = LineString([(0, 0), (50, 200), (100, 100), (20, 50),
+  ... (-100, -20), (-150, -200)]).buffer(100)
+  >>> label = polylabel(polygon, tolerance=10)
+  >>> label.wkt
+  'POINT (59.35615556364569 121.8391962974644)'
 
 STR-packed R-tree
 =================
@@ -2418,8 +2479,8 @@ appropriate type, use ``loads()``.
 
   >> from shapely.wkb import dumps, loads
   >>> wkb = dumps(Point(0, 0))
-  >>> print wkb.encode('hex')
-  010100000000000000000000000000000000000000
+  >>> wkb.encode('hex')
+  '010100000000000000000000000000000000000000'
   >>> loads(wkb).wkt
   'POINT (0.0000000000000000 0.0000000000000000)'
 
@@ -2437,10 +2498,10 @@ All of Shapely's geometry types are supported by these functions.
 
   >> from shapely.wkt import dumps, loads
   >> wkt = dumps(Point(0, 0))
-  >>> print wkt
-  POINT (0.0000000000000000 0.0000000000000000)
-  >>> loads(wkt).wkt
+  >>> wkt
   'POINT (0.0000000000000000 0.0000000000000000)'
+  >>> loads(wkt).wkt
+  'POINT (0 0)'
 
 Numpy and Python Arrays
 -----------------------
@@ -2651,12 +2712,8 @@ References
 
 .. _GEOS: https://trac.osgeo.org/geos/
 .. _Java Topology Suite: https://projects.eclipse.org/projects/locationtech.jts
-.. _JTS: https://projects.eclipse.org/projects/locationtech.jts
 .. _PostGIS: http://postgis.refractions.net
-.. _record: https://pypi.org/project/Shapely/
-.. _Open Geospatial Consortium: http://www.opengeospatial.org/
-.. _Davis: https://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html
-.. _Understanding spatial relations: http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm
+.. _Open Geospatial Consortium: https://www.opengeospatial.org/
 .. _Strobl-PDF: https://giswiki.hsr.ch/images/3/3d/9dem_springer.pdf
 .. |Strobl-PDF| replace:: PDF
 .. _JTS-PDF: https://github.com/locationtech/jts/raw/master/doc/JTS%20Technical%20Specs.pdf
@@ -2664,3 +2721,4 @@ References
 .. _frozenset: https://docs.python.org/library/stdtypes.html#frozenset
 .. _Sorting HowTo: https://wiki.python.org/moin/HowTo/Sorting/
 .. _Python geo interface: https://gist.github.com/2217756
+.. _polylabel: https://github.com/mapbox/polylabel
diff --git a/docs/project.rst b/docs/project.rst
index ce70278..05c3490 100644
--- a/docs/project.rst
+++ b/docs/project.rst
@@ -4,3 +4,4 @@
 
 .. include:: ../CHANGES.txt
 
+.. include:: ../FAQ.rst
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 12088cd..0b686d5 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,7 @@
-setuptools
-Numpy>=1.4.1
-Cython>=0.19
+cython==0.29.14
 descartes==1.0.1
+numpy>=1.4.1
 pytest
 pytest-cov
+setuptools
 wheel
diff --git a/setup.py b/setup.py
index af2489f..702cc2b 100755
--- a/setup.py
+++ b/setup.py
@@ -220,9 +220,10 @@ setup_args = dict(
         'Programming Language :: Python :: 2',
         'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
         'Topic :: Scientific/Engineering :: GIS',
     ],
     cmdclass           = {},
@@ -315,25 +316,18 @@ if (platform.python_implementation() == 'PyPy'):
     ext_modules = []
     libraries = []
 
-if os.path.exists("MANIFEST.in"):
-    pyx_file = "shapely/speedups/_speedups.pyx"
-    c_file = "shapely/speedups/_speedups.c"
 
-    force_cython = False
-    if 'sdist' in sys.argv:
-        force_cython = True
+pyx_file = "shapely/speedups/_speedups.pyx"
+c_file = "shapely/speedups/_speedups.c"
 
-    try:
-        if (force_cython or not os.path.exists(c_file)
-                or os.path.getmtime(pyx_file) > os.path.getmtime(c_file)):
-            log.info("Updating C extension with Cython.")
-            subprocess.check_call(["cython", "shapely/speedups/_speedups.pyx"])
-    except (subprocess.CalledProcessError, OSError):
-        log.warn("Could not (re)create C extension with Cython.")
-        if force_cython:
-            raise
-    if not os.path.exists(c_file):
-        log.warn("speedup extension not found")
+force_cython = False
+# Always regenerate for sdist or absent c file
+if 'sdist' in sys.argv or not os.path.exists(c_file):
+    force_cython = True
+# Also regenerate if pyx_file is outdated.
+elif os.path.exists(c_file):
+    if os.path.getmtime(pyx_file) > os.path.getmtime(c_file):
+        force_cython = True
 
 ext_modules = [
     Extension("shapely.speedups._speedups", ["shapely/speedups/_speedups.c"],
@@ -366,6 +360,19 @@ try:
 except ImportError:
     log.info("Numpy or Cython not available, shapely.vectorized submodule "
              "not being built.")
+    force_cython = False
+
+try:
+    if force_cython:
+        log.info("Updating C extension with Cython.")
+        subprocess.check_call(["cython", "shapely/speedups/_speedups.pyx"])
+except (subprocess.CalledProcessError, OSError):
+    log.warn("Could not (re)create C extension with Cython.")
+    if force_cython:
+        raise
+
+if not os.path.exists(c_file):
+    log.warn("speedup extension not found")
 
 try:
     # try building with speedups
diff --git a/shapely/__init__.py b/shapely/__init__.py
index 5880421..8117773 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "1.7a1"
+__version__ = "1.7a3"
diff --git a/shapely/_buildcfg.py b/shapely/_buildcfg.py
index 37aec8f..8f28b89 100644
--- a/shapely/_buildcfg.py
+++ b/shapely/_buildcfg.py
@@ -165,7 +165,7 @@ if sys.platform.startswith('linux'):
     if not lgeos:
         lgeos = load_dll('geos_c',
                          fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-    free = load_dll('c').free
+    free = load_dll('c', fallbacks=['libc.so.6']).free
     free.argtypes = [c_void_p]
     free.restype = None
 
@@ -239,7 +239,6 @@ def _geos_version():
         geos_version_string = geos_version_string.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])
     capi_version = tuple(int(x) for x in res[1])
 
diff --git a/shapely/_geos.pxi b/shapely/_geos.pxi
index 7cf7777..8975405 100644
--- a/shapely/_geos.pxi
+++ b/shapely/_geos.pxi
@@ -43,6 +43,8 @@ cdef extern from "geos_c.h":
     char GEOSHasZ_r(GEOSContextHandle_t, GEOSGeometry *) nogil
     char GEOSisRing_r(GEOSContextHandle_t, GEOSGeometry *) nogil
     char GEOSisClosed_r(GEOSContextHandle_t, GEOSGeometry *) nogil
+    char GEOSisValid_r(GEOSContextHandle_t, GEOSGeometry *) nogil
+    char GEOSisSimple_r(GEOSContextHandle_t, GEOSGeometry *) nogil
 
 
 cdef GEOSContextHandle_t get_geos_context_handle():
diff --git a/shapely/affinity.py b/shapely/affinity.py
index 7ab7504..95d583f 100755
--- a/shapely/affinity.py
+++ b/shapely/affinity.py
@@ -6,7 +6,7 @@ __all__ = ['affine_transform', 'rotate', 'scale', 'skew', 'translate']
 
 
 def affine_transform(geom, matrix):
-    """Returns a transformed geometry using an affine transformation matrix.
+    r"""Returns a transformed geometry using an affine transformation matrix.
 
     The coefficient matrix is provided as a list or tuple with 6 or 12 items
     for 2D or 3D transformations, respectively.
@@ -17,9 +17,9 @@ def affine_transform(geom, matrix):
 
     which represents the augmented matrix::
 
-                            / a  b xoff \ 
-        [x' y' 1] = [x y 1] | d  e yoff |
-                            \ 0  0   1  /
+        [x']   / a  b xoff \ [x]
+        [y'] = | d  e yoff | [y]
+        [1 ]   \ 0  0   1  / [1]
 
     or the equations for the transformed coordinates::
 
@@ -32,10 +32,10 @@ def affine_transform(geom, matrix):
 
     which represents the augmented matrix::
 
-                                 / a  b  c xoff \ 
-        [x' y' z' 1] = [x y z 1] | d  e  f yoff |
-                                 | g  h  i zoff |
-                                 \ 0  0  0   1  /
+        [x']   / a  b  c xoff \ [x]
+        [y'] = | d  e  f yoff | [y]
+        [z']   | g  h  i zoff | [z]
+        [1 ]   \ 0  0  0   1  / [1]
 
     or the equations for the transformed coordinates::
 
@@ -130,7 +130,7 @@ def interpret_origin(geom, origin, ndim):
 
 
 def rotate(geom, angle, origin='center', use_radians=False):
-    """Returns a rotated geometry on a 2D plane.
+    r"""Returns a rotated geometry on a 2D plane.
 
     The angle of rotation can be specified in either degrees (default) or
     radians by setting ``use_radians=True``. Positive angles are
@@ -151,8 +151,10 @@ def rotate(geom, angle, origin='center', use_radians=False):
         xoff = x0 - x0 * cos(r) + y0 * sin(r)
         yoff = y0 - x0 * sin(r) - y0 * cos(r)
     """
+    if geom.is_empty:
+        return geom
     if not use_radians:  # convert from degrees
-        angle *= pi/180.0
+        angle = angle * pi/180.0
     cosp = cos(angle)
     sinp = sin(angle)
     if abs(cosp) < 2.5e-16:
@@ -169,7 +171,7 @@ def rotate(geom, angle, origin='center', use_radians=False):
 
 
 def scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'):
-    """Returns a scaled geometry, scaled by factors along each dimension.
+    r"""Returns a scaled geometry, scaled by factors along each dimension.
 
     The point of origin can be a keyword 'center' for the 2D bounding box
     center (default), 'centroid' for the geometry's 2D centroid, a Point
@@ -190,6 +192,8 @@ def scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'):
         yoff = y0 - y0 * yfact
         zoff = z0 - z0 * zfact
     """
+    if geom.is_empty:
+        return geom
     x0, y0, z0 = interpret_origin(geom, origin, 3)
 
     matrix = (xfact, 0.0, 0.0,
@@ -200,7 +204,7 @@ def scale(geom, xfact=1.0, yfact=1.0, zfact=1.0, origin='center'):
 
 
 def skew(geom, xs=0.0, ys=0.0, origin='center', use_radians=False):
-    """Returns a skewed geometry, sheared by angles along x and y dimensions.
+    r"""Returns a skewed geometry, sheared by angles along x and y dimensions.
 
     The shear angle can be specified in either degrees (default) or radians
     by setting ``use_radians=True``.
@@ -220,9 +224,11 @@ def skew(geom, xs=0.0, ys=0.0, origin='center', use_radians=False):
         xoff = -y0 * tan(xs)
         yoff = -x0 * tan(ys)
     """
+    if geom.is_empty:
+        return geom
     if not use_radians:  # convert from degrees
-        xs *= pi/180.0
-        ys *= pi/180.0
+        xs = xs * pi/180.0
+        ys = ys * pi/180.0
     tanx = tan(xs)
     tany = tan(ys)
     if abs(tanx) < 2.5e-16:
@@ -239,7 +245,7 @@ def skew(geom, xs=0.0, ys=0.0, origin='center', use_radians=False):
 
 
 def translate(geom, xoff=0.0, yoff=0.0, zoff=0.0):
-    """Returns a translated geometry shifted by offsets along each dimension.
+    r"""Returns a translated geometry shifted by offsets along each dimension.
 
     The general 3D affine transformation matrix for translation is:
 
@@ -248,6 +254,8 @@ def translate(geom, xoff=0.0, yoff=0.0, zoff=0.0):
         | 0  0  1 zoff |
         \ 0  0  0   1  /
     """
+    if geom.is_empty:
+        return geom
     matrix = (1.0, 0.0, 0.0,
               0.0, 1.0, 0.0,
               0.0, 0.0, 1.0,
diff --git a/shapely/algorithms/cga.py b/shapely/algorithms/cga.py
index 3b8d925..bfa1d6e 100644
--- a/shapely/algorithms/cga.py
+++ b/shapely/algorithms/cga.py
@@ -1,7 +1,7 @@
 
 def signed_area(ring):
-    """Return the signed area enclosed by a ring in linear time using the 
-    algorithm at: http://www.cgafaq.info/wiki/Polygon_Area.
+    """Return the signed area enclosed by a ring in linear time using the
+    algorithm at: https://web.archive.org/web/20080209143651/http://cgafaq.info:80/wiki/Polygon_Area
     """
     xs, ys = ring.coords.xy
     xs.append(xs[1])
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index 6d3b0e1..7ecfb3a 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -135,6 +135,28 @@ def prototype(lgeos, geos_version):
             lgeos.GEOSOffsetCurve.argtypes = [
                 c_void_p, c_double, c_int, c_int, c_double]
 
+            lgeos.GEOSBufferParams_create.restype = c_void_p
+            lgeos.GEOSBufferParams_create.argtypes = None
+
+            lgeos.GEOSBufferParams_setEndCapStyle.restype = c_int
+            lgeos.GEOSBufferParams_setEndCapStyle.argtypes = [c_void_p, c_int]
+
+            lgeos.GEOSBufferParams_setJoinStyle.restype = c_int
+            lgeos.GEOSBufferParams_setJoinStyle.argtypes = [c_void_p, c_int]
+
+            lgeos.GEOSBufferParams_setMitreLimit.restype = c_int
+            lgeos.GEOSBufferParams_setMitreLimit.argtypes = [c_void_p, c_double]
+
+            lgeos.GEOSBufferParams_setQuadrantSegments.restype = c_int
+            lgeos.GEOSBufferParams_setQuadrantSegments.argtypes = [c_void_p, c_int]
+
+            lgeos.GEOSBufferParams_setSingleSided.restype = c_int
+            lgeos.GEOSBufferParams_setSingleSided.argtypes = [c_void_p, c_int]
+
+            lgeos.GEOSBufferWithParams.restype = c_void_p
+            lgeos.GEOSBufferWithParams.argtypes = [
+                c_void_p, c_void_p, c_double]
+
         else:
 
             # deprecated in GEOS 3.3.0 in favour of GEOSOffsetCurve
@@ -509,7 +531,7 @@ def prototype(lgeos, geos_version):
 
         lgeos.GEOSSTRtree_destroy.argtypes = [c_void_p]
         lgeos.GEOSSTRtree_destroy.restype = None
-    
+
     if geos_version >= (3, 6, 0):
         lgeos.GEOSDistanceCallback = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p, c_void_p)
 
diff --git a/shapely/examples/dissolve.py b/shapely/examples/dissolve.py
index 3f1d624..faf7ded 100644
--- a/shapely/examples/dissolve.py
+++ b/shapely/examples/dissolve.py
@@ -9,7 +9,7 @@ import random
 import pylab
 
 from shapely.geometry import Point
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 # Use a partial function to make 100 points uniformly distributed in a 40x40 
 # box centered on 0,0.
@@ -19,9 +19,9 @@ points = [Point(r(), r()) for i in range(100)]
 # Buffer the points, producing 100 polygon spots
 spots = [p.buffer(2.5) for p in points]
 
-# Perform a cascaded union of the polygon spots, dissolving them into a 
+# Perform a unary union of the polygon spots, dissolving them into a
 # collection of polygon patches
-patches = cascaded_union(spots)
+patches = unary_union(spots)
 
 if __name__ == "__main__":
     # Illustrate the results using matplotlib's pylab interface
diff --git a/shapely/examples/intersect.py b/shapely/examples/intersect.py
index 76fab15..074dd45 100644
--- a/shapely/examples/intersect.py
+++ b/shapely/examples/intersect.py
@@ -9,13 +9,13 @@ import random
 import pylab
 
 from shapely.geometry import LineString, Point
-from shapely.ops import cascaded_union
+from shapely.ops import unary_union
 
 # Build patches as in dissolved.py
 r = partial(random.uniform, -20.0, 20.0)
 points = [Point(r(), r()) for i in range(100)]
 spots = [p.buffer(2.5) for p in points]
-patches = cascaded_union(spots)
+patches = unary_union(spots)
 
 # Represent the following geolocation parameters
 #
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 1af9447..ef29c80 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -230,6 +230,9 @@ class BaseGeometry(object):
         self._is_empty = True
         self.__geom__ = val
 
+    def __bool__(self):
+        return self.is_empty is False
+
     def __del__(self):
         self.empty(val=None)
         self.__p__ = None
@@ -531,29 +534,42 @@ 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):
+               mitre_limit=5.0, single_sided=False):
         """Returns a geometry with an envelope at a distance from the object's
         envelope
 
-        A negative distance has a "shrink" effect. A zero distance may be used
-        to "tidy" a polygon. The resolution of the buffer around each vertex of
-        the object increases by increasing the resolution keyword parameter
-        or second positional parameter. Note: the use of a `quadsegs` parameter
-        is deprecated and will be gone from the next major release.
-
-        The styles of caps are: CAP_STYLE.round (1), CAP_STYLE.flat (2), and
-        CAP_STYLE.square (3).
-
-        The styles of joins between offset segments are: JOIN_STYLE.round (1),
-        JOIN_STYLE.mitre (2), and JOIN_STYLE.bevel (3).
-
-        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.
+        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:
+                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:
 
@@ -565,9 +581,9 @@ class BaseGeometry(object):
           3.1415138011443009
           >>> g.buffer(1.0, 3).area     # triangle approximation
           3.0
-          >>> list(g.buffer(1.0, cap_style='square').exterior.coords)
+          >>> 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='square').area
+          >>> g.buffer(1.0, cap_style=CAP_STYLE.square).area
           4.0
         """
         if quadsegs is not None:
@@ -580,6 +596,16 @@ class BaseGeometry(object):
         if mitre_limit == 0.0:
             raise ValueError(
                 'Cannot compute offset from zero-length line segment')
+
+        if 'buffer_with_params' in self.impl:
+            params = self._lgeos.GEOSBufferParams_create()
+            self._lgeos.GEOSBufferParams_setEndCapStyle(params, cap_style)
+            self._lgeos.GEOSBufferParams_setJoinStyle(params, join_style)
+            self._lgeos.GEOSBufferParams_setMitreLimit(params, mitre_limit)
+            self._lgeos.GEOSBufferParams_setQuadrantSegments(params, res)
+            self._lgeos.GEOSBufferParams_setSingleSided(params, single_sided)
+            return geom_factory(self.impl['buffer_with_params'](self, params, distance))
+
         if cap_style == CAP_STYLE.round and join_style == JOIN_STYLE.round:
             return geom_factory(self.impl['buffer'](self, distance, res))
 
@@ -700,7 +726,7 @@ class BaseGeometry(object):
 
     def equals(self, other):
         """Returns True if geometries are equal, else False
-        
+
         Refers to point-set equality (or topological equality), and is equivalent to
         (self.within(other) & self.contains(other))
         """
@@ -725,8 +751,8 @@ class BaseGeometry(object):
     def equals_exact(self, other, tolerance):
         """Returns True if geometries are equal to within a specified
         tolerance
-        
-        Refers to coordinate equality, which requires coordinates to be equal 
+
+        Refers to coordinate equality, which requires coordinates to be equal
         and in the same order for all components of a geometry
         """
         return bool(self.impl['equals_exact'](self, other, tolerance))
@@ -817,6 +843,9 @@ class BaseMultipartGeometry(BaseGeometry):
             return []
         return GeometrySequence(self, self.shape_factory)
 
+    def __bool__(self):
+        return self.is_empty is False
+
     def __iter__(self):
         if not self.is_empty:
             return iter(self.geoms)
diff --git a/shapely/geometry/point.py b/shapely/geometry/point.py
index fed086f..7f646ff 100644
--- a/shapely/geometry/point.py
+++ b/shapely/geometry/point.py
@@ -118,7 +118,11 @@ class Point(BaseGeometry):
 
     @property
     def bounds(self):
-        xy = self.coords[0]
+        """Returns minimum bounding region (minx, miny, maxx, maxy)"""
+        try:
+            xy = self.coords[0]
+        except IndexError:
+            return ()
         return (xy[0], xy[1], xy[0], xy[1])
 
     # Coordinate access
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 24a02b0..58cb46f 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -10,11 +10,13 @@ from ctypes import c_void_p, cast, POINTER
 import weakref
 
 from shapely.algorithms.cga import signed_area
-#from shapely.coords import required
 from shapely.geos import lgeos
 from shapely.geometry.base import BaseGeometry, geos_geom_from_py
 from shapely.geometry.linestring import LineString, LineStringAdapter
+from shapely.geometry.point import Point
 from shapely.geometry.proxy import PolygonProxy
+from shapely.errors import TopologicalError
+
 
 __all__ = ['Polygon', 'asPolygon', 'LinearRing', 'asLinearRing']
 
@@ -32,7 +34,8 @@ class LinearRing(LineString):
         Parameters
         ----------
         coordinates : sequence
-            A sequence of (x, y [,z]) numeric coordinate pairs or triples
+            A sequence of (x, y [,z]) numeric coordinate pairs or triples.
+            Also can be a sequence of Point objects.
 
         Rings are implicitly closed. There is no need to specific a final
         coordinate pair identical to the first.
@@ -219,7 +222,8 @@ class Polygon(BaseGeometry):
         Parameters
         ----------
         shell : sequence
-            A sequence of (x, y [,z]) numeric coordinate pairs or triples
+            A sequence of (x, y [,z]) numeric coordinate pairs or triples.
+            Also can be a sequence of Point objects.
         holes : sequence
             A sequence of objects which satisfy the same requirements as the
             shell parameters above
@@ -245,7 +249,7 @@ class Polygon(BaseGeometry):
     @property
     def exterior(self):
         if self.is_empty:
-            return None
+            return LinearRing()
         elif self._exterior is None or self._exterior() is None:
             g = lgeos.GEOSGetExteriorRing(self._geom)
             ring = LinearRing()
@@ -311,7 +315,7 @@ class Polygon(BaseGeometry):
 
     @property
     def __geo_interface__(self):
-        if not self.exterior:
+        if self.exterior == LinearRing():
             coords = []
         else:
             coords = [tuple(self.exterior.coords)]
@@ -403,7 +407,7 @@ def orient(polygon, sign=1.0):
 
 def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     # If a LinearRing is passed in, clone it and return
-    # If a LineString is passed in, clone the coord seq and return a
+    # If a valid LineString is passed in, clone the coord seq and return a
     # LinearRing.
     #
     # NB: access to coordinates using the array protocol has been moved
@@ -412,6 +416,8 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     if isinstance(ob, LineString):
         if type(ob) == LinearRing:
             return geos_geom_from_py(ob)
+        elif not ob.is_valid:
+            raise TopologicalError("An input LineString must be valid.")
         elif ob.is_closed and len(ob.coords) >= 4:
             return geos_geom_from_py(ob, lgeos.GEOSGeom_createLinearRing)
         else:
@@ -426,14 +432,24 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     if m == 0:
         return None
 
-    n = len(ob[0])
+    def _coords(o):
+        if isinstance(o, Point):
+            return o.coords[0]
+        else:
+            return o
+
+    n = len(_coords(ob[0]))
     if m < 3:
         raise ValueError(
             "A LinearRing must have at least 3 coordinate tuples")
     assert (n == 2 or n == 3)
 
     # Add closing coordinates if not provided
-    if m == 3 or ob[0][0] != ob[-1][0] or ob[0][1] != ob[-1][1]:
+    if (
+        m == 3
+        or _coords(ob[0])[0] != _coords(ob[-1])[0]
+        or _coords(ob[0])[1] != _coords(ob[-1])[1]
+    ):
         M = m + 1
     else:
         M = m
@@ -450,7 +466,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
     # add to coordinate sequence
     for i in range(m):
-        coords = ob[i]
+        coords = _coords(ob[i])
         # Because of a bug in the GEOS C API,
         # always set X before Y
         lgeos.GEOSCoordSeq_setX(cs, i, coords[0])
@@ -463,7 +479,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
     # Add closing coordinates to sequence?
     if M > m:
-        coords = ob[0]
+        coords = _coords(ob[0])
         # Because of a bug in the GEOS C API,
         # always set X before Y
         lgeos.GEOSCoordSeq_setX(cs, M-1, coords[0])
@@ -505,7 +521,7 @@ def geos_polygon_from_py(shell, holes=None):
                 N = exemplar._ndim
             if not L >= 1:
                 raise ValueError("number of holes must be non zero")
-            if not N in (2, 3):
+            if N not in (2, 3):
                 raise ValueError("insufficiant coordinate dimension")
 
             # Array of pointers to ring geometries
diff --git a/shapely/geos.py b/shapely/geos.py
index 149a898..7cf372c 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -21,6 +21,12 @@ 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
 
@@ -66,8 +72,7 @@ if sys.platform.startswith('linux'):
         _lgeos = CDLL(geos_whl_so[0])
         LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
     elif hasattr(sys, 'frozen'):
-        geos_pyinstaller_so = glob.glob(os.path.join(sys.prefix,
-                                                    'libgeos_c-*.so.*'))
+        geos_pyinstaller_so = glob.glob(os.path.join(sys.prefix, 'libgeos_c-*.so.*'))
         if len(geos_pyinstaller_so) == 1:
             _lgeos = CDLL(geos_pyinstaller_so[0])
             LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
@@ -80,7 +85,11 @@ if sys.platform.startswith('linux'):
             'libgeos_c.so',
         ]
         _lgeos = load_dll('geos_c', fallbacks=alt_paths)
-    free = load_dll('c').free
+    # Necessary for environments with only libc.musl
+    c_alt_paths = [
+        'libc.musl-x86_64.so.1'
+    ]
+    free = load_dll('c', fallbacks=c_alt_paths).free
     free.argtypes = [c_void_p]
     free.restype = None
 
@@ -88,9 +97,16 @@ elif sys.platform == 'darwin':
     # Test to see if we have a delocated wheel with a GEOS dylib.
     geos_whl_dylib = os.path.abspath(os.path.join(os.path.dirname(
         __file__), '.dylibs/libgeos_c.1.dylib'))
+
     if os.path.exists(geos_whl_dylib):
-        _lgeos = CDLL(geos_whl_dylib)
-        LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
+        handle = CDLL(None)
+        if hasattr(handle, "initGEOS_r"):
+            LOG.debug("GEOS already loaded")
+            _lgeos = handle
+        else:
+            _lgeos = CDLL(geos_whl_dylib)
+            LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
+
     elif os.getenv('CONDA_PREFIX', ''):
         # conda package.
         _lgeos = CDLL(os.path.join(sys.prefix, 'lib', 'libgeos_c.dylib'))
@@ -257,9 +273,12 @@ class WKTReader(object):
 
     def read(self, text):
         """Returns geometry from WKT"""
+        if not isinstance(text, text_types):
+            raise TypeError("Only str is accepted.")
         if sys.version_info[0] >= 3:
-            text = text.encode('ascii')
-        geom = self._lgeos.GEOSWKTReader_read(self._reader, c_char_p(text))
+            text = text.encode()
+        c_string = c_char_p(text)
+        geom = self._lgeos.GEOSWKTReader_read(self._reader, c_string)
         if not geom:
             raise WKTReadingError(
                 "Could not create geometry because of errors "
@@ -823,6 +842,7 @@ class LGEOS330(LGEOS320):
         self.methods['cascaded_union'] = self.methods['unary_union']
         self.methods['snap'] = self.GEOSSnap
         self.methods['shared_paths'] = self.GEOSSharedPaths
+        self.methods['buffer_with_params'] = self.GEOSBufferWithParams
 
 
 class LGEOS340(LGEOS330):
@@ -840,7 +860,7 @@ class LGEOS340(LGEOS330):
 class LGEOS350(LGEOS340):
     """Proxy for GEOS 3.5.0-CAPI-1.9.0
     """
-    
+
     def __init__(self, dll):
         super(LGEOS350, self).__init__(dll)
         self.methods['clip_by_rect'] = self.GEOSClipByRect
diff --git a/shapely/impl.py b/shapely/impl.py
index ac179ac..cc6550a 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -144,7 +144,9 @@ IMPL320 = {
     }
 
 IMPL330 = {
-    'is_closed': (UnaryPredicate, 'is_closed')}
+    'is_closed': (UnaryPredicate, 'is_closed'),
+    'buffer_with_params': (UnaryTopologicalOp, 'buffer_with_params'),
+}
 
 
 def impl_items(defs):
diff --git a/shapely/ops.py b/shapely/ops.py
index b5727a9..eff851f 100644
--- a/shapely/ops.py
+++ b/shapely/ops.py
@@ -11,9 +11,10 @@ else:
 from ctypes import byref, c_void_p, c_double
 
 from shapely.geos import lgeos
-from shapely.geometry.base import geom_factory, BaseGeometry
+from shapely.geometry.base import geom_factory, BaseGeometry, BaseMultipartGeometry
 from shapely.geometry import asShape, asLineString, asMultiLineString, Point, MultiPoint,\
                              LineString, MultiLineString, Polygon, GeometryCollection
+from shapely.geometry.polygon import orient as orient_
 from shapely.algorithms.polylabel import polylabel
 
 
@@ -120,7 +121,7 @@ class CollectionOperator(object):
     def cascaded_union(self, geoms):
         """Returns the union of a sequence of geometries
 
-        This is the most efficient method of dissolving many polygons.
+        This method was superseded by :meth:`unary_union`.
         """
         try:
             L = len(geoms)
@@ -138,7 +139,6 @@ class CollectionOperator(object):
 
         This method replaces :meth:`cascaded_union` as the
         prefered method for dissolving many polygons.
-
         """
         try:
             L = len(geoms)
@@ -520,20 +520,22 @@ def substring(geom, start_dist, end_dist, normalized=False):
         min_dist *= geom.length
         max_dist *= geom.length
     
-    vertex_list = [(start_point.x, start_point.y)]
+    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 pd <= min_dist:
-            pass
-        elif min_dist < pd < max_dist:
+        if min_dist < pd < max_dist:
             vertex_list.append(p)
-        else:
+        elif pd >= max_dist:
             break
-    vertex_list.append((end_point.x, end_point.y))
-
-    # reverse direction of section
-    if start_dist > end_dist:
+    if start_dist < end_dist:
+        vertex_list.append((end_point.x, end_point.y))
+    else:
+        vertex_list.append((start_point.x, start_point.y))
+        # reverse direction result
         vertex_list = reversed(vertex_list)
 
     return LineString(vertex_list)
@@ -568,3 +570,18 @@ def clip_by_rect(geom, xmin, ymin, xmax, ymax):
         return geom
     result = geom_factory(lgeos.methods['clip_by_rect'](geom._geom, xmin, ymin, xmax, ymax))
     return result
+
+
+def orient(geom, sign=1.0):
+    if isinstance(geom, BaseMultipartGeometry):
+        return geom.__class__(
+            list(
+                map(
+                    lambda geom: orient(geom, sign),
+                    geom.geoms,
+                )
+            )
+        )
+    if isinstance(geom, (Polygon,)):
+        return orient_(geom, sign)
+    return geom
diff --git a/shapely/speedups/_speedups.pyx b/shapely/speedups/_speedups.pyx
index 29f1c90..a66ef9b 100644
--- a/shapely/speedups/_speedups.pyx
+++ b/shapely/speedups/_speedups.pyx
@@ -11,6 +11,8 @@ import logging
 from shapely.geos import lgeos
 from shapely.geometry import Point, LineString, LinearRing
 from shapely.geometry.base import geom_factory
+from shapely.errors import TopologicalError
+
 
 include "../_geos.pxi"
 
@@ -216,7 +218,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
     cdef int i, n, M, sm, sn
 
     # If a LinearRing is passed in, just clone it and return
-    # If a LineString is passed in, clone the coord seq and return a LinearRing
+    # If a valid LineString is passed in, clone the coord seq and return a LinearRing
     if isinstance(ob, LineString):
         g = cast_geom(ob._geom)
         if GEOSHasZ_r(handle, g):
@@ -229,9 +231,12 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         else:
             cs = <GEOSCoordSequence*>GEOSGeom_getCoordSeq_r(handle, g)
             GEOSCoordSeq_getSize_r(handle, cs, &m)
-            if GEOSisClosed_r(handle, g) and m >= 4:
+            if not GEOSisValid_r(handle, g):
+                raise TopologicalError("A LineString must be valid.")
+            elif GEOSisClosed_r(handle, g) and m >= 4:
                 cs = GEOSCoordSeq_clone_r(handle, cs)
                 return <uintptr_t>GEOSGeom_createLinearRing_r(handle, cs), n
+            # else continue below.
 
     # If numpy is present, we use numpy.require to ensure that we have a
     # C-continguous array that owns its data. View data will be copied.
@@ -324,14 +329,24 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         if m == 0:
             return None
 
-        n = len(ob[0])
+        def _coords(o):
+            if isinstance(o, Point):
+                return o.coords[0]
+            else:
+                return o
+
+        n = len(_coords(ob[0]))
         if m < 3:
             raise ValueError(
                 "A LinearRing must have at least 3 coordinate tuples")
         assert (n == 2 or n == 3)
 
         # Add closing coordinates if not provided
-        if m == 3 or ob[0][0] != ob[-1][0] or ob[0][1] != ob[-1][1]:
+        if (
+            m == 3
+            or _coords(ob[0])[0] != _coords(ob[-1])[0]
+            or _coords(ob[0])[1] != _coords(ob[-1])[1]
+        ):
             M = m + 1
         else:
             M = m
@@ -348,7 +363,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
         
         # add to coordinate sequence
         for i in xrange(m):
-            coords = ob[i]
+            coords = _coords(ob[i])
             dx = coords[0]
             dy = coords[1]
             dz = 0
@@ -364,7 +379,7 @@ def geos_linearring_from_py(ob, update_geom=None, update_ndim=0):
 
         # Add closing coordinates to sequence?
         if M > m:
-            coords = ob[0]
+            coords = _coords(ob[0])
             dx = coords[0]
             dy = coords[1]
             dz = 0


Test patch:
diff --git a/tests/test_affinity.py b/tests/test_affinity.py
index 94538c5..22f2e53 100755
--- a/tests/test_affinity.py
+++ b/tests/test_affinity.py
@@ -4,6 +4,11 @@ from shapely import affinity
 from shapely.wkt import loads as load_wkt
 from shapely.geometry import Point
 
+try:
+    import numpy
+except ImportError:
+    numpy = False
+
 
 class AffineTestCase(unittest.TestCase):
 
@@ -155,6 +160,26 @@ class TransformOpsTestCase(unittest.TestCase):
         rls = affinity.rotate(ls, 90, origin=Point(0, 0))
         els = load_wkt('LINESTRING(-400 240, -300 240, -300 300)')
         self.assertTrue(rls.equals(els))
+    
+    def test_rotate_empty(self):
+        rls = affinity.rotate(load_wkt('LINESTRING EMPTY'), 90)
+        els = load_wkt('LINESTRING EMPTY')
+        self.assertTrue(rls.equals(els))
+
+    @unittest.skipIf(not numpy, 'numpy not installed')
+    def test_rotate_angle_array(self):
+        ls = load_wkt('LINESTRING(240 400, 240 300, 300 300)')
+        els = load_wkt('LINESTRING(220 320, 320 320, 320 380)')
+        # check with degrees
+        theta = numpy.array([90.0])
+        rls = affinity.rotate(ls, theta)
+        self.assertEqual(theta[0], 90.0)
+        self.assertTrue(rls.equals(els))
+        # check with radians
+        theta = numpy.array([pi/2])
+        rls = affinity.rotate(ls, theta, use_radians=True)
+        self.assertEqual(theta[0], pi/2)
+        self.assertTrue(rls.equals(els))
 
     def test_scale(self):
         ls = load_wkt('LINESTRING(240 400 10, 240 300 30, 300 300 20)')
@@ -190,6 +215,11 @@ class TransformOpsTestCase(unittest.TestCase):
         for a, b in zip(sls.coords, els.coords):
             for ap, bp in zip(a, b):
                 self.assertEqual(ap, bp)
+    
+    def test_scale_empty(self):
+        sls = affinity.scale(load_wkt('LINESTRING EMPTY'))
+        els = load_wkt('LINESTRING EMPTY')
+        self.assertTrue(sls.equals(els))
 
     def test_skew(self):
         ls = load_wkt('LINESTRING(240 400 10, 240 300 30, 300 300 20)')
@@ -227,6 +257,30 @@ class TransformOpsTestCase(unittest.TestCase):
                        '320.3847577293367976 161.4359353944898317, '
                        '380.3847577293367976 126.7949192431122754)')
         self.assertTrue(sls.almost_equals(els))
+    
+    def test_skew_empty(self):
+        sls = affinity.skew(load_wkt('LINESTRING EMPTY'))
+        els = load_wkt('LINESTRING EMPTY')
+        self.assertTrue(sls.equals(els))
+
+    @unittest.skipIf(not numpy, 'numpy not installed')
+    def test_skew_xs_ys_array(self):
+        ls = load_wkt('LINESTRING(240 400 10, 240 300 30, 300 300 20)')
+        els = load_wkt('LINESTRING (253.39745962155615 417.3205080756888, '
+                       '226.60254037844385 317.3205080756888, '
+                       '286.60254037844385 282.67949192431126)')
+        # check with degrees
+        xs_ys = numpy.array([15.0, -30.0])
+        sls = affinity.skew(ls, xs_ys[0:1], xs_ys[1:2])
+        self.assertEqual(xs_ys[0], 15.0)
+        self.assertEqual(xs_ys[1], -30.0)
+        self.assertTrue(sls.almost_equals(els))
+        # check with radians
+        xs_ys = numpy.array([pi/12, -pi/6])
+        sls = affinity.skew(ls, xs_ys[0:1], xs_ys[1:2], use_radians=True)
+        self.assertEqual(xs_ys[0], pi/12)
+        self.assertEqual(xs_ys[1], -pi/6)
+        self.assertTrue(sls.almost_equals(els))
 
     def test_translate(self):
         ls = load_wkt('LINESTRING(240 400 10, 240 300 30, 300 300 20)')
@@ -244,6 +298,11 @@ class TransformOpsTestCase(unittest.TestCase):
         # retest with named parameters for the same result
         tls = affinity.translate(geom=ls, xoff=100, yoff=400, zoff=-10)
         self.assertTrue(tls.equals(els))
+    
+    def test_translate_empty(self):
+        tls = affinity.translate(load_wkt('LINESTRING EMPTY'))
+        els = load_wkt('LINESTRING EMPTY')
+        self.assertTrue(tls.equals(els))
 
 
 def test_suite():
diff --git a/tests/test_buffer.py b/tests/test_buffer.py
new file mode 100644
index 0000000..454c3cf
--- /dev/null
+++ b/tests/test_buffer.py
@@ -0,0 +1,80 @@
+from . import unittest
+from shapely import geometry
+
+
+class BufferSingleSidedCase(unittest.TestCase):
+    """ Test Buffer Point/Line/Polygon with and without single_sided params """
+
+    def test_empty(self):
+        g = geometry.Point(0,0)
+        h = g.buffer(0)
+        assert h.is_empty
+
+    def test_point(self):
+        g = geometry.Point(0, 0)
+        h = g.buffer(1, resolution=1)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_point_single_sidedd(self):
+        g = geometry.Point(0, 0)
+        h = g.buffer(1, resolution=1, single_sided=True)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(1.0, 0.0), (0, -1.0), (-1.0, 0), (0, 1.0), (1.0, 0.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_line(self):
+        g = geometry.LineString([[0, 0], [0, 1]])
+        h = g.buffer(1, resolution=1)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(-1.0, 1.0), (0, 2.0), (1.0, 1.0), (1.0, 0.0), (0, -1.0), (-1.0, 0.0),
+                          (-1.0, 1.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_line_single_sideded_left(self):
+        g = geometry.LineString([[0, 0], [0, 1]])
+        h = g.buffer(1, resolution=1, single_sided=True)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(0.0, 1.0), (0.0, 0.0), (-1.0, 0.0), (-1.0, 1.0), (0.0, 1.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_line_single_sideded_right(self):
+        g = geometry.LineString([[0, 0], [0, 1]])
+        h = g.buffer(-1, resolution=1, single_sided=True)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_polygon(self):
+        g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
+        h = g.buffer(1, resolution=1)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(-1.0, 0.0), (-1.0, 1.0), (0.0, 2.0), (1.0, 2.0), (2.0, 1.0), (2.0, 0.0),
+                          (1.0, -1.0), (0.0, -1.0), (-1.0, 0.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+    def test_polygon_single_sideded(self):
+        g = geometry.Polygon([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]])
+        h = g.buffer(1, resolution=1, single_sided=True)
+        self.assertEqual(h.geom_type, 'Polygon')
+        expected_coord = [(-1.0, 0.0), (-1.0, 1.0), (0.0, 2.0), (1.0, 2.0), (2.0, 1.0), (2.0, 0.0),
+                          (1.0, -1.0), (0.0, -1.0), (-1.0, 0.0)]
+        for index, coord in enumerate(h.exterior.coords):
+            self.assertAlmostEqual(coord[0], expected_coord[index][0])
+            self.assertAlmostEqual(coord[1], expected_coord[index][1])
+
+def test_suite():
+    return unittest.TestLoader().loadTestsFromTestCase(BufferSingleSidedCase)
diff --git a/tests/test_geos_err_handler.py b/tests/test_geos_err_handler.py
index cf7e7d8..29ff874 100644
--- a/tests/test_geos_err_handler.py
+++ b/tests/test_geos_err_handler.py
@@ -1,4 +1,5 @@
 import logging
+import sys
 
 import pytest
 
@@ -34,7 +35,18 @@ def test_error_handler(tmpdir):
     LineString([(0, 0), (2, 2)]).project(LineString([(1, 1), (1.5, 1.5)]))
 
     log = open(logfile).read()
-    assert "third argument of GEOSProject_r must be Point*" in log
+    assert "third argument of GEOSProject_r must be Point" in log
+
+
+def test_error_handler_wrong_type():
+    with pytest.raises(TypeError):
+        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)')
 
 
 def test_info_handler(tmpdir):
diff --git a/tests/test_operations.py b/tests/test_operations.py
index 5c81c37..dee57cb 100644
--- a/tests/test_operations.py
+++ b/tests/test_operations.py
@@ -22,8 +22,7 @@ class OperationsTestCase(unittest.TestCase):
         self.assertIsInstance(point.envelope, Point)
 
         # Intersection
-        self.assertIsInstance(point.intersection(Point(-1, -1)),
-                              GeometryCollection)
+        self.assertTrue(point.intersection(Point(-1, -1)).is_empty)
 
         # Buffer
         self.assertIsInstance(point.buffer(10.0), Polygon)
diff --git a/tests/test_orient.py b/tests/test_orient.py
new file mode 100644
index 0000000..ff76534
--- /dev/null
+++ b/tests/test_orient.py
@@ -0,0 +1,82 @@
+from . import unittest
+from shapely.geometry import (
+    Point,
+    MultiPoint,
+    LineString,
+    MultiLineString,
+    LinearRing,
+    Polygon,
+    MultiPolygon,
+    GeometryCollection,
+)
+
+from shapely.ops import orient
+
+
+class OrientTestCase(unittest.TestCase):
+    def test_point(self):
+        point = Point(0, 0)
+        assert orient(point, 1) == point
+        assert orient(point, -1) == point
+
+    def test_multipoint(self):
+        multipoint = MultiPoint([(0, 0), (1, 1)])
+        assert orient(multipoint, 1) == multipoint
+        assert orient(multipoint, -1) == multipoint
+
+    def test_linestring(self):
+        linestring = LineString(((0, 0), (1, 1)))
+        assert orient(linestring, 1) == linestring
+        assert orient(linestring, -1) == linestring
+
+    def test_multilinestring(self):
+        multilinestring = MultiLineString(
+            [[(0, 0), (1, 1)], [(1, 0), (0, 1)]]
+        )
+        assert orient(multilinestring, 1) == multilinestring
+        assert orient(multilinestring, -1) == multilinestring
+
+    def test_linearring(self):
+        linearring = LinearRing([(0, 0), (0, 1), (1, 0)])
+        assert orient(linearring, 1) == linearring
+        assert orient(linearring, -1) == linearring
+
+    def test_polygon(self):
+        polygon = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon_reversed = Polygon(
+            polygon.exterior.coords[::-1]
+        )
+        assert (orient(polygon, 1)) == polygon_reversed
+        assert (orient(polygon, -1)) == polygon
+
+    def test_multipolygon(self):
+        polygon1 = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon2 = Polygon([(1, 0), (2, 0), (2, 1)])
+        polygon1_reversed = Polygon(
+            polygon1.exterior.coords[::-1]
+        )
+        polygon2_reversed = Polygon(
+            polygon2.exterior.coords[::-1]
+        )
+        multipolygon = MultiPolygon([polygon1, polygon2])
+        assert not polygon1.exterior.is_ccw
+        assert polygon2.exterior.is_ccw
+        assert orient(multipolygon, 1) == MultiPolygon(
+            [polygon1_reversed, polygon2]
+        )
+        assert orient(multipolygon, -1) == MultiPolygon(
+            [polygon1, polygon2_reversed]
+        )
+
+    def test_geometrycollection(self):
+        polygon = Polygon([(0, 0), (0, 1), (1, 0)])
+        polygon_reversed = Polygon(
+            polygon.exterior.coords[::-1]
+        )
+        collection = GeometryCollection([polygon])
+        assert orient(collection, 1) == GeometryCollection(
+            [polygon_reversed]
+        )
+        assert orient(collection, -1) == GeometryCollection(
+            [polygon]
+        )
diff --git a/tests/test_point.py b/tests/test_point.py
index 8135aa5..c959a3f 100644
--- a/tests/test_point.py
+++ b/tests/test_point.py
@@ -144,5 +144,11 @@ class LineStringTestCase(unittest.TestCase):
         self.assertEqual(a.shape[0], 0)
 
 
+def test_empty_point_bounds():
+    """The bounds of an empty point is an empty tuple"""
+    p = Point()
+    assert p.bounds == ()
+
+
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(LineStringTestCase)
diff --git a/tests/test_polygon.py b/tests/test_polygon.py
index 057a3d9..a00b7ed 100644
--- a/tests/test_polygon.py
+++ b/tests/test_polygon.py
@@ -1,7 +1,9 @@
 """Polygons and Linear Rings
 """
+
 from . import unittest, numpy
 from shapely.wkb import loads as load_wkb
+from shapely.errors import TopologicalError
 from shapely.geos import lgeos
 from shapely.geometry import Point, Polygon, asPolygon
 from shapely.geometry.polygon import LinearRing, LineString, asLinearRing
@@ -21,6 +23,9 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual(ring.coords[0], ring.coords[-1])
         self.assertTrue(ring.is_ring)
 
+        # Ring from sequence of Points
+        self.assertEqual(LinearRing((map(Point, coords))), ring)
+
         # Coordinate modification
         ring.coords = ((0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0))
         self.assertEqual(
@@ -135,27 +140,21 @@ class PolygonTestCase(unittest.TestCase):
         self.assertEqual('LinearRing',
                          lgeos.GEOSGeomType(ring._geom).decode('ascii'))
 
-
-    def test_linearring_from_short_closed_linestring(self):
-        # Create linearring from linestring where the coordinate sequence is
-        # too short but appears to be closed (first and last coordinates
-        # are the same)
+    def test_linearring_from_invalid(self):
         coords = [(0.0, 0.0), (0.0, 0.0), (0.0, 0.0)]
         line = LineString(coords)
-        ring1 = LinearRing(coords)
-        ring2 = LinearRing(line)
-        assert ring1.coords[:] == ring2.coords[:]
-
+        self.assertFalse(line.is_valid)
+        with self.assertRaises(TopologicalError):
+            ring = LinearRing(line)
 
     def test_linearring_from_too_short_linestring(self):
         # Creation of LinearRing request at least 3 coordinates (unclosed) or
         # 4 coordinates (closed)
         coords = [(0.0, 0.0), (0.0, 0.0)]
         line = LineString(coords)
-        with self.assertRaises(ValueError):
+        with self.assertRaises(TopologicalError):
             LinearRing(line)
 
-
     @unittest.skipIf(not numpy, 'Numpy required')
     def test_numpy(self):
 
@@ -257,6 +256,10 @@ class PolygonTestCase(unittest.TestCase):
             Polygon(coords),
             Polygon.from_bounds(xmin, ymin, xmax, ymax))
 
+    def test_empty_polygon_exterior(self):
+        p = Polygon()
+        assert p.exterior == LinearRing()
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PolygonTestCase)