summaryrefslogtreecommitdiff
path: root/coverage/codeunit.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/codeunit.py')
-rw-r--r--coverage/codeunit.py213
1 files changed, 190 insertions, 23 deletions
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index c58e237b..c9ab2622 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -1,18 +1,24 @@
"""Code unit (module) handling for Coverage."""
-import glob, os
+import os
-from coverage.backward import open_source, string_class, StringIO
-from coverage.misc import CoverageException
+from coverage.backward import open_python_source, string_class
+from coverage.misc import CoverageException, NoSource
+from coverage.parser import CodeParser, PythonParser
+from coverage.phystokens import source_token_lines, source_encoding
-def code_unit_factory(morfs, file_locator):
+def code_unit_factory(morfs, file_locator, get_plugin=None):
"""Construct a list of CodeUnits from polymorphic inputs.
`morfs` is a module or a filename, or a list of same.
`file_locator` is a FileLocator that can help resolve filenames.
+ `get_plugin` is a function taking a filename, and returning a plugin
+ responsible for the file. It can also return None if there is no plugin
+ claiming the file.
+
Returns a list of CodeUnit objects.
"""
@@ -20,16 +26,30 @@ def code_unit_factory(morfs, file_locator):
if not isinstance(morfs, (list, tuple)):
morfs = [morfs]
- # On Windows, the shell doesn't expand wildcards. Do it here.
- globbed = []
+ code_units = []
for morf in morfs:
- if isinstance(morf, string_class) and ('?' in morf or '*' in morf):
- globbed.extend(glob.glob(morf))
+ plugin = None
+ if isinstance(morf, string_class) and get_plugin:
+ plugin = get_plugin(morf)
+ if plugin:
+ klass = plugin.code_unit_class(morf)
+ #klass = DjangoTracer # NOT REALLY! TODO
+ # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of
+ # the path that indicates the Python file is actually a compiled Mako
+ # template. THIS IS TEMPORARY!
+ #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH')
+ #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf:
+ # # Super hack! Do mako both ways!
+ # if 0:
+ # cu = PythonCodeUnit(morf, file_locator)
+ # cu.name += '_fako'
+ # code_units.append(cu)
+ # klass = MakoCodeUnit
+ #elif isinstance(morf, string_class) and morf.endswith(".html"):
+ # klass = DjangoCodeUnit
else:
- globbed.append(morf)
- morfs = globbed
-
- code_units = [CodeUnit(morf, file_locator) for morf in morfs]
+ klass = PythonCodeUnit
+ code_units.append(klass(morf, file_locator))
return code_units
@@ -44,6 +64,7 @@ class CodeUnit(object):
`relative` is a boolean.
"""
+
def __init__(self, morf, file_locator):
self.file_locator = file_locator
@@ -51,11 +72,7 @@ class CodeUnit(object):
f = morf.__file__
else:
f = morf
- # .pyc files should always refer to a .py instead.
- if f.endswith(('.pyc', '.pyo')):
- f = f[:-1]
- elif f.endswith('$py.class'): # Jython
- f = f[:-9] + ".py"
+ f = self._adjust_filename(f)
self.filename = self.file_locator.canonical_filename(f)
if hasattr(morf, '__name__'):
@@ -73,9 +90,15 @@ class CodeUnit(object):
self.name = n
self.modname = modname
+ self._source = None
+
def __repr__(self):
return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename)
+ def _adjust_filename(self, f):
+ # TODO: This shouldn't be in the base class, right?
+ return f
+
# Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
# of them defined.
@@ -99,7 +122,7 @@ class CodeUnit(object):
the same directory, but need to differentiate same-named files from
different directories.
- For example, the file a/b/c.py might return 'a_b_c'
+ For example, the file a/b/c.py will return 'a_b_c'
"""
if self.modname:
@@ -108,26 +131,105 @@ class CodeUnit(object):
root = os.path.splitdrive(self.name)[1]
return root.replace('\\', '_').replace('/', '_').replace('.', '_')
- def source_file(self):
- """Return an open file for reading the source of the code unit."""
+ def source(self):
+ if self._source is None:
+ self._source = self.get_source()
+ return self._source
+
+ def get_source(self):
+ """Return the source code, as a string."""
if os.path.exists(self.filename):
# A regular text file: open it.
- return open_source(self.filename)
+ with open_python_source(self.filename) as f:
+ return f.read()
# Maybe it's in a zip file?
source = self.file_locator.get_zip_data(self.filename)
if source is not None:
- return StringIO(source)
+ return source
# Couldn't find source.
raise CoverageException(
"No source for code '%s'." % self.filename
)
+ def source_token_lines(self):
+ """Return the 'tokenized' text for the code."""
+ for line in self.source().splitlines():
+ yield [('txt', line)]
+
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 exection of
+ 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.
+ """
+ return False
+
+ def get_parser(self, exclude=None):
+ raise NotImplementedError
+
+
+class PythonCodeUnit(CodeUnit):
+ """Represents a Python file."""
+
+ def _adjust_filename(self, fname):
+ # .pyc files should always refer to a .py instead.
+ if fname.endswith(('.pyc', '.pyo')):
+ fname = fname[:-1]
+ elif fname.endswith('$py.class'): # Jython
+ fname = fname[:-9] + ".py"
+ return fname
+
+ def get_parser(self, exclude=None):
+ actual_filename, source = self._find_source(self.filename)
+ return PythonParser(
+ text=source, filename=actual_filename, exclude=exclude,
+ )
+
+ def _find_source(self, filename):
+ """Find the source for `filename`.
+
+ Returns two values: the actual filename, and the source.
+
+ The source returned depends on which of these cases holds:
+
+ * The filename seems to be a non-source file: returns None
+
+ * The filename is a source file, and actually exists: returns None.
+
+ * The filename is a source file, and is in a zip file or egg:
+ returns the source.
+
+ * The filename is a source file, but couldn't be found: raises
+ `NoSource`.
+
+ """
+ source = None
+
+ base, ext = os.path.splitext(filename)
+ TRY_EXTS = {
+ '.py': ['.py', '.pyw'],
+ '.pyw': ['.pyw'],
+ }
+ try_exts = TRY_EXTS.get(ext)
+ if not try_exts:
+ return filename, None
+
+ for try_ext in try_exts:
+ try_filename = base + try_ext
+ if os.path.exists(try_filename):
+ return try_filename, None
+ source = self.file_locator.get_zip_data(try_filename)
+ if source:
+ return try_filename, source
+ raise NoSource("No source for code: '%s'" % filename)
+
+ 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.
@@ -143,3 +245,68 @@ class CodeUnit(object):
return True
# Everything else is probably not Python.
return False
+
+ def source_token_lines(self):
+ return source_token_lines(self.source())
+
+ def source_encoding(self):
+ return source_encoding(self.source())
+
+
+class MakoParser(CodeParser):
+ def __init__(self, metadata):
+ self.metadata = metadata
+
+ def parse_source(self):
+ """Returns executable_line_numbers, excluded_line_numbers"""
+ executable = set(self.metadata['line_map'].values())
+ return executable, set()
+
+ def translate_lines(self, lines):
+ tlines = set()
+ for l in lines:
+ try:
+ tlines.add(self.metadata['full_line_map'][l])
+ except IndexError:
+ pass
+ return tlines
+
+
+class MakoCodeUnit(CodeUnit):
+ def __init__(self, *args, **kwargs):
+ super(MakoCodeUnit, self).__init__(*args, **kwargs)
+ from mako.template import ModuleInfo
+ py_source = open(self.filename).read()
+ self.metadata = ModuleInfo.get_module_source_metadata(py_source, full_line_map=True)
+
+ def get_source(self):
+ return open(self.metadata['filename']).read()
+
+ def get_parser(self, exclude=None):
+ return MakoParser(self.metadata)
+
+ def source_encoding(self):
+ return self.metadata['source_encoding']
+
+
+class DjangoCodeUnit(CodeUnit):
+ def get_source(self):
+ with open(self.filename) as f:
+ return f.read()
+
+ def get_parser(self, exclude=None):
+ return DjangoParser(self.filename)
+
+ def source_encoding(self):
+ return "utf8"
+
+
+class DjangoParser(CodeParser):
+ def __init__(self, filename):
+ self.filename = filename
+
+ def parse_source(self):
+ with open(self.filename) as f:
+ source = f.read()
+ executable = set(range(1, len(source.splitlines())+1))
+ return executable, set()