summaryrefslogtreecommitdiff
path: root/coverage/python.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/python.py')
-rw-r--r--coverage/python.py44
1 files changed, 34 insertions, 10 deletions
diff --git a/coverage/python.py b/coverage/python.py
index 22ca214e..c3ca0e1e 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -1,14 +1,22 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
"""Python source expertise for coverage.py"""
import os.path
+import types
import zipimport
from coverage import env, files
-from coverage.misc import contract, NoSource, join_regex
+from coverage.misc import (
+ contract, CoverageException, expensive, NoSource, join_regex, isolate_module,
+)
from coverage.parser import PythonParser
from coverage.phystokens import source_token_lines, source_encoding
from coverage.plugin import FileReporter
+os = isolate_module(os)
+
@contract(returns='bytes')
def read_python_source(filename):
@@ -43,8 +51,12 @@ def get_python_source(filename):
break
else:
# Couldn't find source.
- raise NoSource("No source for code: '%s'." % filename)
+ exc_msg = "No source for code: '%s'.\n" % (filename,)
+ exc_msg += "Aborting report output, consider using -i."
+ raise NoSource(exc_msg)
+ # Replace \f because of http://bugs.python.org/issue19035
+ source = source.replace(b'\f', b' ')
source = source.decode(source_encoding(source), "replace")
# Python code should always end with a line with a newline.
@@ -87,9 +99,15 @@ class PythonFileReporter(FileReporter):
if hasattr(morf, '__file__'):
filename = morf.__file__
+ elif isinstance(morf, types.ModuleType):
+ # A module should have had .__file__, otherwise we can't use it.
+ # This could be a PEP-420 namespace package.
+ raise CoverageException("Module {0} has no file".format(morf))
else:
filename = morf
+ filename = files.unicode_filename(filename)
+
# .pyc files should always refer to a .py instead.
if filename.endswith(('.pyc', '.pyo')):
filename = filename[:-1]
@@ -101,6 +119,7 @@ class PythonFileReporter(FileReporter):
if hasattr(morf, '__name__'):
name = morf.__name__
name = name.replace(".", os.sep) + ".py"
+ name = files.unicode_filename(name)
else:
name = files.relative_filename(filename)
self.relname = name
@@ -110,29 +129,28 @@ class PythonFileReporter(FileReporter):
self._statements = None
self._excluded = None
+ @contract(returns='unicode')
def relative_filename(self):
return self.relname
@property
def parser(self):
+ """Lazily create a :class:`PythonParser`."""
if self._parser is None:
self._parser = PythonParser(
filename=self.filename,
exclude=self.coverage._exclude_regex('exclude'),
)
+ self._parser.parse_source()
return self._parser
- def statements(self):
+ def lines(self):
"""Return the line numbers of statements in the file."""
- if self._statements is None:
- self._statements, self._excluded = self.parser.parse_source()
- return self._statements
+ return self.parser.statements
- def excluded_statements(self):
+ def excluded_lines(self):
"""Return the line numbers of statements in the file."""
- if self._excluded is None:
- self._statements, self._excluded = self.parser.parse_source()
- return self._excluded
+ return self.parser.excluded
def translate_lines(self, lines):
return self.parser.translate_lines(lines)
@@ -140,6 +158,7 @@ class PythonFileReporter(FileReporter):
def translate_arcs(self, arcs):
return self.parser.translate_arcs(arcs)
+ @expensive
def no_branch_lines(self):
no_branch = self.parser.lines_matching(
join_regex(self.coverage.config.partial_list),
@@ -147,12 +166,17 @@ class PythonFileReporter(FileReporter):
)
return no_branch
+ @expensive
def arcs(self):
return self.parser.arcs()
+ @expensive
def exit_counts(self):
return self.parser.exit_counts()
+ def missing_arc_description(self, start, end, executed_arcs=None):
+ return self.parser.missing_arc_description(start, end, executed_arcs)
+
@contract(returns='unicode')
def source(self):
if self._source is None: