diff options
-rw-r--r-- | coverage/codeunit.py | 41 | ||||
-rw-r--r-- | coverage/collector.py | 38 | ||||
-rw-r--r-- | coverage/control.py | 18 | ||||
-rw-r--r-- | coverage/django.py | 61 | ||||
-rw-r--r-- | coverage/parser.py | 2 |
5 files changed, 135 insertions, 25 deletions
diff --git a/coverage/codeunit.py b/coverage/codeunit.py index 9282687d..59382c23 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -7,6 +7,8 @@ from coverage.misc import CoverageException, NoSource from coverage.parser import CodeParser, PythonParser from coverage.phystokens import source_token_lines, source_encoding +from coverage.django import DjangoTracer + def code_unit_factory(morfs, file_locator): """Construct a list of CodeUnits from polymorphic inputs. @@ -22,6 +24,8 @@ def code_unit_factory(morfs, file_locator): if not isinstance(morfs, (list, tuple)): morfs = [morfs] + django_tracer = DjangoTracer() + code_units = [] for morf in morfs: # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of @@ -35,6 +39,8 @@ def code_unit_factory(morfs, file_locator): cu.name += '_fako' code_units.append(cu) klass = MakoCodeUnit + elif isinstance(morf, string_class) and morf.endswith(".html"): + klass = DjangoCodeUnit else: klass = PythonCodeUnit code_units.append(klass(morf, file_locator)) @@ -134,6 +140,12 @@ class CodeUnit(object): "No source for code '%s'." % self.filename ) + def source_token_lines(self, source): + """Return the 'tokenized' text for the code.""" + # TODO: Taking source here is wrong, change it? + for line in source.splitlines(): + yield [('txt', line)] + def should_be_python(self): """Does it seem like this file should contain Python? @@ -258,12 +270,29 @@ class MakoCodeUnit(CodeUnit): def get_parser(self, exclude=None): return MakoParser(self.metadata) - def source_token_lines(self, source): - """Return the 'tokenized' text for the code.""" - # TODO: Taking source here is wrong, change it? - for line in source.splitlines(): - yield [('txt', line)] - def source_encoding(self, source): # TODO: Taking source here is wrong, change it! return self.metadata['source_encoding'] + + +class DjangoCodeUnit(CodeUnit): + def 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, source): + 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() diff --git a/coverage/collector.py b/coverage/collector.py index 94af5df5..07a87e03 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -45,6 +45,7 @@ class PyTracer(object): self.should_trace = None self.should_trace_cache = None self.warn = None + self.handler = None self.cur_file_data = None self.last_line = 0 self.data_stack = [] @@ -76,7 +77,7 @@ class PyTracer(object): self.cur_file_data[pair] = None if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] - self.cur_file_data, self.last_line = self.data_stack.pop() + self.handler, self.cur_file_data, self.last_line = self.data_stack.pop() self.last_exc_back = None if event == 'call': @@ -85,19 +86,22 @@ class PyTracer(object): if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] self.last_coroutine = self.coroutine_id_func() - self.data_stack.append((self.cur_file_data, self.last_line)) + self.data_stack.append((self.handler, self.cur_file_data, self.last_line)) filename = frame.f_code.co_filename if filename not in self.should_trace_cache: - tracename = self.should_trace(filename, frame) - self.should_trace_cache[filename] = tracename + tracename, handler = self.should_trace(filename, frame) + self.should_trace_cache[filename] = tracename, handler else: - tracename = self.should_trace_cache[filename] + tracename, handler = self.should_trace_cache[filename] #print("called, stack is %d deep, tracename is %r" % ( # len(self.data_stack), tracename)) + if tracename and handler: + tracename = handler.file_name(frame) if tracename: if tracename not in self.data: self.data[tracename] = {} self.cur_file_data = self.data[tracename] + self.handler = handler else: self.cur_file_data = None # Set the last_line to -1 because the next arc will be entering a @@ -107,14 +111,20 @@ class PyTracer(object): # Record an executed line. #if self.coroutine_id_func: # assert self.last_coroutine == self.coroutine_id_func() - if self.cur_file_data is not None: - if self.arcs: - #print("lin", self.last_line, frame.f_lineno) - self.cur_file_data[(self.last_line, frame.f_lineno)] = None - else: - #print("lin", frame.f_lineno) - self.cur_file_data[frame.f_lineno] = None - self.last_line = frame.f_lineno + if self.handler: + lineno_from, lineno_to = self.handler.line_number_range(frame) + else: + lineno_from, lineno_to = frame.f_lineno, frame.f_lineno + if lineno_from != -1: + if self.cur_file_data is not None: + if self.arcs: + #print("lin", self.last_line, frame.f_lineno) + self.cur_file_data[(self.last_line, lineno_from)] = None + else: + #print("lin", frame.f_lineno) + for lineno in range(lineno_from, lineno_to+1): + self.cur_file_data[lineno] = None + self.last_line = lineno_to elif event == 'return': if self.arcs and self.cur_file_data: first = frame.f_code.co_firstlineno @@ -123,7 +133,7 @@ class PyTracer(object): if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] self.last_coroutine = self.coroutine_id_func() - self.cur_file_data, self.last_line = self.data_stack.pop() + self.handler, self.cur_file_data, self.last_line = self.data_stack.pop() #print("returned, stack is %d deep" % (len(self.data_stack))) elif event == 'exception': #print("exc", self.last_line, frame.f_lineno) diff --git a/coverage/control.py b/coverage/control.py index 14f9b80e..a65a7153 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -18,6 +18,9 @@ from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter +from coverage.django import DjangoTracer + + # Pypy has some unusual stuff in the "stdlib". Consider those locations # when deciding where the stdlib is. try: @@ -152,6 +155,8 @@ class coverage(object): coroutine=self.config.coroutine, ) + self.django_tracer = DjangoTracer() # should this be a class? Singleton... + # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as # `self.run_suffix` now, and promote it to `self.data_suffix` if we @@ -277,6 +282,10 @@ class coverage(object): canonical = self.file_locator.canonical_filename(filename) + # DJANGO HACK + if self.django_tracer.should_trace(canonical): + return canonical, self.django_tracer + # If the user specified source or include, then that's authoritative # about the outer bound of what to measure and we don't have to apply # any canned exclusions. If they didn't, then we have to exclude the @@ -302,7 +311,7 @@ class coverage(object): if self.omit_match and self.omit_match.match(canonical): return None, "is inside an --omit pattern" - return canonical, "because we love you" + return canonical, None def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -310,14 +319,15 @@ class coverage(object): Calls `_should_trace_with_reason`, and returns just the decision. """ - canonical, reason = self._should_trace_with_reason(filename, frame) + canonical, other = self._should_trace_with_reason(filename, frame) if self.debug.should('trace'): if not canonical: - msg = "Not tracing %r: %s" % (filename, reason) + msg = "Not tracing %r: %s" % (filename, other) + other = None else: msg = "Tracing %r" % (filename,) self.debug.write(msg) - return canonical + return canonical, other def _warn(self, msg): """Use `msg` as a warning.""" diff --git a/coverage/django.py b/coverage/django.py new file mode 100644 index 00000000..00f2ed54 --- /dev/null +++ b/coverage/django.py @@ -0,0 +1,61 @@ +import sys + + +ALL_TEMPLATE_MAP = {} + +def get_line_map(filename): + if filename not in ALL_TEMPLATE_MAP: + with open(filename) as template_file: + template_source = template_file.read() + line_lengths = [len(l) for l in template_source.splitlines(True)] + ALL_TEMPLATE_MAP[filename] = list(running_sum(line_lengths)) + return ALL_TEMPLATE_MAP[filename] + +def get_line_number(line_map, offset): + for lineno, line_offset in enumerate(line_map, start=1): + if line_offset >= offset: + return lineno + return -1 + +class DjangoTracer(object): + def should_trace(self, canonical): + return "/django/template/" in canonical + + def source(self, frame): + if frame.f_code.co_name != 'render': + return None + that = frame.f_locals['self'] + return getattr(that, "source", None) + + def file_name(self, frame): + source = self.source(frame) + if not source: + return None + return source[0].name.encode(sys.getfilesystemencoding()) + + def line_number_range(self, frame): + source = self.source(frame) + if not source: + return -1, -1 + filename = source[0].name + line_map = get_line_map(filename) + start = get_line_number(line_map, source[1][0]) + end = get_line_number(line_map, source[1][1]) + if start < 0 or end < 0: + return -1, -1 + return start, end + +def running_sum(seq): + total = 0 + for num in seq: + total += num + yield total + +def ppp(obj): + ret = [] + import inspect + for name, value in inspect.getmembers(obj): + if not callable(value): + ret.append("%s=%r" % (name, value)) + attrs = ", ".join(ret) + return "%s: %s" % (obj.__class__, attrs) diff --git a/coverage/parser.py b/coverage/parser.py index 5bb15466..c5e95baa 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -15,7 +15,7 @@ class CodeParser(object): Base class for any code parser. """ def translate_lines(self, lines): - return lines + return set(lines) def translate_arcs(self, arcs): return arcs |