Repository: ioos/compliance-checker

Issue:
CF check_two_dimensional: maximum recursion depth exception
Running the CF checker on [this file](http://thredds.aodn.org.au/thredds/catalog/IMOS/eMII/checker_test/SOOP/ASF/catalog.html?dataset=eMII/checker_test/SOOP/ASF/IMOS_SOOP-ASF_FMT_20150205T000200Z_ZMFR_FV02.nc), it passes all checks, but there's this exception:

```
WARNING: The following exceptions occured during the cf checker (possibly indicate compliance checker issues):
cf.check_two_dimensional: maximum recursion depth exceeded while calling a Python object
```

I think this is due to self-referencing coordinates attributes, e.g. 

```
   LATITUDE:coordinates = "TIME LATITUDE LONGITUDE" ;
```

... which is not correct, but still the checker should handle it nicely. This was already fixed once (#107), but it seems not completely.



Discussion:

Resolving patch:
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -12,6 +12,16 @@
 from netCDF4 import Dimension, Variable
 from sets import Set
 
+def print_exceptions(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except Exception as e:
+            from traceback import print_exc
+            print_exc()
+    return wrapper
+
 __stdname_table__="v29"
 
 def guess_dim_type(dimension):
@@ -1441,7 +1451,6 @@ def check_two_dimensional(self, ds):
             is either a coordinate variable OR comprises coordinate variables
           
         """
-
         ret_val = []        
         reported_reference_variables = []
         
diff --git a/compliance_checker/cf/util.py b/compliance_checker/cf/util.py
--- a/compliance_checker/cf/util.py
+++ b/compliance_checker/cf/util.py
@@ -155,38 +155,64 @@ def fromkeys(cls, seq, value=None):
         return DotDict(dict.fromkeys(seq, value))
 
 class NCGraph:
-    def __init__(self, ds, name, nc_object, self_reference_variables):
+    def __init__(self, ds, name, nc_object, self_reference_variables, reference_map=None):
 
+        self.ds           = ds
         self.name         = name
         self.coords       = DotDict()
         self.dims         = DotDict()
         self.grid_mapping = DotDict()
         self.obj          = nc_object
+
+        self.reference_variables = self_reference_variables
+        self.reference_map = reference_map or {}
+
+        self.reference_map[name] = self
+
         if isinstance(nc_object, Dimension):
             self._type = 'dim'
+
         elif isinstance(nc_object, Variable):
             self._type = 'var'
-            for dim in nc_object.dimensions:
-                self.dims[dim] = NCGraph(ds, dim, ds.dimensions[dim], self_reference_variables)
-            if hasattr(nc_object, 'coordinates'):
-                coords = nc_object.coordinates.split(' ')
-                for coord in coords:
-                    if coord in ds.variables:
-                        if coord == nc_object.name:
-                            self_reference_variables.add(coord)
-                        else:
-                            self.coords[coord] = NCGraph(ds, coord, ds.variables[coord], self_reference_variables)
-                    else:
-                        self.coords[coord] = None
-            if hasattr(nc_object, 'grid_mapping'):
-                gm = nc_object.grid_mapping
-                self.grid_mapping[gm] = None
-                if gm in ds.variables:
-                    self.grid_mapping[gm] = NCGraph(ds, gm, ds.variables[gm])
+
+            self.get_references()
 
         else:
             raise TypeError("unknown type %s" % repr(type(nc_object)))
 
+    def get_references(self):
+        for dim in self.obj.dimensions:
+            self.dims[dim] = self.get_dimension(dim)
+
+        if hasattr(self.obj, 'coordinates'):
+            coords = self.obj.coordinates.split(' ')
+            for coord in coords:
+                self.coords[coord] = self.get_coordinate(coord)
+
+        if hasattr(self.obj, 'grid_mapping'):
+            gm = self.obj.grid_mapping
+            self.grid_mapping[gm] = self.get_grid_mapping(gm)
+
+    def get_dimension(self, dim):
+        if dim in self.reference_map:
+            return self.reference_map[dim]
+        NCGraph(self.ds, dim, self.ds.dimensions[dim], self.reference_variables, self.reference_map)
+
+    def get_coordinate(self, coord):
+        if coord not in self.ds.variables:
+            return
+        if coord in self.reference_map:
+            self.reference_variables.add(self.obj.name)
+            return self.reference_map[coord]
+        return NCGraph(self.ds, coord, self.ds.variables[coord], self.reference_variables, self.reference_map)
+
+    def get_grid_mapping(self, gm):
+        if gm not in ds.variables:
+            return
+        if gm in self.reference_map:
+            return self.reference_map[gm]
+        return NCGraph(ds, gm, ds.variables[gm], self.reference_map)
+
     def __getattr__(self, key):
         if key in self.__dict__:
             return self.__dict__[key]


Test patch:
diff --git a/compliance_checker/tests/data/non-comp/self_referencing.nc b/compliance_checker/tests/data/non-comp/self_referencing.nc
new file mode 100644
Binary files /dev/null and b/compliance_checker/tests/data/non-comp/self_referencing.nc differ
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -38,6 +38,7 @@
         'coordinates_and_metadata'   : resource_filename('compliance_checker', 'tests/data/coordinates_and_metadata.nc'),
         'ints64'                     : resource_filename('compliance_checker', 'tests/data/ints64.nc'),
         'units_check'                : resource_filename('compliance_checker', 'tests/data/units_check.nc'),
+        'self_referencing'           : resource_filename('compliance_checker', 'tests/data/non-comp/self_referencing.nc'),
         }
 
 class MockVariable(object):
@@ -560,6 +561,18 @@ def test_check_calendar(self):
         assert u'Variable bad_time_1 should have a calendar attribute' in messages
         assert u"Variable bad_time_2 should have a valid calendar: 'nope' is not a valid calendar" in messages
 
+    def test_self_referencing(self):
+        '''
+        This test captures a check where a coordinate has circular references
+        '''
+        dataset = self.get_pair(static_files['self_referencing'])
+        results = self.cf.check_two_dimensional(dataset)
+
+        scored, out_of, messages = self.get_results(results)
+        assert u"Variable TEMP_H's coordinate references itself" in messages
+        assert scored == 0
+        assert out_of == 44
+
     def test_check_independent_axis_dimensions(self):
         dataset = self.get_pair(static_files['example-grid'])
         results = self.cf.check_independent_axis_dimensions(dataset)