Repository: ioos/compliance-checker
Pull request #925 (2022-05-18 19:43:24)

Standard name recommendations
Suggests nearby corrections for incorrect standard names.  Implements #806.

codecov[bot]: # [Codecov](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos) Report
> Merging [#925](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos) (0e59208) into [master](https://codecov.io/gh/ioos/compliance-checker/commit/0949e3b15c53f13a2972a553ea3c9167c93b3684?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos) (0949e3b) will **increase** coverage by `0.07%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #925      +/-   ##
==========================================
+ Coverage   78.72%   78.79%   +0.07%     
==========================================
  Files          23       23              
  Lines        5264     5272       +8     
  Branches     1307     1309       +2     
==========================================
+ Hits         4144     4154      +10     
+ Misses        829      828       -1     
+ Partials      291      290       -1     
```


| [Impacted Files](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos) | Coverage Δ | |
|---|---|---|
| [compliance\_checker/cf/cf\_1\_6.py](https://codecov.io/gh/ioos/compliance-checker/pull/925/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos#diff-Y29tcGxpYW5jZV9jaGVja2VyL2NmL2NmXzFfNi5weQ==) | `88.93% <100.00%> (+0.18%)` | :arrow_up: |
| [compliance\_checker/cf/util.py](https://codecov.io/gh/ioos/compliance-checker/pull/925/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos#diff-Y29tcGxpYW5jZV9jaGVja2VyL2NmL3V0aWwucHk=) | `51.45% <0.00%> (+0.36%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos). Last update [0949e3b...0e59208](https://codecov.io/gh/ioos/compliance-checker/pull/925?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ioos).


Patch:
diff --git a/compliance_checker/cf/cf_1_6.py b/compliance_checker/cf/cf_1_6.py
index afbe6bb..c82964e 100644
--- a/compliance_checker/cf/cf_1_6.py
+++ b/compliance_checker/cf/cf_1_6.py
@@ -22,6 +22,8 @@ from compliance_checker.cf.appendix_f import (grid_mapping_attr_types16,
                                               grid_mapping_dict16)
 
 from compliance_checker.cf.cf_base import CFNCCheck, appendix_a_base
+import difflib
+
 logger = logging.getLogger(__name__)
 
 class CF1_6Check(CFNCCheck):
@@ -936,13 +938,21 @@ class CF1_6Check(CFNCCheck):
                         name
                     ),
                 )
-                if isinstance(standard_name, str):
-                    valid_std_name.assert_true(
-                        standard_name in self._std_names,
-                        "standard_name {} is not defined in Standard Name Table v{}".format(
-                            standard_name or "undefined", self._std_names._version
-                        ),
-                    )
+                valid_std_name.out_of += 1
+                if (standard_name not in self._std_names):
+                    err_msg = (
+                        "standard_name {} is not defined in Standard Name Table v{}."
+                        .format(
+                            standard_name or "undefined",
+                            self._std_names._version))
+                    close_matches = difflib.get_close_matches(standard_name,
+                                                              self._std_names)
+                    if close_matches:
+                        err_msg += f" Possible close match(es): {close_matches}"
+                    valid_std_name.messages.append(err_msg)
+                else:
+                    valid_std_name.score += 1
+
 
                 ret_val.append(valid_std_name.to_result())
 


Test patch:
diff --git a/compliance_checker/tests/helpers.py b/compliance_checker/tests/helpers.py
index 8725df8..cfe3eb6 100644
--- a/compliance_checker/tests/helpers.py
+++ b/compliance_checker/tests/helpers.py
@@ -30,9 +30,15 @@ class MockTimeSeries(MockNetCDF):
     def __init__(self, filename=None, default_fill_value=None):
         super(MockTimeSeries, self).__init__(filename)
         self.createDimension("time", 500)
-        for v in ("time", "lon", "lat", "depth"):
-            self.createVariable(v, "d", ("time",),
-                                fill_value=default_fill_value)
+        for name, std_name, units in (
+                  ("time", "time", "seconds since 1970-01-01"),
+                  ("lon", "longitude", "degrees_east"),
+                  ("lat", "latitude", "degrees_north"),
+                  ("depth", "depth", "m")):
+            var = self.createVariable(name, "d", ("time",),
+                                 fill_value=default_fill_value)
+            var.standard_name = std_name
+            var.units = units
 
         # give some applicable units
         self.variables["time"].units = "seconds since 2019-04-11T00:00:00"
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index 10a3a10..c95c4e8 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -467,11 +467,24 @@ class TestCF1_6(BaseTestCase):
         results = self.cf.check_standard_name(dataset)
         score, out_of, messages = get_results(results)
 
+
+
         # 9 vars checked, 8 fail
         assert len(results) == 9
         assert score < out_of
         assert all(r.name == u"§3.3 Standard Name" for r in results)
 
+        # check recommendations with a misspelled standard name
+        dataset = MockTimeSeries()
+        temperature = dataset.createVariable("temperature", "f8", ("time",))
+        temperature.units = "degree_C"
+        temperature.standard_name = "sea_water_temperatrue"
+        results = self.cf.check_standard_name(dataset)
+        score, out_of, messages = get_results(results)
+        # only check for recommendation substring as recommendations might
+        # vary with differing standard name tables
+        assert " Possible close match(es):" in messages[-1]
+
         # load different ds --  ll vars pass this check
         dataset = self.load_dataset(STATIC_FILES["reduced_horizontal_grid"])
         results = self.cf.check_standard_name(dataset)
diff --git a/compliance_checker/tests/test_ioos_profile.py b/compliance_checker/tests/test_ioos_profile.py
index a3fa96b..e16acda 100644
--- a/compliance_checker/tests/test_ioos_profile.py
+++ b/compliance_checker/tests/test_ioos_profile.py
@@ -655,21 +655,21 @@ class TestIOOS1_2(BaseTestCase):
 
     def test_check_standard_name(self):
         ds = MockTimeSeries()  # time, lat, lon, depth
-
-        # no standard names
+        # all standard names on variables
         results = self.ioos.check_standard_name(ds)
         scored, out_of, messages = get_results(results)
-        self.assertLess(scored, out_of)
+        self.assertEqual(scored, out_of)
 
-        # give standard names to all variables
-        ds.variables["time"].setncattr("standard_name", "time")
-        ds.variables["lon"].setncattr("standard_name", "longitude")
-        ds.variables["lat"].setncattr("standard_name", "latitude")
-        ds.variables["depth"].setncattr("standard_name", "depth")
+        # all standard names except for depth
+        del ds.variables["depth"].standard_name
         results = self.ioos.check_standard_name(ds)
         scored, out_of, messages = get_results(results)
-        self.assertEqual(scored, out_of)
+        self.assertLess(scored, out_of)
 
+
+        # have to recreate here or temperature gives KeyError despite appearing
+        # deleted -- why?
+        ds = MockTimeSeries()  # time, lat, lon, depth
         # add a QARTOD variable, no standard name - should fail
         qr = ds.createVariable("depth_qc", np.byte)
         qr.setncattr("flag_meanings", "blah")