diff options
38 files changed, 408 insertions, 286 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt index f2862633..05794483 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -26,6 +26,7 @@ Christian Heimes Roger Hu Stan Hu Devin Jeanpierre +Krystian Kichewko Ross Lawley Steve Leonard Edward Loper diff --git a/CHANGES.txt b/CHANGES.txt index 9df12fe2..3974a46e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,10 @@ Latest - The original command line switches (`-x` to run a program, etc) are no longer supported. +- A new option: `coverage report --skip-covered` will reduce the number of + files reported by skipping files with 100% coverage. Thanks, Krystian + Kichewko. + - The ``COVERAGE_OPTIONS`` environment variable is no longer supported. It was a hack for ``--timid`` before configuration files were available. @@ -36,6 +36,14 @@ Key: + A pain, b/c of the structure of the tests. + BTW: make an easier way to write those tests. +- Documentation + - Plugins! + Once per process + Once per file + - create a file tracer + - call its has_dynamic_source_file() + Once per call + Once per line * --source stuff: 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: diff --git a/doc/config.rst b/doc/config.rst index cec14e0f..f9168ea6 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -191,6 +191,8 @@ example "87%". A value of 2 will display percentages like "87.32%". ``show_missing`` (boolean, default False): when running a summary report, show missing lines. See :ref:`cmd_summary` for more information. +``skip_covered`` (boolean, default False): when running a cli summary report, +skip 100% covered files. See :ref:`cmd_summary` for more information. .. _config_html: @@ -99,7 +99,7 @@ def run_tests_with_coverage(tracer, *nose_args): if getattr(mod, '__file__', "??").startswith(covdir): covmods[name] = mod del sys.modules[name] - import coverage # don't warn about re-import: pylint: disable=W0404 + import coverage # don't warn about re-import: pylint: disable=reimported sys.modules.update(covmods) # Run nosetests, with the arguments from our command line. @@ -140,7 +140,7 @@ def do_zip_mods(): def do_install_egg(): """Install the egg1 egg for tests.""" # I am pretty certain there are easier ways to install eggs... - # pylint: disable=F0401,E0611,E1101 + # pylint: disable=import-error,no-name-in-module import distutils.core cur_dir = os.getcwd() os.chdir("tests/eggsrc") @@ -223,7 +223,7 @@ def print_banner(label): version = platform.python_version() if '__pypy__' in sys.builtin_module_names: - pypy_version = sys.pypy_version_info # pylint: disable=E1101 + pypy_version = sys.pypy_version_info version += " (pypy %s)" % ".".join(str(v) for v in pypy_version) which_python = os.path.relpath(sys.executable) @@ -20,10 +20,10 @@ profile=no # Add <file or directory> to the black list. It should be a base name, not a # path. You may set this option multiple times. -ignore=CVS +ignore= # Pickle collected data for later comparisons. -persistent=yes +persistent=no # Set the cache size for astng objects. cache-size=500 @@ -50,7 +50,9 @@ load-plugins= #disable-msg-cat= # Enable the message(s) with the given id(s). -#enable-msg= +enable= +# I0021: Useless suppression + I0021 # Disable the message(s) with the given id(s). disable= @@ -75,6 +77,8 @@ disable= # W0212: 86:Reporter.report_files: Access to a protected member _analyze of a client class C0103,W0212 +msg-template={path}:{line}: {msg} ({symbol}) + [REPORTS] # set the output format. Available formats are text, parseable, colorized, msvs @@ -46,9 +46,9 @@ Topic :: Software Development :: Testing import os, sys from setuptools import setup -from distutils.core import Extension # pylint: disable=E0611,F0401 -from distutils.command.build_ext import build_ext # pylint: disable=E0611,F0401,C0301 -from distutils import errors # pylint: disable=E0611,F0401 +from distutils.core import Extension # pylint: disable=no-name-in-module,import-error +from distutils.command.build_ext import build_ext # pylint: disable=no-name-in-module, import-error +from distutils import errors # pylint: disable=no-name-in-module # Get or massage our metadata. We exec coverage/version.py so we can avoid # importing the product code into setup.py. diff --git a/tests/backtest.py b/tests/backtest.py index 439493d1..574e6ac4 100644 --- a/tests/backtest.py +++ b/tests/backtest.py @@ -1,6 +1,6 @@ """Add things to old Pythons so I can pretend they are newer, for tests.""" -# pylint: disable=W0622 +# pylint: disable=redefined-builtin # (Redefining built-in blah) # The whole point of this file is to redefine built-ins, so shut up about it. @@ -20,7 +20,7 @@ def run_command(cmd): stderr=subprocess.STDOUT ) output, _ = proc.communicate() - status = proc.returncode # pylint: disable=E1101 + status = proc.returncode # Get the output, and canonicalize it to strings with newlines. if not isinstance(output, str): diff --git a/tests/plugin1.py b/tests/plugin1.py index 9401e327..21e64aeb 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -1,4 +1,4 @@ -"""Plugins for test_plugins.py to import.""" +"""A plugin for test_plugins.py to import.""" import os.path @@ -12,8 +12,7 @@ class Plugin(coverage.CoveragePlugin): def file_tracer(self, filename): """Trace only files named xyz.py""" if "xyz.py" in filename: - file_tracer = FileTracer(filename) - return file_tracer + return FileTracer(filename) def file_reporter(self, filename): return FileReporter(filename) diff --git a/tests/plugin2.py b/tests/plugin2.py new file mode 100644 index 00000000..1fa66cb2 --- /dev/null +++ b/tests/plugin2.py @@ -0,0 +1,24 @@ +"""A plugin for test_plugins.py to import.""" + +import coverage + +class Plugin(coverage.CoveragePlugin): + def file_tracer(self, filename): + if "render.py" in filename: + return RenderFileTracer(filename) + + +class RenderFileTracer(coverage.plugin.FileTracer): + def __init__(self, filename): + pass + + def has_dynamic_source_filename(self): + return True + + def dynamic_source_filename(self, filename, frame): + filename = "fake%d.html" % frame.f_lineno + print("dynamic filename: %r" % filename) + return filename + + def line_number_range(self, frame): + return 17,19 diff --git a/tests/test_api.py b/tests/test_api.py index 31bfc57f..ca65d6de 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -448,7 +448,7 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): """ cov = coverage.coverage(**kwargs) cov.start() - import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + import usepkgs # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested cov._harvest_data() # private! sshhh... summary = cov.data.summary() @@ -487,7 +487,7 @@ class ReportIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest): """Try coverage.report().""" cov = coverage.coverage() cov.start() - import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + import usepkgs # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested report = StringIO() cov.report(file=report, **kwargs) @@ -506,7 +506,7 @@ class XmlIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest): """Try coverage.xml_report().""" cov = coverage.coverage() cov.start() - import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + import usepkgs # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested cov.xml_report(outfile="-", **kwargs) return self.stdout() diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 695c3bec..e0d10867 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -31,7 +31,7 @@ class BaseCmdLineTest(CoverageTest): ) defaults.report( ignore_errors=None, include=None, omit=None, morfs=[], - show_missing=None, + show_missing=None, skip_covered=None ) defaults.xml_report( ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, @@ -153,7 +153,7 @@ class FakeCoverageForDebugData(object): """Fake coverage().data.has_arcs()""" return False - def summary(self, fullpath): # pylint: disable=W0613 + def summary(self, fullpath): # pylint: disable=unused-argument """Fake coverage().data.summary()""" return self._summary @@ -347,6 +347,11 @@ class CmdLineTest(BaseCmdLineTest): .load() .report(morfs=["mod1", "mod2", "mod3"]) """) + self.cmd_executes("report --skip-covered", """\ + .coverage() + .load() + .report(skip_covered=True) + """) def test_run(self): # coverage run [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] diff --git a/tests/test_codeunit.py b/tests/test_codeunit.py index fe82ea1c..49981264 100644 --- a/tests/test_codeunit.py +++ b/tests/test_codeunit.py @@ -7,7 +7,7 @@ from coverage.files import FileLocator from tests.coveragetest import CoverageTest -# pylint: disable=F0401 +# pylint: disable=import-error # Unable to import 'aa' (No module named aa) class CodeUnitTest(CoverageTest): diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 6fbac4a6..02163d2f 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -10,17 +10,17 @@ from tests.coveragetest import CoverageTest # These libraries aren't always available, we'll skip tests if they aren't. try: - import eventlet # pylint: disable=import-error + import eventlet except ImportError: eventlet = None try: - import gevent # pylint: disable=import-error + import gevent except ImportError: gevent = None try: - import greenlet # pylint: disable=import-error + import greenlet except ImportError: greenlet = None diff --git a/tests/test_config.py b/tests/test_config.py index 232d2289..26a22222 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -209,6 +209,7 @@ class ConfigFileTest(CoverageTest): while True: show_missing= TruE + skip_covered = TruE [{section}html] @@ -272,6 +273,7 @@ class ConfigFileTest(CoverageTest): ["plugins.a_plugin", "plugins.another"] ) self.assertTrue(cov.config.show_missing) + self.assertTrue(cov.config.skip_covered) self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere") self.assertEqual(cov.config.extra_css, "something/extra.css") self.assertEqual(cov.config.html_title, "Title & nums # nums!") @@ -295,6 +297,8 @@ class ConfigFileTest(CoverageTest): self.assert_config_settings_are_correct(cov) def test_config_file_settings_in_setupcfg(self): + # Configuration will be read from setup.cfg from sections prefixed with + # "coverage:" nested = self.LOTSA_SETTINGS.format(section="coverage:") self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) cov = coverage.coverage() @@ -306,7 +310,7 @@ class ConfigFileTest(CoverageTest): include = foo """) self.make_file("setup.cfg", """\ - [run] + [coverage:run] omit = bar branch = true """) @@ -315,6 +319,16 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.omit, None) self.assertEqual(cov.config.branch, False) + def test_setupcfg_only_if_prefixed(self): + self.make_file("setup.cfg", """\ + [run] + omit = bar + branch = true + """) + cov = coverage.coverage() + self.assertEqual(cov.config.omit, None) + self.assertEqual(cov.config.branch, False) + def test_non_ascii(self): self.make_file(".coveragerc", """\ [html] diff --git a/tests/test_farm.py b/tests/test_farm.py index 9e369cf3..661c67b7 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -3,7 +3,8 @@ import difflib, filecmp, fnmatch, glob, os, re, shutil, sys from nose.plugins.skip import SkipTest -from tests.backtest import run_command, execfile # pylint: disable=W0622 +from tests.backtest import run_command +from tests.backtest import execfile # pylint: disable=redefined-builtin from coverage.control import _TEST_NAME_FILE diff --git a/tests/test_misc.py b/tests/test_misc.py index 37191f67..977378d5 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -2,10 +2,11 @@ import sys -from coverage.misc import Hasher, file_be_gone +from coverage.misc import Hasher, file_be_gone, overrides from coverage import __version__, __url__ from tests.coveragetest import CoverageTest + class HasherTest(CoverageTest): """Test our wrapper of md5 hashing.""" @@ -79,3 +80,52 @@ class SetupPyTest(CoverageTest): self.assertGreater(len(long_description), 7) self.assertNotEqual(long_description[0].strip(), "") self.assertNotEqual(long_description[-1].strip(), "") + + +class OverridesTest(CoverageTest): + """Test plugins.py:overrides.""" + + run_in_temp_dir = False + + def test_overrides(self): + # pylint: disable=missing-docstring + class SomeBase(object): + def method1(self): + pass + + def method2(self): + pass + + class Derived1(SomeBase): + def method1(self): + pass + + self.assertTrue(overrides(Derived1(), "method1", SomeBase)) + self.assertFalse(overrides(Derived1(), "method2", SomeBase)) + + class FurtherDerived1(Derived1): + """Derive again from Derived1, inherit its method1.""" + pass + + self.assertTrue(overrides(FurtherDerived1(), "method1", SomeBase)) + self.assertFalse(overrides(FurtherDerived1(), "method2", SomeBase)) + + class FurtherDerived2(Derived1): + """Override the overridden method.""" + def method1(self): + pass + + self.assertTrue(overrides(FurtherDerived2(), "method1", SomeBase)) + self.assertFalse(overrides(FurtherDerived2(), "method2", SomeBase)) + + class Mixin(object): + """A mixin that overrides method1.""" + def method1(self): + pass + + class Derived2(Mixin, SomeBase): + """A class that gets the method from the mixin.""" + pass + + self.assertTrue(overrides(Derived2(), "method1", SomeBase)) + self.assertFalse(overrides(Derived2(), "method2", SomeBase)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 83840ea3..f2658998 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -5,7 +5,7 @@ import sys from nose.plugins.skip import SkipTest import coverage -from coverage.plugin import Plugins, overrides +from coverage.plugin import Plugins import coverage.plugin @@ -137,7 +137,11 @@ class PluginTest(CoverageTest): cov.start() cov.stop() - def test_importing_myself(self): + +class FileTracerTest(CoverageTest): + """Tests of plugins that implement file_tracer.""" + + def test_plugin1(self): if sys.platform == 'win32': raise SkipTest("Plugin stuff is jank on windows.. fixing soon...") @@ -163,52 +167,33 @@ class PluginTest(CoverageTest): _, statements, _, _ = cov.analysis("/src/try_ABC.zz") self.assertEqual(statements, [105, 106, 107, 205, 206, 207]) + def test_plugin2(self): + self.make_file("render.py", """\ + def render(filename, linenum): + fiddle_around = 1 # vamp until ready + return "[{0} @ {1}]".format(filename, linenum) + """) + self.make_file("caller.py", """\ + from render import render -class OverridesTest(CoverageTest): - """Test plugins.py:overrides.""" - - run_in_temp_dir = False - - def test_overrides(self): - class SomeBase(object): - """Base class, two base methods.""" - def method1(self): - pass - - def method2(self): - pass - - class Derived1(SomeBase): - """Simple single inheritance.""" - def method1(self): - pass - - self.assertTrue(overrides(Derived1(), "method1", SomeBase)) - self.assertFalse(overrides(Derived1(), "method2", SomeBase)) - - class FurtherDerived1(Derived1): - """Derive again from Derived1, inherit its method1.""" - pass - - self.assertTrue(overrides(FurtherDerived1(), "method1", SomeBase)) - self.assertFalse(overrides(FurtherDerived1(), "method2", SomeBase)) + assert render("foo.html", 17) == "[foo.html @ 17]" + assert render("bar.html", 23) == "[bar.html @ 23]" + """) - class FurtherDerived2(Derived1): - """Override the overridden method.""" - def method1(self): - pass + cov = coverage.Coverage() + cov.config["run:plugins"] = ["tests.plugin2"] + cov.config["run:debug"] = ["trace"] - self.assertTrue(overrides(FurtherDerived2(), "method1", SomeBase)) - self.assertFalse(overrides(FurtherDerived2(), "method2", SomeBase)) + self.start_import_stop(cov, "caller") - class Mixin(object): - """A mixin that overrides method1.""" - def method1(self): - pass + print(self.stderr()) + cov._harvest_data() + print(cov.data.line_data()) - class Derived2(Mixin, SomeBase): - """A class that gets the method from the mixin.""" - pass + return # TODO: finish this test - self.assertTrue(overrides(Derived2(), "method1", SomeBase)) - self.assertFalse(overrides(Derived2(), "method2", SomeBase)) + _, statements, missing, _ = cov.analysis("simple.py") + self.assertEqual(statements, [1,2,3]) + self.assertEqual(missing, []) + _, statements, _, _ = cov.analysis("/src/try_ABC.zz") + self.assertEqual(statements, [105, 106, 107, 205, 206, 207]) diff --git a/tests/test_process.py b/tests/test_process.py index df26aaf5..09d4c207 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -713,7 +713,7 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): data_file = .mycovdata """) self.set_environ("COVERAGE_PROCESS_START", "coverage.ini") - import main # pylint: disable=F0401,W0612 + import main # pylint: disable=import-error,unused-variable with open("out.txt") as f: self.assertEqual(f.read(), "Hello, world!\n") diff --git a/tests/test_summary.py b/tests/test_summary.py index ce668f76..21817a52 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -201,7 +201,6 @@ class SummaryTest(CoverageTest): self.assertEqual(out, 'x\ny\n') report = self.report_from_command("coverage report --show-missing") - # pylint: disable=C0301 # Name Stmts Miss Branch BrPart Cover Missing # ------------------------------------------------------- # main 1 0 0 0 100% @@ -224,6 +223,96 @@ class SummaryTest(CoverageTest): "TOTAL 11 2 8 3 63%" ) + def test_report_skip_covered_no_branches(self): + self.make_file("main.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + out = self.run_command("coverage run main.py") + self.assertEqual(out, "z\n") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Cover + # --------------------------------- + # not_covered 2 1 50% + + self.assertEqual(self.line_count(report), 3, report) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "not_covered 2 1 50%") + + def test_report_skip_covered_branches(self): + self.make_file("main.py", """ + import not_covered + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + out = self.run_command("coverage run --branch main.py") + self.assertEqual(out, "n\nz\n") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------------- + # not_covered 4 0 2 1 83% + + self.assertEqual(self.line_count(report), 3, report) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "not_covered 4 0 2 1 83%") + + def test_report_skip_covered_branches_with_totals(self): + self.make_file("main.py", """ + import not_covered + import also_not_run + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("also_not_run.py", """ + def does_not_appear_in_this_film(ni): + print("Ni!") + """) + out = self.run_command("coverage run --branch main.py") + self.assertEqual(out, "n\nz\n") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------------- + # also_not_run 2 1 0 0 50% + # not_covered 4 0 2 1 83% + # ----------------------------------------------- + # TOTAL 6 1 2 1 75% + + self.assertEqual(self.line_count(report), 6, report) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "also_not_run 2 1 0 0 50%") + self.assertEqual(squeezed[3], "not_covered 4 0 2 1 83%") + self.assertEqual(squeezed[5], "TOTAL 6 1 2 1 75%") + def test_dotpy_not_python(self): # We run a .py file, and when reporting, we can't parse it as Python. # We should get an error message in the report. @@ -232,7 +321,7 @@ class SummaryTest(CoverageTest): self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage report mycode.py") - # pylint: disable=C0301 + # pylint: disable=line-too-long # Name Stmts Miss Cover # ---------------------------- # mycode NotPython: Couldn't parse '/tmp/test_cover/63354509363/mycode.py' as Python source: 'invalid syntax' at line 1 @@ -298,7 +387,7 @@ class SummaryTest(CoverageTest): """) cov = coverage.coverage(branch=True, source=["."]) cov.start() - import main # pragma: nested # pylint: disable=F0401,W0612 + import main # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested report = self.get_report(cov).splitlines() self.assertIn("mybranch 5 5 2 0 0%", report) @@ -307,7 +396,7 @@ class SummaryTest(CoverageTest): """A helper for the next few tests.""" cov = coverage.coverage() cov.start() - import TheCode # pragma: nested # pylint: disable=F0401,W0612 + import TheCode # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested return self.get_report(cov) @@ -340,7 +429,7 @@ class SummaryTest(CoverageTest): """) cov = coverage.coverage() cov.start() - import start # pragma: nested # pylint: disable=F0401,W0612 + import start # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested report = self.get_report(cov) @@ -370,7 +459,7 @@ class SummaryTest2(CoverageTest): # statements, not one statement. cov = coverage.coverage() cov.start() - import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + import usepkgs # pragma: nested # pylint: disable=import-error,unused-variable cov.stop() # pragma: nested repout = StringIO() diff --git a/tests/test_templite.py b/tests/test_templite.py index aa697b78..b3e21e70 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -4,9 +4,7 @@ import re from coverage.templite import Templite, TempliteSyntaxError from tests.coveragetest import CoverageTest -# pylint: disable=W0612,E1101 -# Disable W0612 (Unused variable) and -# E1101 (Instance of 'foo' has no 'bar' member) +# pylint: disable=unused-variable class AnyOldObject(object): """Simple testing object. diff --git a/tests/test_xml.py b/tests/test_xml.py index 37ada3cb..3c7d2361 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -80,7 +80,7 @@ class XmlReportTest(CoverageTest): def test_filename_format_including_module(self): cov = self.run_doit() - import sub.doit # pylint: disable=F0401 + import sub.doit # pylint: disable=import-error cov.xml_report([sub.doit], outfile="-") xml = self.stdout() doit_line = re_line(xml, "class.*doit") |