summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2010-06-13 21:46:35 -0400
committerNed Batchelder <ned@nedbatchelder.com>2010-06-13 21:46:35 -0400
commitcf7fa58279cf644c47864485260a7139d9608b2d (patch)
treeb568ddf59350961a2c4e137a4321ce12093d685e /coverage
parentf198d9d2c0df551ce79d97eb448a62f8bdb0cf26 (diff)
downloadpython-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.py16
-rw-r--r--coverage/config.py3
-rw-r--r--coverage/control.py129
-rw-r--r--coverage/files.py36
-rw-r--r--coverage/runners/plugin.py6
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,