Repository: ioos/compliance-checker

Issue:
Save text output
@lukecampbell and @daf I am helping @jbosch-noaa with some notebooks in

https://github.com/ioos/notebooks_demos/pull/21

and I noticed that simple text outputs are only redirected to `stdout`. There is on API to save it to a file.
Correct me if I am wrong but I could not see it [here](https://github.com/ioos/compliance-checker/blob/7276abba102e2ef6cb390d49ba0c38726f625913/compliance_checker/runner.py#L43-L50).

I do know that in CLI interface it is easy to redirect to a file but it would be nice to be able to do that from class method too. Not also that the docs give the idea that this is possible and the error message [here](https://github.com/ioos/compliance-checker/blob/7276abba102e2ef6cb390d49ba0c38726f625913/compliance_checker/runner.py#L53) is not very helpful.



Discussion:
It looks like it only outputs the plain text to stdout. That's probably not completely desirable.

@lukecampbell  - For some of us, the output doesn't have to be pretty.  Plain text is ok.  I just need it to go to a file instead of printing on screen.  With that said, if it is easier to output to an HTML file, that's fine also.  Either way, the directions in the readme file is misleading and confusing.

@lukecampbell or @daf are either of you working on this? If not I may try it next week but I want o avoid duplicating efforts here.

I probably won't have time for it this week but it's definitely on my radar. If you could tackle it, that would be a big help!


Resolving patch:
diff --git a/compliance_checker/runner.py b/compliance_checker/runner.py
--- a/compliance_checker/runner.py
+++ b/compliance_checker/runner.py
@@ -3,10 +3,21 @@
 import traceback
 import sys
 import io
-from io import StringIO
 
+from contextlib import contextmanager
 from compliance_checker.suite import CheckSuite
 
+# Py 3.4+ has contextlib.redirect_stdout to redirect stdout to a different
+# stream, but use this decorated function in order to redirect output in
+# previous versions
+@contextmanager
+def stdout_redirector(stream):
+    old_stdout = sys.stdout
+    sys.stdout = stream
+    try:
+        yield
+    finally:
+        sys.stdout = old_stdout
 
 class ComplianceChecker(object):
     """
@@ -40,8 +51,16 @@ def run_checker(cls, ds_loc, checker_names, verbose, criteria, output_filename='
         elif criteria == 'lenient':
             limit = 3
 
-        if output_filename == '-' and output_format == 'text':
-            groups = cls.stdout_output(cs, score_groups, verbose, limit)
+        if output_format == 'text':
+            if output_filename == '-':
+                groups = cls.stdout_output(cs, score_groups, verbose, limit)
+            # need to redirect output from stdout since print functions are
+            # presently used to generate the standard report output
+            else:
+                with io.open(output_filename, 'w', encoding='utf-8') as f:
+                    with stdout_redirector(f):
+                        groups = cls.stdout_output(cs, score_groups, verbose,
+                                                   limit)
 
         elif output_format == 'html':
             groups = cls.html_output(cs, score_groups, output_filename, ds_loc, limit)
@@ -67,6 +86,7 @@ def stdout_output(cls, cs, score_groups, verbose, limit):
         @param verbose      Integer value for verbosity level
         @param limit        The degree of strictness, 1 being the strictest, and going up from there.
         '''
+
         for checker, rpair in score_groups.items():
             groups, errors = rpair
             score_list, points, out_of = cs.standard_output(limit, checker, groups)
@@ -89,7 +109,7 @@ def html_output(cls, cs, score_groups, output_filename, ds_loc, limit):
         for checker, rpair in score_groups.items():
             groups, errors = rpair
             if output_filename == '-':
-                f = StringIO()
+                f = io.StringIO()
                 cs.html_output(checker, groups, f, ds_loc, limit)
                 f.seek(0)
                 print(f.read())
@@ -112,7 +132,7 @@ def json_output(cls, cs, score_groups, output_filename, ds_loc, limit):
         for checker, rpair in score_groups.items():
             groups, errors = rpair
             if output_filename == '-':
-                f = StringIO()
+                f = io.StringIO()
                 cs.json_output(checker, groups, f, ds_loc, limit)
                 f.seek(0)
                 print(f.read())


Test patch:
diff --git a/compliance_checker/tests/test_cli.py b/compliance_checker/tests/test_cli.py
--- a/compliance_checker/tests/test_cli.py
+++ b/compliance_checker/tests/test_cli.py
@@ -91,3 +91,24 @@ def test_json_output(self):
         )
 
         assert os.stat(path).st_size > 0
+
+    def test_json_output(self):
+        '''
+        Tests that the 'text' output can be redirected to file with arguments
+        to the command line
+        '''
+
+        fd, path = tempfile.mkstemp()
+        os.close(fd)
+        self.addCleanup(os.remove, path)
+
+        return_value, errors = ComplianceChecker.run_checker(
+            ds_loc=STATIC_FILES['conv_bad'],
+            verbose=0,
+            criteria='strict',
+            checker_names=['cf'],
+            output_filename=path,
+            output_format='text'
+        )
+
+        assert os.stat(path).st_size > 0