diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-06 14:24:15 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-06 14:24:15 -0400 |
commit | 0891e02eb490494ee40a7f840e4ab9fd6b3d2d7b (patch) | |
tree | 394c5e68d986579d94a153b15dee8ac977c643bd | |
parent | 7862e1fee15b6ef8fcadfd112cf93121ed8f388e (diff) | |
download | python-coveragepy-git-0891e02eb490494ee40a7f840e4ab9fd6b3d2d7b.tar.gz |
Move dispositions closer to useful plugins
-rw-r--r-- | coverage/__init__.py | 2 | ||||
-rw-r--r-- | coverage/collector.py | 75 | ||||
-rw-r--r-- | coverage/control.py | 133 | ||||
-rw-r--r-- | coverage/plugin.py | 32 | ||||
-rw-r--r-- | coverage/tracer.c | 22 |
5 files changed, 189 insertions, 75 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py index 5ae32aba..1097228f 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -7,7 +7,7 @@ http://nedbatchelder.com/code/coverage from coverage.version import __version__, __url__ -from coverage.control import Coverage, process_startup +from coverage.control import Coverage, process_startup, FileDisposition from coverage.data import CoverageData from coverage.cmdline import main, CoverageScript from coverage.misc import CoverageException diff --git a/coverage/collector.py b/coverage/collector.py index 675758d0..26de3727 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -47,12 +47,12 @@ class PyTracer(object): self.should_trace = None self.should_trace_cache = None self.warn = None - self.plugins = None + self.plugin_data = None + + self.plugin = [] + self.cur_file_dict = [] + self.last_line = [0] - self.plugin = None - self.cur_tracename = None # TODO: This is only maintained for the if0 debugging output. Get rid of it eventually. - self.cur_file_data = None - self.last_line = 0 self.data_stack = [] self.data_stacks = collections.defaultdict(list) self.last_exc_back = None @@ -92,18 +92,18 @@ class PyTracer(object): for ident, data_stacks in self.data_stacks.items() ) , width=250) - pprint.pprint(sorted((self.cur_file_data or {}).keys()), width=250) + pprint.pprint(sorted((self.cur_file_dict or {}).keys()), width=250) print("TRYING: {}".format(sorted(next((v for k,v in self.data.items() if k.endswith("try_it.py")), {}).keys()))) - if self.last_exc_back: + if self.last_exc_back: # TODO: bring this up to speed if frame == self.last_exc_back: # Someone forgot a return event. - if self.arcs and self.cur_file_data: + if self.arcs and self.cur_file_dict: pair = (self.last_line, -self.last_exc_firstlineno) - self.cur_file_data[pair] = None + self.cur_file_dict[pair] = None if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] - self.handler, _, self.cur_file_data, self.last_line = self.data_stack.pop() + self.plugin, self.cur_file_dict, self.last_line = self.data_stack.pop() self.last_exc_back = None if event == 'call': @@ -112,7 +112,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.data_stack.append((self.plugin, self.cur_tracename, self.cur_file_data, self.last_line)) + self.data_stack.append((self.plugin, self.cur_file_dict, self.last_line)) filename = frame.f_code.co_filename disp = self.should_trace_cache.get(filename) if disp is None: @@ -120,19 +120,26 @@ class PyTracer(object): self.should_trace_cache[filename] = disp #print("called, stack is %d deep, tracename is %r" % ( # len(self.data_stack), tracename)) - tracename = disp.filename - if tracename and disp.plugin: - tracename = disp.plugin.file_name(frame) + self.plugin = None + self.cur_file_dict = None + if disp.trace: + tracename = disp.source_filename + if disp.plugin: + dyn_func = disp.plugin.dynamic_source_file_name() + if dyn_func: + tracename = dyn_func(tracename, frame) + if tracename: + if not self.check_include(tracename): + tracename = None + else: + tracename = None if tracename: if tracename not in self.data: self.data[tracename] = {} if disp.plugin: - self.plugins[tracename] = disp.plugin.__name__ - self.cur_tracename = tracename - self.cur_file_data = self.data[tracename] + self.plugin_data[tracename] = disp.plugin.__name__ + self.cur_file_dict = self.data[tracename] self.plugin = disp.plugin - else: - self.cur_file_data = None # Set the last_line to -1 because the next arc will be entering a # code block, indicated by (-1, n). self.last_line = -1 @@ -142,29 +149,30 @@ class PyTracer(object): this_coroutine = self.coroutine_id_func() if self.last_coroutine != this_coroutine: print("mismatch: {0} != {1}".format(self.last_coroutine, this_coroutine)) + if self.plugin: lineno_from, lineno_to = self.plugin.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.cur_file_dict is not None: if self.arcs: #print("lin", self.last_line, frame.f_lineno) - self.cur_file_data[(self.last_line, lineno_from)] = None + self.cur_file_dict[(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.cur_file_dict[lineno] = None self.last_line = lineno_to elif event == 'return': - if self.arcs and self.cur_file_data: + if self.arcs and self.cur_file_dict: first = frame.f_code.co_firstlineno - self.cur_file_data[(self.last_line, -first)] = None + self.cur_file_dict[(self.last_line, -first)] = None # Leaving this function, pop the filename stack. if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] self.last_coroutine = self.coroutine_id_func() - self.plugin, _, self.cur_file_data, self.last_line = self.data_stack.pop() + self.plugin, self.cur_file_dict, 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) @@ -224,13 +232,15 @@ class Collector(object): # the top, and resumed when they become the top again. _collectors = [] - def __init__(self, should_trace, timid, branch, warn, coroutine): + def __init__(self, should_trace, check_include, timid, branch, warn, coroutine): """Create a collector. `should_trace` is a function, taking a filename, and returning a canonicalized filename, or None depending on whether the file should be traced or not. + TODO: `check_include` + If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of tracing functions make the faster more sophisticated trace function not @@ -243,10 +253,14 @@ class Collector(object): `warn` is a warning function, taking a single string message argument, to be used if a warning needs to be issued. + TODO: `coroutine` + """ self.should_trace = should_trace + self.check_include = check_include self.warn = warn self.branch = branch + if coroutine == "greenlet": import greenlet self.coroutine_id_func = greenlet.getcurrent @@ -258,6 +272,7 @@ class Collector(object): self.coroutine_id_func = gevent.getcurrent else: self.coroutine_id_func = None + self.reset() if timid: @@ -281,7 +296,7 @@ class Collector(object): # or mapping filenames to dicts with linenumber pairs as keys. self.data = {} - self.plugins = {} + self.plugin_data = {} # A cache of the results from should_trace, the decision about whether # to trace execution in a file. A dict of filename to (filename or @@ -301,8 +316,8 @@ class Collector(object): tracer.warn = self.warn if hasattr(tracer, 'coroutine_id_func'): tracer.coroutine_id_func = self.coroutine_id_func - if hasattr(tracer, 'plugins'): - tracer.plugins = self.plugins + if hasattr(tracer, 'plugin_data'): + tracer.plugin_data = self.plugin_data fn = tracer.start() self.tracers.append(tracer) return fn @@ -421,4 +436,4 @@ class Collector(object): return {} def get_plugin_data(self): - return self.plugins + return self.plugin_data diff --git a/coverage/control.py b/coverage/control.py index fab2ea19..42550508 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -135,8 +135,14 @@ class Coverage(object): self.debug = DebugControl(self.config.debug, debug_file or sys.stderr) # Load plugins - plugins = load_plugins(self.config.plugins, self.config) - self.tracer_plugins = []#[cls() for cls in tracer_classes] + self.plugins = load_plugins(self.config.plugins, self.config) + + self.trace_judges = [] + for plugin in self.plugins: + trace_judge = plugin.trace_judge() + if trace_judge: + self.trace_judges.append((plugin, trace_judge)) + self.trace_judges.append((None, None)) # The Python case. self.auto_data = auto_data @@ -160,8 +166,11 @@ class Coverage(object): self.include = prep_patterns(self.config.include) self.collector = Collector( - self._should_trace, timid=self.config.timid, - branch=self.config.branch, warn=self._warn, + should_trace=self._should_trace, + check_include=self._tracing_check_include_omit_etc, + timid=self.config.timid, + branch=self.config.branch, + warn=self._warn, coroutine=self.config.coroutine, ) @@ -246,21 +255,11 @@ class Coverage(object): Returns a FileDisposition object. """ - disp = FileDisposition(filename) - - if not filename: - # Empty string is pretty useless - return disp.nope("empty string isn't a filename") - - if filename.startswith('memory:'): - return disp.nope("memory isn't traceable") - - if filename.startswith('<'): - # Lots of non-file execution is represented with artificial - # filenames like "<string>", "<doctest readme.txt[0]>", or - # "<exec_function>". Don't ever trace these executions, since we - # can't do anything with the data later anyway. - return disp.nope("not a real filename") + original_filename = filename + def nope(reason): + disp = FileDisposition(original_filename) + disp.nope(reason) + return disp self._check_for_packages() @@ -274,6 +273,20 @@ class Coverage(object): if dunder_file: filename = self._source_for_file(dunder_file) + if not filename: + # Empty string is pretty useless + return nope("empty string isn't a filename") + + if filename.startswith('memory:'): + return nope("memory isn't traceable") + + if filename.startswith('<'): + # Lots of non-file execution is represented with artificial + # filenames like "<string>", "<doctest readme.txt[0]>", or + # "<exec_function>". Don't ever trace these executions, since we + # can't do anything with the data later anyway. + return nope("not a real filename") + # Jython reports the .class file to the tracer, use the source file. if filename.endswith("$py.class"): filename = filename[:-9] + ".py" @@ -281,39 +294,60 @@ class Coverage(object): canonical = self.file_locator.canonical_filename(filename) # Try the plugins, see if they have an opinion about the file. - for tracer in self.tracer_plugins: - plugin_disp = tracer.should_trace(canonical) - if plugin_disp: - plugin_disp.plugin = tracer - return plugin_disp + for plugin, judge in self.trace_judges: + if plugin: + disp = judge(canonical) + else: + disp = FileDisposition(original_filename) + if disp is not None: + if plugin: + disp.source_filename = plugin.source_file_name(canonical) + else: + disp.source_filename = canonical + disp.plugin = plugin + + reason = self._check_include_omit_etc(disp.source_filename) + if reason: + disp.nope(reason) + + return disp + + return nope("no plugin found") # TODO: a test that causes this. + def _check_include_omit_etc(self, filename): + """Check a filename against the include, omit, etc, rules. + + Returns a string or None. String means, don't trace, and is the reason + why. None means no reason found to not trace. + + """ # 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 # stdlib and coverage.py directories. if self.source_match: - if not self.source_match.match(canonical): - return disp.nope("falls outside the --source trees") + if not self.source_match.match(filename): + return "falls outside the --source trees" elif self.include_match: - if not self.include_match.match(canonical): - return disp.nope("falls outside the --include trees") + if not self.include_match.match(filename): + return "falls outside the --include trees" else: # If we aren't supposed to trace installed code, then check if this # is near the Python standard library and skip it if so. - if self.pylib_match and self.pylib_match.match(canonical): - return disp.nope("is in the stdlib") + if self.pylib_match and self.pylib_match.match(filename): + return "is in the stdlib" # We exclude the coverage code itself, since a little of it will be # measured otherwise. - if self.cover_match and self.cover_match.match(canonical): - return disp.nope("is part of coverage.py") + if self.cover_match and self.cover_match.match(filename): + return "is part of coverage.py" # Check the file against the omit pattern. - if self.omit_match and self.omit_match.match(canonical): - return disp.nope("is inside an --omit pattern") + if self.omit_match and self.omit_match.match(filename): + return "is inside an --omit pattern" - disp.filename = canonical - return disp + # No reason found to skip this file. + return None def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -326,6 +360,22 @@ class Coverage(object): self.debug.write(disp.debug_message()) return disp + def _tracing_check_include_omit_etc(self, filename): + """Check a filename against the include, omit, etc, rules, and say so. + + Returns a boolean: True if the file should be traced, False if not. + + """ + reason = self._check_include_omit_etc(filename) + if self.debug.should('trace'): + if not reason: + msg = "Tracing %r" % (filename,) + else: + msg = "Not tracing %r: %s" % (filename, reason) + self.debug.write(msg) + + return not reason + def _warn(self, msg): """Use `msg` as a warning.""" self._warnings.append(msg) @@ -783,21 +833,22 @@ class FileDisposition(object): """A simple object for noting a number of details of files to trace.""" def __init__(self, original_filename): self.original_filename = original_filename - self.filename = None + self.source_filename = None + self.trace = True self.reason = "" self.plugin = None def nope(self, reason): """A helper for returning a NO answer from should_trace.""" + self.trace = False self.reason = reason - return self def debug_message(self): """Produce a debugging message explaining the outcome.""" - if not self.filename: - msg = "Not tracing %r: %s" % (self.original_filename, self.reason) - else: + if self.trace: msg = "Tracing %r" % (self.original_filename,) + else: + msg = "Not tracing %r: %s" % (self.original_filename, self.reason) return msg diff --git a/coverage/plugin.py b/coverage/plugin.py index 8e26ae6b..84d81a28 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -8,6 +8,37 @@ class CoveragePlugin(object): def __init__(self, options): self.options = options + def trace_judge(self): + """Return a callable that can decide whether to trace a file or not. + + The callable should take a filename, and return a coverage.TraceDisposition + object. + + """ + return None + + # TODO: why does trace_judge return a callable, but source_file_name is callable? + def source_file_name(self, filename): + """Return the source name for a given Python filename. + + Can return None if tracing shouldn't continue. + + """ + return filename + + def dynamic_source_file_name(self): + """Returns a callable that can return a source name for a frame. + + The callable should take a filename and a frame, and return either a + filename or None: + + def dynamic_source_filename_func(filename, frame) + + Can return None if dynamic filenames aren't needed. + + """ + return None + def load_plugins(modules, config): """Load plugins from `modules`. @@ -25,6 +56,7 @@ def load_plugins(modules, config): if plugin_class: options = config.get_plugin_options(module) plugin = plugin_class(options) + plugin.__name__ = module plugins.append(plugin) return plugins diff --git a/coverage/tracer.c b/coverage/tracer.c index ca8d61c1..c0066046 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -260,6 +260,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse PyObject * filename = NULL; PyObject * tracename = NULL; PyObject * disposition = NULL; + PyObject * disp_trace = NULL; #if WHAT_LOG || TRACE_LOG PyObject * ascii = NULL; #endif @@ -358,13 +359,28 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse Py_INCREF(disposition); } - /* If tracename is a string, then we're supposed to trace. */ - tracename = PyObject_GetAttrString(disposition, "filename"); - if (tracename == NULL) { + disp_trace = PyObject_GetAttrString(disposition, "trace"); + if (disp_trace == NULL) { STATS( self->stats.errors++; ) Py_DECREF(disposition); return RET_ERROR; } + + tracename = Py_None; + Py_INCREF(tracename); + + if (disp_trace == Py_True) { + /* If tracename is a string, then we're supposed to trace. */ + tracename = PyObject_GetAttrString(disposition, "source_filename"); + if (tracename == NULL) { + STATS( self->stats.errors++; ) + Py_DECREF(disposition); + Py_DECREF(disp_trace); + return RET_ERROR; + } + } + Py_DECREF(disp_trace); + if (MyText_Check(tracename)) { PyObject * file_data = PyDict_GetItem(self->data, tracename); if (file_data == NULL) { |