Repository: ioos/compliance-checker
Pull request #666 (2019-08-09 14:19:56)

Adds a new method `get_test_ctx` to reuse `TestCtx`
Adds a new method to `compliance_checker.base.BaseCheck` called
`get_test_ctx`, which attempts to fetch a `TestCtx` from a defaultdict
specific to the checker instance.  If the check does not exist, it is
created in the dict, otherwise, the corresponding dict entry is returned
for further modifications.  Allows predefined `TestCtx` objects to be
reused across functions so that the same `TestCtx` doesn't have to be
passed around one monolithic function.  Potentially also useful for
extending checking behavior defined in newer checker versions, i.e.
additions to check behavior between CF versions.


Patch:
diff --git a/compliance_checker/base.py b/compliance_checker/base.py
index fe6dfd0..ec10b2f 100644
--- a/compliance_checker/base.py
+++ b/compliance_checker/base.py
@@ -13,6 +13,7 @@ from owslib.swe.sensor.sml import SensorML
 from owslib.namespaces import Namespaces
 from compliance_checker import __version__, MemoizedDataset
 from compliance_checker.util import kvp_convert
+from collections import defaultdict
 from lxml import etree
 import sys
 
@@ -58,6 +59,24 @@ class BaseCheck(object):
         """
         pass
 
+    def __init__(self):
+        self._defined_results = defaultdict(lambda: defaultdict(TestCtx))
+
+    def get_test_ctx(self, severity, name, variable=None):
+        """
+        Creates an existing TestCtx object in _defined_results dict if it does
+        not exist for the current checker instance, or an returns the existing
+        TestCtx for modification. Takes a severity level and name and uses the
+        two element tuple formed by the arguments as a key into the dict.
+
+        :param int severity: A BaseCheck severity level
+        :param str name: The name of the check
+        :rtype compliance_checker.base.TestCtx:
+        :returns: A new or or existing `TestCtx` instance taken from this
+                  instance's _defined_results dict
+        """
+        return self._defined_results[variable][(severity, name)]
+
 
 class BaseNCCheck(object):
     """
@@ -187,7 +206,9 @@ class TestCtx(object):
         self.variable = variable
 
     def to_result(self):
-        return Result(self.category, (self.score, self.out_of), self.description, self.messages, variable_name=self.variable)
+        return Result(self.category, (self.score, self.out_of),
+                      self.description, self.messages,
+                      variable_name=self.variable)
 
     def assert_true(self, test, message):
         '''
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index 061864e..5cf4eb8 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -63,6 +63,7 @@ class CFBaseCheck(BaseCheck):
 
         # Each default dict is a key, value mapping from the dataset object to
         # a list of variables
+        super(CFBaseCheck, self).__init__()
         self._coord_vars       = defaultdict(list)
         self._ancillary_vars   = defaultdict(list)
         self._clim_vars        = defaultdict(list)
@@ -3396,7 +3397,7 @@ class CF1_7Check(CF1_6Check):
 
         self.cell_methods = cell_methods17
         self.grid_mapping_dict = grid_mapping_dict17
-    
+
     def check_actual_range(self, ds):
         """Check the actual_range attribute of variables. As stated in
         section 2.5.1 of version 1.7, this convention defines a two-element
@@ -3411,7 +3412,7 @@ class CF1_7Check(CF1_6Check):
           - if valid_range is specified, both elements of actual_range should
             be within valid_range
 
-        If a variable does not have an actual_range attribute, let it pass; 
+        If a variable does not have an actual_range attribute, let it pass;
         including this attribute is only suggested. However, if the user is
         specifying the actual_range, the Result will be considered
         high-priority."""
@@ -3625,7 +3626,7 @@ class CF1_7Check(CF1_6Check):
                             "Cell measure variable {} referred to by {} is not present in dataset variables".format(
                                 cell_meas_var_name, var.name)
                         )
-                   
+
                     else:
                         valid = True
 
@@ -3634,7 +3635,7 @@ class CF1_7Check(CF1_6Check):
                             valid,
                             (self.section_titles['7.2']),
                             reasoning)
-                    ret_val.append(result)                    
+                    ret_val.append(result)
                     continue # can't test anything on an external var
 
                 else:


Test patch:
diff --git a/compliance_checker/tests/test_base.py b/compliance_checker/tests/test_base.py
index cf7e260..e4301f2 100644
--- a/compliance_checker/tests/test_base.py
+++ b/compliance_checker/tests/test_base.py
@@ -86,6 +86,33 @@ class TestBase(TestCase):
         base.attr_check(attr, self.ds, priority, rv3)
         assert rv3[0] == base.Result(priority, True, 'dummy', [])
 
+    def test_get_test_ctx(self):
+        # acdd refers to a BaseCheck instance here -- perhaps the variable name
+        # should reflect that?
+        ctx = self.acdd.get_test_ctx(base.BaseCheck.HIGH, 'Dummy Name')
+        ctx.assert_true(1 + 1 == 2, 'One plus one equals two')
+        self.assertEqual(ctx.out_of, 1)
+        self.assertEqual(ctx.messages, [])
+
+        # ctx2 should be receive the same test context
+        ctx2 = self.acdd.get_test_ctx(base.BaseCheck.HIGH, 'Dummy Name')
+        self.assertIs(ctx, ctx2)
+        # will fail, obviously
+        ctx2.assert_true(1 + 1 == 3, 'One plus one equals three')
+        self.assertEqual(ctx.out_of, 2)
+        self.assertEqual(ctx2.out_of, 2)
+        self.assertEqual(ctx2.messages, ['One plus one equals three'])
+
+        ctx2 = self.acdd.get_test_ctx(base.BaseCheck.HIGH, 'Test Name',
+                                      'test_var_name')
+        ctx3 = self.acdd.get_test_ctx(base.BaseCheck.HIGH, 'Test Name',
+                                      'test_var_name')
+        # check that variable cache is working
+        self.assertIs(ctx3, (self.acdd._defined_results['test_var_name']
+                                                       [(base.BaseCheck.HIGH,
+                                                         'Test Name')]
+                                                     ))
+
 
 class TestGenericFile(TestCase):
     '''