| Viewing file:  python.py (7.88 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
 
 """Python source expertise for coverage.py"""
 
 from __future__ import annotations
 
 import os.path
 import types
 import zipimport
 
 from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
 
 from coverage import env
 from coverage.exceptions import CoverageException, NoSource
 from coverage.files import canonical_filename, relative_filename, zip_location
 from coverage.misc import expensive, isolate_module, join_regex
 from coverage.parser import PythonParser
 from coverage.phystokens import source_token_lines, source_encoding
 from coverage.plugin import FileReporter
 from coverage.types import TArc, TLineNo, TMorf, TSourceTokenLines
 
 if TYPE_CHECKING:
 from coverage import Coverage
 
 os = isolate_module(os)
 
 
 def read_python_source(filename: str) -> bytes:
 """Read the Python source text from `filename`.
 
 Returns bytes.
 
 """
 with open(filename, "rb") as f:
 source = f.read()
 
 return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
 
 
 def get_python_source(filename: str) -> str:
 """Return the source code, as unicode."""
 base, ext = os.path.splitext(filename)
 if ext == ".py" and env.WINDOWS:
 exts = [".py", ".pyw"]
 else:
 exts = [ext]
 
 source_bytes: Optional[bytes]
 for ext in exts:
 try_filename = base + ext
 if os.path.exists(try_filename):
 # A regular text file: open it.
 source_bytes = read_python_source(try_filename)
 break
 
 # Maybe it's in a zip file?
 source_bytes = get_zip_bytes(try_filename)
 if source_bytes is not None:
 break
 else:
 # Couldn't find source.
 raise NoSource(f"No source for code: '{filename}'.")
 
 # Replace \f because of http://bugs.python.org/issue19035
 source_bytes = source_bytes.replace(b"\f", b" ")
 source = source_bytes.decode(source_encoding(source_bytes), "replace")
 
 # Python code should always end with a line with a newline.
 if source and source[-1] != "\n":
 source += "\n"
 
 return source
 
 
 def get_zip_bytes(filename: str) -> Optional[bytes]:
 """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.
 
 """
 zipfile_inner = zip_location(filename)
 if zipfile_inner is not None:
 zipfile, inner = zipfile_inner
 try:
 zi = zipimport.zipimporter(zipfile)
 except zipimport.ZipImportError:
 return None
 try:
 data = zi.get_data(inner)
 except OSError:
 return None
 return data
 return None
 
 
 def source_for_file(filename: str) -> str:
 """Return the source filename for `filename`.
 
 Given a file name being traced, return the best guess as to the source
 file to attribute it to.
 
 """
 if filename.endswith(".py"):
 # .py files are themselves source files.
 return filename
 
 elif filename.endswith((".pyc", ".pyo")):
 # Bytecode files probably have source files near them.
 py_filename = filename[:-1]
 if os.path.exists(py_filename):
 # Found a .py file, use that.
 return py_filename
 if env.WINDOWS:
 # On Windows, it could be a .pyw file.
 pyw_filename = py_filename + "w"
 if os.path.exists(pyw_filename):
 return pyw_filename
 # Didn't find source, but it's probably the .py file we want.
 return py_filename
 
 # No idea, just use the file name as-is.
 return filename
 
 
 def source_for_morf(morf: TMorf) -> str:
 """Get the source filename for the module-or-file `morf`."""
 if hasattr(morf, "__file__") and 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(f"Module {morf} has no file")
 else:
 filename = morf
 
 filename = source_for_file(filename)
 return filename
 
 
 class PythonFileReporter(FileReporter):
 """Report support for a Python file."""
 
 def __init__(self, morf: TMorf, coverage: Optional[Coverage] = None) -> None:
 self.coverage = coverage
 
 filename = source_for_morf(morf)
 
 fname = filename
 canonicalize = True
 if self.coverage is not None:
 if self.coverage.config.relative_files:
 canonicalize = False
 if canonicalize:
 fname = canonical_filename(filename)
 super().__init__(fname)
 
 if hasattr(morf, "__name__"):
 name = morf.__name__.replace(".", os.sep)
 if os.path.basename(filename).startswith("__init__."):
 name += os.sep + "__init__"
 name += ".py"
 else:
 name = relative_filename(filename)
 self.relname = name
 
 self._source: Optional[str] = None
 self._parser: Optional[PythonParser] = None
 self._excluded = None
 
 def __repr__(self) -> str:
 return f"<PythonFileReporter {self.filename!r}>"
 
 def relative_filename(self) -> str:
 return self.relname
 
 @property
 def parser(self) -> PythonParser:
 """Lazily create a :class:`PythonParser`."""
 assert self.coverage is not None
 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) -> Set[TLineNo]:
 """Return the line numbers of statements in the file."""
 return self.parser.statements
 
 def excluded_lines(self) -> Set[TLineNo]:
 """Return the line numbers of statements in the file."""
 return self.parser.excluded
 
 def translate_lines(self, lines: Iterable[TLineNo]) -> Set[TLineNo]:
 return self.parser.translate_lines(lines)
 
 def translate_arcs(self, arcs: Iterable[TArc]) -> Set[TArc]:
 return self.parser.translate_arcs(arcs)
 
 @expensive
 def no_branch_lines(self) -> Set[TLineNo]:
 assert self.coverage is not None
 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) -> Set[TArc]:
 return self.parser.arcs()
 
 @expensive
 def exit_counts(self) -> Dict[TLineNo, int]:
 return self.parser.exit_counts()
 
 def missing_arc_description(
 self,
 start: TLineNo,
 end: TLineNo,
 executed_arcs: Optional[Iterable[TArc]] = None,
 ) -> str:
 return self.parser.missing_arc_description(start, end, executed_arcs)
 
 def source(self) -> str:
 if self._source is None:
 self._source = get_python_source(self.filename)
 return self._source
 
 def should_be_python(self) -> bool:
 """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) -> TSourceTokenLines:
 return source_token_lines(self.source())
 
 |