Repository: ioos/compliance-checker
Pull request #1021 (2023-05-15 23:28:42)

Formula terms revamp
Implements remaining points of #946 


Patch:
diff --git a/compliance_checker/cf/cf_1_7.py b/compliance_checker/cf/cf_1_7.py
index d527d16..43e0d16 100644
--- a/compliance_checker/cf/cf_1_7.py
+++ b/compliance_checker/cf/cf_1_7.py
@@ -902,13 +902,19 @@ class CF1_7Check(CF1_6Check):
         standard_name = getattr(variable, "standard_name", None)
         formula_terms = getattr(variable, "formula_terms", None)
         # Skip the variable if it's dimensional
+        correct_computed_std_name_ctx = TestCtx(
+            BaseCheck.MEDIUM, self.section_titles["4.3"]
+        )
+        # IMPLEMENTATION CONFORMANCE 4.3.3 REQUIRED
+        correct_computed_std_name_ctx.assert_true(
+            not (formula_terms is None and hasattr(variable, "computed_standard_name")),
+            f"Variable {vname} should have formula_terms attribute when "
+            "computed_standard_name attribute is defined",
+        )
         if formula_terms is None and standard_name not in dim_vert_coords_dict:
             return
 
         # assert that the computed_standard_name is maps to the standard_name correctly
-        correct_computed_std_name_ctx = TestCtx(
-            BaseCheck.MEDIUM, self.section_titles["4.3"]
-        )
         _comp_std_name = dim_vert_coords_dict[standard_name][1]
         correct_computed_std_name_ctx.assert_true(
             getattr(variable, "computed_standard_name", None) in _comp_std_name,
diff --git a/compliance_checker/cf/cf_base.py b/compliance_checker/cf/cf_base.py
index a747c91..c33d12f 100644
--- a/compliance_checker/cf/cf_base.py
+++ b/compliance_checker/cf/cf_base.py
@@ -389,13 +389,21 @@ class CFBaseCheck(BaseCheck):
         # The pattern for formula terms is always component: variable_name
         # the regex grouping always has component names in even positions and
         # the corresponding variable name in odd positions.
-        matches = regex.findall(
-            r"([A-Za-z][A-Za-z0-9_]*: )([A-Za-z][A-Za-z0-9_]*)", variable.formula_terms
-        )
-        terms = set(m[0][:-2] for m in matches)
+        poorly_formed_formula_terms = ("Attribute formula_terms is not well-formed",)
+        matches = [
+            match
+            for match in regex.finditer(
+                r"(\w+):\s+(\w+)(?:\s+(?!$)|$)", variable.formula_terms
+            )
+        ]
+        if not matches:
+            valid_formula_terms.add_failure(poorly_formed_formula_terms)
+            return valid_formula_terms.to_result()
+
+        terms = set(m.group(1) for m in matches)
         # get the variables named in the formula terms and check if any
         # are not present in the dataset
-        missing_vars = sorted(set(m[1] for m in matches) - set(ds.variables))
+        missing_vars = sorted(set(m.group(2) for m in matches) - set(ds.variables))
         missing_fmt = "The following variable(s) referenced in {}:formula_terms are not present in the dataset: {}"
         valid_formula_terms.assert_true(
             len(missing_vars) == 0, missing_fmt.format(coord, ", ".join(missing_vars))
@@ -403,7 +411,7 @@ class CFBaseCheck(BaseCheck):
         # try to reconstruct formula_terms by adding space in between the regex
         # matches.  If it doesn't exactly match the original, the formatting
         # of the attribute is incorrect
-        reconstructed_formula = " ".join(m[0] + m[1] for m in matches)
+        reconstructed_formula = "".join(m.group(0) for m in matches)
         valid_formula_terms.assert_true(
             reconstructed_formula == formula_terms,
             "Attribute formula_terms is not well-formed",


Test patch:
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index f762d8f..f0ba61e 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -1231,6 +1231,11 @@ class TestCF1_6(BaseTestCase):
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
         assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
 
+        # blank string is not valid and won't match, ensure this is caught
+        lev2.formula_terms = ""
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
+        assert "Attribute formula_terms is not well-formed"
+
     def test_is_time_variable(self):
         var1 = MockVariable()
         var1.standard_name = "time"
@@ -2478,8 +2483,7 @@ class TestCF1_7(BaseTestCase):
 
             # one should have failed, as no computed_standard_name is assigned
             score, out_of, messages = get_results(ret_val)
-            assert score == 0
-            assert out_of == 1
+            assert score < out_of
 
             # this time, assign computed_standard_name
             ret_val = []
@@ -2524,6 +2528,16 @@ class TestCF1_7(BaseTestCase):
         assert scored < out_of
         assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
 
+        # TEST CONFORMANCE 4.3.3 REQUIRED
+        del dataset.variables["lev"].formula_terms
+        results = self.cf.check_dimensionless_vertical_coordinates(dataset)
+
+        # FIXME: get_results messages variable doesn't return message here
+        assert (
+            "Variable lev should have formula_terms attribute when "
+            "computed_standard_name attribute is defined" in results[-1].msgs
+        )
+
     def test_check_attr_type(self):
         """
         Ensure the _check_attr_type method works as expected.