Commits

Ned Batchelder committed 6eb9186

New config setting: [report]precision lets you specify the number of digits after the decimal point in coverage percentages. Finishes issue #16.

Comments (0)

Files changed (8)

   percentage is only reported as 0% or 100% if they are truly 0 or 100, and
   are rounded otherwise.  Fixes `issue 41` and issue 70`.
 
+- The precision of reported coverage percentages can be set with the
+  ``[report] precision`` config file setting.  Default is still 0.  Completes
+  ``issue 16``.
+
 .. _issue 70: http://bitbucket.org/ned/coveragepy/issue/70/text-report-and-html-report-disagree-on-coverage
 .. _issue 41: http://bitbucket.org/ned/coveragepy/issue/41/report-says-100-when-it-isnt-quite-there
+.. _issue 16: http://bitbucket.org/ned/coveragepy/issue/16/allow-configuration-of-accuracy-of-percentage-totals
 
 
 Version 3.4b1 --- 21 August 2010

coverage/config.py

         self.ignore_errors = False
         self.omit = None
         self.include = None
+        self.precision = 0
 
         # Defaults for [html]
         self.html_dir = "htmlcov"
             self.omit = self.get_list(cp, 'report', 'omit')
         if cp.has_option('report', 'include'):
             self.include = self.get_list(cp, 'report', 'include')
+        if cp.has_option('report', 'precision'):
+            self.precision = cp.getint('report', 'precision')
 
         # [html]
         if cp.has_option('html', 'directory'):

coverage/control.py

 from coverage.data import CoverageData
 from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
 from coverage.html import HtmlReporter
-from coverage.misc import bool_or_none
-from coverage.results import Analysis
+from coverage.misc import CoverageException, bool_or_none
+from coverage.results import Analysis, Numbers
 from coverage.summary import SummaryReporter
 from coverage.xmlreport import XmlReporter
 
         if config_file:
             if config_file is True:
                 config_file = ".coveragerc"
-            self.config.from_file(config_file)
+            try:
+                self.config.from_file(config_file)
+            except ValueError:
+                _, err, _ = sys.exc_info()
+                raise CoverageException(
+                    "Couldn't read config file %s: %s" % (config_file, err)
+                    )
 
         # 3: from environment variables:
         self.config.from_environment('COVERAGE_OPTIONS')
         # Only _harvest_data once per measurement cycle.
         self._harvested = False
 
-        # When tearing down the coverage object, modules can become None. 
-        # Saving the modules as object attributes avoids problems, but it is 
+        # Set the reporting precision.
+        Numbers.set_precision(self.config.precision)
+
+        # When tearing down the coverage object, modules can become None.
+        # Saving the modules as object attributes avoids problems, but it is
         # quite ad-hoc which modules need to be saved and which references
         # need to use the object attributes.
         self.socket = socket

coverage/results.py

     up statistics across files.
 
     """
+    # A global to determine the precision on coverage percentages, the number
+    # of decimal places.
+    _precision = 0
+    _near0 = 1.0              # These will change when _precision is changed.
+    _near100 = 99.0
+
     def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
                     n_branches=0, n_missing_branches=0
                     ):
         self.n_branches = n_branches
         self.n_missing_branches = n_missing_branches
 
+    def set_precision(cls, precision):
+        """Set the number of decimal places used to report percentages."""
+        assert 0 <= precision < 10
+        cls._precision = precision
+        cls._near0 = 1.0 / 10**precision
+        cls._near100 = 100.0 - cls._near0
+    set_precision = classmethod(set_precision)
+
     def _get_n_executed(self):
         """Returns the number of executed statements."""
         return self.n_statements - self.n_missing
 
         """
         pc = self.pc_covered
-        if 0 < pc < 1:
-            pc = 1.0
-        elif 99 < pc < 100:
-            pc = 99.0
+        if 0 < pc < self._near0:
+            pc = self._near0
+        elif self._near100 < pc < 100:
+            pc = self._near100
         else:
-            pc = round(pc)
-        return "%.0f" % pc
+            pc = round(pc, self._precision)
+        return "%.*f" % (self._precision, pc)
     pc_covered_str = property(_get_pc_covered_str)
 
+    def pc_str_width(cls):
+        """How many characters wide can pc_covered_str be?"""
+        width = 3   # "100"
+        if cls._precision > 0:
+            width += 1 + cls._precision
+        return width
+    pc_str_width = classmethod(pc_str_width)
+
     def __add__(self, other):
         nums = Numbers()
         nums.n_files = self.n_files + other.n_files

coverage/summary.py

         if self.branches:
             header += " Branch BrPart"
             fmt_coverage += " %6d %6d"
-        header += "  Cover"
-        fmt_coverage += " %5s%%"
+        width100 = Numbers.pc_str_width()
+        header += "%*s" % (width100+4, "Cover")
+        fmt_coverage += "%%%ds%%%%" % (width100+3,)
         if self.show_missing:
             header += "   Missing"
             fmt_coverage += "   %s"
 
 :history: 20100223T201600, new for 3.3
 :history: 20100725T211700, updated for 3.4.
+:history: 20100824T092900, added ``precision``.
 
 
 Coverage.py options can be specified in a configuration file.  This makes it
 ``omit`` (multi-string): a list of filename patterns, the files to leave out
 of reporting.  See :ref:`source` for details.
 
+``precision`` (integer): the number of digits after the decimal point to
+display for reported coverage percentages.  The default is 0, displaying
+for example "87%".  A value of 2 will display percentages like "87.32%".
+
 
 [html]
 ------

test/test_config.py

 
 import os, sys
 import coverage
+from coverage.misc import CoverageException
 
 sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
 from coveragetest import CoverageTest
         cov = coverage.coverage(data_file="fromarg.dat")
         self.assertEqual(cov.config.data_file, "fromarg.dat")
 
+    def test_parse_errors(self):
+        # Im-parseable values raise CoverageException
+        self.make_file(".coveragerc", """\
+            [run]
+            timid = maybe?
+            """)
+        self.assertRaises(CoverageException, coverage.coverage)
+
 
 class ConfigFileTest(CoverageTest):
     """Tests of the config file settings in particular."""
             omit =
                 one, another, some_more,
                     yet_more
+            precision = 3
 
             [html]
 
         self.assertEqual(cov.config.omit,
             ["one", "another", "some_more", "yet_more"]
             )
+        self.assertEqual(cov.config.precision, 3)
 
         self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere")
 

test/test_results.py

         self.assertEqual(n1.pc_covered_str, "99")
         self.assertEqual(n999.pc_covered_str, "1")
         self.assertEqual(n1000.pc_covered_str, "0")
+
+    def test_pc_covered_str_precision(self):
+        assert Numbers._precision == 0
+        Numbers.set_precision(1)
+        n0 = Numbers(n_files=1, n_statements=10000, n_missing=0)
+        n1 = Numbers(n_files=1, n_statements=10000, n_missing=1)
+        n9999 = Numbers(n_files=1, n_statements=10000, n_missing=9999)
+        n10000 = Numbers(n_files=1, n_statements=10000, n_missing=10000)
+        self.assertEqual(n0.pc_covered_str, "100.0")
+        self.assertEqual(n1.pc_covered_str, "99.9")
+        self.assertEqual(n9999.pc_covered_str, "0.1")
+        self.assertEqual(n10000.pc_covered_str, "0.0")
+        Numbers.set_precision(0)
+