DebugClients/Python3/coverage/results.py

branch
Py2 comp.
changeset 3495
fac17a82b431
parent 29
391dc0bc4ae5
child 4489
d0d6e4ad31bd
diff -r f1cbc18f88b2 -r fac17a82b431 DebugClients/Python3/coverage/results.py
--- a/DebugClients/Python3/coverage/results.py	Fri Apr 04 22:57:07 2014 +0200
+++ b/DebugClients/Python3/coverage/results.py	Thu Apr 10 23:02:20 2014 +0200
@@ -2,8 +2,8 @@
 
 import os
 
-from .backward import set, sorted           # pylint: disable-msg=W0622
-from .misc import format_lines, NoSource
+from .backward import iitems, set, sorted       # pylint: disable=W0622
+from .misc import format_lines, join_regex, NoSource
 from .parser import CodeParser
 
 
@@ -15,31 +15,33 @@
         self.code_unit = code_unit
 
         self.filename = self.code_unit.filename
-        ext = os.path.splitext(self.filename)[1]
-        source = None
-        if ext == '.py':
-            if not os.path.exists(self.filename):
-                source = self.coverage.file_locator.get_zip_data(self.filename)
-                if not source:
-                    raise NoSource("No source for code: %r" % self.filename)
+        actual_filename, source = self.find_source(self.filename)
 
         self.parser = CodeParser(
-            text=source, filename=self.filename,
-            exclude=self.coverage.exclude_re
+            text=source, filename=actual_filename,
+            exclude=self.coverage._exclude_regex('exclude')
             )
         self.statements, self.excluded = self.parser.parse_source()
 
         # Identify missing statements.
         executed = self.coverage.data.executed_lines(self.filename)
         exec1 = self.parser.first_lines(executed)
-        self.missing = sorted(set(self.statements) - set(exec1))
+        self.missing = self.statements - exec1
 
         if self.coverage.data.has_arcs():
+            self.no_branch = self.parser.lines_matching(
+                join_regex(self.coverage.config.partial_list),
+                join_regex(self.coverage.config.partial_always_list)
+                )
             n_branches = self.total_branches()
             mba = self.missing_branch_arcs()
-            n_missing_branches = sum([len(v) for v in mba.values()])
+            n_partial_branches = sum(
+                [len(v) for k,v in iitems(mba) if k not in self.missing]
+                )
+            n_missing_branches = sum([len(v) for k,v in iitems(mba)])
         else:
-            n_branches = n_missing_branches = 0
+            n_branches = n_partial_branches = n_missing_branches = 0
+            self.no_branch = set()
 
         self.numbers = Numbers(
             n_files=1,
@@ -47,9 +49,48 @@
             n_excluded=len(self.excluded),
             n_missing=len(self.missing),
             n_branches=n_branches,
+            n_partial_branches=n_partial_branches,
             n_missing_branches=n_missing_branches,
             )
 
+    def find_source(self, filename):
+        """Find the source for `filename`.
+
+        Returns two values: the actual filename, and the source.
+
+        The source returned depends on which of these cases holds:
+
+            * The filename seems to be a non-source file: returns None
+
+            * The filename is a source file, and actually exists: returns None.
+
+            * The filename is a source file, and is in a zip file or egg:
+              returns the source.
+
+            * The filename is a source file, but couldn't be found: raises
+              `NoSource`.
+
+        """
+        source = None
+
+        base, ext = os.path.splitext(filename)
+        TRY_EXTS = {
+            '.py':  ['.py', '.pyw'],
+            '.pyw': ['.pyw'],
+        }
+        try_exts = TRY_EXTS.get(ext)
+        if not try_exts:
+            return filename, None
+
+        for try_ext in try_exts:
+            try_filename = base + try_ext
+            if os.path.exists(try_filename):
+                return try_filename, None
+            source = self.coverage.file_locator.get_zip_data(try_filename)
+            if source:
+                return try_filename, source
+        raise NoSource("No source for code: '%s'" % filename)
+
     def missing_formatted(self):
         """The missing line numbers, formatted nicely.
 
@@ -64,7 +105,8 @@
 
     def arc_possibilities(self):
         """Returns a sorted list of the arcs in the code."""
-        return self.parser.arcs()
+        arcs = self.parser.arcs()
+        return arcs
 
     def arcs_executed(self):
         """Returns a sorted list of the arcs actually executed in the code."""
@@ -77,7 +119,11 @@
         """Returns a sorted list of the arcs in the code not executed."""
         possible = self.arc_possibilities()
         executed = self.arcs_executed()
-        missing = [p for p in possible if p not in executed]
+        missing = [
+            p for p in possible
+                if p not in executed
+                    and p[0] not in self.no_branch
+            ]
         return sorted(missing)
 
     def arcs_unpredicted(self):
@@ -89,14 +135,15 @@
         # trouble, and here is where it's the least burden to remove them.
         unpredicted = [
             e for e in executed
-                if e not in possible and e[0] != e[1]
+                if e not in possible
+                    and e[0] != e[1]
             ]
         return sorted(unpredicted)
 
     def branch_lines(self):
-        """Returns lines that have more than one exit."""
+        """Returns a list of line numbers that have more than one exit."""
         exit_counts = self.parser.exit_counts()
-        return [l1 for l1,count in exit_counts.items() if count > 1]
+        return [l1 for l1,count in iitems(exit_counts) if count > 1]
 
     def total_branches(self):
         """How many total branches are there?"""
@@ -119,6 +166,25 @@
                 mba[l1].append(l2)
         return mba
 
+    def branch_stats(self):
+        """Get stats about branches.
+
+        Returns a dict mapping line numbers to a tuple:
+        (total_exits, taken_exits).
+        """
+
+        exit_counts = self.parser.exit_counts()
+        missing_arcs = self.missing_branch_arcs()
+        stats = {}
+        for lnum in self.branch_lines():
+            exits = exit_counts[lnum]
+            try:
+                missing = len(missing_arcs[lnum])
+            except KeyError:
+                missing = 0
+            stats[lnum] = (exits, exits - missing)
+        return stats
+
 
 class Numbers(object):
     """The numerical results of measuring coverage.
@@ -127,16 +193,31 @@
     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
+                    n_branches=0, n_partial_branches=0, n_missing_branches=0
                     ):
         self.n_files = n_files
         self.n_statements = n_statements
         self.n_excluded = n_excluded
         self.n_missing = n_missing
         self.n_branches = n_branches
+        self.n_partial_branches = n_partial_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
@@ -157,6 +238,32 @@
         return pc_cov
     pc_covered = property(_get_pc_covered)
 
+    def _get_pc_covered_str(self):
+        """Returns the percent covered, as a string, without a percent sign.
+
+        Note that "0" is only returned when the value is truly zero, and "100"
+        is only returned when the value is truly 100.  Rounding can never
+        result in either "0" or "100".
+
+        """
+        pc = self.pc_covered
+        if 0 < pc < self._near0:
+            pc = self._near0
+        elif self._near100 < pc < 100:
+            pc = self._near100
+        else:
+            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
@@ -164,12 +271,16 @@
         nums.n_excluded = self.n_excluded + other.n_excluded
         nums.n_missing = self.n_missing + other.n_missing
         nums.n_branches = self.n_branches + other.n_branches
-        nums.n_missing_branches = (self.n_missing_branches +
-                                                    other.n_missing_branches)
+        nums.n_partial_branches = (
+            self.n_partial_branches + other.n_partial_branches
+            )
+        nums.n_missing_branches = (
+            self.n_missing_branches + other.n_missing_branches
+            )
         return nums
 
     def __radd__(self, other):
         # Implementing 0+Numbers allows us to sum() a list of Numbers.
         if other == 0:
             return self
-        raise NotImplemented
\ No newline at end of file
+        return NotImplemented

eric ide

mercurial