diff options
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/backunittest.py | 7 | ||||
-rw-r--r-- | coverage/cmdline.py | 13 | ||||
-rw-r--r-- | coverage/collector.py | 17 | ||||
-rw-r--r-- | coverage/config.py | 77 | ||||
-rw-r--r-- | coverage/control.py | 572 | ||||
-rw-r--r-- | coverage/debug.py | 5 | ||||
-rw-r--r-- | coverage/disposition.py | 37 | ||||
-rw-r--r-- | coverage/execfile.py | 17 | ||||
-rw-r--r-- | coverage/htmlfiles/coverage_html.js | 11 | ||||
-rw-r--r-- | coverage/htmlfiles/style.css | 1 | ||||
-rw-r--r-- | coverage/inorout.py | 461 | ||||
-rw-r--r-- | coverage/multiproc.py | 17 | ||||
-rw-r--r-- | coverage/plugin.py | 3 | ||||
-rw-r--r-- | coverage/python.py | 28 | ||||
-rw-r--r-- | coverage/version.py | 2 |
15 files changed, 717 insertions, 551 deletions
diff --git a/coverage/backunittest.py b/coverage/backunittest.py index 09574ccb..1b084835 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -3,12 +3,7 @@ """Implementations of unittest features from the future.""" -# Use unittest2 if it's available, otherwise unittest. This gives us -# back-ported features for 2.6. -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest def unittest_has(method): diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 7b86054e..ea86b445 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -44,7 +44,7 @@ class Opts(object): ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", - help="Debug options, separated by commas", + help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="DIR", @@ -115,7 +115,11 @@ class Opts(object): ) rcfile = optparse.make_option( '', '--rcfile', action='store', - help="Specify configuration file. Defaults to '.coveragerc'", + help=( + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried. " + "[env: COVERAGE_RCFILE]" + ), ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...", @@ -124,7 +128,7 @@ class Opts(object): timid = optparse.make_option( '', '--timid', action='store_true', help=( - "Use a simpler but slower trace method. Try this if you get " + "Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ), ) @@ -475,6 +479,7 @@ class CoverageScript(object): include=include, debug=debug, concurrency=options.concurrency, + check_preimported=True, ) if options.action == "debug": @@ -656,7 +661,7 @@ class CoverageScript(object): self.coverage.load() data = self.coverage.data print(info_header("data")) - print("path: %s" % self.coverage.data_files.filename) + print("path: %s" % self.coverage._data_files.filename) if data: print("has_arcs: %r" % data.has_arcs()) summary = data.line_counts(fullpath=True) diff --git a/coverage/collector.py b/coverage/collector.py index 72ab32b6..bc385fc2 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -9,6 +9,7 @@ import sys from coverage import env from coverage.backward import litems, range # pylint: disable=redefined-builtin from coverage.debug import short_stack +from coverage.disposition import FileDisposition from coverage.files import abs_file from coverage.misc import CoverageException, isolate_module from coverage.pytracer import PyTracer @@ -33,11 +34,6 @@ except ImportError: CTracer = None -class FileDisposition(object): - """A simple value type for recording what to do with a file.""" - pass - - def should_start_context(frame): """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" fn_name = frame.f_code.co_name @@ -107,6 +103,7 @@ class Collector(object): self.origin = short_stack() self.concur_id_func = None + self.abs_file_cache = {} # We can handle a few concurrency options here, but only one at a time. these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) @@ -373,6 +370,14 @@ class Collector(object): for tracer in self.tracers: tracer.data = data + def cached_abs_file(self, filename): + """A locally cached version of `abs_file`.""" + key = (type(filename), filename) + try: + return self.abs_file_cache[key] + except KeyError: + return self.abs_file_cache.setdefault(key, abs_file(filename)) + def save_data(self, covdata): """Save the collected data to a `CoverageData`. @@ -398,7 +403,7 @@ class Collector(object): else: raise runtime_err # pylint: disable=raising-bad-type - return dict((abs_file(k), v) for k, v in items) + return dict((self.cached_abs_file(k), v) for k, v in items) if self.branch: covdata.add_arcs(abs_file_dict(self.data)) diff --git a/coverage/config.py b/coverage/config.py index 7b8f2bd0..effa382f 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -6,8 +6,8 @@ import collections import os import re -import sys +from coverage import env from coverage.backward import configparser, iitems, string_class from coverage.misc import contract, CoverageException, isolate_module @@ -33,7 +33,7 @@ class HandyConfigParser(configparser.RawConfigParser): def read(self, filenames): """Read a file name as UTF-8 configuration data.""" kwargs = {} - if sys.version_info >= (3, 2): + if env.PYVERSION >= (3, 2): kwargs['encoding'] = "utf-8" return configparser.RawConfigParser.read(self, filenames, **kwargs) @@ -175,8 +175,12 @@ class CoverageConfig(object): def __init__(self): """Initialize the configuration attributes to their defaults.""" # Metadata about the config. + # We tried to read these config files. self.attempted_config_files = [] - self.config_files = [] + # We did read these config files, but maybe didn't find any content for us. + self.config_files_read = [] + # The file that gave us our configuration. + self.config_file = None # Defaults for [run] and [report] self._include = None @@ -262,7 +266,7 @@ class CoverageConfig(object): if not files_read: return False - self.config_files.extend(files_read) + self.config_files_read.extend(files_read) any_set = False try: @@ -305,9 +309,14 @@ class CoverageConfig(object): # then it was used. If we're piggybacking on someone else's file, # then it was only used if we found some settings in it. if our_file: - return True + used = True else: - return any_set + used = any_set + + if used: + self.config_file = filename + + return used CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: @@ -425,6 +434,34 @@ class CoverageConfig(object): raise CoverageException("No such option: %r" % option_name) +def config_files_to_try(config_file): + """What config files should we try to read? + + Returns a list of tuples: + (filename, is_our_file, was_file_specified) + """ + + # Some API users were specifying ".coveragerc" to mean the same as + # True, so make it so. + if config_file == ".coveragerc": + config_file = True + specified_file = (config_file is not True) + if not specified_file: + # No file was specified. Check COVERAGE_RCFILE. + config_file = os.environ.get('COVERAGE_RCFILE') + if config_file: + specified_file = True + if not specified_file: + # Still no file specified. Default to .coveragerc + config_file = ".coveragerc" + files_to_try = [ + (config_file, True, specified_file), + ("setup.cfg", False, False), + ("tox.ini", False, False), + ] + return files_to_try + + def read_coverage_config(config_file, **kwargs): """Read the coverage.py configuration. @@ -435,10 +472,7 @@ def read_coverage_config(config_file, **kwargs): setting values in the configuration. Returns: - config_file, config: - config_file is the value to use for config_file in other - invocations of coverage. - + config: config is a CoverageConfig object read from the appropriate configuration file. @@ -449,25 +483,14 @@ def read_coverage_config(config_file, **kwargs): # 2) from a file: if config_file: - # Some API users were specifying ".coveragerc" to mean the same as - # True, so make it so. - if config_file == ".coveragerc": - config_file = True - specified_file = (config_file is not True) - if not specified_file: - config_file = ".coveragerc" - - for fname, our_file in [(config_file, True), - ("setup.cfg", False), - ("tox.ini", False)]: - config_read = config.from_file(fname, our_file=our_file) - is_config_file = fname == config_file - - if not config_read and is_config_file and specified_file: - raise CoverageException("Couldn't read '%s' as a config file" % fname) + files_to_try = config_files_to_try(config_file) + for fname, our_file, specified_file in files_to_try: + config_read = config.from_file(fname, our_file=our_file) if config_read: break + if specified_file: + raise CoverageException("Couldn't read '%s' as a config file" % fname) # 3) from environment variables: env_data_file = os.environ.get('COVERAGE_FILE') @@ -486,4 +509,4 @@ def read_coverage_config(config_file, **kwargs): config.html_dir = os.path.expanduser(config.html_dir) config.xml_output = os.path.expanduser(config.xml_output) - return config_file, config + return config diff --git a/coverage/control.py b/coverage/control.py index b82c8047..80012f57 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -5,14 +5,10 @@ import atexit -import inspect -import itertools import os import platform -import re import sys import time -import traceback from coverage import env from coverage.annotate import AnnotateReporter @@ -21,16 +17,15 @@ from coverage.collector import Collector from coverage.config import read_coverage_config from coverage.data import CoverageData, CoverageDataFiles from coverage.debug import DebugControl, write_formatted_info -from coverage.files import TreeMatcher, FnmatchMatcher -from coverage.files import PathAliases, find_python_files, prep_patterns -from coverage.files import canonical_filename, set_relative_directory -from coverage.files import ModuleMatcher, abs_file +from coverage.disposition import disposition_debug_msg +from coverage.files import PathAliases, set_relative_directory, abs_file from coverage.html import HtmlReporter +from coverage.inorout import InOrOut 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, source_for_file +from coverage.python import PythonFileReporter from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -43,22 +38,6 @@ except ImportError: # pragma: only jytho os = isolate_module(os) -# Pypy has some unusual stuff in the "stdlib". Consider those locations -# when deciding where the stdlib is. These modules are not used for anything, -# they are modules importable from the pypy lib directories, so that we can -# find those directories. -_structseq = _pypy_irc_topic = None -if env.PYPY: - try: - import _structseq - except ImportError: - pass - - try: - import _pypy_irc_topic - except ImportError: - pass - class Coverage(object): """Programmatic access to coverage.py. @@ -74,11 +53,12 @@ class Coverage(object): cov.html_report(directory='covhtml') """ + def __init__( self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, omit=None, include=None, debug=None, - concurrency=None, + concurrency=None, check_preimported=False, ): """ `data_file` is the base name of the data file to use, defaulting to @@ -132,15 +112,23 @@ class Coverage(object): "eventlet", "gevent", "multiprocessing", or "thread" (the default). This can also be a list of these strings. + If `check_preimported` is true, then when coverage is started, the + aleady-imported files will be checked to see if they should be measured + by coverage. Importing measured files before coverage is started can + mean that code is missed. + .. versionadded:: 4.0 The `concurrency` parameter. .. versionadded:: 4.2 The `concurrency` parameter can now be a list of strings. + .. versionadded:: 4.6 + The `check_preimported` parameter. + """ # Build our configuration from a number of sources. - self.config_file, self.config = read_coverage_config( + self.config = read_coverage_config( config_file=config_file, data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), @@ -153,31 +141,24 @@ class Coverage(object): self._debug_file = None self._auto_load = self._auto_save = auto_data - self._data_suffix = data_suffix - - # The matchers for _should_trace. - self.source_match = None - self.source_pkgs_match = None - self.pylib_match = self.cover_match = None - self.include_match = self.omit_match = None + self._data_suffix_specified = data_suffix # Is it ok for no data to be collected? self._warn_no_data = True self._warn_unimported_source = True + self._warn_preimported_source = check_preimported # A record of all the warnings that have been issued. self._warnings = [] # 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 - self.pylib_paths = self.cover_paths = None - self.data_suffix = self.run_suffix = None + self.data = self._data_files = self._collector = None + self._plugins = None + self._inorout = None + self._inorout_class = InOrOut + self._data_suffix = self._run_suffix = None self._exclude_re = None - self.debug = None + self._debug = None # State machine variables: # Have we initialized everything? @@ -215,7 +196,7 @@ class Coverage(object): self._debug_file = open(debug_file_name, "a") else: self._debug_file = sys.stderr - self.debug = DebugControl(self.config.debug, self._debug_file) + self._debug = DebugControl(self.config.debug, self._debug_file) # _exclude_re is a dict that maps exclusion list names to compiled regexes. self._exclude_re = {} @@ -223,41 +204,28 @@ class Coverage(object): set_relative_directory() # Load plugins - self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) + self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug) # Run configuring plugins. - for plugin in self.plugins.configurers: + for plugin in self._plugins.configurers: # We need an object with set_option and get_option. Either self or # self.config will do. Choosing randomly stops people from doing # other things with those objects, against the public API. Yes, # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - # The source argument can be directories or package names. - self.source = [] - self.source_pkgs = [] - for src in self.config.source or []: - if os.path.isdir(src): - self.source.append(canonical_filename(src)) - else: - self.source_pkgs.append(src) - self.source_pkgs_unmatched = self.source_pkgs[:] - - self.omit = prep_patterns(self.config.run_omit) - self.include = prep_patterns(self.config.run_include) - concurrency = self.config.concurrency or [] if "multiprocessing" in concurrency: if not patch_multiprocessing: raise CoverageException( # pragma: only jython "multiprocessing is not supported on this Python" ) - patch_multiprocessing(rcfile=self.config_file) + patch_multiprocessing(rcfile=self.config.config_file) # Multi-processing uses parallel for the subprocesses, so also use # it for the main process. self.config.parallel = True - self.collector = Collector( + self._collector = Collector( should_trace=self._should_trace, check_include=self._check_include_omit_etc, timid=self.config.timid, @@ -267,320 +235,73 @@ class Coverage(object): ) # Early warning if we aren't going to be able to support plugins. - if self.plugins.file_tracers and not self.collector.supports_plugins: + if self._plugins.file_tracers and not self._collector.supports_plugins: self._warn( "Plugin file tracers (%s) aren't supported with %s" % ( ", ".join( plugin._coverage_plugin_name - for plugin in self.plugins.file_tracers + for plugin in self._plugins.file_tracers ), - self.collector.tracer_name(), + self._collector.tracer_name(), ) ) - for plugin in self.plugins.file_tracers: + for plugin in self._plugins.file_tracers: plugin._coverage_enabled = False + # Create the file classifying substructure. + self._inorout = self._inorout_class(warn=self._warn) + self._inorout.configure(self.config) + self._inorout.plugins = self._plugins + self._inorout.disp_class = self._collector.file_disposition_class + # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as - # `self.run_suffix` now, and promote it to `self.data_suffix` if we + # `self._run_suffix` now, and promote it to `self._data_suffix` if we # find that we are collecting data later. - if self._data_suffix or self.config.parallel: - if not isinstance(self._data_suffix, string_class): + if self._data_suffix_specified or self.config.parallel: + if not isinstance(self._data_suffix_specified, string_class): # if data_suffix=True, use .machinename.pid.random - self._data_suffix = True + self._data_suffix_specified = True else: - self._data_suffix = None - self.data_suffix = None - self.run_suffix = self._data_suffix + self._data_suffix_specified = None + self._data_suffix = None + self._run_suffix = self._data_suffix_specified # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. - self.data = CoverageData(debug=self.debug) - self.data_files = CoverageDataFiles( - basename=self.config.data_file, warn=self._warn, debug=self.debug, + self.data = CoverageData(debug=self._debug) + self._data_files = CoverageDataFiles( + basename=self.config.data_file, warn=self._warn, debug=self._debug, ) - # The directories for files considered "installed with the interpreter". - self.pylib_paths = set() - if not self.config.cover_pylib: - # Look at where some standard modules are located. That's the - # indication for "installed with the interpreter". In some - # environments (virtualenv, for example), these modules may be - # spread across a few locations. Look at all the candidate modules - # we've imported, and take all the different ones. - for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): - if m is not None and hasattr(m, "__file__"): - self.pylib_paths.add(self._canonical_path(m, directory=True)) - - if _structseq and not hasattr(_structseq, '__file__'): - # PyPy 2.4 has no __file__ in the builtin modules, but the code - # objects still have the file names. So dig into one to find - # the path to exclude. - structseq_new = _structseq.structseq_new - try: - structseq_file = structseq_new.func_code.co_filename - except AttributeError: - structseq_file = structseq_new.__code__.co_filename - self.pylib_paths.add(self._canonical_path(structseq_file)) - - # To avoid tracing the coverage.py code itself, we skip anything - # located where we are. - self.cover_paths = [self._canonical_path(__file__, directory=True)] - if env.TESTING: - # Don't include our own test code. - self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) - - # When testing, we use PyContracts, which should be considered - # part of coverage.py, and it uses six. Exclude those directories - # just as we exclude ourselves. - import contracts - import six - for mod in [contracts, six]: - self.cover_paths.append(self._canonical_path(mod)) - # Set the reporting precision. Numbers.set_precision(self.config.precision) atexit.register(self._atexit) - # 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_paths: - self.cover_match = TreeMatcher(self.cover_paths) - if self.pylib_paths: - self.pylib_match = TreeMatcher(self.pylib_paths) - if self.include: - self.include_match = FnmatchMatcher(self.include) - if self.omit: - self.omit_match = FnmatchMatcher(self.omit) - # The user may want to debug things, show info if desired. self._write_startup_debug() def _write_startup_debug(self): """Write out debug info at startup if needed.""" wrote_any = False - with self.debug.without_callers(): - if self.debug.should('config'): + with self._debug.without_callers(): + if self._debug.should('config'): config_info = sorted(self.config.__dict__.items()) - write_formatted_info(self.debug, "config", config_info) + write_formatted_info(self._debug, "config", config_info) wrote_any = True - if self.debug.should('sys'): - write_formatted_info(self.debug, "sys", self.sys_info()) - for plugin in self.plugins: + if self._debug.should('sys'): + write_formatted_info(self._debug, "sys", self.sys_info()) + for plugin in self._plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() - write_formatted_info(self.debug, header, info) + write_formatted_info(self._debug, header, info) wrote_any = True if wrote_any: - write_formatted_info(self.debug, "end", ()) - - def _canonical_path(self, morf, directory=False): - """Return the canonical path of the module or file `morf`. - - If the module is a package, then return its directory. If it is a - module, then return its file, unless `directory` is True, in which - case return its enclosing directory. - - """ - morf_path = PythonFileReporter(morf, self).filename - if morf_path.endswith("__init__.py") or directory: - morf_path = os.path.split(morf_path)[0] - return morf_path - - def _name_for_module(self, module_globals, filename): - """Get the name of the module for a set of globals and file name. - - 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 - file name to get the module's name. In the case that the module name - can't be determined, None is returned. - - """ - if module_globals is None: # pragma: only ironpython - # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 - module_globals = {} - - dunder_name = module_globals.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_globals.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 command line. - inspectedname = inspect.getmodulename(filename) - if inspectedname is not None: - return inspectedname - else: - return dunder_name - - def _should_trace_internal(self, filename, frame): - """Decide whether to trace execution in `filename`, with a reason. - - This function is called from the trace function. As each new file name - is encountered, this function determines whether it is traced or not. - - Returns a FileDisposition object. - - """ - original_filename = filename - disp = _disposition_init(self.collector.file_disposition_class, filename) - - def nope(disp, reason): - """Simple helper to make it easy to return NO.""" - disp.trace = False - disp.reason = reason - return disp - - # Compiled Python files have two file names: frame.f_code.co_filename is - # the file name at the time the .pyc was compiled. The second name is - # __file__, which is where the .pyc was actually loaded from. Since - # .pyc files can be moved after compilation (for example, by being - # installed), we look for __file__ in the frame and prefer it to the - # co_filename value. - dunder_file = frame.f_globals and frame.f_globals.get('__file__') - if 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): - # Files shouldn't be renamed when moved. This happens when - # exec'ing code. If it seems like something is wrong with - # the frame's file name, then just use the original. - filename = original_filename - - if not filename: - # Empty string is pretty useless. - return nope(disp, "empty string isn't a file name") - - if filename.startswith('memory:'): - return nope(disp, "memory isn't traceable") - - if filename.startswith('<'): - # Lots of non-file execution is represented with artificial - # file names like "<string>", "<doctest readme.txt[0]>", or - # "<exec_function>". Don't ever trace these executions, since we - # can't do anything with the data later anyway. - return nope(disp, "not a real file name") - - # pyexpat does a dumb thing, calling the trace function explicitly from - # C code with a C file name. - if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): - return nope(disp, "pyexpat lies about itself") - - # Jython reports the .class file to the tracer, use the source file. - if filename.endswith("$py.class"): - filename = filename[:-9] + ".py" - - canonical = canonical_filename(filename) - disp.canonical_filename = canonical - - # Try the plugins, see if they have an opinion about the file. - plugin = None - for plugin in self.plugins.file_tracers: - if not plugin._coverage_enabled: - continue - - try: - file_tracer = plugin.file_tracer(canonical) - if file_tracer is not None: - file_tracer._coverage_plugin = plugin - disp.trace = True - disp.file_tracer = file_tracer - if file_tracer.has_dynamic_source_filename(): - disp.has_dynamic_filename = True - else: - disp.source_filename = canonical_filename( - file_tracer.source_filename() - ) - break - except Exception: - self._warn( - "Disabling plug-in %r due to an exception:" % ( - plugin._coverage_plugin_name - ) - ) - traceback.print_exc() - plugin._coverage_enabled = False - continue - else: - # No plugin wanted it: it's Python. - disp.trace = True - disp.source_filename = canonical - - if not disp.has_dynamic_filename: - if not disp.source_filename: - raise CoverageException( - "Plugin %r didn't set source_filename for %r" % - (plugin, disp.original_filename) - ) - reason = self._check_include_omit_etc_internal( - disp.source_filename, frame, - ) - if reason: - nope(disp, reason) - - return disp - - def _check_include_omit_etc_internal(self, filename, frame): - """Check a file name 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_unmatched: - self.source_pkgs_unmatched.remove(modulename) - elif not self.source_match.match(filename): - return "falls outside the --source trees" - elif self.include_match: - if not self.include_match.match(filename): - return "falls outside the --include trees" - else: - # 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 self.pylib_match and self.pylib_match.match(filename): - return "is in the stdlib" - - # We exclude the coverage.py code itself, since a little of it - # will be measured otherwise. - if self.cover_match and self.cover_match.match(filename): - return "is part of coverage.py" - - # Check the file against the omit pattern. - if self.omit_match and self.omit_match.match(filename): - return "is inside an --omit pattern" - - # No reason found to skip this file. - return None + write_formatted_info(self._debug, "end", ()) def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -588,9 +309,9 @@ class Coverage(object): Calls `_should_trace_internal`, and returns the FileDisposition. """ - disp = self._should_trace_internal(filename, frame) - if self.debug.should('trace'): - self.debug.write(_disposition_debug_msg(disp)) + disp = self._inorout.should_trace(filename, frame) + if self._debug.should('trace'): + self._debug.write(disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): @@ -599,13 +320,13 @@ class Coverage(object): Returns a boolean: True if the file should be traced, False if not. """ - reason = self._check_include_omit_etc_internal(filename, frame) - if self.debug.should('trace'): + reason = self._inorout.check_include_omit_etc(filename, frame) + if self._debug.should('trace'): if not reason: msg = "Including %r" % (filename,) else: msg = "Not including %r: %s" % (filename, reason) - self.debug.write(msg) + self._debug.write(msg) return not reason @@ -621,7 +342,7 @@ class Coverage(object): self._warnings.append(msg) if slug: msg = "%s (%s)" % (msg, slug) - if self.debug.should('pid'): + if self._debug.should('pid'): msg = "[%d] %s" % (os.getpid(), msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) @@ -673,8 +394,8 @@ class Coverage(object): def load(self): """Load previously-collected coverage data from the data file.""" self._init() - self.collector.reset() - self.data_files.read(self.data) + self._collector.reset() + self._data_files.read(self.data) def start(self): """Start measuring code coverage. @@ -688,29 +409,32 @@ class Coverage(object): """ self._init() - if self.include: - if self.source or self.source_pkgs: - self._warn("--include is ignored because --source is set", slug="include-ignored") - if self.run_suffix: + self._inorout.warn_conflicting_settings() + + if self._run_suffix: # Calling start() means we're running code, so use the run_suffix # as the data_suffix when we eventually save the data. - self.data_suffix = self.run_suffix + self._data_suffix = self._run_suffix if self._auto_load: self.load() - self.collector.start() + # See if we think some code that would eventually be measured has already been imported. + if self._warn_preimported_source: + self._inorout.warn_already_imported_files() + + self._collector.start() self._started = True def stop(self): """Stop measuring code coverage.""" if self._started: - self.collector.stop() + self._collector.stop() self._started = False def _atexit(self): """Clean up on process shutdown.""" - if self.debug.should("process"): - self.debug.write("atexit: {0!r}".format(self)) + if self._debug.should("process"): + self._debug.write("atexit: {0!r}".format(self)) if self._started: self.stop() if self._auto_save: @@ -724,9 +448,9 @@ class Coverage(object): """ self._init() - self.collector.reset() + self._collector.reset() self.data.erase() - self.data_files.erase(parallel=self.config.parallel) + self._data_files.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -779,7 +503,7 @@ class Coverage(object): """Save the collected coverage data to the data file.""" self._init() self.get_data() - self.data_files.write(self.data, suffix=self.data_suffix) + self._data_files.write(self.data, suffix=self._data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -814,7 +538,7 @@ class Coverage(object): for pattern in paths[1:]: aliases.add(pattern, result) - self.data_files.combine_parallel_data( + self._data_files.combine_parallel_data( self.data, aliases=aliases, data_paths=data_paths, strict=strict, ) @@ -830,7 +554,7 @@ class Coverage(object): """ self._init() - if self.collector.save_data(self.data): + if self._collector.save_data(self.data): self._post_save_work() return self.data @@ -845,83 +569,19 @@ class Coverage(object): # 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_unmatched: - self._warn_about_unmeasured_code(pkg) + self._inorout.warn_unimported_source() # Find out if we got any data. if not self.data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. - 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__) - self._find_unexecuted_files(self._canonical_path(pkg_file)) - - for src in self.source: - self._find_unexecuted_files(src) + for file_path, plugin_name in self._inorout.find_unexecuted_files(): + self.data.touch_file(file_path, plugin_name) if self.config.note: self.data.add_run_info(note=self.config.note) - def _warn_about_unmeasured_code(self, pkg): - """Warn about a package or module that we never traced. - - `pkg` is a string, the name of the package or module. - - """ - mod = sys.modules.get(pkg) - if mod is None: - self._warn("Module %s was never imported." % pkg, slug="module-not-imported") - return - - is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') - has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) - - if is_namespace: - # A namespace package. It's OK for this not to have been traced, - # since there is no code directly in it. - return - - if not has_file: - self._warn("Module %s has no Python source." % pkg, slug="module-not-python") - return - - # The module was in sys.modules, and seems like a module with code, but - # we never measured it. I guess that means it was imported before - # coverage even started. - self._warn( - "Module %s was previously imported, but not measured" % pkg, - slug="module-not-measured", - ) - - def _find_plugin_files(self, src_dir): - """Get executable files from the plugins.""" - for plugin in self.plugins.file_tracers: - for x_file in plugin.find_executable_files(src_dir): - yield x_file, plugin._coverage_plugin_name - - def _find_unexecuted_files(self, src_dir): - """Find unexecuted files in `src_dir`. - - Search for files in `src_dir` that are probably importable, - and add them as unexecuted files in `self.data`. - - """ - py_files = ((py_file, None) for py_file in find_python_files(src_dir)) - plugin_files = self._find_plugin_files(src_dir) - - for file_path, plugin_name in itertools.chain(py_files, plugin_files): - file_path = canonical_filename(file_path) - if self.omit_match and self.omit_match.match(file_path): - # Turns out this file was omitted, so don't pull it back - # in as unexecuted. - continue - self.data.touch_file(file_path, plugin_name) - # Backward compatibility with version 1. def analysis(self, morf): """Like `analysis2` but doesn't return excluded line numbers.""" @@ -976,7 +636,7 @@ class Coverage(object): abs_morf = abs_file(morf) plugin_name = self.data.file_tracer(abs_morf) if plugin_name: - plugin = self.plugins.get(plugin_name) + plugin = self._plugins.get(plugin_name) if plugin: file_reporter = plugin.file_reporter(abs_morf) @@ -1165,14 +825,13 @@ class Coverage(object): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_paths', self.cover_paths), - ('pylib_paths', self.pylib_paths), - ('tracer', self.collector.tracer_name()), - ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), - ('plugins.configurers', plugin_info(self.plugins.configurers)), - ('config_files', self.config.attempted_config_files), - ('configs_read', self.config.config_files), - ('data_path', self.data_files.filename), + ('tracer', self._collector.tracer_name()), + ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), + ('plugins.configurers', plugin_info(self._plugins.configurers)), + ('configs_attempted', self.config.attempted_config_files), + ('configs_read', self.config.config_files_read), + ('config_file', self.config.config_file), + ('data_path', self._data_files.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), @@ -1187,51 +846,11 @@ class Coverage(object): ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - matcher_names = [ - 'source_match', 'source_pkgs_match', - 'include_match', 'omit_match', - 'cover_match', 'pylib_match', - ] - - for matcher_name in matcher_names: - matcher = getattr(self, matcher_name) - if matcher: - matcher_info = matcher.info() - else: - matcher_info = '-none-' - info.append((matcher_name, matcher_info)) + info.extend(self._inorout.sys_info()) return info -# FileDisposition "methods": FileDisposition is a pure value object, so it can -# be implemented in either C or Python. Acting on them is done with these -# functions. - -def _disposition_init(cls, original_filename): - """Construct and initialize a new FileDisposition object.""" - disp = cls() - disp.original_filename = original_filename - disp.canonical_filename = original_filename - disp.source_filename = None - disp.trace = False - disp.reason = "" - disp.file_tracer = None - disp.has_dynamic_filename = False - return disp - - -def _disposition_debug_msg(disp): - """Make a nice debug message of what the FileDisposition is doing.""" - if disp.trace: - msg = "Tracing %r" % (disp.original_filename,) - if disp.file_tracer: - msg += ": will be traced by %r" % disp.file_tracer - else: - msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) - return msg - - def process_startup(): """Call this at Python start-up to perhaps measure coverage. @@ -1277,10 +896,11 @@ def process_startup(): cov = Coverage(config_file=cps) process_startup.coverage = cov - cov.start() cov._warn_no_data = False cov._warn_unimported_source = False + cov._warn_preimported_source = False cov._auto_save = True + cov.start() return cov diff --git a/coverage/debug.py b/coverage/debug.py index e68736f6..6e6e8013 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -215,7 +215,7 @@ class DebugOutputFile(object): # pragma: debugging self.write("New process: executable: %s\n" % (sys.executable,)) self.write("New process: cmd: %s\n" % (cmd,)) if hasattr(os, 'getppid'): - self.write("New process: parent pid: %s\n" % (os.getppid(),)) + self.write("New process: pid: %s, parent pid: %s\n" % (os.getpid(), os.getppid())) SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' @@ -234,7 +234,8 @@ class DebugOutputFile(object): # pragma: debugging # on a class attribute. Yes, this is aggressively gross. the_one = sys.modules.get(cls.SYS_MOD_NAME) if the_one is None: - assert fileobj is not None + if fileobj is None: + fileobj = open("/tmp/debug_log.txt", "a") sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) return the_one diff --git a/coverage/disposition.py b/coverage/disposition.py new file mode 100644 index 00000000..e9b8ba65 --- /dev/null +++ b/coverage/disposition.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Simple value objects for tracking what to do with files.""" + + +class FileDisposition(object): + """A simple value type for recording what to do with a file.""" + pass + + +# FileDisposition "methods": FileDisposition is a pure value object, so it can +# be implemented in either C or Python. Acting on them is done with these +# functions. + +def disposition_init(cls, original_filename): + """Construct and initialize a new FileDisposition object.""" + disp = cls() + disp.original_filename = original_filename + disp.canonical_filename = original_filename + disp.source_filename = None + disp.trace = False + disp.reason = "" + disp.file_tracer = None + disp.has_dynamic_filename = False + return disp + + +def disposition_debug_msg(disp): + """Make a nice debug message of what the FileDisposition is doing.""" + if disp.trace: + msg = "Tracing %r" % (disp.original_filename,) + if disp.file_tracer: + msg += ": will be traced by %r" % disp.file_tracer + else: + msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) + return msg diff --git a/coverage/execfile.py b/coverage/execfile.py index 42e0d96a..68417f8a 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -9,6 +9,7 @@ import struct import sys import types +from coverage import env from coverage.backward import BUILTINS from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module @@ -111,7 +112,15 @@ def run_python_module(modulename, args): pathname = os.path.abspath(pathname) args[0] = pathname - run_python_file(pathname, args, package=packagename, modulename=modulename, path0="") + # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It + # used to be an empty string (meaning the current directory). It changed + # to be the actual path to the current directory, so that os.chdir wouldn't + # affect the outcome. + if env.PYVERSION >= (3, 7, 0, 'beta', 3): + path0 = os.getcwd() + else: + path0 = "" + run_python_file(pathname, args, package=packagename, modulename=modulename, path0=path0) def run_python_file(filename, args, package=None, modulename=None, path0=None): @@ -128,7 +137,7 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): function will decide on a value. """ - if modulename is None and sys.version_info >= (3, 3): + if modulename is None and env.PYVERSION >= (3, 3): modulename = '__main__' # Create a module to serve as __main__ @@ -255,7 +264,7 @@ def make_code_from_pyc(filename): raise NoCode("Bad magic number in .pyc file") date_based = True - if sys.version_info >= (3, 7, 0, 'alpha', 4): + if env.PYVERSION >= (3, 7, 0, 'alpha', 4): flags = struct.unpack('<L', fpyc.read(4))[0] hash_based = flags & 0x01 if hash_based: @@ -264,7 +273,7 @@ def make_code_from_pyc(filename): if date_based: # Skip the junk in the header that we don't need. fpyc.read(4) # Skip the moddate. - if sys.version_info >= (3, 3): + if env.PYVERSION >= (3, 3): # 3.3 added another long to the header (size), skip it. fpyc.read(4) diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index f6f5de20..c1a41192 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -555,11 +555,16 @@ coverage.resize_scroll_markers = function () { var previous_line = -99, last_mark, - last_top; + last_top, + offsets = {}; + // Calculate line offsets outside loop to prevent relayouts + c.missed_lines.each(function() { + offsets[this.id] = $(this).offset().top; + }); c.missed_lines.each(function () { - var line_top = Math.round($(this).offset().top * marker_scale), - id_name = $(this).attr('id'), + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), line_number = parseInt(id_name.substring(1, id_name.length)); if (line_number === previous_line + 1) { diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 86b82091..12e90645 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -365,6 +365,7 @@ td.text { height: 100%; background: white; border-left: 1px solid #eee; + will-change: transform; /* for faster scrolling of fixed element in Chrome */ } #scroll_marker .marker { diff --git a/coverage/inorout.py b/coverage/inorout.py new file mode 100644 index 00000000..c0f27d78 --- /dev/null +++ b/coverage/inorout.py @@ -0,0 +1,461 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Determining whether files are being measured/reported or not.""" + +# For finding the stdlib +import atexit +import inspect +import itertools +import os +import platform +import re +import sys +import traceback + +from coverage import env +from coverage.disposition import FileDisposition, disposition_init +from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher +from coverage.files import prep_patterns, find_python_files, canonical_filename +from coverage.misc import CoverageException +from coverage.python import source_for_file, source_for_morf + + +# Pypy has some unusual stuff in the "stdlib". Consider those locations +# when deciding where the stdlib is. These modules are not used for anything, +# they are modules importable from the pypy lib directories, so that we can +# find those directories. +_structseq = _pypy_irc_topic = None +if env.PYPY: + try: + import _structseq + except ImportError: + pass + + try: + import _pypy_irc_topic + except ImportError: + pass + + +def canonical_path(morf, directory=False): + """Return the canonical path of the module or file `morf`. + + If the module is a package, then return its directory. If it is a + module, then return its file, unless `directory` is True, in which + case return its enclosing directory. + + """ + morf_path = canonical_filename(source_for_morf(morf)) + if morf_path.endswith("__init__.py") or directory: + morf_path = os.path.split(morf_path)[0] + return morf_path + + +def name_for_module(filename, frame): + """Get the name of the module for a filename and frame. + + 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 + file name to get the module's name. In the case that the module name + can't be determined, None is returned. + + """ + module_globals = frame.f_globals if frame is not None else {} + if module_globals is None: # pragma: only ironpython + # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 + module_globals = {} + + dunder_name = module_globals.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_globals.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 command line. + inspectedname = inspect.getmodulename(filename) + if inspectedname is not None: + return inspectedname + else: + return dunder_name + + +def module_is_namespace(mod): + """Is the module object `mod` a PEP420 namespace module?""" + return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None + + +def module_has_file(mod): + """Does the module object `mod` have an existing __file__ ?""" + mod__file__ = getattr(mod, '__file__', None) + if mod__file__ is None: + return False + return os.path.exists(mod__file__) + + +class InOrOut(object): + """Machinery for determining what files to measure.""" + + def __init__(self, warn): + self.warn = warn + + # The matchers for should_trace. + self.source_match = None + self.source_pkgs_match = None + self.pylib_paths = self.cover_paths = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + self.plugins = [] + self.disp_class = FileDisposition + + # The source argument can be directories or package names. + self.source = [] + self.source_pkgs = [] + self.source_pkgs_unmatched = [] + self.omit = self.include = None + + def configure(self, config): + """Apply the configuration to get ready for decision-time.""" + for src in config.source or []: + if os.path.isdir(src): + self.source.append(canonical_filename(src)) + else: + self.source_pkgs.append(src) + self.source_pkgs_unmatched = self.source_pkgs[:] + + self.omit = prep_patterns(config.run_omit) + self.include = prep_patterns(config.run_include) + + # The directories for files considered "installed with the interpreter". + self.pylib_paths = set() + if not config.cover_pylib: + # Look at where some standard modules are located. That's the + # indication for "installed with the interpreter". In some + # environments (virtualenv, for example), these modules may be + # spread across a few locations. Look at all the candidate modules + # we've imported, and take all the different ones. + for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): + if m is not None and hasattr(m, "__file__"): + self.pylib_paths.add(canonical_path(m, directory=True)) + + if _structseq and not hasattr(_structseq, '__file__'): + # PyPy 2.4 has no __file__ in the builtin modules, but the code + # objects still have the file names. So dig into one to find + # the path to exclude. + structseq_new = _structseq.structseq_new + try: + structseq_file = structseq_new.func_code.co_filename + except AttributeError: + structseq_file = structseq_new.__code__.co_filename + self.pylib_paths.add(canonical_path(structseq_file)) + + # To avoid tracing the coverage.py code itself, we skip anything + # located where we are. + self.cover_paths = [canonical_path(__file__, directory=True)] + if env.TESTING: + # Don't include our own test code. + self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) + + # When testing, we use PyContracts, which should be considered + # part of coverage.py, and it uses six. Exclude those directories + # just as we exclude ourselves. + import contracts + import six + for mod in [contracts, six]: + self.cover_paths.append(canonical_path(mod)) + + # 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_paths: + self.cover_match = TreeMatcher(self.cover_paths) + if self.pylib_paths: + self.pylib_match = TreeMatcher(self.pylib_paths) + if self.include: + self.include_match = FnmatchMatcher(self.include) + if self.omit: + self.omit_match = FnmatchMatcher(self.omit) + + def should_trace(self, filename, frame=None): + """Decide whether to trace execution in `filename`, with a reason. + + This function is called from the trace function. As each new file name + is encountered, this function determines whether it is traced or not. + + Returns a FileDisposition object. + + """ + original_filename = filename + disp = disposition_init(self.disp_class, filename) + + def nope(disp, reason): + """Simple helper to make it easy to return NO.""" + disp.trace = False + disp.reason = reason + return disp + + if frame is not None: + # Compiled Python files have two file names: frame.f_code.co_filename is + # the file name at the time the .pyc was compiled. The second name is + # __file__, which is where the .pyc was actually loaded from. Since + # .pyc files can be moved after compilation (for example, by being + # installed), we look for __file__ in the frame and prefer it to the + # co_filename value. + dunder_file = frame.f_globals and frame.f_globals.get('__file__') + if 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): + # Files shouldn't be renamed when moved. This happens when + # exec'ing code. If it seems like something is wrong with + # the frame's file name, then just use the original. + filename = original_filename + + if not filename: + # Empty string is pretty useless. + return nope(disp, "empty string isn't a file name") + + if filename.startswith('memory:'): + return nope(disp, "memory isn't traceable") + + if filename.startswith('<'): + # Lots of non-file execution is represented with artificial + # file names like "<string>", "<doctest readme.txt[0]>", or + # "<exec_function>". Don't ever trace these executions, since we + # can't do anything with the data later anyway. + return nope(disp, "not a real file name") + + # pyexpat does a dumb thing, calling the trace function explicitly from + # C code with a C file name. + if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): + return nope(disp, "pyexpat lies about itself") + + # Jython reports the .class file to the tracer, use the source file. + if filename.endswith("$py.class"): + filename = filename[:-9] + ".py" + + canonical = canonical_filename(filename) + disp.canonical_filename = canonical + + # Try the plugins, see if they have an opinion about the file. + plugin = None + for plugin in self.plugins.file_tracers: + if not plugin._coverage_enabled: + continue + + try: + file_tracer = plugin.file_tracer(canonical) + if file_tracer is not None: + file_tracer._coverage_plugin = plugin + disp.trace = True + disp.file_tracer = file_tracer + if file_tracer.has_dynamic_source_filename(): + disp.has_dynamic_filename = True + else: + disp.source_filename = canonical_filename( + file_tracer.source_filename() + ) + break + except Exception: + self.warn( + "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name) + ) + traceback.print_exc() + plugin._coverage_enabled = False + continue + else: + # No plugin wanted it: it's Python. + disp.trace = True + disp.source_filename = canonical + + if not disp.has_dynamic_filename: + if not disp.source_filename: + raise CoverageException( + "Plugin %r didn't set source_filename for %r" % + (plugin, disp.original_filename) + ) + reason = self.check_include_omit_etc(disp.source_filename, frame) + if reason: + nope(disp, reason) + + return disp + + def check_include_omit_etc(self, filename, frame): + """Check a file name 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 = name_for_module(filename, frame) + + # 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_unmatched: + self.source_pkgs_unmatched.remove(modulename) + elif not self.source_match.match(filename): + return "falls outside the --source trees" + elif self.include_match: + if not self.include_match.match(filename): + return "falls outside the --include trees" + else: + # 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 self.pylib_match and self.pylib_match.match(filename): + return "is in the stdlib" + + # We exclude the coverage.py code itself, since a little of it + # will be measured otherwise. + if self.cover_match and self.cover_match.match(filename): + return "is part of coverage.py" + + # Check the file against the omit pattern. + if self.omit_match and self.omit_match.match(filename): + return "is inside an --omit pattern" + + # No reason found to skip this file. + return None + + def warn_conflicting_settings(self): + """Warn if there are settings that conflict.""" + if self.include: + if self.source or self.source_pkgs: + self.warn("--include is ignored because --source is set", slug="include-ignored") + + def warn_already_imported_files(self): + """Warn if files have already been imported that we will be measuring.""" + if self.include or self.source or self.source_pkgs: + warned = set() + for mod in list(sys.modules.values()): + filename = getattr(mod, "__file__", None) + if filename is None: + continue + if filename in warned: + continue + + disp = self.should_trace(filename) + if disp.trace: + msg = "Already imported a file that will be measured: {0}".format(filename) + self.warn(msg, slug="already-imported") + warned.add(filename) + + def warn_unimported_source(self): + """Warn about source packages that were of interest, but never traced.""" + for pkg in self.source_pkgs_unmatched: + self._warn_about_unmeasured_code(pkg) + + def _warn_about_unmeasured_code(self, pkg): + """Warn about a package or module that we never traced. + + `pkg` is a string, the name of the package or module. + + """ + mod = sys.modules.get(pkg) + if mod is None: + self.warn("Module %s was never imported." % pkg, slug="module-not-imported") + return + + if module_is_namespace(mod): + # A namespace package. It's OK for this not to have been traced, + # since there is no code directly in it. + return + + if not module_has_file(mod): + self.warn("Module %s has no Python source." % pkg, slug="module-not-python") + return + + # The module was in sys.modules, and seems like a module with code, but + # we never measured it. I guess that means it was imported before + # coverage even started. + self.warn( + "Module %s was previously imported, but not measured" % pkg, + slug="module-not-measured", + ) + + def find_unexecuted_files(self): + """Find files in the areas of interest that weren't traced. + + Yields pairs: file path, and responsible plug-in name. + """ + for pkg in self.source_pkgs: + if (not pkg in sys.modules or + not module_has_file(sys.modules[pkg])): + continue + pkg_file = source_for_file(sys.modules[pkg].__file__) + for ret in self._find_unexecuted_files(canonical_path(pkg_file)): + yield ret + + for src in self.source: + for ret in self._find_unexecuted_files(src): + yield ret + + def _find_plugin_files(self, src_dir): + """Get executable files from the plugins.""" + for plugin in self.plugins.file_tracers: + for x_file in plugin.find_executable_files(src_dir): + yield x_file, plugin._coverage_plugin_name + + def _find_unexecuted_files(self, src_dir): + """Find unexecuted files in `src_dir`. + + Search for files in `src_dir` that are probably importable, + and add them as unexecuted files in `self.data`. + + """ + py_files = ((py_file, None) for py_file in find_python_files(src_dir)) + plugin_files = self._find_plugin_files(src_dir) + + for file_path, plugin_name in itertools.chain(py_files, plugin_files): + file_path = canonical_filename(file_path) + if self.omit_match and self.omit_match.match(file_path): + # Turns out this file was omitted, so don't pull it back + # in as unexecuted. + continue + yield file_path, plugin_name + + def sys_info(self): + """Our information for Coverage.sys_info. + + Returns a list of (key, value) pairs. + """ + info = [ + ('cover_paths', self.cover_paths), + ('pylib_paths', self.pylib_paths), + ] + + matcher_names = [ + 'source_match', 'source_pkgs_match', + 'include_match', 'omit_match', + 'cover_match', 'pylib_match', + ] + + for matcher_name in matcher_names: + matcher = getattr(self, matcher_name) + if matcher: + matcher_info = matcher.info() + else: + matcher_info = '-none-' + info.append((matcher_name, matcher_info)) + + return info diff --git a/coverage/multiproc.py b/coverage/multiproc.py index fe837318..bbc88fbe 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -6,19 +6,16 @@ import multiprocessing import multiprocessing.process import os -import sys +from coverage import env from coverage.misc import contract # An attribute that will be set on the module to indicate that it has been # monkey-patched. PATCHED_MARKER = "_coverage$patched" -# The environment variable that specifies the rcfile for subprocesses. -COVERAGE_RCFILE_ENV = "_COVERAGE_RCFILE" - -if sys.version_info >= (3, 4): +if env.PYVERSION >= (3, 4): OriginalProcess = multiprocessing.process.BaseProcess else: OriginalProcess = multiprocessing.Process @@ -31,10 +28,10 @@ class ProcessWithCoverage(OriginalProcess): def _bootstrap(self): """Wrapper around _bootstrap to start coverage.""" from coverage import Coverage # avoid circular import - rcfile = os.environ[COVERAGE_RCFILE_ENV] - cov = Coverage(data_suffix=True, config_file=rcfile) + cov = Coverage(data_suffix=True) + cov._warn_preimported_source = False cov.start() - debug = cov.debug + debug = cov._debug try: if debug.should("multiproc"): debug.write("Calling multiprocessing bootstrap") @@ -73,14 +70,14 @@ def patch_multiprocessing(rcfile): if hasattr(multiprocessing, PATCHED_MARKER): return - if sys.version_info >= (3, 4): + if env.PYVERSION >= (3, 4): OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap else: multiprocessing.Process = ProcessWithCoverage # Set the value in ProcessWithCoverage that will be pickled into the child # process. - os.environ[COVERAGE_RCFILE_ENV] = rcfile + os.environ["COVERAGE_RCFILE"] = rcfile # When spawning processes rather than forking them, we have no state in the # new process. We sneak in there with a Stowaway: we stuff one of our own diff --git a/coverage/plugin.py b/coverage/plugin.py index db7ca0a7..415246ab 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -134,7 +134,8 @@ class CoveragePlugin(object): This will only be invoked if `filename` returns non-None from :meth:`file_tracer`. It's an error to return None from this method. - Returns a :class:`FileReporter` object to use to report on `filename`. + Returns a :class:`FileReporter` object to use to report on `filename`, + or the string `"python"` to have coverage.py treat the file as Python. """ _needs_to_implement(self, "file_reporter") diff --git a/coverage/python.py b/coverage/python.py index 372347f5..834bc332 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -97,7 +97,7 @@ def get_zip_bytes(filename): def source_for_file(filename): - """Return the source file for `filename`. + """Return the source filename for `filename`. Given a file name being traced, return the best guess as to the source file to attribute it to. @@ -129,22 +129,28 @@ def source_for_file(filename): return filename +def source_for_morf(morf): + """Get the source filename for the module-or-file `morf`.""" + if hasattr(morf, '__file__') and morf.__file__: + filename = morf.__file__ + elif isinstance(morf, types.ModuleType): + # A module should have had .__file__, otherwise we can't use it. + # This could be a PEP-420 namespace package. + raise CoverageException("Module {0} has no file".format(morf)) + else: + filename = morf + + filename = source_for_file(files.unicode_filename(filename)) + return filename + + class PythonFileReporter(FileReporter): """Report support for a Python file.""" def __init__(self, morf, coverage=None): self.coverage = coverage - if hasattr(morf, '__file__'): - filename = morf.__file__ - elif isinstance(morf, types.ModuleType): - # A module should have had .__file__, otherwise we can't use it. - # This could be a PEP-420 namespace package. - raise CoverageException("Module {0} has no file".format(morf)) - else: - filename = morf - - filename = source_for_file(files.unicode_filename(filename)) + filename = source_for_morf(morf) super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) diff --git a/coverage/version.py b/coverage/version.py index 7dc59e27..0e6b0f9c 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 5, 1, 'final', 0) +version_info = (5, 0, 0, 'alpha', 2) def _make_version(major, minor, micro, releaselevel, serial): |