--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/DebugClients/Python/coverage/python.py Mon Sep 19 22:47:52 2016 +0200 @@ -0,0 +1,205 @@ +# 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, 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): + """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) + + # 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. + 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__ + 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] + 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'), + ) + self._parser.parse_source() + return self._parser + + def lines(self): + """Return the line numbers of statements in the file.""" + return self.parser.statements + + def excluded_lines(self): + """Return the line numbers of statements in the file.""" + return self.parser.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() + + 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: + 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())