summaryrefslogtreecommitdiff
path: root/coverage
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
commit414941cd8cb1e157eb1d5f629958f03c49e6be93 (patch)
tree22d3bd825e2561f80c91562379541b73a6f3edd5 /coverage
parentd182230b96de38b3cd318cf74a84787e1fc9b90d (diff)
parent84505f77650e7c62ba47da5c2b93d291885e7a9b (diff)
downloadpython-coveragepy-414941cd8cb1e157eb1d5f629958f03c49e6be93.tar.gz
Merged pull request 42, fixing issue #328.
Diffstat (limited to 'coverage')
-rw-r--r--coverage/control.py117
-rw-r--r--coverage/execfile.py22
-rw-r--r--coverage/files.py33
-rw-r--r--coverage/pytracer.py2
4 files changed, 122 insertions, 52 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 64175ee..346f655 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 e7e2071..8965d20 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 03df1a7..3a29886 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 b4fd59f..f3f490a 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