summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-11-24 21:30:04 -0500
committerNed Batchelder <ned@nedbatchelder.com>2014-11-24 21:30:04 -0500
commit2b369aa719d2a4b4e755c9030f1d0cc1dfeeeacb (patch)
tree361d0a995b5360a0170aff07cc87aa5afbc7562a
parent8fa9db9f86de0b7cbce45e0a5fe87e38e47212b7 (diff)
parentb3ccb75241566c1e1a814ae99a84637fd0ac2b44 (diff)
downloadpython-coveragepy-git-2b369aa719d2a4b4e755c9030f1d0cc1dfeeeacb.tar.gz
Merged pull request 42, fixing issue #328.
-rw-r--r--coverage/control.py117
-rw-r--r--coverage/execfile.py22
-rw-r--r--coverage/files.py33
-rw-r--r--coverage/pytracer.py2
-rw-r--r--doc/contributing.rst2
-rw-r--r--tests/test_files.py35
-rw-r--r--tests/test_process.py151
-rw-r--r--tests/try_execfile.py5
8 files changed, 308 insertions, 59 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 64175ee4..346f655f 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,6 +1,6 @@
"""Core control stuff for Coverage."""
-import atexit, os, platform, random, socket, sys
+import atexit, inspect, os, platform, random, socket, sys
from coverage.annotate import AnnotateReporter
from coverage.backward import string_class, iitems
@@ -12,6 +12,7 @@ from coverage.debug import DebugControl
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, overrides
@@ -269,6 +270,7 @@ class Coverage(object):
# Create the matchers we need for _should_trace
if self.source or self.source_pkgs:
self.source_match = TreeMatcher(self.source)
+ self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
else:
if self.cover_dir:
self.cover_match = TreeMatcher([self.cover_dir])
@@ -303,6 +305,43 @@ class Coverage(object):
filename = filename[:-9] + ".py"
return filename
+ def _name_for_module(self, module_namespace, filename):
+ """
+ For configurability's sake, we allow __main__ modules to be matched by their importable name.
+
+ If loaded via runpy (aka -m), we can usually recover the "original" full dotted module name,
+ otherwise, we resort to interpreting the filename to get the module's name.
+ In the case that the module name can't be deteremined, None is returned.
+ """
+ # TODO: unit-test
+ dunder_name = module_namespace.get('__name__', None)
+
+ if isinstance(dunder_name, str) and dunder_name != '__main__':
+ # this is the usual case: an imported module
+ return dunder_name
+
+ loader = module_namespace.get('__loader__', None)
+ for attrname in ('fullname', 'name'): # attribute renamed in py3.2
+ if hasattr(loader, attrname):
+ fullname = getattr(loader, attrname)
+ else:
+ continue
+
+ if (
+ isinstance(fullname, str) and
+ fullname != '__main__'
+ ):
+ # module loaded via runpy -m
+ return fullname
+
+ # script as first argument to python cli
+ inspectedname = inspect.getmodulename(filename)
+ if inspectedname is not None:
+ return inspectedname
+ else:
+ return dunder_name
+
+
def _should_trace_with_reason(self, filename, frame):
"""Decide whether to trace execution in `filename`, with a reason.
@@ -319,8 +358,6 @@ class Coverage(object):
disp.reason = reason
return disp
- 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
@@ -378,7 +415,9 @@ class Coverage(object):
(plugin, disp.original_filename)
)
if disp.check_filters:
- reason = self._check_include_omit_etc(disp.source_filename)
+ reason = self._check_include_omit_etc(
+ disp.source_filename, frame,
+ )
if reason:
nope(disp, reason)
@@ -386,18 +425,25 @@ class Coverage(object):
return nope(disp, "no plugin found") # TODO: a test that causes this.
- def _check_include_omit_etc(self, filename):
+ 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
# stdlib and coverage.py directories.
if self.source_match:
+ if self.source_pkgs_match.match(modulename):
+ if modulename in self.source_pkgs:
+ self.source_pkgs.remove(modulename)
+ return None # There's no reason to skip this file.
+
if not self.source_match.match(filename):
return "falls outside the --source trees"
elif self.include_match:
@@ -432,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,)
@@ -453,46 +499,6 @@ class Coverage(object):
self._warnings.append(msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
- 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:
- pkg_file = None
- 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)
- if not os.path.exists(pkg_file):
- pkg_file = None
-
- if pkg_file:
- self.source.append(pkg_file)
- self.source_match.add(pkg_file)
- else:
- self._warn("Module %s has no Python source." % pkg)
-
- 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).
@@ -661,7 +667,20 @@ class Coverage(object):
# encountered those packages.
if self._warn_unimported_source:
for pkg in self.source_pkgs:
- self._warn("Module %s was never imported." % pkg)
+ if pkg not in sys.modules:
+ self._warn("Module %s was never imported." % pkg)
+ elif not (
+ hasattr(sys.modules[pkg], '__file__') and
+ os.path.exists(sys.modules[pkg].__file__)
+ ):
+ self._warn("Module %s has no Python source." % pkg)
+ else:
+ raise AssertionError('''\
+Unexpected third case:
+ name: %s
+ object: %r
+ __file__: %s''' % (pkg, sys.modules[pkg], sys.modules[pkg].__file__)
+ )
# Find out if we got any data.
summary = self.data.summary()
diff --git a/coverage/execfile.py b/coverage/execfile.py
index e7e20715..8965d207 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -7,6 +7,21 @@ from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
from coverage.misc import ExceptionDuringRun, NoCode, NoSource
+if sys.version_info >= (3, 3):
+ DEFAULT_FULLNAME = '__main__'
+else:
+ DEFAULT_FULLNAME = None
+
+
+class DummyLoader(object):
+ """A shim for the pep302 __loader__, emulating pkgutil.ImpLoader.
+
+ Currently only implements the .fullname attribute
+ """
+ def __init__(self, fullname, *args):
+ self.fullname = fullname
+
+
if importlib_util_find_spec:
def find_module(modulename):
"""Find the module named `modulename`.
@@ -91,10 +106,10 @@ def run_python_module(modulename, args):
pathname = os.path.abspath(pathname)
args[0] = pathname
- run_python_file(pathname, args, package=packagename)
+ run_python_file(pathname, args, package=packagename, modulename=modulename)
-def run_python_file(filename, args, package=None):
+def run_python_file(filename, args, package=None, modulename=DEFAULT_FULLNAME):
"""Run a python file as if it were the main program on the command line.
`filename` is the path to the file to execute, it need not be a .py file.
@@ -110,6 +125,9 @@ def run_python_file(filename, args, package=None):
main_mod.__file__ = filename
if package:
main_mod.__package__ = package
+ if modulename:
+ main_mod.__loader__ = DummyLoader(modulename)
+
main_mod.__builtins__ = BUILTINS
# Set sys.argv properly.
diff --git a/coverage/files.py b/coverage/files.py
index 03df1a71..3a298867 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -173,6 +173,39 @@ class TreeMatcher(object):
return False
+class ModuleMatcher(object):
+ """A matcher for modules in a tree."""
+ def __init__(self, module_names):
+ self.modules = list(module_names)
+
+ def __repr__(self):
+ return "<ModuleMatcher %r>" % (self.modules)
+
+ def info(self):
+ """A list of strings for displaying when dumping state."""
+ return self.modules
+
+ def add(self, module):
+ """Add another directory to the list we match for."""
+ self.modules.append(module)
+
+ def match(self, module_name):
+ """Does `module_name` indicate a module in one of our packages?
+ """
+ if not module_name:
+ return False
+
+ for m in self.modules:
+ if module_name.startswith(m):
+ if module_name == m:
+ return True
+ if module_name[len(m)] == '.':
+ # This is a module in the package
+ return True
+
+ return False
+
+
class FnmatchMatcher(object):
"""A matcher for files by filename pattern."""
def __init__(self, pats):
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index b4fd59fa..f3f490a0 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -87,7 +87,7 @@ class PyTracer(object):
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):
+ if not self.check_include(tracename, frame):
tracename = None
else:
tracename = None
diff --git a/doc/contributing.rst b/doc/contributing.rst
index 88c99a99..8e1fd723 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -129,7 +129,7 @@ I try to keep the coverage.py as clean as possible. I use pylint to alert me
to possible problems::
$ make lint
- pylint --rcfile=.pylintrc coverage setup.py tests
+ pylint coverage setup.py tests
python -m tabnanny coverage setup.py tests
python igor.py check_eol
diff --git a/tests/test_files.py b/tests/test_files.py
index 648c76a9..f6976a81 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -2,7 +2,9 @@
import os, os.path
-from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
+from coverage.files import (
+ FileLocator, TreeMatcher, FnmatchMatcher, ModuleMatcher
+)
from coverage.files import PathAliases, find_python_files, abs_file
from coverage.misc import CoverageException
@@ -80,6 +82,37 @@ class MatcherTest(CoverageTest):
for filepath, matches in matches_to_try:
self.assertMatches(tm, filepath, matches)
+ def test_module_matcher(self):
+ matches_to_try = [
+ ('test', True),
+ ('test', True),
+ ('trash', False),
+ ('testing', False),
+ ('test.x', True),
+ ('test.x.y.z', True),
+ ('py', False),
+ ('py.t', False),
+ ('py.test', True),
+ ('py.testing', False),
+ ('py.test.buz', True),
+ ('py.test.buz.baz', True),
+ ('__main__', False),
+ ('mymain', True),
+ ('yourmain', False),
+ ]
+ modules = ['test', 'py.test', 'mymain']
+ mm = ModuleMatcher(modules)
+ self.assertEqual(
+ mm.info(),
+ modules
+ )
+ for modulename, matches in matches_to_try:
+ self.assertEqual(
+ mm.match(modulename),
+ matches,
+ modulename,
+ )
+
def test_fnmatch_matcher(self):
matches_to_try = [
(self.make_file("sub/file1.py"), True),
diff --git a/tests/test_process.py b/tests/test_process.py
index 4584e7d7..09d4c207 100644
--- a/tests/test_process.py
+++ b/tests/test_process.py
@@ -331,6 +331,55 @@ class ProcessTest(CoverageTest):
out_py = self.run_command("python -m tests.try_execfile")
self.assertMultiLineEqual(out_cov, out_py)
+ def test_coverage_run_dashm_equal_to_doubledashsource(self):
+ """regression test for #328
+
+ When imported by -m, a module's __name__ is __main__, but we need the
+ --source machinery to know and respect the original name.
+ """
+ # These -m commands assume the coverage tree is on the path.
+ out_cov = self.run_command(
+ "coverage run --source tests.try_execfile -m tests.try_execfile"
+ )
+ out_py = self.run_command("python -m tests.try_execfile")
+ self.assertMultiLineEqual(out_cov, out_py)
+
+ def test_coverage_run_dashm_superset_of_doubledashsource(self):
+ """Edge case: --source foo -m foo.bar"""
+ # These -m commands assume the coverage tree is on the path.
+ out_cov = self.run_command(
+ "coverage run --source tests -m tests.try_execfile"
+ )
+ out_py = self.run_command("python -m tests.try_execfile")
+ self.assertMultiLineEqual(out_cov, out_py)
+
+ st, out = self.run_command_status("coverage report")
+ self.assertEqual(st, 0)
+ self.assertEqual(self.line_count(out), 6, out)
+
+ def test_coverage_run_script_imports_doubledashsource(self):
+ self.make_file("myscript", """\
+ import sys
+ sys.dont_write_bytecode = True
+
+ def main():
+ import tests.try_execfile
+
+ if __name__ == '__main__':
+ main()
+ """)
+
+ # These -m commands assume the coverage tree is on the path.
+ out_cov = self.run_command(
+ "coverage run --source tests myscript"
+ )
+ out_py = self.run_command("python myscript")
+ self.assertMultiLineEqual(out_cov, out_py)
+
+ st, out = self.run_command_status("coverage report")
+ self.assertEqual(st, 0)
+ self.assertEqual(self.line_count(out), 6, out)
+
def test_coverage_run_dashm_is_like_python_dashm_off_path(self):
# https://bitbucket.org/ned/coveragepy/issue/242
tryfile = os.path.join(here, "try_execfile.py")
@@ -613,11 +662,10 @@ class FailUnderTest(CoverageTest):
self.assertEqual(st, 2)
-class ProcessStartupTest(CoverageTest):
- """Test that we can measure coverage in subprocesses."""
-
+class ProcessCoverageMixin(object):
+ """Set up a .pth file that causes all subprocesses to be coverage'd"""
def setUp(self):
- super(ProcessStartupTest, self).setUp()
+ super(ProcessCoverageMixin, self).setUp()
# Find a place to put a .pth file.
pth_contents = "import coverage; coverage.process_startup()\n"
for d in sys.path: # pragma: part covered
@@ -635,10 +683,14 @@ class ProcessStartupTest(CoverageTest):
raise Exception("Couldn't find a place for the .pth file")
def tearDown(self):
- super(ProcessStartupTest, self).tearDown()
+ super(ProcessCoverageMixin, self).tearDown()
# Clean up the .pth file we made.
os.remove(self.pth_path)
+
+class ProcessStartupTest(ProcessCoverageMixin, CoverageTest):
+ """Test that we can measure coverage in subprocesses."""
+
def test_subprocess_with_pth_files(self): # pragma: not covered
if os.environ.get('COVERAGE_COVERAGE', ''):
raise SkipTest(
@@ -671,3 +723,92 @@ class ProcessStartupTest(CoverageTest):
data = coverage.CoverageData()
data.read_file(".mycovdata")
self.assertEqual(data.summary()['sub.py'], 2)
+
+
+class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest):
+ """Show that we can configure {[run]source} during process-level coverage.
+
+ There are two interesting variables:
+ 1) -m versus a simple script argument (eg `python myscript`)
+ 2) filtering for the top-level (main.py) or second-level (sub.py) module
+ 3) whether the files are in a package or not
+
+ ... for a total of eight tests.
+ """
+ def assert_pth_and_source_work_together(self, dashm, package, source):
+ def fullname(modname):
+ if package and dashm:
+ return '.'.join((package, modname))
+ else:
+ return modname
+
+ def path(basename):
+ return os.path.join(package, basename)
+
+ if os.environ.get('COVERAGE_COVERAGE', ''):
+ raise SkipTest(
+ "Can't test subprocess pth file suppport during metacoverage"
+ )
+ # Main will run sub.py
+ self.make_file(path("main.py"), """\
+ import %s
+ if True: pass
+ """ % fullname('sub'))
+ if package:
+ self.make_file(path("__init__.py"), '')
+ # sub.py will write a few lines.
+ self.make_file(path("sub.py"), """\
+ with open("out.txt", "w") as f:
+ f.write("Hello, world!\\n")
+ """)
+ self.make_file("coverage.ini", """\
+ [run]
+ source = %s
+ """ % fullname(source)
+ )
+
+ self.set_environ("COVERAGE_PROCESS_START", "coverage.ini")
+
+ if dashm:
+ cmd = (sys.executable, dashm, fullname('main'))
+ else:
+ cmd = (sys.executable, path('main.py'))
+
+ from subprocess import Popen
+ Popen(cmd).wait()
+
+ with open("out.txt") as f:
+ self.assertEqual(f.read(), "Hello, world!\n")
+
+ # Read the data from .coverage
+ self.assert_exists(".coverage")
+ data = coverage.CoverageData()
+ data.read_file(".coverage")
+ summary = data.summary()
+ print(summary)
+ self.assertEqual(summary[source + '.py'], 2)
+ self.assertEqual(len(summary), 1)
+
+ def test_dashm_main(self):
+ self.assert_pth_and_source_work_together('-m', '', 'main')
+
+ def test_script_main(self):
+ self.assert_pth_and_source_work_together('', '', 'main')
+
+ def test_dashm_sub(self):
+ self.assert_pth_and_source_work_together('-m', '', 'sub')
+
+ def test_script_sub(self):
+ self.assert_pth_and_source_work_together('', '', 'sub')
+
+ def test_dashm_pkg_main(self):
+ self.assert_pth_and_source_work_together('-m', 'pkg', 'main')
+
+ def test_script_pkg_main(self):
+ self.assert_pth_and_source_work_together('', 'pkg', 'main')
+
+ def test_dashm_pkg_sub(self):
+ self.assert_pth_and_source_work_together('-m', 'pkg', 'sub')
+
+ def test_script_pkg_sub(self):
+ self.assert_pth_and_source_work_together('', 'pkg', 'sub')
diff --git a/tests/try_execfile.py b/tests/try_execfile.py
index 314ffae0..ee5bae5c 100644
--- a/tests/try_execfile.py
+++ b/tests/try_execfile.py
@@ -54,12 +54,17 @@ def my_function(a):
FN_VAL = my_function("fooey")
+loader = globals().get('__loader__')
+fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None)
+
globals_to_check = {
'__name__': __name__,
'__file__': __file__,
'__doc__': __doc__,
'__builtins__.has_open': hasattr(__builtins__, 'open'),
'__builtins__.dir': dir(__builtins__),
+ '__loader__ exists': loader is not None,
+ '__loader__.fullname': fullname,
'__package__': __package__,
'DATA': DATA,
'FN_VAL': FN_VAL,