| # 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 zipimport |
| |
| 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 bytes. |
| |
| """ |
| 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 unicode.""" |
| base, ext = os.path.splitext(filename) |
| if ext == ".py" and env.WINDOWS: |
| exts = [".py", ".pyw"] |
| else: |
| exts = [ext] |
| |
| for ext in exts: |
| try_filename = base + ext |
| if os.path.exists(try_filename): |
| # A regular text file: open it. |
| source = read_python_source(try_filename) |
| break |
| |
| # Maybe it's in a zip file? |
| source = get_zip_bytes(try_filename) |
| if source is not None: |
| 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' |
| |
| return source |
| |
| |
| @contract(returns='bytes|None') |
| def get_zip_bytes(filename): |
| """Get data from `filename` if it is a zip file path. |
| |
| Returns the bytestring data read from the zip file, or None if no zip file |
| could be found or `filename` isn't in it. The data returned will be |
| an empty string if the file is empty. |
| |
| """ |
| markers = ['.zip'+os.sep, '.egg'+os.sep] |
| for marker in markers: |
| if marker in filename: |
| parts = filename.split(marker) |
| try: |
| zi = zipimport.zipimporter(parts[0]+marker[:-1]) |
| except zipimport.ZipImportError: |
| continue |
| try: |
| data = zi.get_data(parts[1]) |
| except IOError: |
| continue |
| return data |
| return None |
| |
| |
| class PythonFileReporter(FileReporter): |
| """Report support for a Python file.""" |
| |
| def __init__(self, morf, coverage=None): |
| self.coverage = coverage |
| |
| 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__(files.canonical_filename(filename)) |
| |
| 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 |
| |
| 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'), |
| ) |
| return self._parser |
| |
| @expensive |
| 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 |
| |
| @expensive |
| 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 |
| |
| def translate_lines(self, lines): |
| return self.parser.translate_lines(lines) |
| |
| 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), |
| join_regex(self.coverage.config.partial_always_list) |
| ) |
| 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) |
| return self._source |
| |
| def should_be_python(self): |
| """Does it seem like this file should contain Python? |
| |
| This is used to decide if a file reported as part of the execution of |
| a program was really likely to have contained Python in the first |
| place. |
| |
| """ |
| # Get the file extension. |
| _, ext = os.path.splitext(self.filename) |
| |
| # Anything named *.py* should be Python. |
| if ext.startswith('.py'): |
| return True |
| # A file with no extension should be Python. |
| if not ext: |
| return True |
| # Everything else is probably not Python. |
| return False |
| |
| def source_token_lines(self): |
| return source_token_lines(self.source()) |