diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2010-06-13 21:46:35 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2010-06-13 21:46:35 -0400 |
commit | cf7fa58279cf644c47864485260a7139d9608b2d (patch) | |
tree | b568ddf59350961a2c4e137a4321ce12093d685e /coverage | |
parent | f198d9d2c0df551ce79d97eb448a62f8bdb0cf26 (diff) | |
download | python-coveragepy-git-cf7fa58279cf644c47864485260a7139d9608b2d.tar.gz |
The 'source' option is a list of directories or packages to limit coverage's attention.
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/cmdline.py | 16 | ||||
-rw-r--r-- | coverage/config.py | 3 | ||||
-rw-r--r-- | coverage/control.py | 129 | ||||
-rw-r--r-- | coverage/files.py | 36 | ||||
-rw-r--r-- | coverage/runners/plugin.py | 6 |
5 files changed, 153 insertions, 37 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py index d8738db7..8bf90e21 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -75,6 +75,10 @@ class Opts(object): '', '--rcfile', action='store', help="Specify configuration file. Defaults to '.coveragerc'" ) + source = optparse.Option( + '', '--source', action='store', metavar="SRC1,SRC2,...", + help="A list of packages or directories of code to be measured." + ) timid = optparse.Option( '', '--timid', action='store_true', help="Use a simpler but slower trace method. Try this if you get " @@ -110,6 +114,7 @@ class CoverageOptionParser(optparse.OptionParser, object): pylib=None, rcfile=True, show_missing=None, + source=None, timid=None, erase_first=None, version=None, @@ -290,6 +295,7 @@ CMDS = { Opts.pylib, Opts.parallel_mode, Opts.timid, + Opts.source, Opts.omit, Opts.include, ] + GLOBAL_ARGS, @@ -440,8 +446,9 @@ class CoverageScript(object): return ERR # Listify the list options. - omit = pattern_list(options.omit) - include = pattern_list(options.include) + source = unshell_list(options.source) + omit = unshell_list(options.omit) + include = unshell_list(options.include) # Do something. self.coverage = self.covpkg.coverage( @@ -450,6 +457,7 @@ class CoverageScript(object): timid = options.timid, branch = options.branch, config_file = options.rcfile, + source = source, omit = omit, include = include, ) @@ -528,8 +536,8 @@ class CoverageScript(object): return OK -def pattern_list(s): - """Turn an argument into a list of patterns.""" +def unshell_list(s): + """Turn a command-line argument into a list.""" if not s: return None if sys.platform == 'win32': diff --git a/coverage/config.py b/coverage/config.py index 9f52ecb1..7c22f64b 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -20,6 +20,7 @@ class CoverageConfig(object): self.data_file = ".coverage" self.parallel = False self.timid = False + self.source = None # Defaults for [report] self.exclude_list = ['(?i)# *pragma[: ]*no *cover'] @@ -68,6 +69,8 @@ class CoverageConfig(object): self.parallel = cp.getboolean('run', 'parallel') if cp.has_option('run', 'timid'): self.timid = cp.getboolean('run', 'timid') + if cp.has_option('run', 'source'): + self.source = self.get_list(cp, 'run', 'source') if cp.has_option('run', 'omit'): self.omit = self.get_list(cp, 'run', 'omit') if cp.has_option('run', 'include'): diff --git a/coverage/control.py b/coverage/control.py index 432a7c27..dafd4930 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,6 +1,6 @@ """Core control stuff for Coverage.""" -import atexit, fnmatch, os, random, socket, sys +import atexit, os, random, socket, sys from coverage.annotate import AnnotateReporter from coverage.backward import string_class @@ -8,7 +8,7 @@ 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.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.html import HtmlReporter from coverage.misc import bool_or_none from coverage.results import Analysis @@ -32,7 +32,7 @@ class coverage(object): def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, - omit=None, include=None): + source=None, omit=None, include=None): """ `data_file` is the base name of the data file to use, defaulting to ".coverage". `data_suffix` is appended (with a dot) to `data_file` to @@ -59,6 +59,10 @@ class coverage(object): standard file is read (".coveragerc"). If it is False, then no file is read. + `source` is a list of file paths or package names. Only code located + in the trees indicated by the file paths or package names will be + measured. + `include` and `omit` are lists of filename patterns. Files that match `include` will be measured, files that match `omit` will not. @@ -85,7 +89,7 @@ class coverage(object): self.config.from_args( data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), - omit=omit, include=include + source=source, omit=omit, include=include ) self.auto_data = auto_data @@ -96,6 +100,15 @@ class coverage(object): self.file_locator = FileLocator() + # The source argument can be directories or package names. + self.source = [] + self.source_pkgs = [] + for src in self.config.source or []: + if os.path.exists(src): + self.source.append(self.file_locator.canonical_filename(src)) + else: + self.source_pkgs.append(src) + self.omit = self._abs_files(self.config.omit) self.include = self._abs_files(self.config.include) @@ -126,11 +139,12 @@ class coverage(object): ) # The dirs for files considered "installed with the interpreter". + self.pylib_dirs = [] if not self.config.cover_pylib: # Look at where the "os" module is located. That's the indication # for "installed with the interpreter". os_dir = self.canonical_dir(os.__file__) - self.pylib_dirs = [os_dir] + self.pylib_dirs.append(os_dir) # In a virtualenv, there're actually two lib directories. Find the # other one. This is kind of ad-hoc, but it works. @@ -142,10 +156,22 @@ class coverage(object): # where we are. self.cover_dir = self.canonical_dir(__file__) + # The matchers for _should_trace, created when tracing starts. + self.source_match = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + def canonical_dir(self, f): """Return the canonical directory of the file `f`.""" return os.path.split(self.file_locator.canonical_filename(f))[0] + def _source_for_file(self, filename): + """Return the source file for `filename`.""" + if not filename.endswith(".py"): + if filename[-4:-1] == ".py": + filename = filename[:-1] + return filename + def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename` @@ -156,13 +182,15 @@ class coverage(object): should not. """ - if filename[0] == '<': + if filename.startswith('<'): # Lots of non-file execution is represented with artificial # filenames like "<string>", "<doctest readme.txt[0]>", or # "<exec_function>". Don't ever trace these executions, since we # can't do anything with the data later anyway. return False + self._check_for_packages() + # Compiled Python files have two filenames: frame.f_code.co_filename is # the filename at the time the .pyc was compiled. The second name # is __file__, which is where the .pyc was actually loaded from. Since @@ -171,35 +199,31 @@ class coverage(object): # co_filename value. dunder_file = frame.f_globals.get('__file__') if dunder_file: - if not dunder_file.endswith(".py"): - if dunder_file[-4:-1] == ".py": - dunder_file = dunder_file[:-1] - filename = dunder_file - + filename = self._source_for_file(dunder_file) canonical = self.file_locator.canonical_filename(filename) - canon_dir = os.path.split(canonical)[0] - # 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.config.cover_pylib: - if canon_dir in self.pylib_dirs: + # If the user specified source, then that's authoritative about what to + # measure. If they didn't, then we have to exclude the stdlib and + # coverage.py directories. + if self.source_match: + if not self.source_match.match(canonical): + return False + else: + # If we aren't supposed to trace installed code, then check if this + # is near the Python standard library and skip it if so. + if self.pylib_match and self.pylib_match.match(canonical): return False - # We exclude the coverage code itself, since a little of it will be - # measured otherwise. - if canon_dir == self.cover_dir: - return False + # We exclude the coverage code itself, since a little of it will be + # measured otherwise. + if self.cover_match and self.cover_match.match(canonical): + return False # Check the file against the include and omit patterns. - if self.include: - for pattern in self.include: - if fnmatch.fnmatch(canonical, pattern): - break - else: - return False - for pattern in self.omit: - if fnmatch.fnmatch(canonical, pattern): - return False + if self.include_match and not self.include_match.match(canonical): + return False + if self.omit_match and self.omit_match.match(canonical): + return False return canonical @@ -217,6 +241,39 @@ class coverage(object): files = files or [] return [self.file_locator.abs_file(f) for f in files] + def _check_for_packages(self): + """Update the source_match matcher with latest imported packages.""" + # Our self.source_pkgs attribute is a list of package names we want to + # measure. Each time through here, we see if we've imported any of + # them yet. If so, we add its file to source_match, and we don't have + # to look for that package any more. + if self.source_pkgs: + found = [] + for pkg in self.source_pkgs: + try: + mod = sys.modules[pkg] + except KeyError: + continue + + found.append(pkg) + + try: + pkg_file = mod.__file__ + except AttributeError: + print "WHOA! No file for module %s" % pkg + else: + d, f = os.path.split(pkg_file) + if f.startswith('__init__.'): + # This is actually a package, return the directory. + pkg_file = d + else: + pkg_file = self._source_for_file(pkg_file) + pkg_file = self.file_locator.canonical_filename(pkg_file) + self.source_match.add(pkg_file) + + for pkg in found: + self.source_pkgs.remove(pkg) + def use_cache(self, usecache): """Control the use of a data file (incorrectly called a cache). @@ -242,6 +299,20 @@ class coverage(object): if not self.atexit_registered: atexit.register(self.save) self.atexit_registered = True + + # Create the matchers we need for _should_trace + if self.source or self.source_pkgs: + self.source_match = TreeMatcher(self.source) + else: + if self.cover_dir: + self.cover_match = TreeMatcher([self.cover_dir]) + if self.pylib_dirs: + self.pylib_match = TreeMatcher(self.pylib_dirs) + if self.include: + self.include_match = FnmatchMatcher(self.include) + if self.omit: + self.omit_match = FnmatchMatcher(self.omit) + self.collector.start() def stop(self): diff --git a/coverage/files.py b/coverage/files.py index 5690679f..d74b4d79 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -1,6 +1,6 @@ """File wrangling.""" -import os, sys +import fnmatch, os, sys class FileLocator(object): """Understand how filenames work.""" @@ -76,3 +76,37 @@ class FileLocator(object): data = data.decode('utf8') # TODO: How to do this properly? return data return None + + +class TreeMatcher(object): + """A matcher for files in a tree.""" + def __init__(self, directories): + self.dirs = directories[:] + + def add(self, directory): + """Add another directory to the list we match for.""" + self.dirs.append(directory) + + def match(self, fpath): + """Does `fpath` indicate a file in one of our trees?""" + for d in self.dirs: + if fpath.startswith(d): + if fpath == d: + # This is the same file! + return True + if fpath[len(d)] == os.sep: + # This is a file in the directory + return True + return False + +class FnmatchMatcher(object): + """A matcher for files by filename pattern.""" + def __init__(self, pats): + self.pats = pats[:] + + def match(self, fpath): + """Does `fpath` match one of our filename patterns?""" + for pat in self.pats: + if fnmatch.fnmatch(fpath, pat): + return True + return False diff --git a/coverage/runners/plugin.py b/coverage/runners/plugin.py index fd2a3a62..cf059c57 100644 --- a/coverage/runners/plugin.py +++ b/coverage/runners/plugin.py @@ -2,7 +2,7 @@ import optparse, sys import coverage -from coverage.cmdline import pattern_list +from coverage.cmdline import unshell_list class CoverageTestWrapper(object): @@ -34,8 +34,8 @@ class CoverageTestWrapper(object): def start(self): """Start coverage before the test suite.""" # cover_omit is a ',' separated list if provided - self.omit = pattern_list(self.options.cover_omit) - self.include = pattern_list(self.options.cover_omit) + self.omit = unshell_list(self.options.cover_omit) + self.include = unshell_list(self.options.cover_omit) self.coverage = self.covpkg.coverage( config_file = self.options.cover_rcfile, |