[python-shapely] 01/14: Imported Upstream version 1.5.10

Sebastiaan Couwenberg sebastic at moszumanska.debian.org
Fri Aug 28 14:21:44 UTC 2015


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository python-shapely.

commit 7369eeff99cc91f59fa34c04d4159298906a2fab
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Fri Aug 28 14:51:41 2015 +0200

    Imported Upstream version 1.5.10
---
 .gitignore                     |   1 +
 CHANGES.txt                    |   5 +
 README.rst                     |   7 +-
 docs/manual.rst                |  42 +++++++-
 requirements-dev.txt           |   1 +
 setup.py                       | 190 +++++++++++++++++++++++----------
 shapely/__init__.py            |   2 +-
 shapely/ctypes_declarations.py |  22 +++-
 shapely/geometry/base.py       |  29 +++--
 shapely/geometry/linestring.py |  15 +--
 shapely/geometry/multipoint.py |   2 +-
 shapely/geometry/polygon.py    |   4 +-
 shapely/geos.py                | 150 ++++++--------------------
 shapely/impl.py                |  31 +++++-
 shapely/libgeos.py             | 236 +++++++++++++++++++++++++++++++++++++++++
 shapely/linref.py              |   4 +-
 shapely/speedups/__init__.py   |   2 +-
 tests/__init__.py              |   9 +-
 tests/test_default_impl.py     |  24 +++++
 tests/test_dlls.py             |   2 +-
 tests/test_multi.py            |   8 ++
 tests/test_multilinestring.py  |  10 +-
 tests/test_multipoint.py       |   9 +-
 tests/test_multipolygon.py     |  10 +-
 tests/test_operations.py       |  11 +-
 tests/test_parallel_offset.py  |  33 ++++++
 tests/test_predicates.py       |  23 +++-
 27 files changed, 663 insertions(+), 219 deletions(-)

diff --git a/.gitignore b/.gitignore
index 7662bd5..76a880b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 *.pyc
+*.pyo
 *.c
 *.so
 
diff --git a/CHANGES.txt b/CHANGES.txt
index 93f3772..b66b6ac 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,11 @@
 Changes
 =======
 
+1.5.10 (2015-08-22)
+-------------------
+- Monkey patch affinity module by absolute reference (#299).
+- Raise TopologicalError in relate() instead of crashing (#294, #295, #303).
+
 1.5.9 (2015-05-27)
 ------------------
 - Fix for 64 bit speedups compatibility (#274).
diff --git a/README.rst b/README.rst
index ea892c0..f80bba3 100644
--- a/README.rst
+++ b/README.rst
@@ -44,12 +44,13 @@ system library path, and install Shapely from the Python package index.
 
     $ pip install shapely
 
-If you've installed GEOS to a non-standard location, you can use the
-geos-config program to find compiler and linker options.
+If you've installed GEOS to a non-standard location, the geos-config program
+will be used to get compiler and linker options. If it is not on the PATH,
+it can be specified with a GEOS_CONFIG environment variable, e.g.:
 
 .. code-block:: console
 
-    $ CFLAGS=`geos-config --cflags` LDFLAGS=`geos-config --clibs` pip install shapely
+    $ GEOS_CONFIG=/path/to/geos-config pip install shapely
 
 If your system's GEOS version is < 3.3.0 you cannot use Shapely 1.3+ and must
 stick to 1.2.x as shown below.
diff --git a/docs/manual.rst b/docs/manual.rst
index 90b0a3d..07971b8 100644
--- a/docs/manual.rst
+++ b/docs/manual.rst
@@ -1054,8 +1054,9 @@ differently.
 
 .. method:: object.contains(other)
 
-  Returns ``True`` if the object's `interior` contains the `boundary` and
-  `interior` of the other object and their boundaries do not touch at all.
+  Returns ``True`` if no points of `other` lie in the exterior of the `object`
+  and at least one point of the interior of `other` lies in the interior of
+  `object`.
 
 This predicate applies to all types, and is inverse to :meth:`within`. The
 expression ``a.contains(b) == b.within(a)`` always evaluates to ``True``.
@@ -1124,8 +1125,8 @@ This predicate applies to all types and is the inverse of :meth:`intersects`.
   Returns ``True`` if the `boundary` and `interior` of the object intersect in
   any way with those of the other.
 
-This predicate is equivalent to the OR-ing of :meth:`contains`, :meth:`crosses`,
-:meth:`equals`, :meth:`touches`, and :meth:`within`.
+In other words, geometric objects intersect if they have any boundary or 
+interior point in common.
 
 .. method:: object.touches(other)
 
@@ -1234,6 +1235,37 @@ elements.
   >>> Point(0, 0).relate(LineString([(0, 0), (1, 1)]))
   'F0FFFF102'
 
+.. method:: object.relate_pattern(other, pattern)
+
+    Returns True if the DE-9IM string code for the relationship between the
+    geometries satisfies the pattern, otherwise False.
+
+The :meth:`relate_pattern` compares the DE-9IM code string for two geometries
+against a specified pattern. If the string matches the pattern then ``True`` is
+returned, otherwise ``False``. The pattern specified can be an exact match
+(``0``, ``1`` or ``2``), a boolean match (``T`` or ``F``), or a wildcard
+(``*``). For example, the pattern for the `within` predicate is ``T*****FF*``.
+
+.. code-block:: pycon
+
+  >> point = Point(0.5, 0.5)
+  >> square = Polygon([(0, 0), (0, 1), (1, 1), (1, 0)])
+  >> square.relate_pattern(point, 'T*****FF*')
+  True
+  >> point.within(square)
+  True
+
+Note that the order or the geometries is significant, as demonstrated below.
+In this example the square contains the point, but the point does not contain
+the square.
+
+.. code-block:: pycon
+
+  >>> point.relate(square)
+  '0FFFFF212'
+  >>> square.relate(point)
+  '0F2FF1FF2'
+
 Further discussion of the DE-9IM matrix is beyond the scope of this manual. See
 [4]_ and http://pypi.python.org/pypi/de9im.
 
@@ -2037,7 +2069,7 @@ one geometry to the vertices in a second geometry with a given tolerance.
    The `tolerance` argument specifies the minimum distance between vertices for
    them to be snapped.
 
-   `New in version 1.4.5`
+   `New in version 1.5.0`
 
 .. code-block:: pycon
 
diff --git a/requirements-dev.txt b/requirements-dev.txt
index b16d71b..04f77c6 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,4 +2,5 @@ setuptools
 Numpy>=1.8.0
 Cython>=0.19
 descartes==1.0.1
+packaging
 pytest
diff --git a/setup.py b/setup.py
index 9748470..b2b7bd5 100755
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,24 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
+# Two environment variables influence this script.
+#
+# GEOS_LIBRARY_PATH: a path to a GEOS C shared library.
+#
+# GEOS_CONFIG: the path to a geos-config program that points to GEOS version,
+# headers, and libraries.
+#
+# NB: within this setup scripts, software versions are evaluated according
+# to https://www.python.org/dev/peps/pep-0440/.
 
+import errno
+import glob
+import logging
+import os
+import platform
+import re
+import shutil
+import subprocess
+import sys
 try:
     # If possible, use setuptools
     from setuptools import setup
@@ -11,36 +28,54 @@ 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 distutils.cmd import Command
 from distutils.errors import CCompilerError, DistutilsExecError, \
     DistutilsPlatformError
-from distutils.sysconfig import get_config_var
-import errno
-import glob
-import os
-import platform
-import shutil
-import subprocess
-import sys
 
+from packaging.version import Version
+
+# Get geos_version from GEOS dynamic library, which depends on
+# GEOS_LIBRARY_PATH and/or GEOS_CONFIG environment variables
+from shapely.libgeos import geos_version_string, geos_version, \
+        geos_config, get_geos_config
+
+logging.basicConfig()
+log = logging.getLogger(__file__)
+
+# python -W all setup.py ...
+if 'all' in sys.warnoptions:
+    log.level = logging.DEBUG
 
 # Get the version from the shapely module
-version = None
+shapely_version = None
 with open('shapely/__init__.py', 'r') as fp:
     for line in fp:
-        if "__version__" in line:
-            exec(line.replace('_', ''))
+        if line.startswith("__version__"):
+            shapely_version = Version(
+                line.split("=")[1].strip().strip("\"'"))
             break
-if version is None:
+
+if not shapely_version:
     raise ValueError("Could not determine Shapely's version")
 
+# Fail installation if the GEOS shared library does not meet the minimum
+# version. We ship it with Shapely for Windows, so no need to check on
+# that platform.
+log.debug('GEOS shared library: %s %s', geos_version_string, geos_version)
+if (set(sys.argv).intersection(['install', 'build', 'build_ext']) and
+        shapely_version >= Version('1.3') and
+        geos_version < (3, 3)):
+    log.critical(
+        "Shapely >= 1.3 requires GEOS >= 3.3. "
+        "Install GEOS 3.3+ and reinstall Shapely.")
+    sys.exit(1)
+
 # 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:
-    fp.write(version)
+    fp.write(str(shapely_version))
 
 with open('README.rst', 'r', **open_kwds) as fp:
     readme = fp.read()
@@ -55,8 +90,8 @@ long_description = readme + '\n\n' + credits + '\n\n' + changes
 
 setup_args = dict(
     name                = 'Shapely',
-    version             = version,
-    requires            = ['Python (>=2.6)', 'libgeos_c (>=3.1)'],
+    version             = str(shapely_version),
+    requires            = ['Python (>=2.6)', 'libgeos_c (>=3.3)'],
     description         = 'Geometric objects, predicates, and operations',
     license             = 'BSD',
     keywords            = 'geometry topology gis',
@@ -74,7 +109,6 @@ setup_args = dict(
         'shapely.speedups',
         'shapely.vectorized',
     ],
-    cmdclass = {},
     classifiers         = [
         'Development Status :: 5 - Production/Stable',
         'Intended Audience :: Developers',
@@ -86,7 +120,8 @@ setup_args = dict(
         'Programming Language :: Python :: 3',
         'Topic :: Scientific/Engineering :: GIS',
     ],
-    data_files         = [('shapely', ['shapely/_geos.pxi'])]
+    data_files         = [('shapely', ['shapely/_geos.pxi'])],
+    cmdclass           = {},
 )
 
 # Add DLLs for Windows
@@ -111,6 +146,47 @@ if sys.platform == 'win32':
     )
 
 
+# Prepare build opts and args for the speedups extension module.
+include_dirs = []
+library_dirs = []
+libraries = []
+extra_link_args = []
+
+try:
+    # Get the version from geos-config. Show error if this version tuple is
+    # different to the GEOS version loaded from the dynamic library.
+    geos_config_version_string = get_geos_config('--version')
+    res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_config_version_string)
+    geos_config_version = tuple(int(x) for x in res[0])
+
+    if geos_config_version != geos_version:
+        log.error("The GEOS dynamic library version is %s %s,",
+                  geos_version_string, geos_version)
+        log.error("but the version reported by %s is %s %s.", geos_config,
+                  geos_config_version_string, geos_config_version)
+        sys.exit(1)
+except OSError as ex:
+    log.error(ex)
+    log.error('Cannot find geos-config to get headers and check version.')
+    log.error('If available, specify a path to geos-config with a '
+              'GEOS_CONFIG environment variable')
+    geos_config = None
+
+if geos_config:
+    # Collect other options from GEOS
+    for item in get_geos_config('--cflags').split():
+        if item.startswith("-I"):
+            include_dirs.extend(item[2:].split(":"))
+    for item in get_geos_config('--clibs').split():
+        if item.startswith("-L"):
+            library_dirs.extend(item[2:].split(":"))
+        elif item.startswith("-l"):
+            libraries.append(item[2:])
+        else:
+            # e.g. -framework GEOS
+            extra_link_args.append(item)
+
+
 # Optional compilation of speedups
 # setuptools stuff from Bob Ippolito's simplejson project
 if sys.platform == 'win32' and sys.version_info > (2, 6):
@@ -149,10 +225,6 @@ if (hasattr(platform, 'python_implementation')
     # python_implementation is only available since 2.6
     ext_modules = []
     libraries = []
-elif sys.platform == 'win32':
-    libraries = ['geos']
-else:
-    libraries = ['geos_c']
 
 
 if os.path.exists("MANIFEST.in"):
@@ -166,67 +238,77 @@ if os.path.exists("MANIFEST.in"):
     try:
         if (force_cython or not os.path.exists(c_file)
                 or os.path.getmtime(pyx_file) > os.path.getmtime(c_file)):
-            print("Updating C extension with Cython.", file=sys.stderr)
+            log.info("Updating C extension with Cython.")
             subprocess.check_call(["cython", "shapely/speedups/_speedups.pyx"])
     except (subprocess.CalledProcessError, OSError):
-        print("Warning: Could not (re)create C extension with Cython.",
-              file=sys.stderr)
+        log.warn("Could not (re)create C extension with Cython.")
         if force_cython:
             raise
-    if not os.path.exists("shapely/speedups/_speedups.c"):
-        print("Warning: speedup extension not found", file=sys.stderr)
+    if not os.path.exists(c_file):
+        log.warn("speedup extension not found")
 
 ext_modules = [
     Extension(
         "shapely.speedups._speedups",
         ["shapely/speedups/_speedups.c"],
+        include_dirs=include_dirs,
+        library_dirs=library_dirs,
         libraries=libraries,
-        include_dirs=[get_config_var('INCLUDEDIR')],),
+        extra_link_args=extra_link_args,
+    ),
 ]
 
 cmd_classes = setup_args.setdefault('cmdclass', {})
 
 try:
-    import numpy as np
+    import numpy
     from Cython.Distutils import build_ext as cython_build_ext
     from distutils.extension import Extension as DistutilsExtension
 
-    if 'build_ext' in cmd_classes:
+    if 'build_ext' in setup_args['cmdclass']:
         raise ValueError('We need to put the Cython build_ext in '
                          'cmd_classes, but it is already defined.')
-    cmd_classes['build_ext'] = cython_build_ext
-
-    ext_modules.append(DistutilsExtension("shapely.vectorized._vectorized",
-                                 sources=["shapely/vectorized/_vectorized.pyx"],
-                                 libraries=libraries + [np.get_include()],
-                                 include_dirs=[get_config_var('INCLUDEDIR'),
-                                               np.get_include()],
-                                 ))
+    setup_args['cmdclass']['build_ext'] = cython_build_ext
+
+    include_dirs.append(numpy.get_include())
+    libraries.append(numpy.get_include())
+
+    ext_modules.append(DistutilsExtension(
+        "shapely.vectorized._vectorized",
+        sources=["shapely/vectorized/_vectorized.pyx"],
+        include_dirs=include_dirs,
+        library_dirs=library_dirs,
+        libraries=libraries,
+        extra_link_args=extra_link_args,
+    ))
 except ImportError:
-    print("Numpy or Cython not available, shapely.vectorized submodule not "
-          "being built.")
+    log.info("Numpy or Cython not available, shapely.vectorized submodule "
+             "not being built.")
 
 
 try:
     # try building with speedups
-    existing_build_ext = setup_args['cmdclass'].get('build_ext', distutils_build_ext)
-    setup_args['cmdclass']['build_ext'] = construct_build_ext(existing_build_ext)
-    setup(
-        ext_modules=ext_modules,
-        **setup_args
-    )
+    existing_build_ext = setup_args['cmdclass'].\
+        get('build_ext', distutils_build_ext)
+    setup_args['cmdclass']['build_ext'] = \
+        construct_build_ext(existing_build_ext)
+    setup(ext_modules=ext_modules, **setup_args)
 except BuildFailed as ex:
-    BUILD_EXT_WARNING = "Warning: The C extension could not be compiled, " \
+    BUILD_EXT_WARNING = "The C extension could not be compiled, " \
                         "speedups are not enabled."
-    print(ex)
-    print(BUILD_EXT_WARNING)
-    print("Failure information, if any, is above.")
-    print("I'm retrying the build without the C extension now.")
+    log.warn(ex)
+    log.warn(BUILD_EXT_WARNING)
+    log.warn("Failure information, if any, is above.")
+    log.warn("I'm retrying the build without the C extension now.")
+
+    # Remove any previously defined build_ext command class.
+    if 'build_ext' in setup_args['cmdclass']:
+        del setup_args['cmdclass']['build_ext']
 
     if 'build_ext' in cmd_classes:
         del cmd_classes['build_ext']
 
     setup(**setup_args)
 
-    print(BUILD_EXT_WARNING)
-    print("Plain-Python installation succeeded.")
+    log.warn(BUILD_EXT_WARNING)
+    log.info("Plain-Python installation succeeded.")
diff --git a/shapely/__init__.py b/shapely/__init__.py
index 424ebc4..63d18fb 100644
--- a/shapely/__init__.py
+++ b/shapely/__init__.py
@@ -1 +1 @@
-__version__ = "1.5.9"
+__version__ = "1.5.10"
diff --git a/shapely/ctypes_declarations.py b/shapely/ctypes_declarations.py
index 3ee5141..ac38fc5 100644
--- a/shapely/ctypes_declarations.py
+++ b/shapely/ctypes_declarations.py
@@ -129,8 +129,16 @@ def prototype(lgeos, geos_version):
         lgeos.GEOSBufferWithStyle.restype = c_void_p
         lgeos.GEOSBufferWithStyle.argtypes = [c_void_p, c_double, c_int, c_int, c_int, c_double]
 
-        lgeos.GEOSSingleSidedBuffer.restype = c_void_p
-        lgeos.GEOSSingleSidedBuffer.argtypes = [c_void_p, c_double, c_int, c_int, c_double, c_int]
+        if geos_version >= (3, 3, 0):
+
+            lgeos.GEOSOffsetCurve.restype = c_void_p
+            lgeos.GEOSOffsetCurve.argtypes = [c_void_p, c_double, c_int, c_int, c_double]
+        
+        else:
+
+            # deprecated in GEOS 3.3.0 in favour of GEOSOffsetCurve
+            lgeos.GEOSSingleSidedBuffer.restype = c_void_p
+            lgeos.GEOSSingleSidedBuffer.argtypes = [c_void_p, c_double, c_int, c_int, c_double, c_int]
 
     '''
     Geometry constructors
@@ -290,12 +298,16 @@ def prototype(lgeos, geos_version):
     Dimensionally Extended 9 Intersection Model related
     '''
 
-    lgeos.GEOSRelatePattern.restype = c_char
-    lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p]
-
     lgeos.GEOSRelate.restype = allocated_c_char_p
     lgeos.GEOSRelate.argtypes = [c_void_p, c_void_p]
 
+    lgeos.GEOSRelatePattern.restype = c_byte
+    lgeos.GEOSRelatePattern.argtypes = [c_void_p, c_void_p, c_char_p]
+
+    if geos_version >= (3, 3, 0):
+        lgeos.GEOSRelatePatternMatch.restype = c_byte
+        lgeos.GEOSRelatePatternMatch.argtypes = [c_char_p, c_char_p]
+
     '''
     Prepared Geometry Binary predicates
     Return 2 on exception, 1 on true, 0 on false
diff --git a/shapely/geometry/base.py b/shapely/geometry/base.py
index 69d8405..6498e06 100644
--- a/shapely/geometry/base.py
+++ b/shapely/geometry/base.py
@@ -14,6 +14,16 @@ 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,)
+except ImportError:
+    pass
+
 
 GEOMETRY_TYPES = [
     'Point',
@@ -63,7 +73,7 @@ def geom_factory(g, parent=None):
         [geom_type],
         )
     ob.__class__ = getattr(mod, geom_type)
-    ob.__geom__ = g
+    ob._geom = g
     ob.__p__ = parent
     if lgeos.methods['has_z'](g):
         ob._ndim = 3
@@ -188,14 +198,11 @@ class BaseGeometry(object):
     _ndim = None
     _crs = None
     _other_owned = False
+    _is_empty = True
 
     # Backend config
     impl = DefaultImplementation
 
-    @property
-    def _is_empty(self):
-        return self.__geom__ in [EMPTY, None]
-
     # a reference to the so/dll proxy to preserve access during clean up
     _lgeos = lgeos
 
@@ -207,6 +214,7 @@ class BaseGeometry(object):
                 self._lgeos.GEOSGeom_destroy(self.__geom__)
             except AttributeError:
                 pass  # _lgeos might be empty on shutdown
+        self._is_empty = True
         self.__geom__ = val
 
     def __del__(self):
@@ -235,6 +243,7 @@ class BaseGeometry(object):
     @_geom.setter
     def _geom(self, val):
         self.empty()
+        self._is_empty = val in [EMPTY, None]
         self.__geom__ = val
 
     # Operators
@@ -528,7 +537,7 @@ class BaseGeometry(object):
 
     @delegated
     def simplify(self, tolerance, preserve_topology=True):
-        """Returns a simplified geometry produced by the Douglas-Puecker
+        """Returns a simplified geometry produced by the Douglas-Peucker
         algorithm
 
         Coordinates of the simplified geometry will be no more than the
@@ -663,6 +672,12 @@ class BaseGeometry(object):
         specified decimal place"""
         return self.equals_exact(other, 0.5 * 10**(-decimal))
 
+    def relate_pattern(self, other, pattern):
+        """Returns True if the DE-9IM string code for the relationship between
+        the geometries satisfies the pattern, else False"""
+        pattern = c_char_p(pattern.encode('ascii'))
+        return bool(self.impl['relate_pattern'](self, other, pattern))
+
     # Linear referencing
     # ------------------
 
@@ -828,7 +843,7 @@ class GeometrySequence(object):
     def __getitem__(self, key):
         self._update()
         m = self.__len__()
-        if isinstance(key, int):
+        if isinstance(key, integer_types):
             if key + m < 0 or key >= m:
                 raise IndexError("index out of range")
             if key < 0:
diff --git a/shapely/geometry/linestring.py b/shapely/geometry/linestring.py
index 7cb5cf1..7a9fb6b 100644
--- a/shapely/geometry/linestring.py
+++ b/shapely/geometry/linestring.py
@@ -110,16 +110,18 @@ class LineString(BaseGeometry):
         return self.coords.xy
 
     def parallel_offset(
-            self, distance, side,
+            self, distance, side='right',
             resolution=16, join_style=JOIN_STYLE.round, mitre_limit=5.0):
 
         """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'. The resolution of the buffer around each vertex of
-        the object increases by increasing the resolution keyword parameter or
-        third positional parameter.
+        The side parameter may be 'left' or 'right' (default is 'right'). The
+        resolution of the buffer around each vertex of the object increases by
+        increasing the resolution keyword parameter or third positional
+        parameter. If the distance parameter is negative the side is inverted,
+        e.g. distance=5.0, side='left' is the same as distance=-5.0,
+        side='right'.
 
         The join style is for outside corners between line segments. Accepted
         values are JOIN_STYLE.round (1), JOIN_STYLE.mitre (2), and
@@ -136,8 +138,7 @@ class LineString(BaseGeometry):
                 'Cannot compute offset from zero-length line segment')
         try:
             return geom_factory(self.impl['parallel_offset'](
-                self, distance, resolution, join_style, mitre_limit,
-                bool(side == 'left')))
+                self, distance, resolution, join_style, mitre_limit, side))
         except OSError:
             raise TopologicalError()
 
diff --git a/shapely/geometry/multipoint.py b/shapely/geometry/multipoint.py
index 9ea8a6e..d9f8908 100644
--- a/shapely/geometry/multipoint.py
+++ b/shapely/geometry/multipoint.py
@@ -53,7 +53,7 @@ class MultiPoint(BaseMultipartGeometry):
         """
         super(MultiPoint, self).__init__()
 
-        if points is None:
+        if points is None or len(points) == 0:
             # allow creation of empty multipoints, to support unpickling
             pass
         else:
diff --git a/shapely/geometry/polygon.py b/shapely/geometry/polygon.py
index 167f4d9..7c7c0cb 100644
--- a/shapely/geometry/polygon.py
+++ b/shapely/geometry/polygon.py
@@ -176,7 +176,7 @@ class InteriorRingSequence(object):
         if i not in self.__rings__:
             g = lgeos.GEOSGetInteriorRingN(self._geom, i)
             ring = LinearRing()
-            ring.__geom__ = g
+            ring._geom = g
             ring.__p__ = self
             ring._other_owned = True
             ring._ndim = self._ndim
@@ -235,7 +235,7 @@ class Polygon(BaseGeometry):
         elif self._exterior is None or self._exterior() is None:
             g = lgeos.GEOSGetExteriorRing(self._geom)
             ring = LinearRing()
-            ring.__geom__ = g
+            ring._geom = g
             ring.__p__ = self
             ring._other_owned = True
             ring._ndim = self._ndim
diff --git a/shapely/geos.py b/shapely/geos.py
index a00aa26..c767d72 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -1,8 +1,7 @@
 """
-Proxies for the libgeos_c shared lib, GEOS-specific exceptions, and utilities
+Proxies for libgeos, GEOS-specific exceptions, and utilities
 """
 
-import os
 import re
 import sys
 import atexit
@@ -12,8 +11,9 @@ from ctypes import CDLL, cdll, pointer, string_at, cast, POINTER
 from ctypes import c_void_p, c_size_t, c_char_p, c_int, c_float
 from ctypes.util import find_library
 
-from . import ftools
 from .ctypes_declarations import prototype, EXCEPTION_HANDLER_FUNCTYPE
+from .libgeos import lgeos as _lgeos, geos_version
+from . import ftools
 
 
 # Add message handler to this module's logger
@@ -31,111 +31,6 @@ else:
     LOG.addHandler(NullHandler())
 
 
-# Find and load the GEOS and C libraries
-# If this ever gets any longer, we'll break it into separate modules
-
-def load_dll(libname, fallbacks=None):
-    lib = find_library(libname)
-    if lib is not None:
-        try:
-            return CDLL(lib)
-        except OSError:
-            pass
-    if fallbacks is not None:
-        for name in fallbacks:
-            try:
-                return CDLL(name)
-            except OSError:
-                # move on to the next fallback
-                pass
-    # No shared library was loaded. Raise OSError.
-    raise OSError(
-        "Could not find library %s or load any of its variants %s" % (
-            libname, fallbacks or []))
-
-
-if sys.platform.startswith('linux'):
-    _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-    free = load_dll('c').free
-    free.argtypes = [c_void_p]
-    free.restype = None
-
-elif sys.platform == 'darwin':
-    # First test to see if this is 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)
-    else:
-        if hasattr(sys, 'frozen'):
-            try:
-                # .app file from py2app
-                alt_paths = [os.path.join(os.environ['RESOURCEPATH'],
-                            '..', 'Frameworks', 'libgeos_c.dylib')]
-            except KeyError:
-                # binary from pyinstaller
-                alt_paths = [os.path.join(sys.executable, 'libgeos_c.dylib')]
-        else:
-            alt_paths = [
-                # The Framework build from Kyng Chaos
-                "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
-                # macports
-                '/opt/local/lib/libgeos_c.dylib',
-            ]
-        _lgeos = load_dll('geos_c', fallbacks=alt_paths)
-
-    free = load_dll('c').free
-    free.argtypes = [c_void_p]
-    free.restype = None
-
-elif sys.platform == 'win32':
-    try:
-        egg_dlls = os.path.abspath(os.path.join(os.path.dirname(__file__),
-                                   "DLLs"))
-        wininst_dlls = os.path.abspath(os.__file__ + "../../../DLLs")
-        original_path = os.environ['PATH']
-        os.environ['PATH'] = "%s;%s;%s" % \
-            (egg_dlls, wininst_dlls, original_path)
-        _lgeos = CDLL("geos.dll")
-    except (ImportError, WindowsError, OSError):
-        raise
-
-    def free(m):
-        try:
-            cdll.msvcrt.free(m)
-        except WindowsError:
-            # XXX: See http://trac.gispython.org/projects/PCL/ticket/149
-            pass
-
-elif sys.platform == 'sunos5':
-    _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-    free = CDLL('libc.so.1').free
-    free.argtypes = [c_void_p]
-    free.restype = None
-else:  # other *nix systems
-    _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-    free = load_dll('c', fallbacks=['libc.so.6']).free
-    free.argtypes = [c_void_p]
-    free.restype = None
-
-
-def _geos_version():
-    # extern const char GEOS_DLL *GEOSversion();
-    GEOSversion = _lgeos.GEOSversion
-    GEOSversion.restype = c_char_p
-    GEOSversion.argtypes = []
-    #define GEOS_CAPI_VERSION "@VERSION at -CAPI-@CAPI_VERSION@"
-    geos_version_string = GEOSversion()
-    if sys.version_info[0] >= 3:
-        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])
-    return geos_version_string, geos_version, capi_version
-
-geos_version_string, geos_version, geos_capi_version = _geos_version()
-
 # If we have the new interface, then record a baseline so that we know what
 # additional functions are declared in ctypes_declarations.
 if geos_version >= (3, 1, 0):
@@ -430,7 +325,7 @@ class WKBWriter(object):
     def big_endian(self):
         """Byte order is big endian, True (default) or False"""
         return (self._lgeos.GEOSWKBWriter_getByteOrder(self._writer) ==
-            self._ENDIAN_BIG)
+                self._ENDIAN_BIG)
 
     @big_endian.setter
     def big_endian(self, value):
@@ -500,7 +395,7 @@ class WKBWriter(object):
 # Errcheck functions for ctypes
 
 def errcheck_wkb(result, func, argtuple):
-    '''Returns bytes from a C pointer'''
+    """Returns bytes from a C pointer"""
     if not result:
         return None
     size_ref = argtuple[-1]
@@ -511,7 +406,7 @@ def errcheck_wkb(result, func, argtuple):
 
 
 def errcheck_just_free(result, func, argtuple):
-    '''Returns string from a C pointer'''
+    """Returns string from a C pointer"""
     retval = string_at(result)
     lgeos.GEOSFree(result)
     if sys.version_info[0] >= 3:
@@ -519,9 +414,15 @@ def errcheck_just_free(result, func, argtuple):
     else:
         return retval
 
+def errcheck_null_exception(result, func, argtuple):
+    """Wraps errcheck_just_free, raising a TopologicalError if result is NULL"""
+    if not result:
+        raise TopologicalError("The operation '{0}' could not be performed."
+            "Likely cause is invalidity of the geometry.".format(func.__name__))
+    return errcheck_just_free(result, func, argtuple)
 
 def errcheck_predicate(result, func, argtuple):
-    '''Result is 2 on exception, 1 on True, 0 on False'''
+    """Result is 2 on exception, 1 on True, 0 on False"""
     if result == 2:
         raise PredicateError("Failed to evaluate %s" % repr(func))
     return result
@@ -562,7 +463,7 @@ class LGEOS300(LGEOSBase):
         # Deprecated
         self.GEOSGeomToWKB_buf.errcheck = errcheck_wkb
         self.GEOSGeomToWKT.errcheck = errcheck_just_free
-        self.GEOSRelate.errcheck = errcheck_just_free
+        self.GEOSRelate.errcheck = errcheck_null_exception
         for pred in (
                 self.GEOSDisjoint,
                 self.GEOSTouches,
@@ -573,6 +474,7 @@ class LGEOS300(LGEOSBase):
                 self.GEOSOverlaps,
                 self.GEOSEquals,
                 self.GEOSEqualsExact,
+                self.GEOSRelatePattern,
                 self.GEOSisEmpty,
                 self.GEOSisValid,
                 self.GEOSisSimple,
@@ -608,6 +510,7 @@ class LGEOS300(LGEOSBase):
         self.methods['symmetric_difference'] = self.GEOSSymDifference
         self.methods['union'] = self.GEOSUnion
         self.methods['intersection'] = self.GEOSIntersection
+        self.methods['relate_pattern'] = self.GEOSRelatePattern
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
             self.GEOSTopologyPreserveSimplify
@@ -637,7 +540,7 @@ class LGEOS310(LGEOSBase):
         # Deprecated
         self.GEOSGeomToWKB_buf.func.errcheck = errcheck_wkb
         self.GEOSGeomToWKT.func.errcheck = errcheck_just_free
-        self.GEOSRelate.func.errcheck = errcheck_just_free
+        self.GEOSRelate.func.errcheck = errcheck_null_exception
         for pred in (
                 self.GEOSDisjoint,
                 self.GEOSTouches,
@@ -658,6 +561,7 @@ class LGEOS310(LGEOSBase):
                 self.GEOSPreparedContainsProperly,
                 self.GEOSPreparedCovers,
                 self.GEOSPreparedIntersects,
+                self.GEOSRelatePattern,
                 self.GEOSisEmpty,
                 self.GEOSisValid,
                 self.GEOSisSimple,
@@ -706,6 +610,7 @@ class LGEOS310(LGEOSBase):
             self.GEOSPreparedContainsProperly
         self.methods['prepared_overlaps'] = self.GEOSPreparedOverlaps
         self.methods['prepared_covers'] = self.GEOSPreparedCovers
+        self.methods['relate_pattern'] = self.GEOSRelatePattern
         self.methods['simplify'] = self.GEOSSimplify
         self.methods['topology_preserve_simplify'] = \
             self.GEOSTopologyPreserveSimplify
@@ -731,7 +636,15 @@ class LGEOS320(LGEOS311):
     def __init__(self, dll):
         super(LGEOS320, self).__init__(dll)
 
-        self.methods['parallel_offset'] = self.GEOSSingleSidedBuffer
+        if geos_version >= (3, 2, 0):
+            def parallel_offset(geom, distance, resolution=16, join_style=1, mitre_limit=5.0, side='right'):
+                side = side == 'left'
+                if distance < 0:
+                    distance = abs(distance)
+                    side = not side
+                return self.GEOSSingleSidedBuffer(geom, distance, resolution, join_style, mitre_limit, side)
+            self.methods['parallel_offset'] = parallel_offset
+
         self.methods['project'] = self.GEOSProject
         self.methods['project_normalized'] = self.GEOSProjectNormalized
         self.methods['interpolate'] = self.GEOSInterpolate
@@ -760,6 +673,12 @@ class LGEOS330(LGEOS320):
         for pred in (self.GEOSisClosed,):
             pred.func.errcheck = errcheck_predicate
 
+        def parallel_offset(geom, distance, resolution=16, join_style=1, mitre_limit=5.0, side='right'):
+            if side == 'right':
+                distance *= -1
+            return self.GEOSOffsetCurve(geom, distance, resolution, join_style, mitre_limit)
+        self.methods['parallel_offset'] = parallel_offset
+
         self.methods['unary_union'] = self.GEOSUnaryUnion
         self.methods['is_closed'] = self.GEOSisClosed
         self.methods['cascaded_union'] = self.methods['unary_union']
@@ -793,6 +712,7 @@ else:
 
 lgeos = L(_lgeos)
 
+
 def cleanup(proxy):
     del proxy
 
diff --git a/shapely/impl.py b/shapely/impl.py
index fda6281..828e0ae 100644
--- a/shapely/impl.py
+++ b/shapely/impl.py
@@ -21,6 +21,13 @@ from shapely.predicates import BinaryPredicate, UnaryPredicate
 from shapely.topology import BinaryRealProperty, BinaryTopologicalOp
 from shapely.topology import UnaryRealProperty, UnaryTopologicalOp
 
+
+class ImplementationError(
+        AttributeError, KeyError, NotImplementedError):
+    """To be raised when the registered implementation does not
+    support the requested method."""
+
+
 def delegated(func):
     """A delegated method raises AttributeError in the absence of backend
     support."""
@@ -29,27 +36,43 @@ def delegated(func):
         try:
             return func(*args, **kwargs)
         except KeyError:
-            raise AttributeError("Method %r is not supported by %r" %
-                                 (func.__name__, args[0].impl))
+            raise ImplementationError(
+                "Method '%s' not provided by registered "
+                "implementation '%s'" % (func.__name__, args[0].impl))
     return wrapper
 
 # Map geometry methods to their GEOS delegates
 
+
 class BaseImpl(object):
+    """Base class for registrable implementations."""
+
     def __init__(self, values):
         self.map = dict(values)
+
     def update(self, values):
         self.map.update(values)
+
     def __getitem__(self, key):
-        return self.map[key]
+        try:
+            return self.map[key]
+        except KeyError:
+            raise ImplementationError(
+                "Method '%s' not provided by registered "
+                "implementation '%s'" % (key, self.map))
+
     def __contains__(self, key):
         return key in self.map
 
+
 class GEOSImpl(BaseImpl):
+    """GEOS implementation"""
+
     def __repr__(self):
         return '<GEOSImpl object: GEOS C API version %s>' % (
             lgeos.geos_capi_version,)
 
+
 IMPL300 = {
     'area': (UnaryRealProperty, 'area'),
     'distance': (BinaryRealProperty, 'distance'),
@@ -85,6 +108,7 @@ IMPL300 = {
     'within': (BinaryPredicate, 'within'),
     'covers': (BinaryPredicate, 'covers'),
     'equals_exact': (BinaryPredicate, 'equals_exact'),
+    'relate_pattern': (BinaryPredicate, 'relate_pattern'),
 
     # First pure Python implementation
     'is_ccw': (cga.is_ccw_impl, 'is_ccw'),
@@ -121,6 +145,7 @@ IMPL320 = {
 IMPL330 = {
     'is_closed': (UnaryPredicate, 'is_closed')}
 
+
 def impl_items(defs):
     return [(k, v[0](v[1])) for k, v in list(defs.items())]
 
diff --git a/shapely/libgeos.py b/shapely/libgeos.py
new file mode 100644
index 0000000..8bae437
--- /dev/null
+++ b/shapely/libgeos.py
@@ -0,0 +1,236 @@
+"""
+Minimal proxy to a GEOS C dynamic library, which is system dependant
+
+Two environment variables influence this module: GEOS_LIBRARY_PATH and/or
+GEOS_CONFIG.
+
+If GEOS_LIBRARY_PATH is set to a path to a GEOS C shared library, this is
+used. Otherwise GEOS_CONFIG can be set to a path to `geos-config`. If
+`geos-config` is already on the PATH environment variable, then it will
+be used to help better guess the name for the GEOS C dynamic library.
+"""
+
+import os
+import logging
+import re
+import subprocess
+import sys
+from ctypes import CDLL, cdll, c_void_p, c_char_p
+from ctypes.util import find_library
+
+
+logging.basicConfig()
+log = logging.getLogger(__name__)
+if 'all' in sys.warnoptions:
+    log.level = logging.DEBUG
+
+
+# The main point of this module is to load a dynamic library to this variable
+lgeos = None
+
+# First try: use GEOS_LIBRARY_PATH environment variable
+if 'GEOS_LIBRARY_PATH' in os.environ:
+    geos_library_path = os.environ['GEOS_LIBRARY_PATH']
+    try:
+        lgeos = CDLL(geos_library_path)
+    except:
+        log.warn('cannot open shared object from GEOS_LIBRARY_PATH: %s',
+                 geos_library_path)
+    if lgeos:
+        if hasattr(lgeos, 'GEOSversion'):
+            log.debug('found GEOS C library using GEOS_LIBRARY_PATH')
+        else:
+            raise OSError(
+                'shared object GEOS_LIBRARY_PATH is not a GEOS C library: '
+                + str(geos_library_path))
+
+# Second try: use GEOS_CONFIG environment variable
+if 'GEOS_CONFIG' in os.environ:
+    geos_config = os.environ['GEOS_CONFIG']
+    log.debug('geos_config: %s', geos_config)
+else:
+    geos_config = 'geos-config'
+
+
+def get_geos_config(option):
+    '''Get configuration option from the `geos-config` development utility
+
+    Path to utility is set with a module-level `geos_config` variable, which
+    can be changed or unset.
+    '''
+    geos_config = globals().get('geos_config')
+    if not geos_config or not isinstance(geos_config, str):
+        raise OSError('Path to geos-config is not set')
+    try:
+        stdout, stderr = subprocess.Popen(
+            [geos_config, option],
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
+    except OSError as ex:
+        # e.g., [Errno 2] No such file or directory
+        raise OSError(
+            '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()
+    log.debug('%s %s: %r', geos_config, option, result)
+    return result
+
+# Now try and use the utility to load from `geos-config --clibs` with
+# some magic smoke to guess the other parts of the library name
+try:
+    clibs = get_geos_config('--clibs')
+except OSError:
+    geos_config = None
+if not lgeos and geos_config:
+    base = ''
+    name = 'geos_c'
+    for item in clibs.split():
+        if item.startswith("-L"):
+            base = item[2:]
+        elif item.startswith("-l"):
+            name = item[2:]
+    # Now guess the actual library name using a list of possible formats
+    if sys.platform == 'win32':
+        # Unlikely, since geos-config is a shell script, but you never know...
+        fmts = ['{name}.dll']
+    elif sys.platform == 'darwin':
+        fmts = ['lib{name}.dylib', '{name}.dylib', '{name}.framework/{name}']
+    elif os.name == 'posix':
+        fmts = ['lib{name}.so', 'lib{name}.so.1']
+    guesses = []
+    for fmt in fmts:
+        lib_name = fmt.format(name=name)
+        geos_library_path = os.path.join(base, lib_name)
+        try:
+            lgeos = CDLL(geos_library_path)
+            break
+        except:
+            guesses.append(geos_library_path)
+    if lgeos:
+        if hasattr(lgeos, 'GEOSversion'):
+            log.debug('found GEOS C library using geos-config')
+        else:
+            raise OSError(
+                'shared object found by geos-config is not a GEOS C library: '
+                + str(geos_library_path))
+    else:
+        log.warn("cannot open shared object from '%s --clibs': %r",
+                 geos_config, clibs)
+        log.warn("there were %d guess(es) for this path:\n\t%s",
+                 len(guesses), '\n\t'.join(guesses))
+
+
+# Platform-specific attempts, and build `free` object
+
+def load_dll(libname, fallbacks=[]):
+    '''Load GEOS dynamic library'''
+    lib = find_library(libname)
+    if lib is not None:
+        try:
+            return CDLL(lib)
+        except OSError:
+            pass
+    for name in fallbacks:
+        try:
+            return CDLL(name)
+        except OSError:
+            # move on to the next fallback
+            pass
+    raise OSError(
+        "Could not find library %s or load any of its variants %s" % (
+            libname, fallbacks))
+
+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.argtypes = [c_void_p]
+    free.restype = None
+
+elif sys.platform == 'darwin':
+    if not lgeos:
+        # First test to see if this is 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)
+        else:
+            if hasattr(sys, 'frozen'):
+                # .app file from py2app
+                alt_paths = [os.path.join(os.environ['RESOURCEPATH'],
+                             '..', 'Frameworks', 'libgeos_c.dylib')]
+            else:
+                alt_paths = [
+                    # The Framework build from Kyng Chaos
+                    "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
+                    # macports
+                    '/opt/local/lib/libgeos_c.dylib',
+                ]
+            lgeos = load_dll('geos_c', fallbacks=alt_paths)
+
+    free = load_dll('c', fallbacks=['/usr/lib/libc.dylib']).free
+    free.argtypes = [c_void_p]
+    free.restype = None
+
+elif sys.platform == 'win32':
+    if not lgeos:
+        try:
+            egg_dlls = os.path.abspath(
+                os.path.join(os.path.dirname(__file__), "DLLs"))
+            wininst_dlls = os.path.abspath(os.__file__ + "../../../DLLs")
+            original_path = os.environ['PATH']
+            os.environ['PATH'] = "%s;%s;%s" % \
+                (egg_dlls, wininst_dlls, original_path)
+            lgeos = CDLL("geos.dll")
+        except (ImportError, WindowsError, OSError):
+            raise
+
+    def free(m):
+        try:
+            cdll.msvcrt.free(m)
+        except WindowsError:
+            # TODO: http://web.archive.org/web/20070810024932/
+            #     + http://trac.gispython.org/projects/PCL/ticket/149
+            pass
+
+elif sys.platform == 'sunos5':
+    if not lgeos:
+        lgeos = load_dll('geos_c',
+                         fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
+    free = CDLL('libc.so.1').free
+    free.argtypes = [c_void_p]
+    free.restype = None
+
+else:  # other *nix systems
+    if not lgeos:
+        lgeos = load_dll('geos_c',
+                         fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
+    free = load_dll('c', fallbacks=['libc.so.6']).free
+    free.argtypes = [c_void_p]
+    free.restype = None
+
+# TODO: what to do with 'free'? It isn't used.
+
+
+def _geos_version():
+    # extern const char GEOS_DLL *GEOSversion();
+    GEOSversion = lgeos.GEOSversion
+    GEOSversion.restype = c_char_p
+    GEOSversion.argtypes = []
+    # #define GEOS_CAPI_VERSION "@VERSION at -CAPI-@CAPI_VERSION@"
+    geos_version_string = GEOSversion()
+    if sys.version_info[0] >= 3:
+        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])
+
+    return geos_version_string, geos_version, capi_version
+
+geos_version_string, geos_version, geos_capi_version = _geos_version()
diff --git a/shapely/linref.py b/shapely/linref.py
index fc3d0f4..8e727fc 100644
--- a/shapely/linref.py
+++ b/shapely/linref.py
@@ -7,9 +7,7 @@ from shapely.topology import Delegating
 class LinearRefBase(Delegating):
     def _validate_line(self, ob):
         super(LinearRefBase, self)._validate(ob)
-        try:
-            assert ob.geom_type in ['LineString', 'MultiLineString']
-        except AssertionError:
+        if not ob.geom_type in ['LinearRing', 'LineString', 'MultiLineString']:
             raise TypeError("Only linear types support this operation")
 
 class ProjectOp(LinearRefBase):
diff --git a/shapely/speedups/__init__.py b/shapely/speedups/__init__.py
index 60d9d0b..2e84a1f 100644
--- a/shapely/speedups/__init__.py
+++ b/shapely/speedups/__init__.py
@@ -58,5 +58,5 @@ def disable():
     coords.CoordinateSequence.__iter__ = _orig['CoordinateSequence.__iter__']
     linestring.geos_linestring_from_py = _orig['geos_linestring_from_py']
     polygon.geos_linearring_from_py = _orig['geos_linearring_from_py']
-    affinity.affine_transform = _orig['affine_transform']
+    shapely.affinity.affine_transform = _orig['affine_transform']
     _orig.clear()
diff --git a/tests/__init__.py b/tests/__init__.py
index b4cf8ab..057380d 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,10 +1,17 @@
 import sys
-from shapely.geos import geos_version_string, lgeos, WKTWriter
+from shapely.libgeos import geos_version_string
+from shapely.geos import lgeos, WKTWriter
 from shapely import speedups
 
+from packaging.version import Version
+
+
+test_int_types = [int]
+
 try:
     import numpy
     numpy_version = numpy.version.version
+    test_int_types.extend([int, numpy.int16, numpy.int32, numpy.int64])
 except ImportError:
     numpy = False
     numpy_version = 'not available'
diff --git a/tests/test_default_impl.py b/tests/test_default_impl.py
new file mode 100644
index 0000000..7a33193
--- /dev/null
+++ b/tests/test_default_impl.py
@@ -0,0 +1,24 @@
+import pytest
+
+from shapely.geometry import Point
+from shapely.impl import delegated, ImplementationError
+
+
+def test_error():
+    with pytest.raises(ImplementationError):
+        Point(0, 0).impl['bogus']()
+    with pytest.raises(NotImplementedError):
+        Point(0, 0).impl['bogus']()
+    with pytest.raises(KeyError):
+        Point(0, 0).impl['bogus']()
+
+
+def test_delegated():
+    class Poynt(Point):
+        @delegated
+        def bogus(self):
+            return self.impl['bogus']()
+    with pytest.raises(ImplementationError):
+        Poynt(0, 0).bogus()
+    with pytest.raises(AttributeError):
+        Poynt(0, 0).bogus()
diff --git a/tests/test_dlls.py b/tests/test_dlls.py
index 8c82bcb..a887411 100644
--- a/tests/test_dlls.py
+++ b/tests/test_dlls.py
@@ -1,6 +1,6 @@
 from . import unittest
 
-from shapely.geos import load_dll
+from shapely.libgeos import load_dll
 
 
 class LoadingTestCase(unittest.TestCase):
diff --git a/tests/test_multi.py b/tests/test_multi.py
new file mode 100644
index 0000000..49fd4e3
--- /dev/null
+++ b/tests/test_multi.py
@@ -0,0 +1,8 @@
+from . import unittest, test_int_types
+
+class MultiGeometryTestCase(unittest.TestCase):
+    def subgeom_access_test(self, cls, geoms):
+        geom = cls(geoms)
+        for t in test_int_types:
+            for i, g in enumerate(geoms):
+                self.assertEqual(geom[t(i)], geoms[i])
diff --git a/tests/test_multilinestring.py b/tests/test_multilinestring.py
index 61b6bc3..be3f4ab 100644
--- a/tests/test_multilinestring.py
+++ b/tests/test_multilinestring.py
@@ -1,10 +1,11 @@
-from . import unittest, numpy
+from . import unittest, numpy, test_int_types
+from .test_multi import MultiGeometryTestCase
 from shapely.geos import lgeos
 from shapely.geometry import LineString, MultiLineString, asMultiLineString
 from shapely.geometry.base import dump_coords
 
 
-class MultiLineStringTestCase(unittest.TestCase):
+class MultiLineStringTestCase(MultiGeometryTestCase):
 
     def test_multilinestring(self):
 
@@ -73,6 +74,11 @@ class MultiLineStringTestCase(unittest.TestCase):
 
         # TODO: is there an inverse?
 
+    def test_subgeom_access(self):
+        line0 = LineString([(0.0, 1.0), (2.0, 3.0)])
+        line1 = LineString([(4.0, 5.0), (6.0, 7.0)])
+        self.subgeom_access_test(MultiLineString, [line0, line1])
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiLineStringTestCase)
diff --git a/tests/test_multipoint.py b/tests/test_multipoint.py
index 013ba0d..c84443c 100644
--- a/tests/test_multipoint.py
+++ b/tests/test_multipoint.py
@@ -1,9 +1,10 @@
-from . import unittest, numpy
+from . import unittest, numpy, test_int_types
+from .test_multi import MultiGeometryTestCase
 from shapely.geometry import Point, MultiPoint, asMultiPoint
 from shapely.geometry.base import dump_coords
 
 
-class MultiPointTestCase(unittest.TestCase):
+class MultiPointTestCase(MultiGeometryTestCase):
 
     def test_multipoint(self):
 
@@ -68,6 +69,10 @@ class MultiPointTestCase(unittest.TestCase):
         pas = asarray(geoma)
         assert_array_equal(pas, array([[1., 2.], [3., 4.]]))
 
+    def test_subgeom_access(self):
+        p0 = Point(1.0, 2.0)
+        p1 = Point(3.0, 4.0)
+        self.subgeom_access_test(MultiPoint, [p0, p1])
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiPointTestCase)
diff --git a/tests/test_multipolygon.py b/tests/test_multipolygon.py
index 9e7d257..188beb9 100644
--- a/tests/test_multipolygon.py
+++ b/tests/test_multipolygon.py
@@ -1,9 +1,10 @@
-from . import unittest
+from . import unittest, numpy, test_int_types
+from .test_multi import MultiGeometryTestCase
 from shapely.geometry import Polygon, MultiPolygon, asMultiPolygon
 from shapely.geometry.base import dump_coords
 
 
-class MultiPolygonTestCase(unittest.TestCase):
+class MultiPolygonTestCase(MultiGeometryTestCase):
 
     def test_multipolygon(self):
 
@@ -67,6 +68,11 @@ class MultiPolygonTestCase(unittest.TestCase):
         self.assertEqual(len(mpa.geoms[0].interiors), 1)
         self.assertEqual(len(mpa.geoms[0].interiors[0].coords), 5)
 
+    def test_subgeom_access(self):
+        poly0 = Polygon([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
+        poly1 = Polygon([(0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25)])
+        self.subgeom_access_test(MultiPolygon, [poly0, poly1])
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(MultiPolygonTestCase)
diff --git a/tests/test_operations.py b/tests/test_operations.py
index bdea03a..3377646 100644
--- a/tests/test_operations.py
+++ b/tests/test_operations.py
@@ -1,7 +1,8 @@
 from . import unittest
+import pytest
 from shapely.geometry import Point, Polygon, MultiPoint, GeometryCollection
 from shapely.wkt import loads
-
+from shapely.geos import TopologicalError
 
 class OperationsTestCase(unittest.TestCase):
 
@@ -62,9 +63,15 @@ class OperationsTestCase(unittest.TestCase):
 
         self.assertIsInstance(point.centroid, Point)
 
+    def test_relate(self):
         # Relate
-        self.assertEqual(point.relate(Point(-1, -1)), 'FF0FFF0F2')
+        self.assertEqual(Point(0, 0).relate(Point(-1, -1)), 'FF0FFF0F2')
 
+        # issue #294: should raise TopologicalError on exception
+        invalid_polygon = loads('POLYGON ((40 100, 80 100, 80 60, 40 60, 40 100), (60 60, 80 60, 80 40, 60 40, 60 60))')
+        assert(not invalid_polygon.is_valid)
+        with pytest.raises(TopologicalError):
+            invalid_polygon.relate(invalid_polygon)
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(OperationsTestCase)
diff --git a/tests/test_parallel_offset.py b/tests/test_parallel_offset.py
new file mode 100644
index 0000000..ae7a2f1
--- /dev/null
+++ b/tests/test_parallel_offset.py
@@ -0,0 +1,33 @@
+from . import unittest
+from shapely.geos import geos_version
+from shapely.geometry import LineString, LinearRing
+from shapely.wkt import loads
+
+class OperationsTestCase(unittest.TestCase):
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_parallel_offset_linestring(self):
+        line1 = LineString([(0, 0), (10, 0)])
+        left = line1.parallel_offset(5, 'left')
+        self.assertEqual(left, LineString([(0, 5), (10, 5)]))
+        right = line1.parallel_offset(5, 'right')
+        self.assertEqual(right, LineString([(10, -5), (0, -5)]))
+        right = line1.parallel_offset(-5, 'left')
+        self.assertEqual(right, LineString([(10, -5), (0, -5)]))
+        left = line1.parallel_offset(-5, 'right')
+        self.assertEqual(left, LineString([(0, 5), (10, 5)]))
+
+        # by default, parallel_offset is right-handed
+        self.assertEqual(line1.parallel_offset(5), right)
+
+        line2 = LineString([(0, 0), (5, 0), (5, -5)])
+        self.assertEqual(line2.parallel_offset(2, 'left', resolution=1),
+                         LineString([(0, 2), (5, 2), (7, 0), (7, -5)]))
+        self.assertEqual(line2.parallel_offset(2, 'left', join_style=2,
+                         resolution=1),
+                         LineString([(0, 2), (7, 2), (7, -5)]))
+
+    @unittest.skipIf(geos_version < (3, 2, 0), 'GEOS 3.2.0 required')
+    def test_parallel_offset_linear_ring(self):
+        lr1 = LinearRing([(0, 0), (5, 0), (5, 5), (0, 5), (0, 0)])
+        self.assertEqual(lr1.parallel_offset(2, 'left', resolution=1),
+                         LineString([(2, 2), (3, 2), (3, 3), (2, 3), (2, 2)]))
diff --git a/tests/test_predicates.py b/tests/test_predicates.py
index 3062eac..c3c1cb4 100644
--- a/tests/test_predicates.py
+++ b/tests/test_predicates.py
@@ -2,8 +2,8 @@
 """
 from . import unittest
 from shapely.geometry import Point, Polygon
-from shapely.geos import TopologicalError
-
+from shapely.geos import TopologicalError, PredicateError
+import pytest
 
 class PredicatesTestCase(unittest.TestCase):
 
@@ -41,6 +41,25 @@ class PredicatesTestCase(unittest.TestCase):
               (399, 450), (339, 207)]
         self.assertRaises(TopologicalError, Polygon(p1).within, Polygon(p2))
 
+    def test_relate_pattern(self):
+
+        # a pair of partially overlapping polygons, and a nearby point
+        g1 = Polygon([(0, 0), (0, 1), (3, 1), (3, 0), (0, 0)])
+        g2 = Polygon([(1, -1), (1, 2), (2, 2), (2, -1), (1, -1)])
+        g3 = Point(5, 5)
+
+        assert(g1.relate(g2) == '212101212')
+        assert(g1.relate_pattern(g2, '212101212'))
+        assert(g1.relate_pattern(g2, '*********'))
+        assert(g1.relate_pattern(g2, '2********'))
+        assert(g1.relate_pattern(g2, 'T********'))
+        assert(not g1.relate_pattern(g2, '112101212'))
+        assert(not g1.relate_pattern(g2, '1********'))
+        assert(g1.relate_pattern(g3, 'FF2FF10F2'))
+
+        # an invalid pattern should raise an exception
+        with pytest.raises(PredicateError):
+            g1.relate_pattern(g2, 'fail')
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromTestCase(PredicatesTestCase)

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/python-shapely.git



More information about the Pkg-grass-devel mailing list