summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--coverage/__init__.py2
-rw-r--r--coverage/backward.py7
-rw-r--r--coverage/config.py59
-rw-r--r--coverage/control.py65
-rw-r--r--coverage/data.py7
-rw-r--r--test/coveragetest.py43
-rw-r--r--test/test_api.py22
-rw-r--r--test/test_config.py89
-rw-r--r--test/test_farm.py2
10 files changed, 265 insertions, 34 deletions
diff --git a/Makefile b/Makefile
index 7ae4bf0d..cc584c32 100644
--- a/Makefile
+++ b/Makefile
@@ -28,6 +28,9 @@ lint:
python /Python25/Lib/tabnanny.py $(LINTABLE)
python checkeol.py
+pep8:
+ pep8 --filename=*.py --ignore=E401,E301 --repeat coverage
+
testready: testdata devinst
tests: testready
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 7ab9746c..9b355adb 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -5,7 +5,7 @@ http://nedbatchelder.com/code/coverage
"""
-__version__ = "3.2" # see detailed history in CHANGES.txt
+__version__ = "3.3-config" # see detailed history in CHANGES.txt
__url__ = "http://nedbatchelder.com/code/coverage"
diff --git a/coverage/backward.py b/coverage/backward.py
index 66cfbb96..624daa2c 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -70,3 +70,10 @@ def exec_function(source, filename, global_map):
""",
"<exec_function>", "exec"
))
+
+# ConfigParser was renamed to the more-standard configparser
+
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
diff --git a/coverage/config.py b/coverage/config.py
new file mode 100644
index 00000000..0955347b
--- /dev/null
+++ b/coverage/config.py
@@ -0,0 +1,59 @@
+"""Config file for coverage.py"""
+
+import os
+from coverage.backward import configparser # pylint: disable-msg=W0622
+
+
+class CoverageConfig(object):
+ """Coverage.py configuration.
+
+ The attributes of this class are the various settings that control the
+ operation of coverage.py.
+
+ """
+
+ def __init__(self):
+ """Initialize the configuration attributes to their defaults."""
+ # Defaults.
+ self.cover_pylib = False
+ self.timid = False
+ self.branch = False
+ self.exclude_list = ['# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]']
+ self.data_file = ".coverage"
+
+ def from_environment(self, env_var):
+ """Read configuration from the `env_var` environment variable."""
+ # Timidity: for nose users, read an environment variable. This is a
+ # cheap hack, since the rest of the command line arguments aren't
+ # recognized, but it solves some users' problems.
+ env = os.environ.get(env_var, '')
+ if env:
+ self.timid = ('--timid' in env)
+
+ def from_args(self, **kwargs):
+ """Read config values from `kwargs`."""
+ for k, v in kwargs.items():
+ if v is not None:
+ setattr(self, k, v)
+
+ def from_file(self, *files):
+ """Read configurating from .rc files.
+
+ Each argument in `files` is a file name to read.
+
+ """
+ cp = configparser.RawConfigParser()
+ cp.read(files)
+
+ if cp.has_option('run', 'timid'):
+ self.timid = cp.getboolean('run', 'timid')
+ if cp.has_option('run', 'cover_pylib'):
+ self.cover_pylib = cp.getboolean('run', 'cover_pylib')
+ if cp.has_option('run', 'branch'):
+ self.branch = cp.getboolean('run', 'branch')
+ if cp.has_option('report', 'exclude'):
+ # Exclude is a list of lines, leave out the blank ones.
+ exclude_list = cp.get('report', 'exclude')
+ self.exclude_list = filter(None, exclude_list.split('\n'))
+ if cp.has_option('run', 'data_file'):
+ self.data_file = cp.get('run', 'data_file')
diff --git a/coverage/control.py b/coverage/control.py
index 15bbe982..c1d32e9d 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -3,9 +3,10 @@
import atexit, os, socket
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class # pylint: disable-msg=W0622
+from coverage.backward import string_class
from coverage.codeunit import code_unit_factory, CodeUnit
from coverage.collector import Collector
+from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator
from coverage.html import HtmlReporter
@@ -28,8 +29,8 @@ class coverage(object):
"""
- def __init__(self, data_file=None, data_suffix=False, cover_pylib=False,
- auto_data=False, timid=False, branch=False):
+ def __init__(self, data_file=None, data_suffix=False, cover_pylib=None,
+ auto_data=False, timid=None, branch=None, config_file=True):
"""
`data_file` is the base name of the data file to use, defaulting to
".coverage". `data_suffix` is appended to `data_file` to create the
@@ -51,24 +52,47 @@ class coverage(object):
If `branch` is true, then branch coverage will be measured in addition
to the usual statement coverage.
+ `config_file` determines what config file to read. If it is a string,
+ it is the name of the config file to read. If it is True, then a
+ standard file is read (".coveragerc"). If it is False, then no file is
+ read.
+
"""
from coverage import __version__
- self.cover_pylib = cover_pylib
+ # Build our configuration from a number of sources:
+ # 1: defaults:
+ self.config = CoverageConfig()
+
+ # 2: from the coveragerc file:
+ if config_file:
+ if config_file is True:
+ config_file = ".coveragerc"
+ self.config.from_file(config_file)
+
+ # 3: from environment variables:
+ self.config.from_environment('COVERAGE_OPTIONS')
+ env_data_file = os.environ.get('COVERAGE_FILE')
+ if env_data_file:
+ self.config.data_file = env_data_file
+
+ # 4: from constructor arguments:
+ self.config.from_args(
+ data_file=data_file, cover_pylib=cover_pylib, timid=timid,
+ branch=branch
+ )
+
self.auto_data = auto_data
self.atexit_registered = False
self.exclude_re = ""
- self.exclude_list = []
+ self._compile_exclude()
self.file_locator = FileLocator()
- # Timidity: for nose users, read an environment variable. This is a
- # cheap hack, since the rest of the command line arguments aren't
- # recognized, but it solves some users' problems.
- timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', ''))
self.collector = Collector(
- self._should_trace, timid=timid, branch=branch
+ self._should_trace, timid=self.config.timid,
+ branch=self.config.branch
)
# Create the data file.
@@ -80,15 +104,12 @@ class coverage(object):
data_suffix = None
self.data = CoverageData(
- basename=data_file, suffix=data_suffix,
+ basename=self.config.data_file, suffix=data_suffix,
collector="coverage v%s" % __version__
)
- # The default exclude pattern.
- self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
-
# The prefix for files considered "installed with the interpreter".
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
# Look at where the "os" module is located. That's the indication
# for "installed with the interpreter".
os_file = self.file_locator.canonical_filename(os.__file__)
@@ -131,7 +152,7 @@ class coverage(object):
# If we aren't supposed to trace installed code, then check if this is
# near the Python standard library and skip it if so.
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
if canonical.startswith(self.pylib_prefix):
return False
@@ -191,7 +212,7 @@ class coverage(object):
def clear_exclude(self):
"""Clear the exclude list."""
- self.exclude_list = []
+ self.config.exclude_list = []
self.exclude_re = ""
def exclude(self, regex):
@@ -203,12 +224,16 @@ class coverage(object):
Matching any of the regexes excludes a source line.
"""
- self.exclude_list.append(regex)
- self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")"
+ self.config.exclude_list.append(regex)
+ self._compile_exclude()
+
+ def _compile_exclude(self):
+ """Build the internal usable form of the exclude list."""
+ self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")"
def get_exclude_list(self):
"""Return the list of excluded regex patterns."""
- return self.exclude_list
+ return self.config.exclude_list
def save(self):
"""Save the collected coverage data to the data file."""
diff --git a/coverage/data.py b/coverage/data.py
index 11c7c01d..bd147756 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -2,7 +2,7 @@
import os
-from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
+from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
class CoverageData(object):
@@ -39,14 +39,13 @@ class CoverageData(object):
`collector` is a string describing the coverage measurement software.
"""
- self.collector = collector
+ self.collector = collector or 'unknown'
self.use_file = True
# Construct the filename that will be used for data file storage, if we
# ever do any file storage.
- self.filename = (basename or
- os.environ.get(self.filename_env, self.filename_default))
+ self.filename = basename or ".coverage"
if suffix:
self.filename += suffix
self.filename = os.path.abspath(self.filename)
diff --git a/test/coveragetest.py b/test/coveragetest.py
index 4471392f..fb6a5bcc 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -38,9 +38,7 @@ class CoverageTest(TestCase):
self.old_dir = os.getcwd()
os.chdir(self.temp_dir)
- # Preserve changes to PYTHONPATH.
- self.old_pypath = os.environ.get('PYTHONPATH', '')
-
+
# Modules should be importable from this temp directory.
self.old_syspath = sys.path[:]
sys.path.insert(0, '')
@@ -48,6 +46,9 @@ class CoverageTest(TestCase):
# Keep a counter to make every call to check_coverage unique.
self.n = 0
+ # Record environment variables that we changed with set_environ.
+ self.environ_undos = {}
+
# Use a Tee to capture stdout.
self.old_stdout = sys.stdout
self.captured_stdout = StringIO()
@@ -55,17 +56,45 @@ class CoverageTest(TestCase):
def tearDown(self):
if self.run_in_temp_dir:
- # Restore the original sys.path and PYTHONPATH
+ # Restore the original sys.path.
sys.path = self.old_syspath
- os.environ['PYTHONPATH'] = self.old_pypath
# Get rid of the temporary directory.
os.chdir(self.old_dir)
shutil.rmtree(self.temp_root)
+ # Restore the environment.
+ self.undo_environ()
+
# Restore stdout.
sys.stdout = self.old_stdout
+ def set_environ(self, name, value):
+ """Set an environment variable `name` to be `value`.
+
+ The environment variable is set, and record is kept that it was set,
+ so that `tearDown` can restore its original value.
+
+ """
+ if name not in self.environ_undos:
+ self.environ_undos[name] = os.environ.get(name)
+ os.environ[name] = value
+
+ def original_environ(self, name):
+ """The environment variable `name` from when the test started."""
+ if name in self.environ_undos:
+ return self.environ_undos[name]
+ else:
+ return os.environ[name]
+
+ def undo_environ(self):
+ """Undo all the changes made by `set_environ`."""
+ for name, value in self.environ_undos.items():
+ if value is None:
+ del os.environ[name]
+ else:
+ os.environ[name] = value
+
def stdout(self):
"""Return the data written to stdout during the test."""
return self.captured_stdout.getvalue()
@@ -254,11 +283,11 @@ class CoverageTest(TestCase):
here = os.path.dirname(self.nice_file(coverage.__file__, ".."))
testmods = self.nice_file(here, 'test/modules')
zipfile = self.nice_file(here, 'test/zipmods.zip')
- pypath = self.old_pypath
+ pypath = self.original_environ('PYTHONPATH')
if pypath:
pypath += os.pathsep
pypath += testmods + os.pathsep + zipfile
- os.environ['PYTHONPATH'] = pypath
+ self.set_environ('PYTHONPATH', pypath)
_, output = run_command(cmd)
print(output)
diff --git a/test/test_api.py b/test/test_api.py
index 2552d114..0df2df4a 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -164,7 +164,7 @@ class ApiTest(CoverageTest):
# Measure without the stdlib.
cov1 = coverage.coverage()
- self.assertEqual(cov1.cover_pylib, False)
+ self.assertEqual(cov1.config.cover_pylib, False)
cov1.start()
self.import_module("mymain")
cov1.stop()
@@ -246,6 +246,26 @@ class ApiTest(CoverageTest):
self.assertSameElements(os.listdir("."),
["datatest3.py", "datatest3.pyc", "cov.data.14"])
+ def testDatafileFromRcFile(self):
+ # You can specify the data file name in the .coveragerc file
+ self.make_file("datatest4.py", """\
+ fooey = 17
+ """)
+ self.make_file(".coveragerc", """\
+ [run]
+ data_file = mydata.dat
+ """)
+
+ self.assertSameElements(os.listdir("."),
+ ["datatest4.py", ".coveragerc"])
+ cov = coverage.coverage()
+ cov.start()
+ self.import_module("datatest4")
+ cov.stop()
+ cov.save()
+ self.assertSameElements(os.listdir("."),
+ ["datatest4.py", "datatest4.pyc", ".coveragerc", "mydata.dat"])
+
def testEmptyReporting(self):
# Used to be you'd get an exception reporting on nothing...
cov = coverage.coverage()
diff --git a/test/test_config.py b/test/test_config.py
new file mode 100644
index 00000000..a1abf51b
--- /dev/null
+++ b/test/test_config.py
@@ -0,0 +1,89 @@
+"""Test the config file handling for coverage.py"""
+
+import os, sys
+import coverage
+
+sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
+from coveragetest import CoverageTest
+
+
+class ConfigTest(CoverageTest):
+ """Tests of the config file support."""
+
+ def test_default_config(self):
+ # Just constructing a coverage() object gets the right defaults.
+ cov = coverage.coverage()
+ self.assertFalse(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, ".coverage")
+
+ def test_arguments(self):
+ # Arguments to the constructor are applied to the configuation.
+ cov = coverage.coverage(timid=True, data_file="fooey.dat")
+ self.assert_(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, "fooey.dat")
+
+ def test_config_file(self):
+ # A .coveragerc file will be read into the configuration.
+ self.make_file(".coveragerc", """\
+ # This is just a bogus .rc file for testing.
+ [run]
+ timid = True
+ data_file = .hello_kitty.data
+ """)
+ cov = coverage.coverage()
+ self.assert_(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, ".hello_kitty.data")
+
+ def test_named_config_file(self):
+ # You can name the config file what you like.
+ self.make_file("my_cov.ini", """\
+ [run]
+ timid = True
+ ; I wouldn't really use this as a data file...
+ data_file = delete.me
+ """)
+ cov = coverage.coverage(config_file="my_cov.ini")
+ self.assert_(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, "delete.me")
+
+ def test_ignored_config_file(self):
+ # You can disable reading the .coveragerc file.
+ self.make_file(".coveragerc", """\
+ [run]
+ timid = True
+ data_file = delete.me
+ """)
+ cov = coverage.coverage(config_file=False)
+ self.assertFalse(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, ".coverage")
+
+ def test_config_file_then_args(self):
+ # The arguments override the .coveragerc file.
+ self.make_file(".coveragerc", """\
+ [run]
+ timid = True
+ data_file = weirdo.file
+ """)
+ cov = coverage.coverage(timid=False, data_file=".mycov")
+ self.assertFalse(cov.config.timid)
+ self.assertFalse(cov.config.branch)
+ self.assertEqual(cov.config.data_file, ".mycov")
+
+ def test_data_file_from_environment(self):
+ # There's an environment variable for the data_file.
+ self.make_file(".coveragerc", """\
+ [run]
+ timid = True
+ data_file = weirdo.file
+ """)
+ self.set_environ("COVERAGE_FILE", "fromenv.dat")
+ cov = coverage.coverage()
+ self.assertEqual(cov.config.data_file, "fromenv.dat")
+ # But the constructor args override the env var.
+ cov = coverage.coverage(data_file="fromarg.dat")
+ self.assertEqual(cov.config.data_file, "fromarg.dat")
diff --git a/test/test_farm.py b/test/test_farm.py
index de07541b..8f7d5712 100644
--- a/test/test_farm.py
+++ b/test/test_farm.py
@@ -3,7 +3,7 @@
import difflib, filecmp, fnmatch, glob, os, re, shutil, sys
sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k
-from backtest import run_command, execfile # pylint: disable-msg=W0622
+from backtest import run_command, execfile # pylint: disable-msg=W0622
def test_farm(clean_only=False):