diff options
Diffstat (limited to 'coverage')
| -rw-r--r-- | coverage/__init__.py | 2 | ||||
| -rw-r--r-- | coverage/cmdline.py | 21 | ||||
| -rw-r--r-- | coverage/collector.py | 9 | ||||
| -rw-r--r-- | coverage/config.py | 4 | ||||
| -rw-r--r-- | coverage/control.py | 106 | ||||
| -rw-r--r-- | coverage/data.py | 10 | ||||
| -rw-r--r-- | coverage/debug.py | 54 | ||||
| -rw-r--r-- | coverage/files.py | 8 | ||||
| -rw-r--r-- | coverage/html.py | 29 | ||||
| -rw-r--r-- | coverage/parser.py | 5 | ||||
| -rw-r--r-- | coverage/version.py | 2 |
11 files changed, 184 insertions, 66 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py index 0ccc699f..193b7a10 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -92,7 +92,7 @@ except KeyError: # COPYRIGHT AND LICENSE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2012 Ned Batchelder. All rights reserved. +# Copyright 2004-2013 Ned Batchelder. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are diff --git a/coverage/cmdline.py b/coverage/cmdline.py index ac803109..0881313e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -5,6 +5,7 @@ import optparse, os, sys, traceback from coverage.backward import sorted # pylint: disable=W0622 from coverage.execfile import run_python_file, run_python_module from coverage.misc import CoverageException, ExceptionDuringRun, NoSource +from coverage.debug import info_formatter class Opts(object): @@ -19,6 +20,10 @@ class Opts(object): '', '--branch', action='store_true', help="Measure branch coverage in addition to statement coverage." ) + debug = optparse.make_option( + '', '--debug', action='store', metavar="OPTS", + help="Debug options, separated by commas" + ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="DIR", help="Write the output files to DIR." @@ -117,6 +122,7 @@ class CoverageOptionParser(optparse.OptionParser, object): self.set_defaults( actions=[], branch=None, + debug=None, directory=None, fail_under=None, help=None, @@ -310,6 +316,7 @@ CMDS = { [ Opts.append, Opts.branch, + Opts.debug, Opts.pylib, Opts.parallel_mode, Opts.module, @@ -404,6 +411,7 @@ class CoverageScript(object): source = unshell_list(options.source) omit = unshell_list(options.omit) include = unshell_list(options.include) + debug = unshell_list(options.debug) # Do something. self.coverage = self.covpkg.coverage( @@ -415,6 +423,7 @@ class CoverageScript(object): source = source, omit = omit, include = include, + debug = debug, ) if 'debug' in options.actions: @@ -584,16 +593,8 @@ class CoverageScript(object): for info in args: if info == 'sys': print("-- sys ----------------------------------------") - for label, info in self.coverage.sysinfo(): - if info == []: - info = "-none-" - if isinstance(info, list): - prefix = "%15s:" % label - for e in info: - print("%16s %s" % (prefix, e)) - prefix = "" - else: - print("%15s: %s" % (label, info)) + for line in info_formatter(self.coverage.sysinfo()): + print(" %s" % line) elif info == 'data': print("-- data ---------------------------------------") self.coverage.load() diff --git a/coverage/collector.py b/coverage/collector.py index 781a0fa6..9a74700d 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -75,10 +75,11 @@ class PyTracer(object): # in this file. self.data_stack.append((self.cur_file_data, self.last_line)) filename = frame.f_code.co_filename - tracename = self.should_trace_cache.get(filename) - if tracename is None: + if filename not in self.should_trace_cache: tracename = self.should_trace(filename, frame) self.should_trace_cache[filename] = tracename + else: + tracename = self.should_trace_cache[filename] #print("called, stack is %d deep, tracename is %r" % ( # len(self.data_stack), tracename)) if tracename: @@ -166,7 +167,7 @@ class Collector(object): """Create a collector. `should_trace` is a function, taking a filename, and returning a - canonicalized filename, or False depending on whether the file should + canonicalized filename, or None depending on whether the file should be traced or not. If `timid` is true, then a slower simpler trace function will be @@ -210,7 +211,7 @@ class Collector(object): # 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 - # False). + # None). self.should_trace_cache = {} # Our active Tracers. diff --git a/coverage/config.py b/coverage/config.py index c2ebecb2..87318ff1 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -111,6 +111,7 @@ class CoverageConfig(object): self.parallel = False self.timid = False self.source = None + self.debug = [] # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] @@ -142,7 +143,7 @@ class CoverageConfig(object): if env: self.timid = ('--timid' in env) - MUST_BE_LIST = ["omit", "include"] + MUST_BE_LIST = ["omit", "include", "debug"] def from_args(self, **kwargs): """Read config values from `kwargs`.""" @@ -178,6 +179,7 @@ class CoverageConfig(object): ('branch', 'run:branch', 'boolean'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), + ('debug', 'run:debug', 'list'), ('include', 'run:include', 'list'), ('omit', 'run:omit', 'list'), ('parallel', 'run:parallel', 'boolean'), diff --git a/coverage/control.py b/coverage/control.py index 8821bfbb..4b76121c 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -3,11 +3,12 @@ import atexit, os, random, socket, sys from coverage.annotate import AnnotateReporter -from coverage.backward import string_class, iitems +from coverage.backward import string_class, iitems, sorted # pylint: disable=W0622 from coverage.codeunit import code_unit_factory, CodeUnit from coverage.collector import Collector from coverage.config import CoverageConfig from coverage.data import CoverageData +from coverage.debug import DebugControl from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.html import HtmlReporter @@ -17,6 +18,14 @@ from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter +# Pypy has some unusual stuff in the "stdlib". Consider those locations +# when deciding where the stdlib is. +try: + import _structseq # pylint: disable=F0401 +except ImportError: + _structseq = None + + class coverage(object): """Programmatic access to coverage.py. @@ -33,7 +42,8 @@ class coverage(object): """ def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, - source=None, omit=None, include=None): + source=None, omit=None, include=None, debug=None, + debug_file=None): """ `data_file` is the base name of the data file to use, defaulting to ".coverage". `data_suffix` is appended (with a dot) to `data_file` to @@ -68,6 +78,10 @@ class coverage(object): `include` will be measured, files that match `omit` will not. Each will also accept a single string argument. + `debug` is a list of strings indicating what debugging information is + desired. `debug_file` is the file to write debug messages to, + defaulting to stderr. + """ from coverage import __version__ @@ -100,9 +114,12 @@ class coverage(object): self.config.from_args( data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), - source=source, omit=omit, include=include + source=source, omit=omit, include=include, debug=debug, ) + # Create and configure the debugging controller. + self.debug = DebugControl(self.config.debug, debug_file or sys.stderr) + self.auto_data = auto_data # _exclude_re is a dict mapping exclusion list names to compiled @@ -147,7 +164,8 @@ class coverage(object): # started rather than wherever the process eventually chdir'd to. self.data = CoverageData( basename=self.config.data_file, - collector="coverage v%s" % __version__ + collector="coverage v%s" % __version__, + debug=self.debug, ) # The dirs for files considered "installed with the interpreter". @@ -158,8 +176,8 @@ class coverage(object): # environments (virtualenv, for example), these modules may be # spread across a few locations. Look at all the candidate modules # we've imported, and take all the different ones. - for m in (atexit, os, random, socket): - if hasattr(m, "__file__"): + for m in (atexit, os, random, socket, _structseq): + if m is not None and hasattr(m, "__file__"): m_dir = self._canonical_dir(m) if m_dir not in self.pylib_dirs: self.pylib_dirs.append(m_dir) @@ -168,7 +186,7 @@ class coverage(object): # where we are. self.cover_dir = self._canonical_dir(__file__) - # The matchers for _should_trace, created when tracing starts. + # The matchers for _should_trace. self.source_match = None self.pylib_match = self.cover_match = None self.include_match = self.omit_match = None @@ -201,26 +219,28 @@ class coverage(object): filename = filename[:-9] + ".py" return filename - def _should_trace(self, filename, frame): - """Decide whether to trace execution in `filename` + def _should_trace_with_reason(self, filename, frame): + """Decide whether to trace execution in `filename`, with a reason. This function is called from the trace function. As each new file name is encountered, this function determines whether it is traced or not. - Returns a canonicalized filename if it should be traced, False if it - should not. + Returns a pair of values: the first indicates whether the file should + be traced: it's a canonicalized filename if it should be traced, None + if it should not. The second value is a string, the resason for the + decision. """ if not filename: # Empty string is pretty useless - return False + return None, "empty string isn't a filename" 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 False + return None, "not a real filename" self._check_for_packages() @@ -246,35 +266,41 @@ class coverage(object): # stdlib and coverage.py directories. if self.source_match: if not self.source_match.match(canonical): - return False + return None, "falls outside the --source trees" elif self.include_match: if not self.include_match.match(canonical): - return False + return None, "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 False + return None, "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 False + return None, "is part of coverage.py" # Check the file against the omit pattern. if self.omit_match and self.omit_match.match(canonical): - return False + return None, "is inside an --omit pattern" - return canonical + return canonical, "because we love you" + + def _should_trace(self, filename, frame): + """Decide whether to trace execution in `filename`. - # To log what should_trace returns, change this to "if 1:" - if 0: - _real_should_trace = _should_trace - def _should_trace(self, filename, frame): # pylint: disable=E0102 - """A logging decorator around the real _should_trace function.""" - ret = self._real_should_trace(filename, frame) - print("should_trace: %r -> %r" % (filename, ret)) - return ret + Calls `_should_trace_with_reason`, and returns just the decision. + + """ + canonical, reason = self._should_trace_with_reason(filename, frame) + if self.debug.should('trace'): + if not canonical: + msg = "Not tracing %r: %s" % (filename, reason) + else: + msg = "Tracing %r" % (filename,) + self.debug.write(msg) + return canonical def _warn(self, msg): """Use `msg` as a warning.""" @@ -364,6 +390,16 @@ class coverage(object): if self.omit: self.omit_match = FnmatchMatcher(self.omit) + # The user may want to debug things, show info if desired. + if self.debug.should('config'): + self.debug.write("Configuration values:") + config_info = sorted(self.config.__dict__.items()) + self.debug.write_formatted_info(config_info) + + if self.debug.should('sys'): + self.debug.write("Debugging info:") + self.debug.write_formatted_info(self.sysinfo()) + self.collector.start() self._started = True self._measured = True @@ -688,11 +724,23 @@ class coverage(object): ('executable', sys.executable), ('cwd', os.getcwd()), ('path', sys.path), - ('environment', [ + ('environment', sorted([ ("%s = %s" % (k, v)) for k, v in iitems(os.environ) if re.search(r"^COV|^PY", k) - ]), + ])), + ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] + if self.source_match: + info.append(('source_match', self.source_match.info())) + if self.include_match: + info.append(('include_match', self.include_match.info())) + if self.omit_match: + info.append(('omit_match', self.omit_match.info())) + if self.cover_match: + info.append(('cover_match', self.cover_match.info())) + if self.pylib_match: + info.append(('pylib_match', self.pylib_match.info())) + return info diff --git a/coverage/data.py b/coverage/data.py index c86a77f2..fb88c5b1 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -23,15 +23,18 @@ class CoverageData(object): """ - def __init__(self, basename=None, collector=None): + def __init__(self, basename=None, collector=None, debug=None): """Create a CoverageData. `basename` is the name of the file to use for storing data. `collector` is a string describing the coverage measurement software. + `debug` is a `DebugControl` object for writing debug messages. + """ self.collector = collector or 'unknown' + self.debug = debug self.use_file = True @@ -121,6 +124,9 @@ class CoverageData(object): if self.collector: data['collector'] = self.collector + if self.debug and self.debug.should('dataio'): + self.debug.write("Writing data to %r" % (filename,)) + # Write the pickle to the file. fdata = open(filename, 'wb') try: @@ -134,6 +140,8 @@ class CoverageData(object): def raw_data(self, filename): """Return the raw pickled data from `filename`.""" + if self.debug and self.debug.should('dataio'): + self.debug.write("Reading data from %r" % (filename,)) fdata = open(filename, 'rb') try: data = pickle.load(fdata) diff --git a/coverage/debug.py b/coverage/debug.py new file mode 100644 index 00000000..104f3b1d --- /dev/null +++ b/coverage/debug.py @@ -0,0 +1,54 @@ +"""Control of and utilities for debugging.""" + +import os + + +# When debugging, it can be helpful to force some options, especially when +# debugging the configuration mechanisms you usually use to control debugging! +# This is a list of forced debugging options. +FORCED_DEBUG = [] + + +class DebugControl(object): + """Control and output for debugging.""" + + def __init__(self, options, output): + """Configure the options and output file for debugging.""" + self.options = options + self.output = output + + def should(self, option): + """Decide whether to output debug information in category `option`.""" + return (option in self.options or option in FORCED_DEBUG) + + def write(self, msg): + """Write a line of debug output.""" + if self.should('pid'): + msg = "pid %5d: %s" % (os.getpid(), msg) + self.output.write(msg+"\n") + self.output.flush() + + def write_formatted_info(self, info): + """Write a sequence of (label,data) pairs nicely.""" + for line in info_formatter(info): + self.write(" %s" % line) + + +def info_formatter(info): + """Produce a sequence of formatted lines from info. + + `info` is a sequence of pairs (label, data). The produced lines are + nicely formatted, ready to print. + + """ + label_len = max([len(l) for l, _d in info]) + for label, data in info: + if data == []: + data = "-none-" + if isinstance(data, (list, tuple)): + prefix = "%*s:" % (label_len, label) + for e in data: + yield "%*s %s" % (label_len+1, prefix, e) + prefix = "" + else: + yield "%*s: %s" % (label_len, label, data) diff --git a/coverage/files.py b/coverage/files.py index 4c55151e..8d154c6f 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -144,6 +144,10 @@ class TreeMatcher(object): def __repr__(self): return "<TreeMatcher %r>" % self.dirs + def info(self): + """A list of strings for displaying when dumping state.""" + return self.dirs + def add(self, directory): """Add another directory to the list we match for.""" self.dirs.append(directory) @@ -169,6 +173,10 @@ class FnmatchMatcher(object): def __repr__(self): return "<FnmatchMatcher %r>" % self.pats + def info(self): + """A list of strings for displaying when dumping state.""" + return self.pats + def match(self, fpath): """Does `fpath` match one of our filename patterns?""" for pat in self.pats: diff --git a/coverage/html.py b/coverage/html.py index ed8920f2..aef43be3 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -10,10 +10,6 @@ from coverage.report import Reporter from coverage.results import Numbers from coverage.templite import Templite -# Disable pylint msg W0612, because a bunch of variables look unused, but -# they're accessed in a Templite context via locals(). -# pylint: disable=W0612 - def data_filename(fname): """Return the path to a data file of ours.""" return os.path.join(os.path.split(__file__)[0], fname) @@ -162,7 +158,6 @@ class HtmlReporter(Reporter): nums = analysis.numbers missing_branch_arcs = analysis.missing_branch_arcs() - arcs = self.arcs # These classes determine which lines are highlighted by default. c_run = "run hide_run" @@ -220,13 +215,17 @@ class HtmlReporter(Reporter): }) # Write the HTML page for this file. - html_filename = flat_rootname + ".html" - html_path = os.path.join(self.directory, html_filename) - extra_css = self.extra_css + html = spaceless(self.source_tmpl.render({ + 'c_exc': c_exc, 'c_mis': c_mis, 'c_par': c_par, 'c_run': c_run, + 'arcs': self.arcs, 'extra_css': self.extra_css, + 'cu': cu, 'nums': nums, 'lines': lines, + })) - html = spaceless(self.source_tmpl.render(locals())) if sys.version_info < (3, 0): html = html.decode(encoding) + + html_filename = flat_rootname + ".html" + html_path = os.path.join(self.directory, html_filename) self.write_html(html_path, html) # Save this file's information for the index file. @@ -244,13 +243,15 @@ class HtmlReporter(Reporter): data("htmlfiles/index.html"), self.template_globals ) - files = self.files - arcs = self.arcs + self.totals = sum([f['nums'] for f in self.files]) - self.totals = totals = sum([f['nums'] for f in files]) - extra_css = self.extra_css + html = index_tmpl.render({ + 'arcs': self.arcs, + 'extra_css': self.extra_css, + 'files': self.files, + 'totals': self.totals, + }) - html = index_tmpl.render(locals()) if sys.version_info < (3, 0): html = html.decode("utf-8") self.write_html( diff --git a/coverage/parser.py b/coverage/parser.py index 8d6a077b..2d777a5d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -512,11 +512,6 @@ class ByteParser(object): chunk.exits.add(block_stack[-1][1]) chunk = None if bc.op == OP_END_FINALLY: - if block_stack: - # A break that goes through a finally will jump to whatever - # block is on top of the stack. - # print self._block_stack_repr(block_stack) - chunk.exits.add(block_stack[-1][1]) # For the finally clause we need to find the closest exception # block, and use its jump target as an exit. for block in reversed(block_stack): diff --git a/coverage/version.py b/coverage/version.py index 422fbadb..db4bca5d 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -1,7 +1,7 @@ """The version and URL for coverage.py""" # This file is exec'ed in setup.py, don't import anything! -__version__ = "3.6.1a0" # see detailed history in CHANGES.txt +__version__ = "3.6.1a1" # see detailed history in CHANGES.txt __url__ = "http://nedbatchelder.com/code/coverage" if max(__version__).isalpha(): |
