diff options
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/__init__.py | 4 | ||||
-rw-r--r-- | coverage/backunittest.py | 2 | ||||
-rw-r--r-- | coverage/backward.py | 6 | ||||
-rw-r--r-- | coverage/cmdline.py | 13 | ||||
-rw-r--r-- | coverage/codeunit.py | 59 | ||||
-rw-r--r-- | coverage/collector.py | 6 | ||||
-rw-r--r-- | coverage/config.py | 4 | ||||
-rw-r--r-- | coverage/control.py | 54 | ||||
-rw-r--r-- | coverage/django.py | 61 | ||||
-rw-r--r-- | coverage/execfile.py | 1 | ||||
-rw-r--r-- | coverage/files.py | 5 | ||||
-rw-r--r-- | coverage/misc.py | 24 | ||||
-rw-r--r-- | coverage/parser.py | 6 | ||||
-rw-r--r-- | coverage/plugin.py | 75 | ||||
-rw-r--r-- | coverage/pytracer.py | 13 | ||||
-rw-r--r-- | coverage/summary.py | 13 |
16 files changed, 142 insertions, 204 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py index 67dd6e88..3a52c1d6 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -34,9 +34,9 @@ def _singleton_method(name): called. """ - # Disable pylint msg W0612, because a bunch of variables look unused, but + # Disable pylint message, because a bunch of variables look unused, but # they're accessed via locals(). - # pylint: disable=W0612 + # pylint: disable=unused-variable def wrapper(*args, **kwargs): """Singleton wrapper around a coverage method.""" diff --git a/coverage/backunittest.py b/coverage/backunittest.py index b2b7ca2f..648e74ef 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -3,7 +3,7 @@ # Use unittest2 if it's available, otherwise unittest. This gives us # backported features for 2.6. try: - import unittest2 as unittest # pylint: disable=F0401 + import unittest2 as unittest except ImportError: import unittest diff --git a/coverage/backward.py b/coverage/backward.py index e839f6bd..3372a8b3 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -2,8 +2,6 @@ # This file does lots of tricky stuff, so disable a bunch of lintisms. # pylint: disable=redefined-builtin -# pylint: disable=import-error -# pylint: disable=no-member # pylint: disable=unused-import # pylint: disable=no-name-in-module @@ -50,7 +48,7 @@ else: if sys.version_info >= (3, 0): # Python 3.2 provides `tokenize.open`, the best way to open source files. import tokenize - open_python_source = tokenize.open # pylint: disable=E1101 + open_python_source = tokenize.open else: def open_python_source(fname): """Open a source file the best way.""" @@ -153,7 +151,7 @@ def import_local_file(modname): break with open(modfile, 'r') as f: - # pylint: disable=W0631 + # pylint: disable=undefined-loop-variable # (Using possibly undefined loop variable 'suff') mod = imp.load_module(modname, f, modfile, suff) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index e7efe5c4..1b4d322c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -62,6 +62,10 @@ class Opts(object): help="Show line numbers of statements in each module that weren't " "executed." ) + skip_covered = optparse.make_option( + '--skip-covered', action='store_true', + help="Skip files with 100% coverage." + ) omit = optparse.make_option( '', '--omit', action='store', metavar="PAT1,PAT2,...", @@ -135,6 +139,7 @@ class CoverageOptionParser(optparse.OptionParser, object): pylib=None, rcfile=True, show_missing=None, + skip_covered=None, source=None, timid=None, title=None, @@ -153,7 +158,7 @@ class CoverageOptionParser(optparse.OptionParser, object): """Used to stop the optparse error handler ending the process.""" pass - def parse_args(self, args=None, options=None): + def parse_args_ok(self, args=None, options=None): """Call optparse.parse_args, but return a triple: (ok, options, args) @@ -283,6 +288,7 @@ CMDS = { Opts.omit, Opts.include, Opts.show_missing, + Opts.skip_covered ] + GLOBAL_ARGS, usage = "[options] [modules]", description = "Report coverage statistics on modules." @@ -369,7 +375,7 @@ class CoverageScript(object): argv = argv[1:] parser.help_fn = self.help_fn - ok, options, args = parser.parse_args(argv) + ok, options, args = parser.parse_args_ok(argv) if not ok: return ERR @@ -426,7 +432,8 @@ class CoverageScript(object): if options.action == "report": total = self.coverage.report( - show_missing=options.show_missing, **report_args) + show_missing=options.show_missing, + skip_covered=options.skip_covered, **report_args) if options.action == "annotate": self.coverage.annotate( directory=options.directory, **report_args) diff --git a/coverage/codeunit.py b/coverage/codeunit.py index da617913..207383e0 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -243,62 +243,3 @@ class PythonCodeUnit(CodeUnit): 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() diff --git a/coverage/collector.py b/coverage/collector.py index 001bc3d3..6ffdc7c4 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -7,7 +7,7 @@ from coverage.pytracer import PyTracer try: # Use the C extension code when we can, for speed. - from coverage.tracer import CTracer # pylint: disable=F0401,E0611 + from coverage.tracer import CTracer # pylint: disable=no-name-in-module except ImportError: # Couldn't import the C extension, maybe it isn't built. if os.getenv('COVERAGE_TEST_TRACER') == 'c': @@ -24,7 +24,7 @@ except ImportError: CTracer = None try: - import __pypy__ # pylint: disable=import-error + import __pypy__ except ImportError: __pypy__ = None @@ -186,6 +186,8 @@ class Collector(object): tracer.plugin_data = self.plugin_data if hasattr(tracer, 'threading'): tracer.threading = self.threading + if hasattr(tracer, 'check_include'): + tracer.check_include = self.check_include fn = tracer.start() self.tracers.append(tracer) diff --git a/coverage/config.py b/coverage/config.py index 4d599ee7..9598f74d 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -7,7 +7,7 @@ from coverage.misc import CoverageException # In py3, # ConfigParser was renamed to the more-standard configparser try: - import configparser # pylint: disable=F0401 + import configparser except ImportError: import ConfigParser as configparser @@ -160,6 +160,7 @@ class CoverageConfig(object): self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] self.precision = 0 self.show_missing = False + self.skip_covered = False # Defaults for [html] self.html_dir = "htmlcov" @@ -248,6 +249,7 @@ class CoverageConfig(object): ('partial_always_list', 'report:partial_branches_always', 'linelist'), ('precision', 'report:precision', 'int'), ('show_missing', 'report:show_missing', 'boolean'), + ('skip_covered', 'report:skip_covered', 'boolean'), # [html] ('html_dir', 'html:directory'), diff --git a/coverage/control.py b/coverage/control.py index e568f643..346f655f 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -9,13 +9,13 @@ from coverage.collector import Collector from coverage.config import CoverageConfig from coverage.data import CoverageData from coverage.debug import DebugControl -from coverage.plugin import CoveragePlugin, Plugins, overrides +from coverage.plugin import CoveragePlugin, Plugins from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.files import ModuleMatcher from coverage.html import HtmlReporter from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import file_be_gone +from coverage.misc import file_be_gone, overrides from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -24,7 +24,7 @@ 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 + import _structseq except ImportError: _structseq = None @@ -353,6 +353,7 @@ class Coverage(object): """ disp = FileDisposition(filename) def nope(disp, reason): + """Simple helper to make it easy to return NO.""" disp.trace = False disp.reason = reason return disp @@ -371,8 +372,6 @@ class Coverage(object): # Empty string is pretty useless return nope(disp, "empty string isn't a filename") - modulename = self._name_for_module(frame.f_globals, filename) - if filename.startswith('memory:'): return nope(disp, "memory isn't traceable") @@ -398,7 +397,10 @@ class Coverage(object): file_tracer.plugin_name = plugin.plugin_name disp.trace = True disp.file_tracer = file_tracer - disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename()) + if file_tracer.has_dynamic_source_filename(): + disp.has_dynamic_filename = True + else: + disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename()) else: disp.trace = True disp.source_filename = canonical @@ -406,29 +408,32 @@ class Coverage(object): if disp.trace: if file_tracer: disp.file_tracer = file_tracer - if disp.source_filename is None: - raise CoverageException( - "Plugin %r didn't set source_filename for %r" % - (plugin, disp.original_filename) - ) - if disp.check_filters: - reason = self._check_include_omit_etc( - disp.source_filename, modulename, - ) - if reason: - nope(disp, reason) + if not disp.has_dynamic_filename: + if disp.source_filename is None: + raise CoverageException( + "Plugin %r didn't set source_filename for %r" % + (plugin, disp.original_filename) + ) + if disp.check_filters: + reason = self._check_include_omit_etc( + disp.source_filename, frame, + ) + if reason: + nope(disp, reason) return disp return nope(disp, "no plugin found") # TODO: a test that causes this. - def _check_include_omit_etc(self, filename, modulename): + def _check_include_omit_etc(self, filename, frame): """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. """ + modulename = self._name_for_module(frame.f_globals, filename) + # 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 @@ -473,13 +478,13 @@ 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. + def _tracing_check_include_omit_etc(self, filename, frame): + """Check a filename against the include/omit/etc, rules, verbosely. Returns a boolean: True if the file should be traced, False if not. """ - reason = self._check_include_omit_etc(filename) + reason = self._check_include_omit_etc(filename, frame) if self.debug.should('trace'): if not reason: msg = "Tracing %r" % (filename,) @@ -750,8 +755,8 @@ Unexpected third case: return Analysis(self, it) def report(self, morfs=None, show_missing=True, ignore_errors=None, - file=None, # pylint: disable=W0622 - omit=None, include=None + file=None, # pylint: disable=redefined-builtin + omit=None, include=None, skip_covered=False, ): """Write a summary report to `file`. @@ -768,7 +773,7 @@ Unexpected third case: self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, - show_missing=show_missing, + show_missing=show_missing, skip_covered=skip_covered, ) reporter = SummaryReporter(self, self.config) return reporter.report(morfs, outfile=file) @@ -921,6 +926,7 @@ class FileDisposition(object): self.trace = False self.reason = "" self.file_tracer = None + self.has_dynamic_filename = False def debug_message(self): """Produce a debugging message explaining the outcome.""" diff --git a/coverage/django.py b/coverage/django.py deleted file mode 100644 index 00f2ed54..00000000 --- a/coverage/django.py +++ /dev/null @@ -1,61 +0,0 @@ -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/execfile.py b/coverage/execfile.py index 4b5f3af4..8965d207 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -29,7 +29,6 @@ if importlib_util_find_spec: Returns the file path of the module, and the name of the enclosing package. """ - # pylint: disable=no-member try: spec = importlib_util_find_spec(modulename) except ImportError as err: diff --git a/coverage/files.py b/coverage/files.py index 332c4225..3a298867 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -286,11 +286,10 @@ class PathAliases(object): pattern = abs_file(pattern) pattern += pattern_sep - # Make a regex from the pattern. fnmatch always adds a \Z or $ to + # Make a regex from the pattern. fnmatch always adds a \Z to # match the whole string, which we don't want. regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') - if regex_pat.endswith("$"): - regex_pat = regex_pat[:-1] + # We want */a/b.py to match on Windows too, so change slash to match # either separator. regex_pat = regex_pat.replace(r"\/", r"[\\/]") diff --git a/coverage/misc.py b/coverage/misc.py index f9a30bc5..f0e043b9 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -4,6 +4,7 @@ import errno import hashlib import inspect import os +import sys from coverage.backward import string_class, to_bytes @@ -135,6 +136,29 @@ class Hasher(object): return self.md5.hexdigest() +def overrides(obj, method_name, base_class): + """Does `obj` override the `method_name` it got from `base_class`? + + Determine if `obj` implements the method called `method_name`, which it + inherited from `base_class`. + + Returns a boolean. + + """ + klass = obj.__class__ + klass_func = getattr(klass, method_name) + base_func = getattr(base_class, method_name) + + # Python 2/3 compatibility: Python 2 returns an instancemethod object, the + # function is the .im_func attribute. Python 3 returns a plain function + # object already. + if sys.version_info < (3, 0): + klass_func = klass_func.im_func + base_func = base_func.im_func + + return klass_func is not base_func + + class CoverageException(Exception): """An exception specific to Coverage.""" pass diff --git a/coverage/parser.py b/coverage/parser.py index e7b9c029..9041e349 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -3,7 +3,7 @@ import collections, dis, re, token, tokenize from coverage.backward import StringIO -from coverage.backward import range # pylint: disable=W0622 +from coverage.backward import range # pylint: disable=redefined-builtin from coverage.backward import bytes_to_ints, open_python_source from coverage.bytecode import ByteCodes, CodeObjects from coverage.misc import nice_pair, expensive, join_regex @@ -216,7 +216,7 @@ class PythonParser(CodeParser): try: self._raw_parse() except (tokenize.TokenError, IndentationError) as tokerr: - msg, lineno = tokerr.args + msg, lineno = tokerr.args # pylint: disable=unpacking-non-sequence raise NotPython( "Couldn't parse '%s' as Python source: '%s' at %s" % (self.filename, msg, lineno) @@ -545,7 +545,7 @@ class ByteParser(object): chunks.append(chunk) # Give all the chunks a length. - chunks[-1].length = bc.next_offset - chunks[-1].byte # pylint: disable=W0631,C0301 + chunks[-1].length = bc.next_offset - chunks[-1].byte for i in range(len(chunks)-1): chunks[i].length = chunks[i+1].byte - chunks[i].byte diff --git a/coverage/plugin.py b/coverage/plugin.py index 3d41aab9..24a2b9a3 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -41,22 +41,58 @@ class CoveragePlugin(object): `file_tracer`. It's an error to return None. """ - raise Exception("Plugin %r needs to implement file_reporter" % self.plugin_name) + raise NotImplementedError("Plugin %r needs to implement file_reporter" % self.plugin_name) class FileTracer(object): - """Support needed for files during the tracing phase.""" + """Support needed for files during the tracing phase. + + You may construct this object from CoveragePlugin.file_tracer any way you + like. A natural choice would be to pass the filename given to file_tracer. + + """ def source_filename(self): - return "xyzzy" + """The source filename for this file. + + This may be any filename you like. A key responsibility of a plugin is + to own the mapping from Python execution back to whatever source + filename was originally the source of the code. + + Returns: + The filename to credit with this execution. - def dynamic_source_file_name(self): - """Returns a callable that can return a source name for a frame. + """ + return None + + def has_dynamic_source_filename(self): + """Does this FileTracer have dynamic source filenames? - The callable should take a filename and a frame, and return either a - filename or None: + FileTracers can provide dynamically determined filenames by implementing + dynamic_source_filename. Invoking that function is expensive. To + determine whether it should invoke it, coverage.py uses the result of + this function to know if it needs to bother invoking + dynamic_source_filename. - def dynamic_source_filename_func(filename, frame) + Returns: + A boolean, true if `dynamic_source_filename` should be called to + get dynamic source filenames. + + """ + return False + + def dynamic_source_filename(self, filename, frame): + """Returns a dynamically computed source filename. + + Some plugins need to compute the source filename dynamically for each + frame. + + This function will not be invoked if `has_dynamic_source_filename` + returns False. + + Returns: + The source filename for this frame, or None if this frame shouldn't + be measured. Can return None if dynamic filenames aren't needed. @@ -129,26 +165,3 @@ class Plugins(object): def get(self, module): return self.names[module] - - -def overrides(obj, method_name, base_class): - """Does `obj` override the `method_name` it got from `base_class`? - - Determine if `obj` implements the method called `method_name`, which it - inherited from `base_class`. - - Returns a boolean. - - """ - klass = obj.__class__ - klass_func = getattr(klass, method_name) - base_func = getattr(base_class, method_name) - - # Python 2/3 compatibility: Python 2 returns an instancemethod object, the - # function is the .im_func attribute. Python 3 returns a plain function - # object already. - if sys.version_info < (3, 0): - klass_func = klass_func.im_func - base_func = base_func.im_func - - return klass_func is not base_func diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 84071bb1..f3f490a0 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -28,6 +28,7 @@ class PyTracer(object): self.arcs = False self.should_trace = None self.should_trace_cache = None + self.check_include = None self.warn = None self.plugin_data = None # The threading module to use, if any. @@ -83,13 +84,11 @@ class PyTracer(object): self.cur_file_dict = None if disp.trace: tracename = disp.source_filename - if disp.file_tracer: - dyn_func = disp.file_tracer.dynamic_source_file_name() - if dyn_func: - tracename = dyn_func(tracename, frame) - if tracename: - if not self.check_include(tracename): - tracename = None + if disp.file_tracer and disp.has_dynamic_filename: + tracename = disp.file_tracer.dynamic_source_filename(tracename, frame) + if tracename: + if not self.check_include(tracename, frame): + tracename = None else: tracename = None if tracename: diff --git a/coverage/summary.py b/coverage/summary.py index a166ec2c..10ac7e2c 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -54,6 +54,17 @@ class SummaryReporter(Reporter): try: analysis = self.coverage._analyze(cu) nums = analysis.numbers + + if self.config.skip_covered: + # Don't report on 100% files. + no_missing_lines = (nums.n_missing == 0) + if self.branches: + no_missing_branches = (nums.n_partial_branches == 0) + else: + no_missing_branches = True + if no_missing_lines and no_missing_branches: + continue + args = (cu.name, nums.n_statements, nums.n_missing) if self.branches: args += (nums.n_branches, nums.n_partial_branches) @@ -69,8 +80,6 @@ class SummaryReporter(Reporter): args += (missing_fmtd,) outfile.write(fmt_coverage % args) total += nums - except KeyboardInterrupt: # pragma: not covered - raise except Exception: report_it = not self.config.ignore_errors if report_it: |