summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/control.py66
-rw-r--r--coverage/python.py41
-rw-r--r--tests/modules/usepkgs.py2
-rw-r--r--tests/test_api.py7
-rw-r--r--tests/test_python.py27
5 files changed, 91 insertions, 52 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 037fc6d2..ebb449c3 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -26,7 +26,7 @@ from coverage.misc import CoverageException, bool_or_none, join_regex
from coverage.misc import file_be_gone, isolate_module
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
-from coverage.python import PythonFileReporter
+from coverage.python import PythonFileReporter, source_for_file
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -164,6 +164,7 @@ class Coverage(object):
# Other instance attributes, set later.
self.omit = self.include = self.source = None
+ self.source_pkgs_unmatched = None
self.source_pkgs = None
self.data = self.data_files = self.collector = None
self.plugins = None
@@ -228,6 +229,7 @@ class Coverage(object):
self.source.append(files.canonical_filename(src))
else:
self.source_pkgs.append(src)
+ self.source_pkgs_unmatched = self.source_pkgs[:]
self.omit = prep_patterns(self.config.omit)
self.include = prep_patterns(self.config.include)
@@ -367,38 +369,6 @@ class Coverage(object):
morf_filename = PythonFileReporter(morf, self).filename
return os.path.split(morf_filename)[0]
- def _source_for_file(self, filename):
- """Return the source file for `filename`.
-
- Given a file name being traced, return the best guess as to the source
- file to attribute it to.
-
- """
- if filename.endswith(".py"):
- # .py files are themselves source files.
- return filename
-
- elif filename.endswith((".pyc", ".pyo")):
- # Bytecode files probably have source files near them.
- py_filename = filename[:-1]
- if os.path.exists(py_filename):
- # Found a .py file, use that.
- return py_filename
- if env.WINDOWS:
- # On Windows, it could be a .pyw file.
- pyw_filename = py_filename + "w"
- if os.path.exists(pyw_filename):
- return pyw_filename
- # Didn't find source, but it's probably the .py file we want.
- return py_filename
-
- elif filename.endswith("$py.class"):
- # Jython is easy to guess.
- return filename[:-9] + ".py"
-
- # No idea, just use the file name as-is.
- return filename
-
def _name_for_module(self, module_globals, filename):
"""Get the name of the module for a set of globals and file name.
@@ -461,7 +431,7 @@ class Coverage(object):
# co_filename value.
dunder_file = frame.f_globals.get('__file__')
if dunder_file:
- filename = self._source_for_file(dunder_file)
+ filename = source_for_file(dunder_file)
if original_filename and not original_filename.startswith('<'):
orig = os.path.basename(original_filename)
if orig != os.path.basename(filename):
@@ -558,8 +528,8 @@ class Coverage(object):
# 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)
+ if modulename in self.source_pkgs_unmatched:
+ self.source_pkgs_unmatched.remove(modulename)
return None # There's no reason to skip this file.
if not self.source_match.match(filename):
@@ -824,10 +794,10 @@ class Coverage(object):
self.collector.save_data(self.data)
- # If there are still entries in the source_pkgs list, then we never
+ # If there are still entries in the source_pkgs_unmatched list, then we never
# encountered those packages.
if self._warn_unimported_source:
- for pkg in self.source_pkgs:
+ for pkg in self.source_pkgs_unmatched:
if pkg not in sys.modules:
self._warn("Module %s was never imported." % pkg)
elif not (
@@ -842,8 +812,26 @@ class Coverage(object):
if not self.data and self._warn_no_data:
self._warn("No data was collected.")
+ src_directories = self.source[:]
+
+ for pkg in self.source_pkgs:
+ if (not pkg in sys.modules or
+ not hasattr(sys.modules[pkg], '__file__') or
+ not os.path.exists(sys.modules[pkg].__file__)):
+ continue
+ pkg_file = source_for_file(sys.modules[pkg].__file__)
+ #
+ # Do not explore the souce tree of a module that is
+ # not a directory tree. For instance if
+ # sys.modules['args'].__file__ == /lib/python2.7/site-packages/args.pyc
+ # we do not want to explore all of /lib/python2.7/site-packages
+ #
+ if not pkg_file.endswith('__init__.py'):
+ continue
+ src_directories.append(self._canonical_dir(sys.modules[pkg]))
+
# Find files that were never executed at all.
- for src in self.source:
+ for src in src_directories:
for py_file in find_python_files(src):
py_file = files.canonical_filename(py_file)
diff --git a/coverage/python.py b/coverage/python.py
index c3ca0e1e..f75be60a 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -91,6 +91,39 @@ def get_zip_bytes(filename):
return None
+def source_for_file(filename):
+ """Return the source file for `filename`.
+
+ Given a file name being traced, return the best guess as to the source
+ file to attribute it to.
+
+ """
+ if filename.endswith(".py"):
+ # .py files are themselves source files.
+ return filename
+
+ elif filename.endswith((".pyc", ".pyo")):
+ # Bytecode files probably have source files near them.
+ py_filename = filename[:-1]
+ if os.path.exists(py_filename):
+ # Found a .py file, use that.
+ return py_filename
+ if env.WINDOWS:
+ # On Windows, it could be a .pyw file.
+ pyw_filename = py_filename + "w"
+ if os.path.exists(pyw_filename):
+ return pyw_filename
+ # Didn't find source, but it's probably the .py file we want.
+ return py_filename
+
+ elif filename.endswith("$py.class"):
+ # Jython is easy to guess.
+ return filename[:-9] + ".py"
+
+ # No idea, just use the file name as-is.
+ return filename
+
+
class PythonFileReporter(FileReporter):
"""Report support for a Python file."""
@@ -106,13 +139,7 @@ class PythonFileReporter(FileReporter):
else:
filename = morf
- filename = files.unicode_filename(filename)
-
- # .pyc files should always refer to a .py instead.
- if filename.endswith(('.pyc', '.pyo')):
- filename = filename[:-1]
- elif filename.endswith('$py.class'): # Jython
- filename = filename[:-9] + ".py"
+ filename = source_for_file(files.unicode_filename(filename))
super(PythonFileReporter, self).__init__(files.canonical_filename(filename))
diff --git a/tests/modules/usepkgs.py b/tests/modules/usepkgs.py
index 4e94acaa..222e68ce 100644
--- a/tests/modules/usepkgs.py
+++ b/tests/modules/usepkgs.py
@@ -1,7 +1,7 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-import pkg1.p1a, pkg1.p1b
+import pkg1.p1a, pkg1.p1b, pkg1.sub
import pkg2.p2a, pkg2.p2b
import othermods.othera, othermods.otherb
import othermods.sub.osa, othermods.sub.osb
diff --git a/tests/test_api.py b/tests/test_api.py
index 9de83fb2..c7fa5ca2 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -461,12 +461,11 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest):
summary[k[:-3]] = v
return summary
- def test_source_package(self):
- lines = self.coverage_usepkgs(source=["pkg1"])
- self.filenames_in(lines, "p1a p1b")
+ def test_source_package_unexecuted(self):
+ lines = self.coverage_usepkgs(source=["pkg1.sub"])
self.filenames_not_in(lines, "p2a p2b othera otherb osa osb")
# Because source= was specified, we do search for unexecuted files.
- self.assertEqual(lines['p1c'], 0)
+ self.assertEqual(lines['runmod3'], 0)
def test_source_package_dotted(self):
lines = self.coverage_usepkgs(source=["pkg1.p1b"])
diff --git a/tests/test_python.py b/tests/test_python.py
index ee1e1f95..c4e333ef 100644
--- a/tests/test_python.py
+++ b/tests/test_python.py
@@ -5,8 +5,9 @@
import os
import sys
+from coverage import env
-from coverage.python import get_zip_bytes
+from coverage.python import get_zip_bytes, source_for_file
from tests.coveragetest import CoverageTest
@@ -28,3 +29,27 @@ class GetZipBytesTest(CoverageTest):
self.assertIn('All OK', zip_text)
# Run the code to see that we really got it encoded properly.
__import__("encoded_"+encoding)
+
+def test_source_for_file(tmpdir):
+ path = tmpdir.join("a.py")
+ src = str(path)
+ assert src == source_for_file(src)
+ assert src == source_for_file(src + 'c')
+ assert src == source_for_file(src + 'o')
+ unknown = src + 'FOO'
+ assert unknown == source_for_file(unknown)
+ #
+ # On windows if a pyw exists, it is an acceptable source
+ #
+ windows = env.WINDOWS
+ env.WINDOWS = True
+ path_windows = tmpdir.ensure("a.pyw")
+ assert str(path_windows) == source_for_file(src + 'c')
+ #
+ # If both pyw and py exist, py is preferred
+ #
+ path.ensure(file=True)
+ assert src == source_for_file(src + 'c')
+ env.WINDOWS = windows
+ jython_src = "a"
+ assert jython_src + ".py" == source_for_file(jython_src + "$py.class")