Repository: Toblerity/Shapely
Pull request #239 (2015-02-03 06:13:33)

Move system-dependant logic to load dynamic library into minimal module, and use geos-config to get development headers
The main thing done here is to move the system-dependant loading of the GEOS dynamic library into a separate module `shapely.libgeos`, which can be used by setup.py for checking versions before install, and by `shapely.geos` for run-time use the GEOS of dynamic library.

A TODO item is to figure out a different way to specify the dynamic library (#234). Someone else might need to take the lead on this, but at least it will be in one place, rather than repeated in `setup.py` and `shapely/geos.py`. CC @kynan for ideas.

Secondly, `geos-config`, where available, is used to obtain libraries and headers for developing Cython extensions. This is useful where GEOS is installed on a non-standard prefix. An alternative location for this utility can be proved for setup.py using a `GEOS_CONFIG` environment variable. Note that this is only needed for setup.py, so I don't think it's suitable for setting dynamic library prefix. Also, `geos-config` is a shell script, and is not available for MS Windows.

Other edits use the logging module for setup.py, and fixing the `requires` metadata. Extra debugging info can be shown e.g. `python -W all setup.py ...`. Also add TODO that `free` is not used anywhere.

Please test and comment, as these files are rather sensitive components.

This PR supersedes #236 due to GitHub limitations.


Patch:
diff --git a/setup.py b/setup.py
index a3402c6..c03ee08 100755
--- a/setup.py
+++ b/setup.py
@@ -1,47 +1,68 @@
 #!/usr/bin/env python
 
-from __future__ import print_function
+# One environment variable influence this script.
+#
+# GEOS_CONFIG: the path to a geos-config program that points to GEOS version,
+# headers, and libraries.
 
-import ctypes
-import ctypes.util
+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
     from setuptools.extension import Extension
     from setuptools.command.build_ext import build_ext as distutils_build_ext
-    from setuptools.command.install import install
 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.command.install import install
-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 re
-import shutil
-import subprocess
-import sys
 
+# Get geos_version from GEOS dynamic library
+# TODO: find a way to load a specific dynamic library
+from shapely.libgeos import geos_version_string, geos_version
+
+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
 with open('shapely/__init__.py', 'r') as fp:
     for line in fp:
-        if "__version__" in line:
-            exec(line.replace('_', ''))
+        if line.startswith("__version__"):
+            version = line.split("=")[1].strip().strip("\"'")
             break
-if version is None:
+if not version:
     raise ValueError("Could not determine Shapely's version")
+shapely_version = tuple(int(x) for x in version.split('.'))
+
+# 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 >= (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,):
+if sys.version_info[0] > 3:
     open_kwds['encoding'] = 'utf-8'
 
 with open('VERSION.txt', 'w', **open_kwds) as fp:
@@ -58,78 +79,10 @@ with open('CHANGES.txt', 'r', **open_kwds) as fp:
 
 long_description = readme + '\n\n' + credits + '\n\n' + changes
 
-# Fail installation if we can't find a GEOS shared library with the right
-# version. We ship it with Shapely for Windows, so no need to check on that
-# platform. Code below copied from shapely/geos.py.
-class InstallCommand(install):
-
-    def run(self):
-        def load_dll(libname, fallbacks=None):
-            lib = ctypes.util.find_library(libname)
-            if lib is not None:
-                try:
-                    return ctypes.CDLL(lib)
-                except OSError:
-                    pass
-            if fallbacks is not None:
-                for name in fallbacks:
-                    try:
-                        return ctypes.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'])
-        elif sys.platform == 'darwin':
-            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)
-        elif sys.platform == 'sunos5':
-            _lgeos = load_dll(
-                'geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-        else:  # other *nix systems
-            _lgeos = load_dll(
-                'geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
-
-        GEOSversion = _lgeos.GEOSversion
-        GEOSversion.restype = ctypes.c_char_p
-        GEOSversion.argtypes = []
-        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])
-        shapely_version = tuple(int(x) for x in version.split('.'))
-
-        if shapely_version >= (1, 3):
-            if geos_version >= (3, 3):
-                install.run(self)
-            else:
-                print(
-                    "Shapely >= 1.3 requires GEOS >= 3.3. "
-                    "Install GEOS 3.3+ and reinstall Shapely.")
-                sys.exit(1)
-
 setup_args = dict(
     name                = 'Shapely',
     version             = version,
-    requires            = ['Python (>=2.6)', 'libgeos_c (>=3.1)'],
+    requires            = ['Python (>=2.6)', 'libgeos_c (>=3.3)'],
     description         = 'Geometric objects, predicates, and operations',
     license             = 'BSD',
     keywords            = 'geometry topology gis',
@@ -147,7 +100,6 @@ setup_args = dict(
         'shapely.speedups',
         'shapely.vectorized',
     ],
-    cmdclass = {'install': InstallCommand},
     classifiers         = [
         'Development Status :: 5 - Production/Stable',
         'Intended Audience :: Developers',
@@ -159,7 +111,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 to Windows packages.
@@ -183,6 +136,70 @@ if sys.platform == 'win32':
         include_package_data=True,
     )
 
+# Build cython extensions, which require development parameters
+include_dirs = [get_config_var('INCLUDEDIR')]
+library_dirs = []
+libraries = []
+extra_link_args = []
+
+# Use geos-config utility to get development parameters
+# (Note: not available for Windows, since it is a shell script)
+geos_config = os.environ.get('GEOS_CONFIG', 'geos-config')
+log.debug('geos_config: %s', geos_config)
+
+
+def get_geos_config(option):
+    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: %s', geos_config, option, result)
+    return result
+
+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
@@ -222,10 +239,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"):
@@ -239,63 +252,72 @@ 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,
+    ),
 ]
 
 try:
-    import numpy as np
+    import numpy
     from Cython.Distutils import build_ext as cython_build_ext
     from distutils.extension import Extension as DistutilsExtension
 
-    cmd_classes = setup_args.setdefault('cmdclass', {})
-    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']
 
     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/geos.py b/shapely/geos.py
index 5db949f..b0e89f1 100644
--- a/shapely/geos.py
+++ b/shapely/geos.py
@@ -1,19 +1,16 @@
 """
-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
 import logging
 import threading
-from ctypes import CDLL, cdll, pointer, c_void_p, c_size_t, c_char_p, string_at
-from ctypes.util import find_library
+from ctypes import pointer, c_void_p, c_size_t, c_char_p, string_at
 
-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
 LOG = logging.getLogger(__name__)
@@ -30,107 +27,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/GEOS'))
-    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').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@-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):
@@ -411,7 +307,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):
@@ -758,6 +654,7 @@ else:
 
 lgeos = L(_lgeos)
 
+
 def cleanup(proxy):
     del proxy
 
diff --git a/shapely/libgeos.py b/shapely/libgeos.py
new file mode 100644
index 0000000..1783b4f
--- /dev/null
+++ b/shapely/libgeos.py
@@ -0,0 +1,116 @@
+"""
+Proxies for the GEOS shared library, versions, and configure options
+TODO: find a flexible way to load a dynamic library from a specified path
+"""
+
+__all__ = [
+    'lgeos', 'geos_version_string', 'geos_version', 'geos_capi_version'
+]
+import os
+import re
+import sys
+from ctypes import CDLL, cdll, c_void_p, c_char_p
+from ctypes.util import find_library
+
+
+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))
+
+
+# Load dynamic library into a 'lgeos' object, which is system dependant
+
+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'):
+            # .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').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:
+            # TODO: http://web.archive.org/web/20070810024932/
+            #     + 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
+
+# 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@-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()


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index b4cf8ab..7310301 100755
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,5 +1,6 @@
 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
 
 try:
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):