diff options
Diffstat (limited to 'coverage/python.py')
-rw-r--r-- | coverage/python.py | 73 |
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): |