Repository: ioos/compliance-checker
Pull request #920 (2022-04-13 17:18:05)

Add checks/tests for external variables


Patch:
diff --git a/compliance_checker/cf/cf_1_7.py b/compliance_checker/cf/cf_1_7.py
index 10b4e71..28c5a38 100644
--- a/compliance_checker/cf/cf_1_7.py
+++ b/compliance_checker/cf/cf_1_7.py
@@ -75,8 +75,47 @@ class CF1_7Check(CF1_6Check):
         self.grid_mapping_dict = grid_mapping_dict17
         self.grid_mapping_attr_types = grid_mapping_attr_types17
 
+    def check_external_variables(self, ds):
+        """
+        The global external_variables attribute is a blank-separated list of the
+        names of variables which are named by attributes in the file but which
+        are not present in the file. These variables are to be found in other
+        files (called "external files") but CF does not provide conventions for
+        identifying the files concerned. The only attribute for which CF
+        standardises the use of external variables is cell_measures.
+
+        :param netCDF4.Dataset ds: An open netCDF dataset
+        :rtype: compliance_checker.base.Result
+        """
+        external_vars_ctx = TestCtx(
+            BaseCheck.MEDIUM, self.section_titles["2.6.3"]
+        )
+        # IMPLEMENTATION CONFORMANCE 2.6.3 REQUIRED 2/2
+        try:
+            external_var_names = set(ds.external_variables.strip().split())
+
+            bad_external_var_names = (external_var_names.intersection(ds.variables))
+
+            if bad_external_var_names:
+                external_vars_ctx.out_of += 1
+                bad_msg = ("Global attribute external_variables should not "
+                           "have any variable names which are present in the dataset. "
+                           "Currently, the following names appear in both external_variables "
+                           f"and the dataset's variables: {bad_external_var_names}")
+                external_vars_ctx.messages.append(bad_msg)
+
+        # string/global attributes are handled in Appendix A checks
+        except (AttributeError, ValueError):
+            pass
+
+        return external_vars_ctx.to_result()
+
+
+
+
     def check_actual_range(self, ds):
-        """Check the actual_range attribute of variables. As stated in
+        """
+        Check the actual_range attribute of variables. As stated in
         section 2.5.1 of version 1.7, this convention defines a two-element
         vector attribute designed to describe the actual minimum and actual
         maximum values of variables containing numeric data. Conditions:
diff --git a/compliance_checker/cf/cf_base.py b/compliance_checker/cf/cf_base.py
index 2af6376..9dafad2 100644
--- a/compliance_checker/cf/cf_base.py
+++ b/compliance_checker/cf/cf_base.py
@@ -63,6 +63,7 @@ class CFBaseCheck(BaseCheck):
             "2.4": "§2.4 Dimensions",
             "2.5": "§2.5 Variables",
             "2.6": "§2.6 Attributes",
+            "2.6.3": "§2.6.3 External Variables",
             "3.1": "§3.1 Units",
             "3.2": "§3.2 Long Name",
             "3.3": "§3.3 Standard Name",


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index 3d89989..1686ad6 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -1555,6 +1555,26 @@ class TestCF1_7(BaseTestCase):
 
         self.cf = CF1_7Check()
 
+    def test_check_external_variables(self):
+        dataset = MockTimeSeries()
+        # bad type should be ignored here and instead handled by CF Appendix A
+        dataset.external_variables = 1
+        result = self.cf.check_external_variables(dataset)
+        assert result.value[0] == result.value[1] == 0
+        dataset.external_variables = "ext1 ext2 ext3"
+        result = self.cf.check_external_variables(dataset)
+        assert result.value[0] == result.value[1]
+        # TEST CONFORMANCE 2.6.3 REQUIRED 2/2
+        # dataset should not contain any external variables which are present in
+        # the dataset's variables
+        dataset.createVariable("ext3", "i4", ())
+        result = self.cf.check_external_variables(dataset)
+        assert result.value[0] < result.value[1]
+        assert ("Global attribute external_variables should not have any "
+                "variable names which are present in the dataset. Currently, "
+                "the following names appear in both external_variables "
+                "and the dataset's variables: {'ext3'}" in result.msgs)
+
     def test_check_actual_range(self):
         """Test the check_actual_range method works as expected"""
 
@@ -1677,7 +1697,9 @@ class TestCF1_7(BaseTestCase):
         dataset.close()
 
     def test_check_cell_boundaries(self):
-        """Check our over-ridden check_cell_boundaries emthod behaves as expected"""
+        """
+        Check our over-ridden check_cell_boundaries method behaves as expected
+        """
 
         dataset = self.load_dataset(STATIC_FILES["grid-boundaries"])
         results = self.cf.check_cell_boundaries(dataset)