Repository: networkx/networkx

Issue:
Bug in write_shp when some edges miss attribute(s)

When writing shapefiles with v1.11, `write_shp` uses the last valid values for attributes an edge doesn't have. The reason is that the values of missing `key`s in the dictionary `attributes` are not updated in the edge loop. A work-around is to make sure all attributes are set before using `write_shp` or to remove edges with missing attributes but this requires a separate edge loop. So, it would be nice to have a fix integrated in `write_shp`. Thanks!


Discussion:

Resolving patch:
diff --git a/networkx/readwrite/nx_shp.py b/networkx/readwrite/nx_shp.py
--- a/networkx/readwrite/nx_shp.py
+++ b/networkx/readwrite/nx_shp.py
@@ -23,7 +23,7 @@
 __all__ = ['read_shp', 'write_shp']
 
 
-def read_shp(path, simplify=True, geom_attrs=True):
+def read_shp(path, simplify=True, geom_attrs=True, strict=True):
     """Generates a networkx.DiGraph from shapefiles. Point geometries are
     translated into nodes, lines into edges. Coordinate tuples are used as
     keys. Attributes are preserved, line geometries are simplified into start
@@ -53,11 +53,27 @@ def read_shp(path, simplify=True, geom_attrs=True):
         the edge geometry as well (as they do when they are read via
         this method) and they change, your geomety will be out of sync.
 
+    strict: bool
+        If True, raise NetworkXError when feature geometry is missing or
+        GeometryType is not supported.
+        If False, silently ignore missing or unsupported geometry in features.
 
     Returns
     -------
     G : NetworkX graph
 
+    Raises
+    ------
+    ImportError
+       If ogr module is not available.
+
+    RuntimeError
+       If file cannot be open or read.
+
+    NetworkXError
+       If strict=True and feature is missing geometry or GeometryType is
+       not supported.
+
     Examples
     --------
     >>> G=nx.read_shp('test.shp') # doctest: +SKIP
@@ -76,11 +92,18 @@ def read_shp(path, simplify=True, geom_attrs=True):
 
     net = nx.DiGraph()
     shp = ogr.Open(path)
+    if shp is None:
+        raise RuntimeError("Unable to open {}".format(path))
     for lyr in shp:
         fields = [x.GetName() for x in lyr.schema]
         for f in lyr:
-            flddata = [f.GetField(f.GetFieldIndex(x)) for x in fields]
             g = f.geometry()
+            if g is None:
+                if strict:
+                    raise nx.NetworkXError("Bad data: feature missing geometry")
+                else:
+                    continue
+            flddata = [f.GetField(f.GetFieldIndex(x)) for x in fields]
             attributes = dict(zip(fields, flddata))
             attributes["ShpName"] = lyr.GetName()
             # Note:  Using layer level geometry type
@@ -94,8 +117,9 @@ def read_shp(path, simplify=True, geom_attrs=True):
                     net.add_edge(e1, e2)
                     net[e1][e2].update(attr)
             else:
-                raise ImportError("GeometryType {} not supported".
-                                  format(g.GetGeometryType()))
+                if strict:
+                    raise nx.NetworkXError("GeometryType {} not supported".
+                                           format(g.GetGeometryType()))
 
     return net
 
@@ -256,13 +280,13 @@ def create_feature(geometry, lyr, attributes=None):
 
     # New edge attribute write support merged into edge loop
     fields = {}      # storage for field names and their data types
-    attributes = {}  # storage for attribute data (indexed by field names)
 
     # Conversion dict between python and ogr types
     OGRTypes = {int: ogr.OFTInteger, str: ogr.OFTString, float: ogr.OFTReal}
 
     # Edge loop
     for e in G.edges(data=True):
+        attributes = {}  # storage for attribute data (indexed by field names)
         data = G.get_edge_data(*e)
         g = netgeometry(e, data)
         # Loop through attribute data in edges


Test patch:
diff --git a/networkx/readwrite/tests/test_shp.py b/networkx/readwrite/tests/test_shp.py
--- a/networkx/readwrite/tests/test_shp.py
+++ b/networkx/readwrite/tests/test_shp.py
@@ -5,6 +5,7 @@
 import tempfile
 from nose import SkipTest
 from nose.tools import assert_equal
+from nose.tools import raises
 
 import networkx as nx
 
@@ -208,3 +209,93 @@ def test_wkt_export(self):
 
     def tearDown(self):
         self.deletetmp(self.drv, self.testdir, self.shppath)
+
+
+@raises(RuntimeError)
+def test_read_shp_nofile():
+    try:
+        from osgeo import ogr
+    except ImportError:
+        raise SkipTest('ogr not available.')
+    G = nx.read_shp("hopefully_this_file_will_not_be_available")
+
+
+class TestMissingGeometry(object):
+    @classmethod
+    def setup_class(cls):
+        global ogr
+        try:
+            from osgeo import ogr
+        except ImportError:
+            raise SkipTest('ogr not available.')
+
+    def setUp(self):
+        self.setup_path()
+        self.delete_shapedir()
+        self.create_shapedir()
+
+    def tearDown(self):
+        self.delete_shapedir()
+
+    def setup_path(self):
+        self.path = os.path.join(tempfile.gettempdir(), 'missing_geometry')
+
+    def create_shapedir(self):
+        drv = ogr.GetDriverByName("ESRI Shapefile")
+        shp = drv.CreateDataSource(self.path)
+        lyr = shp.CreateLayer("nodes", None, ogr.wkbPoint)
+        feature = ogr.Feature(lyr.GetLayerDefn())
+        feature.SetGeometry(None)
+        lyr.CreateFeature(feature)
+        feature.Destroy()
+
+    def delete_shapedir(self):
+        drv = ogr.GetDriverByName("ESRI Shapefile")
+        if os.path.exists(self.path):
+            drv.DeleteDataSource(self.path)
+
+    @raises(nx.NetworkXError)
+    def test_missing_geometry(self):
+        G = nx.read_shp(self.path)
+
+
+class TestMissingAttrWrite(object):
+    @classmethod
+    def setup_class(cls):
+        global ogr
+        try:
+            from osgeo import ogr
+        except ImportError:
+            raise SkipTest('ogr not available.')
+
+    def setUp(self):
+        self.setup_path()
+        self.delete_shapedir()
+
+    def tearDown(self):
+        self.delete_shapedir()
+
+    def setup_path(self):
+        self.path = os.path.join(tempfile.gettempdir(), 'missing_attributes')
+
+    def delete_shapedir(self):
+        drv = ogr.GetDriverByName("ESRI Shapefile")
+        if os.path.exists(self.path):
+            drv.DeleteDataSource(self.path)
+
+    def test_missing_attributes(self):
+        G = nx.DiGraph()
+        A = (0, 0)
+        B = (1, 1)
+        C = (2, 2)
+        G.add_edge(A, B, foo=100)
+        G.add_edge(A, C)
+
+        nx.write_shp(G, self.path)
+        H = nx.read_shp(self.path)
+
+        for u, v, d in H.edges(data=True):
+                if u == A and v == B:
+                        assert_equal(d['foo'], 100)
+                if u == A and v == C:
+                        assert_equal(d['foo'], None)