diff options
Diffstat (limited to 'coverage/control.py')
-rw-r--r-- | coverage/control.py | 460 |
1 files changed, 226 insertions, 234 deletions
diff --git a/coverage/control.py b/coverage/control.py index 2c8d384e..0a5ccae6 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,34 +1,38 @@ -"""Core control stuff for Coverage.""" +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Core control stuff for coverage.py.""" import atexit import inspect import os import platform -import random -import socket +import re import sys import traceback -from coverage import env +from coverage import env, files from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems from coverage.collector import Collector from coverage.config import CoverageConfig -from coverage.data import CoverageData +from coverage.data import CoverageData, CoverageDataFiles from coverage.debug import DebugControl -from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher +from coverage.files import TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.files import ModuleMatcher, abs_file from coverage.html import HtmlReporter from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import file_be_gone, overrides +from coverage.misc import file_be_gone, isolate_module from coverage.monkey import patch_multiprocessing -from coverage.plugin import CoveragePlugin, FileReporter +from coverage.plugin import FileReporter +from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter +os = isolate_module(os) # Pypy has some unusual stuff in the "stdlib". Consider those locations # when deciding where the stdlib is. @@ -97,7 +101,7 @@ class Coverage(object): in the trees indicated by the file paths or package names will be measured. - `include` and `omit` are lists of filename patterns. Files that match + `include` and `omit` are lists of file name patterns. Files that match `include` will be measured, files that match `omit` will not. Each will also accept a single string argument. @@ -109,6 +113,9 @@ class Coverage(object): results. Valid strings are "greenlet", "eventlet", "gevent", or "thread" (the default). + .. versionadded:: 4.0 + The `concurrency` parameter. + """ # Build our configuration from a number of sources: # 1: defaults: @@ -138,6 +145,9 @@ class Coverage(object): env_data_file = os.environ.get('COVERAGE_FILE') if env_data_file: self.config.data_file = env_data_file + debugs = os.environ.get('COVERAGE_DEBUG') + if debugs: + self.config.debug.extend(debugs.split(",")) # 4: from constructor arguments: self.config.from_args( @@ -166,10 +176,10 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None - self.source_pkgs = self.file_locator = None - self.data = self.collector = None - self.plugins = self.file_tracing_plugins = None - self.pylib_dirs = self.cover_dir = None + self.source_pkgs = None + self.data = self.data_files = self.collector = None + self.plugins = None + self.pylib_dirs = self.cover_dirs = None self.data_suffix = self.run_suffix = None self._exclude_re = None self.debug = None @@ -186,41 +196,40 @@ class Coverage(object): """Set all the initial state. This is called by the public methods to initialize state. This lets us - construct a Coverage object, then tweak its state before this function - is called. + construct a :class:`Coverage` object, then tweak its state before this + function is called. """ - from coverage import __version__ - if self._inited: return - # Create and configure the debugging controller. + # Create and configure the debugging controller. COVERAGE_DEBUG_FILE + # is an environment variable, the name of a file to append debug logs + # to. if self._debug_file is None: - self._debug_file = sys.stderr + debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE") + if debug_file_name: + self._debug_file = open(debug_file_name, "a") + else: + self._debug_file = sys.stderr self.debug = DebugControl(self.config.debug, self._debug_file) # Load plugins - self.plugins = Plugins.load_plugins(self.config.plugins, self.config) - - self.file_tracing_plugins = [] - for plugin in self.plugins: - if overrides(plugin, "file_tracer", CoveragePlugin): - self.file_tracing_plugins.append(plugin) + self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) # _exclude_re is a dict that maps exclusion list names to compiled # regexes. self._exclude_re = {} self._exclude_regex_stale() - self.file_locator = FileLocator() + files.set_relative_directory() # The source argument can be directories or package names. self.source = [] self.source_pkgs = [] for src in self.config.source or []: if os.path.exists(src): - self.source.append(self.file_locator.canonical_filename(src)) + self.source.append(files.canonical_filename(src)) else: self.source_pkgs.append(src) @@ -242,17 +251,17 @@ class Coverage(object): ) # Early warning if we aren't going to be able to support plugins. - if self.file_tracing_plugins 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.file_tracing_plugins + for plugin in self.plugins.file_tracers ), self.collector.tracer_name(), ) ) - for plugin in self.file_tracing_plugins: + for plugin in self.plugins.file_tracers: plugin._coverage_enabled = False # Suffixes are a bit tricky. We want to use the data suffix only when @@ -271,13 +280,10 @@ class Coverage(object): # 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( - basename=self.config.data_file, - collector="coverage v%s" % __version__, - debug=self.debug, - ) + self.data = CoverageData(debug=self.debug) + self.data_files = CoverageDataFiles(basename=self.config.data_file, warn=self._warn) - # The dirs for files considered "installed with the interpreter". + # The directories for files considered "installed with the interpreter". self.pylib_dirs = set() if not self.config.cover_pylib: # Look at where some standard modules are located. That's the @@ -285,12 +291,12 @@ class Coverage(object): # 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, os, platform, random, socket, _structseq): + for m in (atexit, inspect, os, platform, re, _structseq, traceback): if m is not None and hasattr(m, "__file__"): self.pylib_dirs.add(self._canonical_dir(m)) if _structseq and not hasattr(_structseq, '__file__'): # PyPy 2.4 has no __file__ in the builtin modules, but the code - # objects still have the filenames. So dig into one to find + # objects still have the file names. So dig into one to find # the path to exclude. structseq_new = _structseq.structseq_new try: @@ -299,9 +305,16 @@ class Coverage(object): structseq_file = structseq_new.__code__.co_filename self.pylib_dirs.add(self._canonical_dir(structseq_file)) - # To avoid tracing the coverage code itself, we skip anything located - # where we are. - self.cover_dir = self._canonical_dir(__file__) + # To avoid tracing the coverage.py code itself, we skip anything + # located where we are. + self.cover_dirs = [self._canonical_dir(__file__)] + if env.TESTING: + # 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, six + for mod in [contracts, six]: + self.cover_dirs.append(self._canonical_dir(mod)) # Set the reporting precision. Numbers.set_precision(self.config.precision) @@ -315,8 +328,8 @@ class Coverage(object): 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]) + if self.cover_dirs: + self.cover_match = TreeMatcher(self.cover_dirs) if self.pylib_dirs: self.pylib_match = TreeMatcher(self.pylib_dirs) if self.include: @@ -350,7 +363,7 @@ class Coverage(object): def _source_for_file(self, filename): """Return the source file for `filename`. - Given a filename being traced, return the best guess as to the source + Given a file name being traced, return the best guess as to the source file to attribute it to. """ @@ -376,18 +389,18 @@ class Coverage(object): # Jython is easy to guess. return filename[:-9] + ".py" - # No idea, just use the filename as-is. + # 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 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 - filename to get the module's name. In the case that the module name + file name to get the module's name. In the case that the module name can't be determined, None is returned. """ @@ -424,7 +437,8 @@ class Coverage(object): Returns a FileDisposition object. """ - disp = FileDisposition(filename) + 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.""" @@ -432,8 +446,8 @@ class Coverage(object): disp.reason = reason return disp - # 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 + # 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 @@ -441,31 +455,43 @@ class Coverage(object): dunder_file = frame.f_globals.get('__file__') if dunder_file: filename = self._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 filename") + 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 - # filenames like "<string>", "<doctest readme.txt[0]>", or + # 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 filename") + 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 = self.file_locator.canonical_filename(filename) + canonical = files.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.file_tracing_plugins: + for plugin in self.plugins.file_tracers: if not plugin._coverage_enabled: continue @@ -478,10 +504,9 @@ class Coverage(object): if file_tracer.has_dynamic_source_filename(): disp.has_dynamic_filename = True else: - disp.source_filename = \ - self.file_locator.canonical_filename( - file_tracer.source_filename() - ) + disp.source_filename = files.canonical_filename( + file_tracer.source_filename() + ) break except Exception: self._warn( @@ -512,7 +537,7 @@ class Coverage(object): return disp def _check_include_omit_etc_internal(self, filename, frame): - """Check a filename against the include, omit, etc, rules. + """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. @@ -541,8 +566,8 @@ class Coverage(object): if self.pylib_match and self.pylib_match.match(filename): return "is in the stdlib" - # We exclude the coverage code itself, since a little of it will be - # measured otherwise. + # 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" @@ -561,11 +586,11 @@ class Coverage(object): """ disp = self._should_trace_internal(filename, frame) if self.debug.should('trace'): - self.debug.write(disp.debug_message()) + self.debug.write(_disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): - """Check a filename against the include/omit/etc, rules, verbosely. + """Check a file name against the include/omit/etc, rules, verbosely. Returns a boolean: True if the file should be traced, False if not. @@ -583,33 +608,70 @@ class Coverage(object): def _warn(self, msg): """Use `msg` as a warning.""" self._warnings.append(msg) - 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) - def use_cache(self, usecache): - """Control the use of a data file (incorrectly called a cache). + def get_option(self, option_name): + """Get an option from the configuration. + + `option_name` is a colon-separated string indicating the section and + option name. For example, the ``branch`` option in the ``[run]`` + section of the config file would be indicated with `"run:branch"`. - `usecache` is true or false, whether to read and write data on disk. + Returns the value of the option. + + .. versionadded:: 4.0 """ + return self.config.get_option(option_name) + + def set_option(self, option_name, value): + """Set an option in the configuration. + + `option_name` is a colon-separated string indicating the section and + option name. For example, the ``branch`` option in the ``[run]`` + section of the config file would be indicated with ``"run:branch"``. + + `value` is the new value for the option. This should be a Python + value where appropriate. For example, use True for booleans, not the + string ``"True"``. + + As an example, calling:: + + cov.set_option("run:branch", True) + + has the same effect as this configuration file:: + + [run] + branch = True + + .. versionadded:: 4.0 + + """ + self.config.set_option(option_name, value) + + def use_cache(self, usecache): + """Obsolete method.""" self._init() - self.data.usefile(usecache) + if not usecache: + self._warn("use_cache(False) is no longer supported.") def load(self): """Load previously-collected coverage data from the data file.""" self._init() self.collector.reset() - self.data.read() + self.data_files.read(self.data) def start(self): """Start measuring code coverage. - Coverage measurement actually occurs in functions called after `start` - is invoked. Statements in the same scope as `start` won't be measured. + Coverage measurement actually occurs in functions called after + :meth:`start` is invoked. Statements in the same scope as + :meth:`start` won't be measured. - Once you invoke `start`, you must also call `stop` eventually, or your - process might not shut down cleanly. + Once you invoke :meth:`start`, you must also call :meth:`stop` + eventually, or your process might not shut down cleanly. """ self._init() @@ -647,6 +709,7 @@ class Coverage(object): self._init() self.collector.reset() self.data.erase() + self.data_files.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -688,8 +751,8 @@ class Coverage(object): def get_exclude_list(self, which='exclude'): """Return a list of excluded regex patterns. - `which` indicates which list is desired. See `exclude` for the lists - that are available, and their meaning. + `which` indicates which list is desired. See :meth:`exclude` for the + lists that are available, and their meaning. """ self._init() @@ -698,62 +761,53 @@ class Coverage(object): def save(self): """Save the collected coverage data to the data file.""" self._init() - data_suffix = self.data_suffix - if data_suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name - data_suffix = "%s%s.%s.%06d" % ( - socket.gethostname(), extra, os.getpid(), - random.randint(0, 999999) - ) + self.get_data() + self.data_files.write(self.data, suffix=self.data_suffix) - self._harvest_data() - self.data.write(suffix=data_suffix) - - def combine(self, data_dirs=None): + def combine(self, data_paths=None): """Combine together a number of similarly-named coverage data files. All coverage data files whose name starts with `data_file` (from the coverage() constructor) will be read, and combined together into the current measurements. - `data_dirs` is a list of directories from which data files should be - combined. If no list is passed, then the data files from the current - directory will be combined. + `data_paths` is a list of files or directories from which data should + be combined. If no list is passed, then the data files from the + directory indicated by the current data file (probably the current + directory) will be combined. + + .. versionadded:: 4.0 + The `data_paths` parameter. """ self._init() + self.get_data() + aliases = None if self.config.paths: - aliases = PathAliases(self.file_locator) + aliases = PathAliases() for paths in self.config.paths.values(): result = paths[0] for pattern in paths[1:]: aliases.add(pattern, result) - self.data.combine_parallel_data(aliases=aliases, data_dirs=data_dirs) - def _harvest_data(self): + self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths) + + def get_data(self): """Get the collected data and reset the collector. Also warn about various problems collecting data. + Returns a :class:`coverage.CoverageData`, the collected coverage data. + + .. versionadded:: 4.0 + """ self._init() if not self._measured: - return + return self.data - # TODO: seems like this parallel structure is getting kinda old... - self.data.add_line_data(self.collector.get_line_data()) - self.data.add_arc_data(self.collector.get_arc_data()) - self.data.add_plugin_data(self.collector.get_plugin_data()) - self.collector.reset() + self.collector.save_data(self.data) # If there are still entries in the source_pkgs list, then we never # encountered those packages. @@ -767,20 +821,16 @@ class Coverage(object): ): self._warn("Module %s has no Python source." % pkg) else: - self._warn( - "Module %s was previously imported, " - "but not measured." % pkg - ) + self._warn("Module %s was previously imported, but not measured." % pkg) # Find out if we got any data. - summary = self.data.summary() - if not summary and self._warn_no_data: + if not self.data and self._warn_no_data: self._warn("No data was collected.") # Find files that were never executed at all. for src in self.source: for py_file in find_python_files(src): - py_file = self.file_locator.canonical_filename(py_file) + py_file = files.canonical_filename(py_file) if self.omit_match and self.omit_match.match(py_file): # Turns out this file was omitted, so don't pull it back @@ -789,7 +839,11 @@ class Coverage(object): self.data.touch_file(py_file) + if self.config.note: + self.data.add_run_info(note=self.config.note) + self._measured = False + return self.data # Backward compatibility with version 1. def analysis(self, morf): @@ -800,10 +854,10 @@ class Coverage(object): def analysis2(self, morf): """Analyze a module. - `morf` is a module or a filename. It will be analyzed to determine + `morf` is a module or a file name. It will be analyzed to determine its coverage statistics. The return value is a 5-tuple: - * The filename for the module. + * The file name for the module. * A list of line numbers of executable statements. * A list of line numbers of excluded statements. * A list of line numbers of statements not run (missing from @@ -830,19 +884,20 @@ class Coverage(object): Returns an `Analysis` object. """ - self._harvest_data() + self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(self, it) + return Analysis(self.data, it) def _get_file_reporter(self, morf): - """Get a FileReporter for a module or filename.""" + """Get a FileReporter for a module or file name.""" plugin = None + file_reporter = "python" if isinstance(morf, string_class): abs_morf = abs_file(morf) - plugin_name = self.data.plugin_data().get(abs_morf) + plugin_name = self.data.file_tracer(abs_morf) if plugin_name: plugin = self.plugins.get(plugin_name) @@ -854,25 +909,19 @@ class Coverage(object): plugin._coverage_plugin_name, morf ) ) - else: - file_reporter = PythonFileReporter(morf, self) - # The FileReporter can have a name attribute, but if it doesn't, we'll - # supply it as the relative path to self.filename. - if not hasattr(file_reporter, "name"): - file_reporter.name = self.file_locator.relative_filename( - file_reporter.filename - ) + if file_reporter == "python": + file_reporter = PythonFileReporter(morf, self) return file_reporter def _get_file_reporters(self, morfs=None): - """Get a list of FileReporters for a list of modules or filenames. + """Get a list of FileReporters for a list of modules or file names. - For each module or filename in `morfs`, find a FileReporter. Return + For each module or file name in `morfs`, find a FileReporter. Return the list of FileReporters. - If `morfs` is a single module or filename, this returns a list of one + If `morfs` is a single module or file name, this returns a list of one FileReporter. If `morfs` is empty or None, then the list of all files measured is used to find the FileReporters. @@ -901,14 +950,14 @@ class Coverage(object): Each module in `morfs` is listed, with counts of statements, executed statements, missing statements, and a list of lines missed. - `include` is a list of filename patterns. Files that match will be + `include` is a list of file name patterns. Files that match will be included in the report. Files matching `omit` will not be included in the report. Returns a float, the total percentage covered. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, show_missing=show_missing, skip_covered=skip_covered, @@ -927,10 +976,10 @@ class Coverage(object): marker to indicate the coverage of the line. Covered lines have ">", excluded lines have "-", and missing lines have "!". - See `coverage.report()` for other arguments. + See :meth:`report` for other arguments. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include ) @@ -951,12 +1000,12 @@ class Coverage(object): `title` is a text string (not HTML) to use as the title of the HTML report. - See `coverage.report()` for other arguments. + See :meth:`report` for other arguments. Returns a float, the total percentage covered. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, html_dir=directory, extra_css=extra_css, html_title=title, @@ -975,12 +1024,12 @@ class Coverage(object): Each module in `morfs` is included in the report. `outfile` is the path to write the file to, "-" will write to stdout. - See `coverage.report()` for other arguments. + See :meth:`report` for other arguments. Returns a float, the total percentage covered. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, xml_output=outfile, @@ -998,10 +1047,13 @@ class Coverage(object): output_dir = os.path.dirname(self.config.xml_output) if output_dir and not os.path.isdir(output_dir): os.makedirs(output_dir) - outfile = open(self.config.xml_output, "w") + open_kwargs = {} + if env.PY3: + open_kwargs['encoding'] = 'utf8' + outfile = open(self.config.xml_output, "w", **open_kwargs) file_to_close = outfile try: - reporter = XmlReporter(self, self.config, self.file_locator) + reporter = XmlReporter(self, self.config) return reporter.report(morfs, outfile=outfile) except CoverageException: delete_file = True @@ -1018,13 +1070,9 @@ class Coverage(object): import coverage as covmod self._init() - try: - implementation = platform.python_implementation() - except AttributeError: - implementation = "unknown" ft_plugins = [] - for ft in self.file_tracing_plugins: + for ft in self.plugins.file_tracers: ft_name = ft._coverage_plugin_name if not ft._coverage_enabled: ft_name += " (disabled)" @@ -1033,16 +1081,16 @@ class Coverage(object): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_dir', self.cover_dir), + ('cover_dirs', self.cover_dirs), ('pylib_dirs', self.pylib_dirs), ('tracer', self.collector.tracer_name()), - ('file_tracing_plugins', ft_plugins), + ('plugins.file_tracers', ft_plugins), ('config_files', self.config.attempted_config_files), ('configs_read', self.config.config_files), - ('data_path', self.data.filename), + ('data_path', self.data_files.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), - ('implementation', implementation), + ('implementation', platform.python_implementation()), ('executable', sys.executable), ('cwd', os.getcwd()), ('path', sys.path), @@ -1071,36 +1119,32 @@ class Coverage(object): return info -class FileDisposition(object): - """A simple object for noting a number of details of files to trace.""" - def __init__(self, original_filename): - self.original_filename = original_filename - self.canonical_filename = original_filename - self.source_filename = None - self.trace = False - self.reason = "" - self.file_tracer = None - self.has_dynamic_filename = False - - def __repr__(self): - ret = "FileDisposition %r" % (self.original_filename,) - if self.trace: - ret += " trace" - else: - ret += " notrace=%r" % (self.reason,) - if self.file_tracer: - ret += " file_tracer=%r" % (self.file_tracer,) - return "<" + ret + ">" - - def debug_message(self): - """Produce a debugging message explaining the outcome.""" - if self.trace: - msg = "Tracing %r" % (self.original_filename,) - if self.file_tracer: - msg += ": will be traced by %r" % self.file_tracer - else: - msg = "Not tracing %r: %s" % (self.original_filename, self.reason) - return msg +# 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(): @@ -1132,7 +1176,7 @@ def process_startup(): # because some virtualenv configurations make the same directory visible # twice in sys.path. This means that the .pth file will be found twice, # and executed twice, executing this function twice. We set a global - # flag (an attribute on this function) to indicate that coverage has + # flag (an attribute on this function) to indicate that coverage.py has # already been started, so we can avoid doing it twice. # # https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more @@ -1140,7 +1184,7 @@ def process_startup(): if hasattr(process_startup, "done"): # We've annotated this function before, so we must have already - # started coverage in this process. Nothing to do. + # started coverage.py in this process. Nothing to do. return process_startup.done = True @@ -1148,55 +1192,3 @@ def process_startup(): cov.start() cov._warn_no_data = False cov._warn_unimported_source = False - - -# A hack for debugging testing in sub-processes. -_TEST_NAME_FILE = "" # "/tmp/covtest.txt" - - -class Plugins(object): - """The currently loaded collection of coverage.py plugins.""" - - def __init__(self): - self.order = [] - self.names = {} - - @classmethod - def load_plugins(cls, modules, config): - """Load plugins from `modules`. - - Returns a list of loaded and configured plugins. - - """ - plugins = cls() - - for module in modules: - __import__(module) - mod = sys.modules[module] - - plugin_class = getattr(mod, "Plugin", None) - if plugin_class: - options = config.get_plugin_options(module) - plugin = plugin_class(options) - plugin._coverage_plugin_name = module - plugin._coverage_enabled = True - plugins.order.append(plugin) - plugins.names[module] = plugin - else: - raise CoverageException( - "Plugin module %r didn't define a Plugin class" % module - ) - - return plugins - - def __nonzero__(self): - return bool(self.order) - - __bool__ = __nonzero__ - - def __iter__(self): - return iter(self.order) - - def get(self, plugin_name): - """Return a plugin by name.""" - return self.names[plugin_name] |