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.


Changed files:
compliance_checker/tests/test_cli.py, compliance_checker/runner.py

diff --git a/compliance_checker/runner.py b/compliance_checker/runner.py
index b4df921..7478690 100644
--- a/compliance_checker/runner.py
+++ b/compliance_checker/runner.py
@@ -3,10 +3,21 @@ from __future__ import print_function
 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 @@ class ComplianceChecker(object):
         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 @@ class ComplianceChecker(object):
         @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 @@ class ComplianceChecker(object):
         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 @@ class ComplianceChecker(object):
         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())
diff --git a/compliance_checker/tests/test_cli.py b/compliance_checker/tests/test_cli.py
index e591124..4db226f 100644
--- a/compliance_checker/tests/test_cli.py
+++ b/compliance_checker/tests/test_cli.py
@@ -91,3 +91,24 @@ class TestCLI(TestCase):
         )
 
         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