summaryrefslogtreecommitdiff
path: root/coverage/python.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/python.py')
-rw-r--r--coverage/python.py73
1 files changed, 36 insertions, 37 deletions
diff --git a/coverage/python.py b/coverage/python.py
index 19212a5b..5e563828 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -1,37 +1,34 @@
+# 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 sys
-import tokenize
import zipimport
-from coverage import env
-from coverage.backward import unicode_class
-from coverage.files import FileLocator
-from coverage.misc import NoSource, join_regex
+from coverage import env, files
+from coverage.misc import contract, 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):
"""Read the Python source text from `filename`.
- Returns a str: unicode on Python 3, bytes on Python 2.
+ Returns bytes.
"""
- # Python 3.2 provides `tokenize.open`, the best way to open source files.
- if sys.version_info >= (3, 2):
- f = tokenize.open(filename)
- else:
- f = open(filename, "rU")
-
- with f:
- return f.read()
+ with open(filename, "rb") as f:
+ return f.read().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
+@contract(returns='unicode')
def get_python_source(filename):
- """Return the source code, as a str."""
+ """Return the source code, as unicode."""
base, ext = os.path.splitext(filename)
if ext == ".py" and env.WINDOWS:
exts = [".py", ".pyw"]
@@ -48,13 +45,13 @@ def get_python_source(filename):
# Maybe it's in a zip file?
source = get_zip_bytes(try_filename)
if source is not None:
- if env.PY3:
- source = source.decode(source_encoding(source))
break
else:
# Couldn't find source.
raise NoSource("No source for code: '%s'." % filename)
+ source = source.decode(source_encoding(source), "replace")
+
# Python code should always end with a line with a newline.
if source and source[-1] != '\n':
source += '\n'
@@ -62,6 +59,7 @@ def get_python_source(filename):
return source
+@contract(returns='bytes|None')
def get_zip_bytes(filename):
"""Get data from `filename` if it is a zip file path.
@@ -82,7 +80,6 @@ def get_zip_bytes(filename):
data = zi.get_data(parts[1])
except IOError:
continue
- assert isinstance(data, bytes)
return data
return None
@@ -92,55 +89,57 @@ class PythonFileReporter(FileReporter):
def __init__(self, morf, coverage=None):
self.coverage = coverage
- file_locator = coverage.file_locator if coverage else FileLocator()
if hasattr(morf, '__file__'):
filename = morf.__file__
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]
elif filename.endswith('$py.class'): # Jython
filename = filename[:-9] + ".py"
- super(PythonFileReporter, self).__init__(
- file_locator.canonical_filename(filename)
- )
+ super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
if hasattr(morf, '__name__'):
name = morf.__name__
name = name.replace(".", os.sep) + ".py"
+ name = files.unicode_filename(name)
else:
- name = file_locator.relative_filename(filename)
- self.name = name
+ name = files.relative_filename(filename)
+ self.relname = name
self._source = None
self._parser = None
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)
@@ -148,6 +147,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),
@@ -155,19 +155,18 @@ class PythonFileReporter(FileReporter):
)
return no_branch
+ @expensive
def arcs(self):
return self.parser.arcs()
+ @expensive
def exit_counts(self):
return self.parser.exit_counts()
+ @contract(returns='unicode')
def source(self):
if self._source is None:
self._source = get_python_source(self.filename)
- if env.PY2:
- encoding = source_encoding(self._source)
- self._source = self._source.decode(encoding, "replace")
- assert isinstance(self._source, unicode_class)
return self._source
def should_be_python(self):