Repository: obspy/obspy

Issue:
Shapefile tests do not work with GDAL 2.0.0
This came up in #1121 and those tests were changed to be skipped when running with GDAL 2.0.0. They should probably be updated to work correctly in all cases.

Only the `.dbf` files have been changed, and I suspect there is some version field that has been updated.



Discussion:
Should eventually revert 414ec8ceb6bae6c9faf172bcadad6f38ee30a928.


Resolving patch:
diff --git a/obspy/io/shapefile/core.py b/obspy/io/shapefile/core.py
--- a/obspy/io/shapefile/core.py
+++ b/obspy/io/shapefile/core.py
@@ -4,29 +4,22 @@
 from future.builtins import *  # NOQA
 from future.utils import native_str
 
+import re
+import warnings
+
 from obspy import Catalog
 from obspy.core.event import Origin, Magnitude
 from obspy.core.inventory import Inventory
-from obspy.core.util import to_int_or_zero
 
 try:
-    import gdal
-    from osgeo import ogr, osr
-    from osgeo.gdal import __version__ as __gdal_version__
+    import shapefile
 except ImportError as e:
-    HAS_GDAL = False
-    GDAL_VERSION_SUFFICIENT = False
+    HAS_PYSHP = False
     IMPORTERROR_MSG = str(e) + (
-        ". ObsPy's write support for shapefiles requires the 'gdal' module "
+        ". ObsPy's write support for shapefiles requires the 'pyshp' module "
         "to be installed in addition to the general ObsPy dependencies.")
-    GDAL_TOO_OLD_MSG = str(e) + (
-        ". ObsPy's write support for shapefiles requires the 'gdal' module "
-        "to be installed in a more recent version.")
 else:
-    HAS_GDAL = True
-    GDAL_VERSION_SUFFICIENT = [1, 7, 3] < list(map(
-        to_int_or_zero, __gdal_version__.split(".")))
-    gdal.UseExceptions()
+    HAS_PYSHP = True
 
 
 def _write_shapefile(obj, filename, **kwargs):
@@ -44,45 +37,35 @@ def _write_shapefile(obj, filename, **kwargs):
         it will be appended. Other files will be created with respective
         suffixes accordingly.
     """
-    if not HAS_GDAL:
+    if not HAS_PYSHP:
         raise ImportError(IMPORTERROR_MSG)
-    if not GDAL_VERSION_SUFFICIENT:
-        raise ImportError(GDAL_TOO_OLD_MSG)
     if not filename.endswith(".shp"):
         filename += ".shp"
 
-    driver = ogr.GetDriverByName(native_str("ESRI Shapefile"))
+    writer = shapefile.Writer(shapefile.POINT)
+    writer.autoBalance = 1
 
-    driver.DeleteDataSource(filename)
-    data_source = driver.CreateDataSource(filename)
+    # create the layer
+    if isinstance(obj, Catalog):
+        _add_catalog_layer(writer, obj)
+    elif isinstance(obj, Inventory):
+        _add_inventory_layer(writer, obj)
+    else:
+        msg = ("Object for shapefile output must be "
+               "a Catalog or Inventory.")
+        raise TypeError(msg)
 
-    try:
-        # create the layer
-        if isinstance(obj, Catalog):
-            _add_catalog_layer(data_source, obj)
-        elif isinstance(obj, Inventory):
-            _add_inventory_layer(data_source, obj)
-        else:
-            msg = ("Object for shapefile output must be "
-                   "a Catalog or Inventory.")
-            raise TypeError(msg)
-    finally:
-        # Destroy the data source to free resources
-        data_source.Destroy()
+    writer.save(filename)
+    _save_projection_file(filename.rsplit('.', 1)[0] + '.prj')
 
 
-def _add_catalog_layer(data_source, catalog):
+def _add_catalog_layer(writer, catalog):
     """
-    :type data_source: :class:`osgeo.ogr.DataSource`.
-    :param data_source: OGR data source the layer is added to.
+    :type writer: :class:`shapefile.Writer`.
+    :param writer: pyshp Writer object
     :type catalog: :class:`~obspy.core.event.Catalog`
     :param catalog: Event data to add as a new layer.
     """
-    if not HAS_GDAL:
-        raise ImportError(IMPORTERROR_MSG)
-    if not GDAL_VERSION_SUFFICIENT:
-        raise ImportError(GDAL_TOO_OLD_MSG)
-
     # [name, type, width, precision]
     # field name is 10 chars max
     # ESRI shapefile attributes are stored in dbf files, which can not
@@ -91,21 +74,24 @@ def _add_catalog_layer(data_source, catalog):
     # use POSIX timestamp for exact origin time, set time of first pick
     # for events with no origin
     field_definitions = [
-        ["EventID", ogr.OFTString, 100, None],
-        ["OriginID", ogr.OFTString, 100, None],
-        ["MagID", ogr.OFTString, 100, None],
-        ["Date", ogr.OFTDate, None, None],
-        ["OriginTime", ogr.OFTReal, 20, 6],
-        ["FirstPick", ogr.OFTReal, 20, 6],
-        ["Longitude", ogr.OFTReal, 16, 10],
-        ["Latitude", ogr.OFTReal, 16, 10],
-        ["Depth", ogr.OFTReal, 8, 3],
-        ["Magnitude", ogr.OFTReal, 8, 3]
+        ["EventID", 'C', 100, None],
+        ["OriginID", 'C', 100, None],
+        ["MagID", 'C', 100, None],
+        ["Date", 'D', None, None],
+        ["OriginTime", 'N', 20, 6],
+        ["FirstPick", 'N', 20, 6],
+        ["Longitude", 'N', 16, 10],
+        ["Latitude", 'N', 16, 10],
+        ["Depth", 'N', 8, 3],
+        ["MinHorUncM", 'N', 12, 3],
+        ["MaxHorUncM", 'N', 12, 3],
+        ["MaxHorAzi", 'N', 7, 3],
+        ["OriUncDesc", 'C', 40, None],
+        ["Magnitude", 'N', 8, 3],
     ]
 
-    layer = _create_layer(data_source, "earthquakes", field_definitions)
+    _create_layer(writer, field_definitions)
 
-    layer_definition = layer.GetLayerDefn()
     for event in catalog:
         # try to use preferred origin/magnitude, fall back to first or use
         # empty one with `None` values in it
@@ -121,67 +107,70 @@ def _add_catalog_layer(data_source, catalog):
         t_pick = pick_times and min(pick_times) or None
         date = t_origin or t_pick
 
-        feature = ogr.Feature(layer_definition)
-
-        try:
-            # setting fields with `None` results in values of `0.000`
-            # need to really omit setting values if they are `None`
-            if event.resource_id is not None:
-                feature.SetField(native_str("EventID"),
-                                 native_str(event.resource_id))
-            if origin.resource_id is not None:
-                feature.SetField(native_str("OriginID"),
-                                 native_str(origin.resource_id))
-            if t_origin is not None:
-                # Use timestamp for exact timing
-                feature.SetField(native_str("OriginTime"), t_origin.timestamp)
-            if t_pick is not None:
-                # Use timestamp for exact timing
-                feature.SetField(native_str("FirstPick"), t_pick.timestamp)
-            if date is not None:
-                # ESRI shapefile attributes are stored in dbf files, which can
-                # not store datetimes, only dates. We still need to use the
-                # GDAL API with precision up to seconds (aiming at other output
-                # drivers of GDAL; `100` stands for GMT)
-                feature.SetField(native_str("Date"), date.year, date.month,
-                                 date.day, date.hour, date.minute, date.second,
-                                 100)
-            if origin.latitude is not None:
-                feature.SetField(native_str("Latitude"), origin.latitude)
-            if origin.longitude is not None:
-                feature.SetField(native_str("Longitude"), origin.longitude)
-            if origin.depth is not None:
-                feature.SetField(native_str("Depth"), origin.depth / 1e3)
-            if magnitude.mag is not None:
-                feature.SetField(native_str("Magnitude"), magnitude.mag)
-            if magnitude.resource_id is not None:
-                feature.SetField(native_str("MagID"),
-                                 native_str(magnitude.resource_id))
-
-            if origin.latitude is not None and origin.longitude is not None:
-                point = ogr.Geometry(ogr.wkbPoint)
-                point.AddPoint(origin.longitude, origin.latitude)
-                feature.SetGeometry(point)
-
-            layer.CreateFeature(feature)
-
-        finally:
-            # Destroy the feature to free resources
-            feature.Destroy()
-
-
-def _add_inventory_layer(data_source, inventory):
+        feature = {}
+
+        # setting fields with `None` results in values of `0.000`
+        # need to really omit setting values if they are `None`
+        if event.resource_id is not None:
+            feature["EventID"] = str(event.resource_id)
+        if origin.resource_id is not None:
+            feature["OriginID"] = str(origin.resource_id)
+        if t_origin is not None:
+            # Use timestamp for exact timing
+            feature["OriginTime"] = t_origin.timestamp
+        if t_pick is not None:
+            # Use timestamp for exact timing
+            feature["FirstPick"] = t_pick.timestamp
+        if date is not None:
+            # ESRI shapefile attributes are stored in dbf files, which can
+            # not store datetimes, only dates. We still need to use the
+            # GDAL API with precision up to seconds (aiming at other output
+            # drivers of GDAL; `100` stands for GMT)
+            feature["Date"] = date.datetime
+        if origin.latitude is not None:
+            feature["Latitude"] = origin.latitude
+        if origin.longitude is not None:
+            feature["Longitude"] = origin.longitude
+        if origin.depth is not None:
+            feature["Depth"] = origin.depth / 1e3
+        if magnitude.mag is not None:
+            feature["Magnitude"] = magnitude.mag
+        if magnitude.resource_id is not None:
+            feature["MagID"] = str(magnitude.resource_id)
+        if origin.origin_uncertainty is not None:
+            ou = origin.origin_uncertainty
+            ou_description = ou.preferred_description
+            if ou_description == 'uncertainty ellipse':
+                feature["MinHorUncM"] = ou.min_horizontal_uncertainty
+                feature["MaxHorUncM"] = ou.max_horizontal_uncertainty
+                feature["MaxHorAzi"] = \
+                    ou.azimuth_max_horizontal_uncertainty
+                feature["OriUncDesc"] = ou_description
+            elif ou_description == 'horizontal uncertainty':
+                feature["MinHorUncM"] = ou.horizontal_uncertainty
+                feature["MaxHorUncM"] = ou.horizontal_uncertainty
+                feature["MaxHorAzi"] = 0.0
+                feature["OriUncDesc"] = ou_description
+            else:
+                msg = ('Encountered an event with origin uncertainty '
+                       'description of type "{}". This is not yet '
+                       'implemented for output as shapefile. No origin '
+                       'uncertainty will be added to shapefile for such '
+                       'events.').format(ou_description)
+                warnings.warn(msg)
+
+        if origin.latitude is not None and origin.longitude is not None:
+            writer.point(origin.longitude, origin.latitude)
+            _add_record(writer, feature)
+
+
+def _add_inventory_layer(writer, inventory):
     """
-    :type data_source: :class:`osgeo.ogr.DataSource`.
-    :param data_source: OGR data source the layer is added to.
+    :type writer: :class:`shapefile.Writer`.
+    :param writer: pyshp Writer object
     :type inventory: :class:`~obspy.core.inventory.Inventory`
     :param inventory: Inventory data to add as a new layer.
     """
-    if not HAS_GDAL:
-        raise ImportError(IMPORTERROR_MSG)
-    if not GDAL_VERSION_SUFFICIENT:
-        raise ImportError(GDAL_TOO_OLD_MSG)
-
     # [name, type, width, precision]
     # field name is 10 chars max
     # ESRI shapefile attributes are stored in dbf files, which can not
@@ -190,113 +179,100 @@ def _add_inventory_layer(data_source, inventory):
     # use POSIX timestamp for exact origin time, set time of first pick
     # for events with no origin
     field_definitions = [
-        ["Network", ogr.OFTString, 20, None],
-        ["Station", ogr.OFTString, 20, None],
-        ["Longitude", ogr.OFTReal, 16, 10],
-        ["Latitude", ogr.OFTReal, 16, 10],
-        ["Elevation", ogr.OFTReal, 9, 3],
-        ["StartDate", ogr.OFTDate, None, None],
-        ["EndDate", ogr.OFTDate, None, None],
-        ["Channels", ogr.OFTString, 254, None]
+        ["Network", 'C', 20, None],
+        ["Station", 'C', 20, None],
+        ["Longitude", 'N', 16, 10],
+        ["Latitude", 'N', 16, 10],
+        ["Elevation", 'N', 9, 3],
+        ["StartDate", 'D', None, None],
+        ["EndDate", 'D', None, None],
+        ["Channels", 'C', 254, None],
     ]
 
-    layer = _create_layer(data_source, "stations", field_definitions)
+    _create_layer(writer, field_definitions)
 
-    layer_definition = layer.GetLayerDefn()
     for net in inventory:
         for sta in net:
             channel_list = ",".join(["%s.%s" % (cha.location_code, cha.code)
                                      for cha in sta])
 
-            feature = ogr.Feature(layer_definition)
-
-            try:
-                # setting fields with `None` results in values of `0.000`
-                # need to really omit setting values if they are `None`
-                if net.code is not None:
-                    feature.SetField(native_str("Network"),
-                                     native_str(net.code))
-                if sta.code is not None:
-                    feature.SetField(native_str("Station"),
-                                     native_str(sta.code))
-                if sta.latitude is not None:
-                    feature.SetField(native_str("Latitude"), sta.latitude)
-                if sta.longitude is not None:
-                    feature.SetField(native_str("Longitude"), sta.longitude)
-                if sta.elevation is not None:
-                    feature.SetField(native_str("Elevation"), sta.elevation)
-                if sta.start_date is not None:
-                    date = sta.start_date
-                    # ESRI shapefile attributes are stored in dbf files, which
-                    # can not store datetimes, only dates. We still need to use
-                    # the GDAL API with precision up to seconds (aiming at
-                    # other output drivers of GDAL; `100` stands for GMT)
-                    feature.SetField(native_str("StartDate"), date.year,
-                                     date.month, date.day, date.hour,
-                                     date.minute, date.second, 100)
-                if sta.end_date is not None:
-                    date = sta.end_date
-                    # ESRI shapefile attributes are stored in dbf files, which
-                    # can not store datetimes, only dates. We still need to use
-                    # the GDAL API with precision up to seconds (aiming at
-                    # other output drivers of GDAL; `100` stands for GMT)
-                    feature.SetField(native_str("StartDate"), date.year,
-                                     date.month, date.day, date.hour,
-                                     date.minute, date.second, 100)
-                if channel_list:
-                    feature.SetField(native_str("Channels"),
-                                     native_str(channel_list))
-
-                if sta.latitude is not None and sta.longitude is not None:
-                    point = ogr.Geometry(ogr.wkbPoint)
-                    point.AddPoint(sta.longitude, sta.latitude)
-                    feature.SetGeometry(point)
-
-                layer.CreateFeature(feature)
-
-            finally:
-                # Destroy the feature to free resources
-                feature.Destroy()
-
-
-def _get_wgs84_spatial_reference():
-    # create the spatial reference
-    sr = osr.SpatialReference()
-    # Simpler and feels cleaner to initialize by EPSG code but that depends on
-    # a csv file shipping with GDAL and some GDAL environment paths being set
-    # correctly which was not the case out of the box in anaconda, so better
-    # hardcode this bit.
-    # sr.ImportFromEPSG(4326)
-    wgs84_wkt = \
-        """
-        GEOGCS["WGS 84",
-            DATUM["WGS_1984",
-                SPHEROID["WGS 84",6378137,298.257223563,
-                    AUTHORITY["EPSG","7030"]],
-                AUTHORITY["EPSG","6326"]],
-            PRIMEM["Greenwich",0,
-                AUTHORITY["EPSG","8901"]],
-            UNIT["degree",0.0174532925199433,
-                AUTHORITY["EPSG","9122"]],
-            AUTHORITY["EPSG","4326"]]
-        """
-    sr.ImportFromWkt(wgs84_wkt)
-    return sr
-
-
-def _create_layer(data_source, layer_name, field_definitions):
-    sr = _get_wgs84_spatial_reference()
-    layer = data_source.CreateLayer(native_str(layer_name), sr,
-                                    ogr.wkbPoint)
+            feature = {}
+
+            # setting fields with `None` results in values of `0.000`
+            # need to really omit setting values if they are `None`
+            if net.code is not None:
+                feature["Network"] = net.code
+            if sta.code is not None:
+                feature["Station"] = sta.code
+            if sta.latitude is not None:
+                feature["Latitude"] = sta.latitude
+            if sta.longitude is not None:
+                feature["Longitude"] = sta.longitude
+            if sta.elevation is not None:
+                feature["Elevation"] = sta.elevation
+            if sta.start_date is not None:
+                # ESRI shapefile attributes are stored in dbf files, which
+                # can not store datetimes, only dates. We still need to use
+                # the GDAL API with precision up to seconds (aiming at
+                # other output drivers of GDAL; `100` stands for GMT)
+                feature["StartDate"] = sta.start_date.datetime
+            if sta.end_date is not None:
+                # ESRI shapefile attributes are stored in dbf files, which
+                # can not store datetimes, only dates. We still need to use
+                # the GDAL API with precision up to seconds (aiming at
+                # other output drivers of GDAL; `100` stands for GMT)
+                feature["EndDate"] = sta.end_date.datetime
+            if channel_list:
+                feature["Channels"] = channel_list
+
+            if sta.latitude is not None and sta.longitude is not None:
+                writer.point(sta.longitude, sta.latitude)
+                _add_record(writer, feature)
+
+
+wgs84_wkt = \
+    """
+    GEOGCS["WGS 84",
+        DATUM["WGS_1984",
+            SPHEROID["WGS 84",6378137,298.257223563,
+                AUTHORITY["EPSG","7030"]],
+            AUTHORITY["EPSG","6326"]],
+        PRIMEM["Greenwich",0,
+            AUTHORITY["EPSG","8901"]],
+        UNIT["degree",0.0174532925199433,
+            AUTHORITY["EPSG","9122"]],
+        AUTHORITY["EPSG","4326"]]
+    """
+wgs84_wkt = re.sub(r'\s+', '', wgs84_wkt)
+
+
+def _save_projection_file(filename):
+    with open(filename, 'wt') as fh:
+        fh.write(wgs84_wkt)
+
+
+def _create_layer(writer, field_definitions):
     # Add the fields we're interested in
     for name, type_, width, precision in field_definitions:
-        field = ogr.FieldDefn(native_str(name), type_)
-        if width is not None:
-            field.SetWidth(width)
-        if precision is not None:
-            field.SetPrecision(precision)
-        layer.CreateField(field)
-    return layer
+        type_ = native_str(type_)
+        name = native_str(name)
+        kwargs = dict(fieldType=type_, size=width, decimal=precision)
+        # remove None's because shapefile.Writer.field() doesn't use None as
+        # placeholder but the default values directly
+        for key in list(kwargs.keys()):
+            if kwargs[key] is None:
+                kwargs.pop(key)
+        writer.field(name, **kwargs)
+
+
+def _add_record(writer, feature):
+    values = []
+    for key, type_, width, precision in writer.fields:
+        value = feature.get(key)
+        if type_ == 'C' and value is not None:
+            value = native_str(value)
+        values.append(value)
+    writer.record(*values)
 
 
 if __name__ == '__main__':
diff --git a/obspy/scripts/runtests.py b/obspy/scripts/runtests.py
--- a/obspy/scripts/runtests.py
+++ b/obspy/scripts/runtests.py
@@ -115,7 +115,7 @@
     "future", "numpy", "scipy", "matplotlib", "lxml.etree", "setuptools",
     "sqlalchemy", "decorator", "requests"]
 OPTIONAL_DEPENDENCIES = [
-    "flake8", "pyimgur", "pyproj", "pep8-naming", "m2crypto", "osgeo.gdal",
+    "flake8", "pyimgur", "pyproj", "pep8-naming", "m2crypto", "shapefile",
     "mpl_toolkits.basemap", "mock", "pyflakes", "geographiclib", "cartopy"]
 DEPENDENCIES = HARD_DEPENDENCIES + OPTIONAL_DEPENDENCIES
 
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -115,7 +115,7 @@
     'tests': ['flake8>=2', 'pyimgur', 'pyproj', 'pep8-naming'],
     # arclink decryption also works with: pycrypto, cryptography, pycryptodome
     'arclink': ['m2crypto'],
-    'io.shapefile': ['gdal'],
+    'io.shapefile': ['pyshp'],
     }
 # PY2
 if sys.version_info[0] == 2:


Test patch:
diff --git a/obspy/io/shapefile/tests/test_core.py b/obspy/io/shapefile/tests/test_core.py
--- a/obspy/io/shapefile/tests/test_core.py
+++ b/obspy/io/shapefile/tests/test_core.py
@@ -3,29 +3,102 @@
                         unicode_literals)
 from future.builtins import *  # NOQA
 
-import filecmp
+import datetime
 import os
 import unittest
+import warnings
+
+import shapefile
 
 from obspy import read_events, read_inventory
 from obspy.core.util.misc import TemporaryWorkingDirectory
-from obspy.io.shapefile.core import (
-    _write_shapefile, GDAL_VERSION_SUFFICIENT)
-
-try:
-    from osgeo import gdal, ogr, osr  # NOQA
-except ImportError:
-    HAS_GDAL = False
-else:
-    HAS_GDAL = True
-    no_filecmp = gdal.VersionInfo() >= '2000000'
+from obspy.io.shapefile.core import _write_shapefile, HAS_PYSHP
 
 
 SHAPEFILE_SUFFIXES = (".shp", ".shx", ".dbf", ".prj")
+expected_catalog_fields = [
+    ('DeletionFlag', 'C', 1, 0),
+    ['EventID', 'C', 100, 0],
+    ['OriginID', 'C', 100, 0],
+    ['MagID', 'C', 100, 0],
+    ['Date', 'D', 8, 0],
+    ['OriginTime', 'N', 20, 6],
+    ['FirstPick', 'N', 20, 6],
+    ['Longitude', 'N', 16, 10],
+    ['Latitude', 'N', 16, 10],
+    ['Depth', 'N', 8, 3],
+    ['MinHorUncM', 'N', 12, 3],
+    ['MaxHorUncM', 'N', 12, 3],
+    ['MaxHorAzi', 'N', 7, 3],
+    ['OriUncDesc', 'C', 40, 0],
+    ['Magnitude', 'N', 8, 3]]
+expected_catalog_records = [
+    ['quakeml:us.anss.org/event/20120101052755.98',
+     'quakeml:us.anss.org/origin/20120101052755.98',
+     'quakeml:us.anss.org/magnitude/20120101052755.98/mb',
+     datetime.date(2012, 1, 1),
+     1325395675.98,
+     1325395728.18,
+     138.072,
+     31.456,
+     365.3,
+     None,
+     None,
+     None,
+     'None',
+     6.2],
+    ['smi:local/0eee2e6f-064b-458a-934f-c5d3105e9529',
+     'smi:local/a2260002-95c6-42f7-8c44-f46124355228',
+     'None',
+     datetime.date(2006, 7, 15),
+     1152984080.19567,
+     1152984080.63,
+     7.736781,
+     51.657659,
+     1.434,
+     1136.0,
+     1727.42,
+     69.859,
+     'uncertainty ellipse',
+     None]]
+expected_inventory_fields = [
+    ('DeletionFlag', 'C', 1, 0),
+    ['Network', 'C', 20, 0],
+    ['Station', 'C', 20, 0],
+    ['Longitude', 'N', 16, 10],
+    ['Latitude', 'N', 16, 10],
+    ['Elevation', 'N', 9, 3],
+    ['StartDate', 'D', 8, 0],
+    ['EndDate', 'D', 8, 0],
+    ['Channels', 'C', 254, 0]]
+expected_inventory_records = [
+    ['GR', 'FUR', 11.2752, 48.162899, 565.0, datetime.date(2006, 12, 16),
+     None, '.HHZ,.HHN,.HHE,.BHZ,.BHN,.BHE,.LHZ,.LHN,.LHE,.VHZ,.VHN,.VHE'],
+    ['GR', 'WET', 12.8782, 49.144001, 613.0, datetime.date(2007, 2, 2), None,
+     '.HHZ,.HHN,.HHE,.BHZ,.BHN,.BHE,.LHZ,.LHN,.LHE'],
+    ['BW', 'RJOB', 12.795714, 47.737167, 860.0, datetime.date(2001, 5, 15),
+     datetime.date(2006, 12, 12), '.EHZ,.EHN,.EHE'],
+    ['BW', 'RJOB', 12.795714, 47.737167, 860.0, datetime.date(2006, 12, 13),
+     datetime.date(2007, 12, 17), '.EHZ,.EHN,.EHE'],
+    ['BW', 'RJOB', 12.795714, 47.737167, 860.0, datetime.date(2007, 12, 17),
+     None, '.EHZ,.EHN,.EHE']]
+
+
+def _close_shapefile_reader(reader):
+    """
+    Current pyshp version 1.2.12 doesn't properly close files, so for now do
+    this manually during tests. (see GeospatialPython/pyshp#107)
+    """
+    for key in ('dbf', 'shx', 'shp'):
+        attribute = getattr(reader, key, None)
+        if attribute is not None:
+            try:
+                attribute.close()
+            except (AttributeError, IOError):
+                pass
 
 
-@unittest.skipIf(not HAS_GDAL or not GDAL_VERSION_SUFFICIENT,
-                 'gdal not installed or version is too old')
+@unittest.skipIf(not HAS_PYSHP, 'pyshp not installed')
 class ShapefileTestCase(unittest.TestCase):
     def setUp(self):
         self.path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
@@ -34,28 +107,70 @@ def setUp(self):
         self.inventory_shape_basename = os.path.join(self.path, 'inventory')
 
     def test_write_catalog_shapefile(self):
-        cat = read_events()
+        # read two events with uncertainties, one deserializes with "confidence
+        # ellipsoid" origin uncertainty which is not yet implemented for
+        # shapefile output and should show a warning
+        cat = read_events('/path/to/mchedr.dat')
+        cat += read_events('/path/to/nlloc.qml')
         with TemporaryWorkingDirectory():
-            _write_shapefile(cat, "catalog.shp")
+            with warnings.catch_warnings(record=True) as w:
+                warnings.filterwarnings('always')
+                _write_shapefile(cat, "catalog.shp")
+            for w_ in w:
+                try:
+                    self.assertEqual(
+                        str(w_.message),
+                        'Encountered an event with origin uncertainty '
+                        'description of type "confidence ellipsoid". This is '
+                        'not yet implemented for output as shapefile. No '
+                        'origin uncertainty will be added to shapefile for '
+                        'such events.')
+                except:
+                    continue
+                break
+            else:
+                raise
             for suffix in SHAPEFILE_SUFFIXES:
                 self.assertTrue(os.path.isfile("catalog" + suffix))
-                self.assertTrue(
-                    no_filecmp or filecmp.cmp(
-                        "catalog" + suffix,
-                        self.catalog_shape_basename + suffix),
-                    msg="%s not binary equal." % ("catalog" + suffix))
+            shp = shapefile.Reader("catalog.shp")
+            # check contents of shapefile that we just wrote
+            self.assertEqual(shp.fields, expected_catalog_fields)
+            self.assertEqual(shp.records(), expected_catalog_records)
+            self.assertEqual(shp.shapeType, shapefile.POINT)
+            _close_shapefile_reader(shp)
 
     def test_write_catalog_shapefile_via_plugin(self):
-        cat = read_events()
+        # read two events with uncertainties, one deserializes with "confidence
+        # ellipsoid" origin uncertainty which is not yet implemented for
+        # shapefile output and should show a warning
+        cat = read_events('/path/to/mchedr.dat')
+        cat += read_events('/path/to/nlloc.qml')
         with TemporaryWorkingDirectory():
-            cat.write("catalog.shp", "SHAPEFILE")
+            with warnings.catch_warnings(record=True) as w:
+                warnings.filterwarnings('always')
+                cat.write("catalog.shp", "SHAPEFILE")
+            for w_ in w:
+                try:
+                    self.assertEqual(
+                        str(w_.message),
+                        'Encountered an event with origin uncertainty '
+                        'description of type "confidence ellipsoid". This is '
+                        'not yet implemented for output as shapefile. No '
+                        'origin uncertainty will be added to shapefile for '
+                        'such events.')
+                except:
+                    continue
+                break
+            else:
+                raise
             for suffix in SHAPEFILE_SUFFIXES:
                 self.assertTrue(os.path.isfile("catalog" + suffix))
-                self.assertTrue(
-                    no_filecmp or filecmp.cmp(
-                        "catalog" + suffix,
-                        self.catalog_shape_basename + suffix),
-                    msg="%s not binary equal." % ("catalog" + suffix))
+            shp = shapefile.Reader("catalog.shp")
+            # check contents of shapefile that we just wrote
+            self.assertEqual(shp.fields, expected_catalog_fields)
+            self.assertEqual(shp.records(), expected_catalog_records)
+            self.assertEqual(shp.shapeType, shapefile.POINT)
+            _close_shapefile_reader(shp)
 
     def test_write_inventory_shapefile(self):
         inv = read_inventory()
@@ -63,11 +178,12 @@ def test_write_inventory_shapefile(self):
             _write_shapefile(inv, "inventory.shp")
             for suffix in SHAPEFILE_SUFFIXES:
                 self.assertTrue(os.path.isfile("inventory" + suffix))
-                self.assertTrue(
-                    no_filecmp or filecmp.cmp(
-                        "inventory" + suffix,
-                        self.inventory_shape_basename + suffix),
-                    msg="%s not binary equal." % ("inventory" + suffix))
+            shp = shapefile.Reader("inventory.shp")
+            # check contents of shapefile that we just wrote
+            self.assertEqual(shp.fields, expected_inventory_fields)
+            self.assertEqual(shp.records(), expected_inventory_records)
+            self.assertEqual(shp.shapeType, shapefile.POINT)
+            _close_shapefile_reader(shp)
 
     def test_write_inventory_shapefile_via_plugin(self):
         inv = read_inventory()
@@ -75,11 +191,12 @@ def test_write_inventory_shapefile_via_plugin(self):
             inv.write("inventory.shp", "SHAPEFILE")
             for suffix in SHAPEFILE_SUFFIXES:
                 self.assertTrue(os.path.isfile("inventory" + suffix))
-                self.assertTrue(
-                    no_filecmp or filecmp.cmp(
-                        "inventory" + suffix,
-                        self.inventory_shape_basename + suffix),
-                    msg="%s not binary equal." % ("inventory" + suffix))
+            shp = shapefile.Reader("inventory.shp")
+            # check contents of shapefile that we just wrote
+            self.assertEqual(shp.fields, expected_inventory_fields)
+            self.assertEqual(shp.records(), expected_inventory_records)
+            self.assertEqual(shp.shapeType, shapefile.POINT)
+            _close_shapefile_reader(shp)
 
 
 def suite():