diff options
Diffstat (limited to 'coverage')
| -rw-r--r-- | coverage/__init__.py | 6 | ||||
| -rw-r--r-- | coverage/backward.py | 31 | ||||
| -rw-r--r-- | coverage/bytecode.py | 2 | ||||
| -rw-r--r-- | coverage/cmdline.py | 101 | ||||
| -rw-r--r-- | coverage/collector.py | 2 | ||||
| -rw-r--r-- | coverage/config.py | 95 | ||||
| -rw-r--r-- | coverage/control.py | 189 | ||||
| -rw-r--r-- | coverage/data.py | 19 | ||||
| -rw-r--r-- | coverage/execfile.py | 28 | ||||
| -rw-r--r-- | coverage/files.py | 7 | ||||
| -rw-r--r-- | coverage/html.py | 32 | ||||
| -rw-r--r-- | coverage/htmlfiles/coverage_html.js | 21 | ||||
| -rw-r--r-- | coverage/htmlfiles/index.html | 161 | ||||
| -rw-r--r-- | coverage/htmlfiles/pyfile.html | 106 | ||||
| -rw-r--r-- | coverage/htmlfiles/style.css | 4 | ||||
| -rw-r--r-- | coverage/misc.py | 16 | ||||
| -rw-r--r-- | coverage/parser.py | 17 | ||||
| -rw-r--r-- | coverage/phystokens.py | 3 | ||||
| -rw-r--r-- | coverage/summary.py | 2 | ||||
| -rw-r--r-- | coverage/templite.py | 8 |
20 files changed, 567 insertions, 283 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py index 4db12408..9fd4a8d2 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -5,11 +5,11 @@ http://nedbatchelder.com/code/coverage """ -__version__ = "3.2b4" # see detailed history in CHANGES.txt +__version__ = "3.3.2a1" # see detailed history in CHANGES.txt __url__ = "http://nedbatchelder.com/code/coverage" -from coverage.control import coverage +from coverage.control import coverage, process_startup from coverage.data import CoverageData from coverage.cmdline import main, CoverageScript from coverage.misc import CoverageException @@ -55,7 +55,7 @@ annotate = _singleton_method('annotate') # COPYRIGHT AND LICENSE # # Copyright 2001 Gareth Rees. All rights reserved. -# Copyright 2004-2009 Ned Batchelder. All rights reserved. +# Copyright 2004-2010 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/backward.py b/coverage/backward.py index 66cfbb96..425bcc6e 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -14,7 +14,6 @@ try: except NameError: from sets import Set as set - # Python 2.3 doesn't have `sorted`. try: sorted = sorted @@ -26,7 +25,6 @@ except NameError: return lst # Pythons 2 and 3 differ on where to get StringIO - try: from cStringIO import StringIO BytesIO = StringIO @@ -34,39 +32,42 @@ except ImportError: from io import StringIO, BytesIO # What's a string called? - try: string_class = basestring except NameError: string_class = str # Where do pickles come from? - try: import cPickle as pickle except ImportError: import pickle # range or xrange? - try: range = xrange except NameError: range = range # Exec is a statement in Py2, a function in Py3 - -if sys.hexversion > 0x03000000: - def exec_function(source, filename, global_map): +if sys.version_info >= (3, 0): + def exec_code_object(code, global_map): """A wrapper around exec().""" - exec(compile(source, filename, "exec"), global_map) + exec(code, global_map) else: # OK, this is pretty gross. In Py2, exec was a statement, but that will # be a syntax error if we try to put it in a Py3 file, even if it is never # executed. So hide it inside an evaluated string literal instead. - eval(compile("""\ -def exec_function(source, filename, global_map): - exec compile(source, filename, "exec") in global_map -""", - "<exec_function>", "exec" - )) + eval( + compile( + "def exec_code_object(code, global_map):\n" + " exec code in global_map\n", + "<exec_function>", "exec" + ) + ) + +# ConfigParser was renamed to the more-standard configparser +try: + import configparser +except ImportError: + import ConfigParser as configparser diff --git a/coverage/bytecode.py b/coverage/bytecode.py index ac280342..ab522d6c 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -22,7 +22,7 @@ class ByteCodes(object): self.code = code self.offset = 0 - if sys.hexversion > 0x03000000: + if sys.version_info >= (3, 0): def __getitem__(self, i): return self.code[i] else: diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 27c28d48..9e15074b 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,9 +1,10 @@ """Command-line support for Coverage.""" -import optparse, re, sys +import optparse, re, sys, traceback +from coverage.backward import sorted # pylint: disable-msg=W0622 from coverage.execfile import run_python_file -from coverage.misc import CoverageException +from coverage.misc import CoverageException, ExceptionDuringRun class Opts(object): @@ -60,8 +61,13 @@ class Opts(object): ) parallel_mode = optparse.Option( '-p', '--parallel-mode', action='store_true', - help="Include the machine name and process id in the .coverage " - "data file name." + help="Append the machine name, process id and random number to the " + ".coverage data file name to simplify collecting data from " + "many processes." + ) + rcfile = optparse.Option( + '', '--rcfile', action='store', + help="Specify configuration file. Defaults to '.coveragerc'" ) timid = optparse.Option( '', '--timid', action='store_true', @@ -95,6 +101,7 @@ class CoverageOptionParser(optparse.OptionParser, object): omit=None, parallel_mode=None, pylib=None, + rcfile=True, show_missing=None, timid=None, erase_first=None, @@ -102,7 +109,11 @@ class CoverageOptionParser(optparse.OptionParser, object): ) self.disable_interspersed_args() - self.help_fn = lambda: None + self.help_fn = self.help_noop + + def help_noop(self, error=None, topic=None, parser=None): + """No-op help function.""" + pass class OptionParserError(Exception): """Used to stop the optparse error handler ending the process.""" @@ -197,6 +208,10 @@ class CmdOptionParser(CoverageOptionParser): # results, and they will compare equal to objects. return (other == "<CmdOptionParser:%s>" % self.cmd) +GLOBAL_ARGS = [ + Opts.rcfile, + Opts.help, + ] CMDS = { 'annotate': CmdOptionParser("annotate", @@ -204,40 +219,21 @@ CMDS = { Opts.directory, Opts.ignore_errors, Opts.omit, - Opts.help, - ], + ] + GLOBAL_ARGS, usage = "[options] [modules]", description = "Make annotated copies of the given files, marking " "statements that are executed with > and statements that are " "missed with !." ), - 'help': CmdOptionParser("help", [Opts.help], - usage = "[command]", - description = "Describe how to use coverage.py" - ), - - 'html': CmdOptionParser("html", - [ - Opts.directory, - Opts.ignore_errors, - Opts.omit, - Opts.help, - ], - usage = "[options] [modules]", - description = "Create an HTML report of the coverage of the files. " - "Each file gets its own page, with the source decorated to show " - "executed, excluded, and missed lines." - ), - - 'combine': CmdOptionParser("combine", [Opts.help], + 'combine': CmdOptionParser("combine", GLOBAL_ARGS, usage = " ", description = "Combine data from multiple coverage files collected " "with 'run -p'. The combined results are written to a single " "file representing the union of the data." ), - 'debug': CmdOptionParser("debug", [Opts.help], + 'debug': CmdOptionParser("debug", GLOBAL_ARGS, usage = "<topic>", description = "Display information on the internals of coverage.py, " "for diagnosing problems. " @@ -245,18 +241,34 @@ CMDS = { "or 'sys' to show installation information." ), - 'erase': CmdOptionParser("erase", [Opts.help], + 'erase': CmdOptionParser("erase", GLOBAL_ARGS, usage = " ", description = "Erase previously collected coverage data." ), + 'help': CmdOptionParser("help", GLOBAL_ARGS, + usage = "[command]", + description = "Describe how to use coverage.py" + ), + + 'html': CmdOptionParser("html", + [ + Opts.directory, + Opts.ignore_errors, + Opts.omit, + ] + GLOBAL_ARGS, + usage = "[options] [modules]", + description = "Create an HTML report of the coverage of the files. " + "Each file gets its own page, with the source decorated to show " + "executed, excluded, and missed lines." + ), + 'report': CmdOptionParser("report", [ Opts.ignore_errors, Opts.omit, Opts.show_missing, - Opts.help, - ], + ] + GLOBAL_ARGS, usage = "[options] [modules]", description = "Report coverage statistics on modules." ), @@ -268,8 +280,7 @@ CMDS = { Opts.pylib, Opts.parallel_mode, Opts.timid, - Opts.help, - ], + ] + GLOBAL_ARGS, defaults = {'erase_first': True}, cmd = "run", usage = "[options] <pyfile> [program options]", @@ -281,8 +292,7 @@ CMDS = { Opts.ignore_errors, Opts.omit, Opts.output_xml, - Opts.help, - ], + ] + GLOBAL_ARGS, cmd = "xml", defaults = {'outfile': 'coverage.xml'}, usage = "[options] [modules]", @@ -418,10 +428,11 @@ class CoverageScript(object): # Do something. self.coverage = self.covpkg.coverage( - data_suffix = bool(options.parallel_mode), + data_suffix = options.parallel_mode, cover_pylib = options.pylib, timid = options.timid, branch = options.branch, + config_file = options.rcfile, ) if 'debug' in options.actions: @@ -496,8 +507,6 @@ class CoverageScript(object): directory=options.directory, **report_args) if 'xml' in options.actions: outfile = options.outfile - if outfile == '-': - outfile = None self.coverage.xml_report(outfile=outfile, **report_args) return OK @@ -580,16 +589,30 @@ Coverage.py, version %(__version__)s. %(__url__)s """ -def main(): +def main(argv=None): """The main entrypoint to Coverage. This is installed as the script entrypoint. """ + if argv is None: + argv = sys.argv[1:] try: - status = CoverageScript().command_line(sys.argv[1:]) + status = CoverageScript().command_line(argv) + except ExceptionDuringRun: + # An exception was caught while running the product code. The + # sys.exc_info() return tuple is packed into an ExceptionDuringRun + # exception. + _, err, _ = sys.exc_info() + traceback.print_exception(*err.args) + status = ERR except CoverageException: + # A controlled error inside coverage.py: print the message to the user. _, err, _ = sys.exc_info() print(err) status = ERR + except SystemExit: + # The user called `sys.exit()`. Exit with their status code. + _, err, _ = sys.exc_info() + status = err.args[0] return status diff --git a/coverage/collector.py b/coverage/collector.py index 29dddf6b..1837aae2 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -251,7 +251,7 @@ class Collector(object): line_data = {} for f, arcs in self.data.items(): line_data[f] = ldf = {} - for l1, _ in arcs: + for l1, _ in list(arcs.keys()): if l1: ldf[l1] = None return line_data diff --git a/coverage/config.py b/coverage/config.py new file mode 100644 index 00000000..8f508f28 --- /dev/null +++ b/coverage/config.py @@ -0,0 +1,95 @@ +"""Config file for coverage.py""" + +import os +from coverage.backward import configparser # pylint: disable-msg=W0622 + + +class CoverageConfig(object): + """Coverage.py configuration. + + The attributes of this class are the various settings that control the + operation of coverage.py. + + """ + + def __init__(self): + """Initialize the configuration attributes to their defaults.""" + # Defaults for [run] + self.branch = False + self.cover_pylib = False + self.data_file = ".coverage" + self.parallel = False + self.timid = False + + # Defaults for [report] + self.exclude_list = ['(?i)# *pragma[: ]*no *cover'] + self.ignore_errors = False + self.omit_prefixes = None + + # Defaults for [html] + self.html_dir = "htmlcov" + + # Defaults for [xml] + self.xml_output = "coverage.xml" + + def from_environment(self, env_var): + """Read configuration from the `env_var` environment variable.""" + # Timidity: for nose users, read an environment variable. This is a + # cheap hack, since the rest of the command line arguments aren't + # recognized, but it solves some users' problems. + env = os.environ.get(env_var, '') + if env: + self.timid = ('--timid' in env) + + def from_args(self, **kwargs): + """Read config values from `kwargs`.""" + for k, v in kwargs.items(): + if v is not None: + setattr(self, k, v) + + def from_file(self, *files): + """Read configuration from .rc files. + + Each argument in `files` is a file name to read. + + """ + cp = configparser.RawConfigParser() + cp.read(files) + + # [run] + if cp.has_option('run', 'branch'): + self.branch = cp.getboolean('run', 'branch') + if cp.has_option('run', 'cover_pylib'): + self.cover_pylib = cp.getboolean('run', 'cover_pylib') + if cp.has_option('run', 'data_file'): + self.data_file = cp.get('run', 'data_file') + if cp.has_option('run', 'parallel'): + self.parallel = cp.getboolean('run', 'parallel') + if cp.has_option('run', 'timid'): + self.timid = cp.getboolean('run', 'timid') + + # [report] + if cp.has_option('report', 'exclude_lines'): + # exclude_lines is a list of lines, leave out the blank ones. + exclude_list = cp.get('report', 'exclude_lines') + self.exclude_list = list(filter(None, exclude_list.split('\n'))) + if cp.has_option('report', 'ignore_errors'): + self.ignore_errors = cp.getboolean('report', 'ignore_errors') + if cp.has_option('report', 'omit'): + # omit is a list of prefixes, on separate lines, or separated by + # commas. + omit_list = cp.get('report', 'omit') + self.omit_prefixes = [] + for omit_line in omit_list.split('\n'): + for omit in omit_line.split(','): + omit = omit.strip() + if omit: + self.omit_prefixes.append(omit) + + # [html] + if cp.has_option('html', 'directory'): + self.html_dir = cp.get('html', 'directory') + + # [xml] + if cp.has_option('xml', 'output'): + self.xml_output = cp.get('xml', 'output') diff --git a/coverage/control.py b/coverage/control.py index 15bbe982..1456178a 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,14 +1,16 @@ """Core control stuff for Coverage.""" -import atexit, os, socket +import atexit, os, random, socket, sys from coverage.annotate import AnnotateReporter -from coverage.backward import string_class # pylint: disable-msg=W0622 +from coverage.backward import string_class 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.files import FileLocator from coverage.html import HtmlReporter +from coverage.misc import bool_or_none from coverage.results import Analysis from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -28,13 +30,13 @@ class coverage(object): """ - def __init__(self, data_file=None, data_suffix=False, cover_pylib=False, - auto_data=False, timid=False, branch=False): + def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, + auto_data=False, timid=None, branch=None, config_file=True): """ `data_file` is the base name of the data file to use, defaulting to - ".coverage". `data_suffix` is appended to `data_file` to create the - final file name. If `data_suffix` is simply True, then a suffix is - created with the machine and process identity included. + ".coverage". `data_suffix` is appended (with a dot) to `data_file` to + create the final file name. If `data_suffix` is simply True, then a + suffix is created with the machine and process identity included. `cover_pylib` is a boolean determining whether Python code installed with the Python interpreter is measured. This includes the Python @@ -51,44 +53,67 @@ class coverage(object): If `branch` is true, then branch coverage will be measured in addition to the usual statement coverage. + `config_file` determines what config file to read. If it is a string, + it is the name of the config file to read. If it is True, then a + standard file is read (".coveragerc"). If it is False, then no file is + read. + """ from coverage import __version__ - self.cover_pylib = cover_pylib + # Build our configuration from a number of sources: + # 1: defaults: + self.config = CoverageConfig() + + # 2: from the coveragerc file: + if config_file: + if config_file is True: + config_file = ".coveragerc" + self.config.from_file(config_file) + + # 3: from environment variables: + self.config.from_environment('COVERAGE_OPTIONS') + env_data_file = os.environ.get('COVERAGE_FILE') + if env_data_file: + self.config.data_file = env_data_file + + # 4: from constructor arguments: + self.config.from_args( + data_file=data_file, cover_pylib=cover_pylib, timid=timid, + branch=branch, parallel=bool_or_none(data_suffix) + ) + self.auto_data = auto_data self.atexit_registered = False self.exclude_re = "" - self.exclude_list = [] + self._compile_exclude() self.file_locator = FileLocator() - # Timidity: for nose users, read an environment variable. This is a - # cheap hack, since the rest of the command line arguments aren't - # recognized, but it solves some users' problems. - timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', '')) self.collector = Collector( - self._should_trace, timid=timid, branch=branch + self._should_trace, timid=self.config.timid, + branch=self.config.branch ) # Create the data file. - if data_suffix: + if data_suffix or self.config.parallel: if not isinstance(data_suffix, string_class): - # if data_suffix=True, use .machinename.pid - data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid()) + # if data_suffix=True, use .machinename.pid.random + data_suffix = "%s.%s.%06d" % ( + socket.gethostname(), os.getpid(), random.randint(0, 99999) + ) else: data_suffix = None + self.run_suffix = data_suffix self.data = CoverageData( - basename=data_file, suffix=data_suffix, + basename=self.config.data_file, collector="coverage v%s" % __version__ ) - # The default exclude pattern. - self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') - # The prefix for files considered "installed with the interpreter". - if not self.cover_pylib: + if not self.config.cover_pylib: # Look at where the "os" module is located. That's the indication # for "installed with the interpreter". os_file = self.file_locator.canonical_filename(os.__file__) @@ -131,7 +156,7 @@ class coverage(object): # 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 not self.cover_pylib: + if not self.config.cover_pylib: if canonical.startswith(self.pylib_prefix): return False @@ -166,6 +191,14 @@ class coverage(object): def start(self): """Start measuring code coverage.""" + if self.run_suffix: + # If the .coveragerc file specifies parallel=True, then we need to + # remake the data file for collection, with a suffix. + from coverage import __version__ + self.data = CoverageData( + basename=self.config.data_file, suffix=self.run_suffix, + collector="coverage v%s" % __version__ + ) if self.auto_data: self.load() # Save coverage data when Python exits. @@ -191,7 +224,7 @@ class coverage(object): def clear_exclude(self): """Clear the exclude list.""" - self.exclude_list = [] + self.config.exclude_list = [] self.exclude_re = "" def exclude(self, regex): @@ -203,12 +236,16 @@ class coverage(object): Matching any of the regexes excludes a source line. """ - self.exclude_list.append(regex) - self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")" + self.config.exclude_list.append(regex) + self._compile_exclude() + + def _compile_exclude(self): + """Build the internal usable form of the exclude list.""" + self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")" def get_exclude_list(self): """Return the list of excluded regex patterns.""" - return self.exclude_list + return self.config.exclude_list def save(self): """Save the collected coverage data to the data file.""" @@ -271,7 +308,7 @@ class coverage(object): return Analysis(self, it) - def report(self, morfs=None, show_missing=True, ignore_errors=False, + def report(self, morfs=None, show_missing=True, ignore_errors=None, file=None, omit_prefixes=None): # pylint: disable-msg=W0622 """Write a summary report to `file`. @@ -279,10 +316,18 @@ class coverage(object): statements, missing statements, and a list of lines missed. """ - reporter = SummaryReporter(self, show_missing, ignore_errors) - reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes) + self.config.from_args( + ignore_errors=ignore_errors, + omit_prefixes=omit_prefixes + ) + reporter = SummaryReporter( + self, show_missing, self.config.ignore_errors + ) + reporter.report( + morfs, outfile=file, omit_prefixes=self.config.omit_prefixes + ) - def annotate(self, morfs=None, directory=None, ignore_errors=False, + def annotate(self, morfs=None, directory=None, ignore_errors=None, omit_prefixes=None): """Annotate a list of modules. @@ -292,40 +337,67 @@ class coverage(object): excluded lines have "-", and missing lines have "!". """ - reporter = AnnotateReporter(self, ignore_errors) + self.config.from_args( + ignore_errors=ignore_errors, + omit_prefixes=omit_prefixes + ) + reporter = AnnotateReporter(self, self.config.ignore_errors) reporter.report( - morfs, directory=directory, omit_prefixes=omit_prefixes) + morfs, directory=directory, omit_prefixes=self.config.omit_prefixes + ) - def html_report(self, morfs=None, directory=None, ignore_errors=False, + def html_report(self, morfs=None, directory=None, ignore_errors=None, omit_prefixes=None): """Generate an HTML report. """ - reporter = HtmlReporter(self, ignore_errors) + self.config.from_args( + ignore_errors=ignore_errors, + omit_prefixes=omit_prefixes, + html_dir=directory, + ) + reporter = HtmlReporter(self, self.config.ignore_errors) reporter.report( - morfs, directory=directory, omit_prefixes=omit_prefixes) + morfs, directory=self.config.html_dir, + omit_prefixes=self.config.omit_prefixes + ) - def xml_report(self, morfs=None, outfile=None, ignore_errors=False, + def xml_report(self, morfs=None, outfile=None, ignore_errors=None, omit_prefixes=None): """Generate an XML report of coverage results. The report is compatible with Cobertura reports. + Each module in `morfs` is included in the report. `outfile` is the + path to write the file to, "-" will write to stdout. + """ - if outfile: - outfile = open(outfile, "w") + self.config.from_args( + ignore_errors=ignore_errors, + omit_prefixes=omit_prefixes, + xml_output=outfile, + ) + file_to_close = None + if self.config.xml_output: + if self.config.xml_output == '-': + outfile = sys.stdout + else: + outfile = open(self.config.xml_output, "w") + file_to_close = outfile try: - reporter = XmlReporter(self, ignore_errors) + reporter = XmlReporter(self, self.config.ignore_errors) reporter.report( - morfs, omit_prefixes=omit_prefixes, outfile=outfile) + morfs, omit_prefixes=self.config.omit_prefixes, outfile=outfile + ) finally: - outfile.close() + if file_to_close: + file_to_close.close() def sysinfo(self): - """Return a list of key,value pairs showing internal information.""" + """Return a list of (key, value) pairs showing internal information.""" import coverage as covmod - import platform, re, sys + import platform, re info = [ ('version', covmod.__version__), @@ -344,3 +416,32 @@ class coverage(object): ]), ] return info + + +def process_startup(): + """Call this at Python startup to perhaps measure coverage. + + If the environment variable COVERAGE_PROCESS_START is defined, coverage + measurement is started. The value of the variable is the config file + to use. + + There are two ways to configure your Python installation to invoke this + function when Python starts: + + #. Create or append to sitecustomize.py to add these lines:: + + import coverage + coverage.process_startup() + + #. Create a .pth file in your Python installation containing:: + + import coverage; coverage.process_startup() + + """ + cps = os.environ.get("COVERAGE_PROCESS_START") + if cps: + cov = coverage(config_file=cps, auto_data=True) + if os.environ.get("COVERAGE_COVERAGE"): + # Measuring coverage within coverage.py takes yet more trickery. + cov.cover_prefix = "Please measure coverage.py!" + cov.start() diff --git a/coverage/data.py b/coverage/data.py index 11c7c01d..9359af12 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -2,7 +2,7 @@ import os -from coverage.backward import pickle, sorted # pylint: disable-msg=W0622 +from coverage.backward import pickle, sorted # pylint: disable-msg=W0622 class CoverageData(object): @@ -34,21 +34,21 @@ class CoverageData(object): `suffix` is a suffix to append to the base file name. This can be used for multiple or parallel execution, so that many coverage data files - can exist simultaneously. + can exist simultaneously. A dot will be used to join the base name and + the suffix. `collector` is a string describing the coverage measurement software. """ - self.collector = collector + self.collector = collector or 'unknown' self.use_file = True # Construct the filename that will be used for data file storage, if we # ever do any file storage. - self.filename = (basename or - os.environ.get(self.filename_env, self.filename_default)) + self.filename = basename or ".coverage" if suffix: - self.filename += suffix + self.filename += "." + suffix self.filename = os.path.abspath(self.filename) # A map from canonical Python source file name to a dictionary in @@ -169,18 +169,21 @@ class CoverageData(object): """Combine a number of data files together. Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix. + of the data files starting with that prefix plus a dot. """ data_dir, local = os.path.split(self.filename) + localdot = local + '.' for f in os.listdir(data_dir or '.'): - if f.startswith(local): + if f.startswith(localdot): full_path = os.path.join(data_dir, f) new_lines, new_arcs = self._read_file(full_path) for filename, file_data in new_lines.items(): self.lines.setdefault(filename, {}).update(file_data) for filename, file_data in new_arcs.items(): self.arcs.setdefault(filename, {}).update(file_data) + if f != local: + os.remove(full_path) def add_line_data(self, line_data): """Add executed line data. diff --git a/coverage/execfile.py b/coverage/execfile.py index 15f0a5f8..333163f8 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -2,8 +2,8 @@ import imp, os, sys -from coverage.backward import exec_function -from coverage.misc import NoSource +from coverage.backward import exec_code_object +from coverage.misc import NoSource, ExceptionDuringRun try: @@ -36,11 +36,33 @@ def run_python_file(filename, args): sys.path[0] = os.path.dirname(filename) try: + # Open the source file. try: source = open(filename, 'rU').read() except IOError: raise NoSource("No file to run: %r" % filename) - exec_function(source, filename, main_mod.__dict__) + + # We have the source. `compile` still needs the last line to be clean, + # so make sure it is, then compile a code object from it. + if source[-1] != '\n': + source += '\n' + code = compile(source, filename, "exec") + + # Execute the source file. + try: + exec_code_object(code, main_mod.__dict__) + except SystemExit: + # The user called sys.exit(). Just pass it along to the upper + # layers, where it will be handled. + raise + except: + # Something went wrong while executing the user code. + # Get the exc_info, and pack them into an exception that we can + # throw up to the outer loop. We peel two layers off the traceback + # so that the coverage.py code doesn't appear in the final printed + # traceback. + typ, err, tb = sys.exc_info() + raise ExceptionDuringRun(typ, err, tb.tb_next.tb_next) finally: # Restore the old __main__ sys.modules['__main__'] = old_main_mod diff --git a/coverage/files.py b/coverage/files.py index fb597329..28331a18 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -24,7 +24,8 @@ class FileLocator(object): """ common_prefix = os.path.commonprefix( - [filename, self.relative_dir + os.sep]) + [filename, self.relative_dir + os.sep] + ) return filename[len(common_prefix):] def canonical_filename(self, filename): @@ -40,6 +41,8 @@ class FileLocator(object): f = os.path.basename(f) if not os.path.isabs(f): for path in [os.curdir] + sys.path: + if path is None: + continue g = os.path.join(path, f) if os.path.exists(g): f = g @@ -69,7 +72,7 @@ class FileLocator(object): data = zi.get_data(parts[1]) except IOError: continue - if sys.hexversion > 0x03000000: + if sys.version_info >= (3, 0): data = data.decode('utf8') # TODO: How to do this properly? return data return None diff --git a/coverage/html.py b/coverage/html.py index 4d51eb34..f8de2e4c 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -69,27 +69,27 @@ class HtmlReporter(Reporter): arcs = self.arcs # These classes determine which lines are highlighted by default. - c_run = " run hide_run" - c_exc = " exc" - c_mis = " mis" - c_par = " par" + c_run + c_run = "run hide_run" + c_exc = "exc" + c_mis = "mis" + c_par = "par " + c_run lines = [] for lineno, line in enumerate(source_token_lines(source)): lineno += 1 # 1-based line numbers. # Figure out how to mark this line. - line_class = "" + line_class = [] annotate_html = "" annotate_title = "" if lineno in analysis.statements: - line_class += " stm" + line_class.append("stm") if lineno in analysis.excluded: - line_class += c_exc + line_class.append(c_exc) elif lineno in analysis.missing: - line_class += c_mis + line_class.append(c_mis) elif self.arcs and lineno in missing_branch_arcs: - line_class += c_par + line_class.append(c_par) n_par += 1 annlines = [] for b in missing_branch_arcs[lineno]: @@ -103,21 +103,23 @@ class HtmlReporter(Reporter): elif len(annlines) == 1: annotate_title = "no jump to this line number" elif lineno in analysis.statements: - line_class += c_run + line_class.append(c_run) # Build the HTML for the line - html = "" + html = [] for tok_type, tok_text in line: if tok_type == "ws": - html += escape(tok_text) + html.append(escape(tok_text)) else: tok_html = escape(tok_text) or ' ' - html += "<span class='%s'>%s</span>" % (tok_type, tok_html) + html.append( + "<span class='%s'>%s</span>" % (tok_type, tok_html) + ) lines.append({ - 'html': html, + 'html': ''.join(html), 'number': lineno, - 'class': line_class.strip() or "pln", + 'class': ' '.join(line_class) or "pln", 'annotate': annotate_html, 'annotate_title': annotate_title, }) diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index c0abab81..b70712c4 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -33,7 +33,7 @@ function index_page_ready($) { } else { // This is not the first load - something has - // already defined sorting so we'll just update + // already defined sorting so we'll just update // our stored value to match: sort_list = table.config.sortList; } @@ -58,7 +58,22 @@ function index_page_ready($) { }); // Watch for page unload events so we can save the final sort settings: - $(window).unload(function() { - document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/" + $(window).unload(function() { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/" }); } + +// -- pyfile stuff -- + +function toggle_lines(btn, cls) { + var btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +} diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index 0f387644..700ff8bb 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -1,80 +1,81 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
- <head>
- <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
- <title>Coverage report</title>
- <link rel='stylesheet' href='style.css' type='text/css'>
- <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
- <script type='text/javascript' src='jquery.tablesorter.min.js'></script>
- <script type='text/javascript' src='coverage_html.js'></script>
- <script type="text/javascript" charset="utf-8">
- jQuery(document).ready(index_page_ready);
- </script>
- </head>
- <body>
-
- <div id='header'>
- <div class='content'>
- <h1>Coverage report:
- <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span>
- </h1>
- </div>
- </div>
-
- <div id='index'>
- <table class='index'>
- <thead>
- {# The title='' attr doesn't work in Safari. #}
- <tr class='tablehead' title='Click to sort'>
- <th class='name left headerSortDown'>Module</th>
- <th>statements</th>
- <th>run</th>
- <th>excluded</th>
- {% if arcs %}
- <th>branches</th>
- <th>br exec</th>
- {% endif %}
- <th class='right'>coverage</th>
- </tr>
- </thead>
- {# HTML syntax requires thead, tfoot, tbody #}
- <tfoot>
- <tr class='total'>
- <td class='name left'>Total</td>
- <td>{{totals.n_statements}}</td>
- <td>{{totals.n_executed}}</td>
- <td>{{totals.n_excluded}}</td>
- {% if arcs %}
- <td>{{totals.n_branches}}</td>
- <td>{{totals.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{totals.pc_covered|format_pct}}%</td>
- </tr>
- </tfoot>
- <tbody>
- {% for file in files %}
- <tr class='file'>
- <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
- <td>{{file.nums.n_statements}}</td>
- <td>{{file.nums.n_executed}}</td>
- <td>{{file.nums.n_excluded}}</td>
- {% if arcs %}
- <td>{{file.nums.n_branches}}</td>
- <td>{{file.nums.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{file.nums.pc_covered|format_pct}}%</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
-
- <div id='footer'>
- <div class='content'>
- <p>
- <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
- </p>
- </div>
- </div>
- </body>
-</html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <meta http-equiv='Content-Type' content='text/html; charset=utf-8'> + <title>Coverage report</title> + <link rel='stylesheet' href='style.css' type='text/css'> + <script type='text/javascript' src='jquery-1.3.2.min.js'></script> + <script type='text/javascript' src='jquery.tablesorter.min.js'></script> + <script type='text/javascript' src='coverage_html.js'></script> + <script type='text/javascript' charset='utf-8'> + jQuery(document).ready(index_page_ready); + </script> +</head> +<body> + +<div id='header'> + <div class='content'> + <h1>Coverage report: + <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span> + </h1> + </div> +</div> + +<div id='index'> + <table class='index'> + <thead> + {# The title='' attr doesn't work in Safari. #} + <tr class='tablehead' title='Click to sort'> + <th class='name left headerSortDown'>Module</th> + <th>statements</th> + <th>run</th> + <th>excluded</th> + {% if arcs %} + <th>branches</th> + <th>br exec</th> + {% endif %} + <th class='right'>coverage</th> + </tr> + </thead> + {# HTML syntax requires thead, tfoot, tbody #} + <tfoot> + <tr class='total'> + <td class='name left'>Total</td> + <td>{{totals.n_statements}}</td> + <td>{{totals.n_executed}}</td> + <td>{{totals.n_excluded}}</td> + {% if arcs %} + <td>{{totals.n_branches}}</td> + <td>{{totals.n_executed_branches}}</td> + {% endif %} + <td class='right'>{{totals.pc_covered|format_pct}}%</td> + </tr> + </tfoot> + <tbody> + {% for file in files %} + <tr class='file'> + <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td> + <td>{{file.nums.n_statements}}</td> + <td>{{file.nums.n_executed}}</td> + <td>{{file.nums.n_excluded}}</td> + {% if arcs %} + <td>{{file.nums.n_branches}}</td> + <td>{{file.nums.n_executed_branches}}</td> + {% endif %} + <td class='right'>{{file.nums.pc_covered|format_pct}}%</td> + </tr> + {% endfor %} + </tbody> + </table> +</div> + +<div id='footer'> + <div class='content'> + <p> + <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a> + </p> + </div> +</div> + +</body> +</html> diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index 62518ba0..ec69556d 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -1,59 +1,47 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
-<title>Coverage for {{cu.name|escape}}</title>
-<link rel='stylesheet' href='style.css' type='text/css'>
-<script type='text/javascript' src='jquery-1.3.2.min.js'></script>
-<script type='text/javascript'>
-function toggle_lines(btn, cls) {
- var btn = $(btn);
- var hide = "hide_"+cls;
- if (btn.hasClass(hide)) {
- $("#source ."+cls).removeClass(hide);
- btn.removeClass(hide);
- }
- else {
- $("#source ."+cls).addClass(hide);
- btn.addClass(hide);
- }
-}
-</script>
-</head>
-<body>
-<div id='header'>
- <div class='content'>
- <h1>Coverage for <b>{{cu.name|escape}}</b> :
- <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span>
- </h1>
- <h2 class='stats'>
- {{nums.n_statements}} statements
- <span class='{{c_run.strip}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span>
- <span class='{{c_exc.strip}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span>
- <span class='{{c_mis.strip}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span>
- {% if arcs %}
- <span class='{{c_par.strip}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span>
- {% endif %}
- </h2>
- </div>
-</div>
-
-<div id='source'>
-<table cellspacing='0' cellpadding='0'>
-<tr>
-<td class='linenos' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{{line.number}}</p>
- {% endfor %}
-</td>
-<td class='text' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{% if line.annotate %}<span class='annotate' title='{{line.annotate_title}}'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'> </span></p>
- {% endfor %}
-</td>
-</tr>
-</table>
-</div>
-
-</body>
-</html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> + <meta http-equiv='Content-Type' content='text/html; charset=utf-8'> + <title>Coverage for {{cu.name|escape}}: {{nums.pc_covered|format_pct}}%</title> + <link rel='stylesheet' href='style.css' type='text/css'> + <script type='text/javascript' src='jquery-1.3.2.min.js'></script> + <script type='text/javascript' src='coverage_html.js'></script> +</head> +<body> + +<div id='header'> + <div class='content'> + <h1>Coverage for <b>{{cu.name|escape}}</b> : + <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span> + </h1> + <h2 class='stats'> + {{nums.n_statements}} statements + <span class='{{c_run}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span> + <span class='{{c_exc}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span> + <span class='{{c_mis}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span> + {% if arcs %} + <span class='{{c_par}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span> + {% endif %} + </h2> + </div> +</div> + +<div id='source'> + <table cellspacing='0' cellpadding='0'> + <tr> + <td class='linenos' valign='top'> + {% for line in lines %} + <p id='n{{line.number}}' class='{{line.class}}'>{{line.number}}</p> + {% endfor %} + </td> + <td class='text' valign='top'> + {% for line in lines %} + <p id='t{{line.number}}' class='{{line.class}}'>{% if line.annotate %}<span class='annotate' title='{{line.annotate_title}}'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'> </span></p> + {% endfor %} + </td> + </tr> + </table> +</div> + +</body> +</html> diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index b2987ea3..25e7d115 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -20,7 +20,7 @@ body { html>body { font-size: 16px; - } + } /* Set base font size to 12/16 */ p { @@ -115,7 +115,7 @@ td.text { margin: 0; padding: 0 0 0 .5em; border-left: 2px solid #ffffff; - white-space: nowrap; + white-space: nowrap; } .text p.mis { diff --git a/coverage/misc.py b/coverage/misc.py index 0e6bcf99..4218536d 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -60,6 +60,14 @@ def expensive(fn): return _wrapped +def bool_or_none(b): + """Return bool(b), but preserve None.""" + if b is None: + return None + else: + return bool(b) + + class CoverageException(Exception): """An exception specific to Coverage.""" pass @@ -67,3 +75,11 @@ class CoverageException(Exception): class NoSource(CoverageException): """Used to indicate we couldn't find the source for a module.""" pass + +class ExceptionDuringRun(CoverageException): + """An exception happened while running customer code. + + Construct it with three arguments, the values from `sys.exc_info`. + + """ + pass diff --git a/coverage/parser.py b/coverage/parser.py index 43f691f5..aea05f3d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -256,6 +256,11 @@ OPS_CHUNK_END = _opcode_set( 'BREAK_LOOP', 'CONTINUE_LOOP', ) +# Opcodes that unconditionally begin a new code chunk. By starting new chunks +# with unconditional jump instructions, we neatly deal with jumps to jumps +# properly. +OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD') + # Opcodes that push a block on the block stack. OPS_PUSH_BLOCK = _opcode_set('SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY') @@ -310,7 +315,7 @@ class ByteParser(object): return map(lambda c: ByteParser(code=c), CodeObjects(self.code)) # Getting numbers from the lnotab value changed in Py3.0. - if sys.hexversion >= 0x03000000: + if sys.version_info >= (3, 0): def _lnotab_increments(self, lnotab): """Return a list of ints from the lnotab bytes in 3.x""" return list(lnotab) @@ -399,12 +404,20 @@ class ByteParser(object): ult = penult = None for bc in ByteCodes(self.code.co_code): - # Maybe have to start a new block + # Maybe have to start a new chunk if bc.offset in bytes_lines_map: + # Start a new chunk for each source line number. if chunk: chunk.exits.add(bc.offset) chunk = Chunk(bc.offset, bytes_lines_map[bc.offset]) chunks.append(chunk) + elif bc.op in OPS_CHUNK_BEGIN: + # Jumps deserve their own unnumbered chunk. This fixes + # problems with jumps to jumps getting confused. + if chunk: + chunk.exits.add(bc.offset) + chunk = Chunk(bc.offset) + chunks.append(chunk) if not chunk: chunk = Chunk(bc.offset) diff --git a/coverage/phystokens.py b/coverage/phystokens.py index 5824b9b9..60b87932 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -77,7 +77,8 @@ def source_token_lines(source): ws_tokens = [token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL] line = [] col = 0 - tokgen = tokenize.generate_tokens(StringIO(source.expandtabs(8)).readline) + source = source.expandtabs(8).replace('\r\n', '\n') + tokgen = tokenize.generate_tokens(StringIO(source).readline) for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen): mark_start = True for part in re.split('(\n)', ttext): diff --git a/coverage/summary.py b/coverage/summary.py index f4d3c2c6..3db7b767 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -58,7 +58,7 @@ class SummaryReporter(Reporter): args += (analysis.missing_formatted(),) outfile.write(fmt_coverage % args) total += nums - except KeyboardInterrupt: #pragma: no cover + except KeyboardInterrupt: # pragma: no cover raise except: if not self.ignore_errors: diff --git a/coverage/templite.py b/coverage/templite.py index d3c673c6..c39e061e 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -101,14 +101,14 @@ class Templite(object): # Run it through an engine, and return the result. engine = _TempliteEngine(ctx) engine.execute(self.ops) - return engine.result + return "".join(engine.result) class _TempliteEngine(object): """Executes Templite objects to produce strings.""" def __init__(self, context): self.context = context - self.result = "" + self.result = [] def execute(self, ops): """Execute `ops` in the engine. @@ -118,10 +118,10 @@ class _TempliteEngine(object): """ for op, args in ops: if op == 'lit': - self.result += args + self.result.append(args) elif op == 'exp': try: - self.result += str(self.evaluate(args)) + self.result.append(str(self.evaluate(args))) except: exc_class, exc, _ = sys.exc_info() new_exc = exc_class("Couldn't evaluate {{ %s }}: %s" |
