diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-11-24 21:30:04 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-11-24 21:30:04 -0500 |
commit | 414941cd8cb1e157eb1d5f629958f03c49e6be93 (patch) | |
tree | 22d3bd825e2561f80c91562379541b73a6f3edd5 /coverage | |
parent | d182230b96de38b3cd318cf74a84787e1fc9b90d (diff) | |
parent | 84505f77650e7c62ba47da5c2b93d291885e7a9b (diff) | |
download | python-coveragepy-414941cd8cb1e157eb1d5f629958f03c49e6be93.tar.gz |
Merged pull request 42, fixing issue #328.
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/control.py | 117 | ||||
-rw-r--r-- | coverage/execfile.py | 22 | ||||
-rw-r--r-- | coverage/files.py | 33 | ||||
-rw-r--r-- | coverage/pytracer.py | 2 |
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 |