Repository: ioos/compliance-checker
Pull request #999 (2023-03-28 18:36:13)

Merge in development branch for CF 1.9


Patch:
diff --git a/.gitignore b/.gitignore
index bffc4bd..8302bda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,7 +89,6 @@ ENV/
 # IDE project settings
 .spyderproject
 .vscode
-pyrightconfig.json
 
 # Rope project settings
 .ropeproject
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index 41b8d8a..7fb34fd 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -30,3 +30,4 @@ from compliance_checker.cf.appendix_f import (  # noqa: F401
 from compliance_checker.cf.cf_1_6 import CF1_6Check  # noqa: F401
 from compliance_checker.cf.cf_1_7 import CF1_7Check  # noqa: F401
 from compliance_checker.cf.cf_1_8 import CF1_8Check  # noqa: F401
+from compliance_checker.cf.cf_1_9 import CF1_9Check  # noqa: F401
diff --git a/compliance_checker/cf/cf_1_6.py b/compliance_checker/cf/cf_1_6.py
index 3504913..dfdfa27 100644
--- a/compliance_checker/cf/cf_1_6.py
+++ b/compliance_checker/cf/cf_1_6.py
@@ -2776,7 +2776,7 @@ class CF1_6Check(CFNCCheck):
                         dic_to_be_verified = {
                             (var.cell_measures).split(":")[0]: cell_meas_var.units
                         }
-                        
+
                         if not set(dic_to_be_verified).issubset(dic_expected):
                             valid = False
                             reasoning.append(
@@ -2784,16 +2784,18 @@ class CF1_6Check(CFNCCheck):
                                 "units that are consistent with the measure type."
                                 "i.e. {}.".format(cell_meas_var_name, dic_expected)
                             )
-                       
+
                         # verify units are recognized by UDUNITS
-                        valid_udunits = self._check_valid_udunits(ds, cell_meas_var_name)
+                        valid_udunits = self._check_valid_udunits(
+                            ds, cell_meas_var_name
+                        )
                         if valid_udunits.value[0] != valid_udunits.value[1]:
                             valid = False
                             reasoning.append(
-                                 "Cell measure variable {} referred to by "
-                                 "{} has a unit {} not recognized by UDUNITS".format(
-                                 cell_meas_var_name, var.name, cell_meas_var.units 
-                                 )
+                                "Cell measure variable {} referred to by "
+                                "{} has a unit {} not recognized by UDUNITS".format(
+                                    cell_meas_var_name, var.name, cell_meas_var.units
+                                )
                             )
                     if not set(cell_meas_var.dimensions).issubset(var.dimensions):
                         valid = False
diff --git a/compliance_checker/cf/cf_1_9.py b/compliance_checker/cf/cf_1_9.py
new file mode 100644
index 0000000..0e6d9cd
--- /dev/null
+++ b/compliance_checker/cf/cf_1_9.py
@@ -0,0 +1,73 @@
+from netCDF4 import Dataset
+
+from compliance_checker.base import BaseCheck, TestCtx
+from compliance_checker.cf.cf_1_8 import CF1_8Check
+from compliance_checker.cf.util import VariableReferenceError, reference_attr_variables
+
+
+class CF1_9Check(CF1_8Check):
+    _cc_spec_version = "1.9"
+    _cc_url = "http://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html"
+
+    def __init__(self, options=None):
+        super(CF1_9Check, self).__init__(options)
+        self.section_titles.update({"5.8": "§5.8 Domain Variables"})
+
+    def check_domain_variables(self, ds: Dataset):
+        # Domain variables should have coordinates attribute, but should have
+        # scalar dimensions
+        results = []
+        for domain_var in (
+            var
+            for var in ds.get_variables_by_attributes(
+                coordinates=lambda c: c is not None
+            )
+            if not var.dimensions
+        ):
+            domain_valid = TestCtx(BaseCheck.MEDIUM, self.section_titles["5.8"])
+            domain_valid.out_of += 1
+            domain_coord_vars = reference_attr_variables(
+                ds, domain_var.coordinates, " "
+            )
+            errors = [
+                maybe_error.name
+                for maybe_error in domain_coord_vars
+                if isinstance(maybe_error, VariableReferenceError)
+            ]
+            if errors:
+                errors_str = ", ".join(errors)
+                domain_valid.messages.append(
+                    "Could not find the following "
+                    "variables referenced in "
+                    "coordinates attribute from "
+                    "domain variable "
+                    f"{domain_var.name}: {errors_str}"
+                )
+
+            else:
+                long_name = getattr(domain_var, "long_name", None)
+                if long_name is None or not isinstance(long_name, str):
+                    domain_valid.messages.append(
+                        f"For domain variable {domain_var.name} "
+                        f"it is recommended that attribute long_name be present and a string"
+                    )
+                    results.append(domain_valid.to_result())
+                    continue
+                appendix_a_not_recommended_attrs = []
+                for attr_name in domain_var.ncattrs():
+                    if "D" not in self.appendix_a[attr_name]["attr_loc"]:
+                        appendix_a_not_recommended_attrs.append(attr_name)
+
+                if appendix_a_not_recommended_attrs:
+                    domain_valid.messages.append(
+                        f"The following attributes appear in variable {domain_var.name} "
+                        "and CF Appendix A, but are not for use in domain variables: "
+                        f"{appendix_a_not_recommended_attrs}"
+                    )
+
+                # no errors occurred
+                domain_valid.score += 1
+
+            results.append(domain_valid.to_result())
+
+        return results
diff --git a/compliance_checker/cf/util.py b/compliance_checker/cf/util.py
index 713280d..5cf9ea0 100644
--- a/compliance_checker/cf/util.py
+++ b/compliance_checker/cf/util.py
@@ -5,7 +5,6 @@ import sys
 from collections import defaultdict
 from copy import deepcopy
 from pkgutil import get_data
-from urllib.parse import urljoin
 
 import requests
 from cf_units import Unit


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index dd293cd..5d6f8fd 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -20,6 +20,7 @@ from compliance_checker.cf.cf import (
     CF1_6Check,
     CF1_7Check,
     CF1_8Check,
+    CF1_9Check,
     dimless_vertical_coordinates_1_6,
     dimless_vertical_coordinates_1_7,
 )
@@ -212,7 +213,8 @@ class TestCF1_6(BaseTestCase):
         result = self.cf.check_child_attr_data_types(ds)
         self.assert_result_is_good(result)
 
-        # now give invalid integer for valid_min; above two should still check out, this one should fail
+        # now give invalid integer for valid_min; above two should still check
+        # out, this one should fail
         ds.variables["temp"].setncattr("valid_min", 45)
         result = self.cf.check_child_attr_data_types(ds)
         self.assert_result_is_bad(result)
@@ -2989,6 +2991,52 @@ class TestCF1_8(BaseTestCase):
         assert results[0].value[0] == results[0].value[1]
 
 
+class TestCF1_9(BaseTestCase):
+    def setUp(self):
+        self.cf = CF1_9Check()
+
+    def test_domain(self):
+        dataset = MockTimeSeries()
+        domain_var = dataset.createVariable("domain", "c", ())
+        domain_var.long_name = "Domain variable"
+        domain_var.coordinates = "lon lat depth"
+        results = self.cf.check_domain_variables(dataset)
+        self.assertEqual(results[0].value[0], results[0].value[1])
+        self.assertFalse(results[0].msgs)
+
+        # missing long_name attribute
+        del domain_var.long_name
+        results = self.cf.check_domain_variables(dataset)
+        self.assertNotEqual(results[0].value[0], results[0].value[1])
+        self.assertTrue(results[0].msgs)
+        self.assertTrue(
+            results[0].msgs[0]
+            == "For domain variable domain it is recommended that attribute long_name be present and a string"
+        )
+
+        # bad coordinates variable
+        domain_var.coordinates = "lon lat depth xyxz abc"
+        domain_var.long_name = "Domain variable"
+        results = self.cf.check_domain_variables(dataset)
+        self.assertNotEqual(results[0].value[0], results[0].value[1])
+        self.assertTrue(
+            results[0].msgs[0]
+            == "Could not find the following variables referenced in "
+            "coordinates attribute from domain variable domain: "
+            "xyxz, abc"
+        )
+
+        del dataset
+        dataset = MockTimeSeries()
+        # domain should be dimensionless -- currently not an error in
+        # compliance checker, but not detected as a domain variable either
+        domain_var = dataset.createVariable("domain", "c", ("time",))
+        domain_var.long_name = "Domain variable"
+        domain_var.coordinates = "lon lat depth"
+        results = self.cf.check_domain_variables(dataset)
+        assert len(results) == 0
+
+
 class TestCFUtil(BaseTestCase):
     """
     Class to test the cfutil module.