12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226 |
- # 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 re
- import sys
- import traceback
- 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, CoverageDataFiles
- from coverage.debug import DebugControl
- 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, isolate_module
- from coverage.multiproc import patch_multiprocessing
- 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.
- try:
- import _structseq
- except ImportError:
- _structseq = None
- class Coverage(object):
- """Programmatic access to coverage.py.
- To use::
- from coverage import Coverage
- cov = Coverage()
- cov.start()
- #.. call your code ..
- cov.stop()
- 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,
- ):
- """
- `data_file` is the base name of the data file to use, defaulting to
- ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
- create the final file name. If `data_suffix` is simply True, then a
- suffix is created with the machine and process identity included.
- `cover_pylib` is a boolean determining whether Python code installed
- with the Python interpreter is measured. This includes the Python
- standard library and any packages installed with the interpreter.
- If `auto_data` is true, then any existing data file will be read when
- coverage measurement starts, and data will be saved automatically when
- measurement stops.
- If `timid` is true, then a slower and simpler trace function will be
- used. This is important for some environments where manipulation of
- tracing functions breaks the faster trace function.
- If `branch` is true, then branch coverage will be measured in addition
- to the usual statement coverage.
- `config_file` determines what configuration file to read:
- * If it is ".coveragerc", it is interpreted as if it were True,
- for backward compatibility.
- * If it is a string, it is the name of the file to read. If the
- file can't be read, it is an error.
- * If it is True, then a few standard files names are tried
- (".coveragerc", "setup.cfg"). It is not an error for these files
- to not be found.
- * If it is False, then no configuration file is read.
- `source` is a list of file paths or package names. Only code located
- in the trees indicated by the file paths or package names will be
- measured.
- `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.
- `debug` is a list of strings indicating what debugging information is
- desired.
- `concurrency` is a string indicating the concurrency library being used
- in the measured code. Without this, coverage.py will get incorrect
- results if these libraries are in use. Valid strings are "greenlet",
- "eventlet", "gevent", "multiprocessing", or "thread" (the default).
- This can also be a list of these strings.
- .. versionadded:: 4.0
- The `concurrency` parameter.
- .. versionadded:: 4.2
- The `concurrency` parameter can now be a list of strings.
- """
- # Build our configuration from a number of sources:
- # 1: defaults:
- self.config = CoverageConfig()
- # 2: from the rcfile, .coveragerc or setup.cfg file:
- if config_file:
- # pylint: disable=redefined-variable-type
- did_read_rc = False
- # 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"
- self.config_file = config_file
- did_read_rc = self.config.from_file(config_file)
- if not did_read_rc:
- if specified_file:
- raise CoverageException(
- "Couldn't read '%s' as a config file" % config_file
- )
- self.config.from_file("setup.cfg", section_prefix="coverage:")
- # 3: from environment variables:
- 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(
- data_file=data_file, cover_pylib=cover_pylib, timid=timid,
- branch=branch, parallel=bool_or_none(data_suffix),
- source=source, omit=omit, include=include, debug=debug,
- concurrency=concurrency,
- )
- self._debug_file = None
- self._auto_data = 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
- # Is it ok for no data to be collected?
- self._warn_no_data = True
- self._warn_unimported_source = True
- # 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 = 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
- # State machine variables:
- # Have we initialized everything?
- self._inited = False
- # Have we started collecting and not stopped it?
- self._started = False
- # Have we measured some data and not harvested it?
- self._measured = False
- # If we have sub-process measurement happening automatically, then we
- # want any explicit creation of a Coverage object to mean, this process
- # is already coverage-aware, so don't auto-measure it. By now, the
- # auto-creation of a Coverage object has already happened. But we can
- # find it and tell it not to save its data.
- if not env.METACOV:
- _prevent_sub_process_measurement()
- def _init(self):
- """Set all the initial state.
- This is called by the public methods to initialize state. This lets us
- construct a :class:`Coverage` object, then tweak its state before this
- function is called.
- """
- if self._inited:
- return
- # 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:
- 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.debug)
- # _exclude_re is a dict that maps exclusion list names to compiled
- # regexes.
- self._exclude_re = {}
- self._exclude_regex_stale()
- 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(files.canonical_filename(src))
- else:
- self.source_pkgs.append(src)
- self.omit = prep_patterns(self.config.omit)
- self.include = prep_patterns(self.config.include)
- concurrency = self.config.concurrency or []
- if "multiprocessing" in concurrency:
- patch_multiprocessing(rcfile=self.config_file)
- #concurrency = None
- # Multi-processing uses parallel for the subprocesses, so also use
- # it for the main process.
- self.config.parallel = True
- self.collector = Collector(
- should_trace=self._should_trace,
- check_include=self._check_include_omit_etc,
- timid=self.config.timid,
- branch=self.config.branch,
- warn=self._warn,
- concurrency=concurrency,
- )
- # Early warning if we aren't going to be able to support 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
- ),
- self.collector.tracer_name(),
- )
- )
- 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
- # collecting data, not when combining data. So we save it as
- # `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 data_suffix=True, use .machinename.pid.random
- self._data_suffix = True
- else:
- self._data_suffix = None
- self.data_suffix = None
- self.run_suffix = self._data_suffix
- # 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)
- # 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
- # 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, 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 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_dirs.add(self._canonical_dir(structseq_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
- import six
- for mod in [contracts, six]:
- self.cover_dirs.append(self._canonical_dir(mod))
- # Set the reporting precision.
- Numbers.set_precision(self.config.precision)
- atexit.register(self._atexit)
- self._inited = True
- # 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_dirs:
- self.cover_match = TreeMatcher(self.cover_dirs)
- if self.pylib_dirs:
- self.pylib_match = TreeMatcher(self.pylib_dirs)
- 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.
- wrote_any = False
- if self.debug.should('config'):
- config_info = sorted(self.config.__dict__.items())
- self.debug.write_formatted_info("config", config_info)
- wrote_any = True
- if self.debug.should('sys'):
- self.debug.write_formatted_info("sys", self.sys_info())
- for plugin in self.plugins:
- header = "sys: " + plugin._coverage_plugin_name
- info = plugin.sys_info()
- self.debug.write_formatted_info(header, info)
- wrote_any = True
- if wrote_any:
- self.debug.write_formatted_info("end", ())
- def _canonical_dir(self, morf):
- """Return the canonical directory of the module or file `morf`."""
- morf_filename = PythonFileReporter(morf, self).filename
- return os.path.split(morf_filename)[0]
- def _source_for_file(self, filename):
- """Return the source file for `filename`.
- Given a file name being traced, return the best guess as to the source
- file to attribute it to.
- """
- if filename.endswith(".py"):
- # .py files are themselves source files.
- return filename
- elif filename.endswith((".pyc", ".pyo")):
- # Bytecode files probably have source files near them.
- py_filename = filename[:-1]
- if os.path.exists(py_filename):
- # Found a .py file, use that.
- return py_filename
- if env.WINDOWS:
- # On Windows, it could be a .pyw file.
- pyw_filename = py_filename + "w"
- if os.path.exists(pyw_filename):
- return pyw_filename
- # Didn't find source, but it's probably the .py file we want.
- return py_filename
- elif filename.endswith("$py.class"):
- # Jython is easy to guess.
- return filename[:-9] + ".py"
- # No idea, just use the file name as-is.
- return filename
- def _name_for_module(self, module_globals, filename):
- """Get the name of the module for a set of globals and file name.
- 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.
- """
- 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.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 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 = 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.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 = files.canonical_filename(
- file_tracer.source_filename()
- )
- break
- except Exception:
- self._warn(
- "Disabling plugin %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:
- self.source_pkgs.remove(modulename)
- return None # There's no reason to skip this file.
- if not self.source_match.match(filename):
- return "falls outside the --source trees"
- elif self.include_match:
- 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 _should_trace(self, filename, frame):
- """Decide whether to trace execution in `filename`.
- 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))
- return disp
- def _check_include_omit_etc(self, filename, frame):
- """Check a file name against the include/omit/etc, rules, verbosely.
- Returns a boolean: True if the file should be traced, False if not.
- """
- reason = self._check_include_omit_etc_internal(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)
- return not reason
- def _warn(self, msg):
- """Use `msg` as a warning."""
- self._warnings.append(msg)
- if self.debug.should('pid'):
- msg = "[%d] %s" % (os.getpid(), msg)
- sys.stderr.write("Coverage.py warning: %s\n" % msg)
- 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"`.
- 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()
- 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_files.read(self.data)
- def start(self):
- """Start measuring code coverage.
- 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 :meth:`start`, you must also call :meth:`stop`
- eventually, or your process might not shut down cleanly.
- """
- self._init()
- 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
- if self._auto_data:
- self.load()
- self.collector.start()
- self._started = True
- self._measured = True
- def stop(self):
- """Stop measuring code coverage."""
- if self._started:
- self.collector.stop()
- self._started = False
- def _atexit(self):
- """Clean up on process shutdown."""
- if self._started:
- self.stop()
- if self._auto_data:
- self.save()
- def erase(self):
- """Erase previously-collected coverage data.
- This removes the in-memory data collected in this session as well as
- discarding the data file.
- """
- 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."""
- self._init()
- setattr(self.config, which + "_list", [])
- self._exclude_regex_stale()
- def exclude(self, regex, which='exclude'):
- """Exclude source lines from execution consideration.
- A number of lists of regular expressions are maintained. Each list
- selects lines that are treated differently during reporting.
- `which` determines which list is modified. The "exclude" list selects
- lines that are not considered executable at all. The "partial" list
- indicates lines with branches that are not taken.
- `regex` is a regular expression. The regex is added to the specified
- list. If any of the regexes in the list is found in a line, the line
- is marked for special treatment during reporting.
- """
- self._init()
- excl_list = getattr(self.config, which + "_list")
- excl_list.append(regex)
- self._exclude_regex_stale()
- def _exclude_regex_stale(self):
- """Drop all the compiled exclusion regexes, a list was modified."""
- self._exclude_re.clear()
- def _exclude_regex(self, which):
- """Return a compiled regex for the given exclusion list."""
- if which not in self._exclude_re:
- excl_list = getattr(self.config, which + "_list")
- self._exclude_re[which] = join_regex(excl_list)
- return self._exclude_re[which]
- def get_exclude_list(self, which='exclude'):
- """Return a list of excluded regex patterns.
- `which` indicates which list is desired. See :meth:`exclude` for the
- lists that are available, and their meaning.
- """
- self._init()
- return getattr(self.config, which + "_list")
- def save(self):
- """Save the collected coverage data to the data file."""
- self._init()
- self.get_data()
- self.data_files.write(self.data, suffix=self.data_suffix)
- 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_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()
- for paths in self.config.paths.values():
- result = paths[0]
- for pattern in paths[1:]:
- aliases.add(pattern, result)
- 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 self.data
- self.collector.save_data(self.data)
- # If there are still entries in the source_pkgs list, then we never
- # encountered those packages.
- if self._warn_unimported_source:
- for pkg in self.source_pkgs:
- if pkg not in sys.modules:
- self._warn("Module %s was never imported." % pkg)
- elif not (
- hasattr(sys.modules[pkg], '__file__') and
- os.path.exists(sys.modules[pkg].__file__)
- ):
- self._warn("Module %s has no Python source." % pkg)
- else:
- self._warn("Module %s was previously imported, but not measured." % pkg)
- # Find out if we got any 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 = 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
- # in as unexecuted.
- continue
- 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):
- """Like `analysis2` but doesn't return excluded line numbers."""
- f, s, _, m, mf = self.analysis2(morf)
- return f, s, m, mf
- def analysis2(self, morf):
- """Analyze a module.
- `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 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
- execution).
- * A readable formatted string of the missing line numbers.
- The analysis uses the source file itself and the current measured
- coverage data.
- """
- self._init()
- analysis = self._analyze(morf)
- return (
- analysis.filename,
- sorted(analysis.statements),
- sorted(analysis.excluded),
- sorted(analysis.missing),
- analysis.missing_formatted(),
- )
- def _analyze(self, it):
- """Analyze a single morf or code unit.
- Returns an `Analysis` object.
- """
- self.get_data()
- if not isinstance(it, FileReporter):
- it = self._get_file_reporter(it)
- return Analysis(self.data, it)
- def _get_file_reporter(self, morf):
- """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.file_tracer(abs_morf)
- if plugin_name:
- plugin = self.plugins.get(plugin_name)
- if plugin:
- file_reporter = plugin.file_reporter(abs_morf)
- if file_reporter is None:
- raise CoverageException(
- "Plugin %r did not provide a file reporter for %r." % (
- plugin._coverage_plugin_name, morf
- )
- )
- if file_reporter == "python":
- # pylint: disable=redefined-variable-type
- 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 file names.
- For each module or file name in `morfs`, find a FileReporter. Return
- the list of FileReporters.
- 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.
- """
- if not morfs:
- morfs = self.data.measured_files()
- # Be sure we have a list.
- if not isinstance(morfs, (list, tuple)):
- morfs = [morfs]
- file_reporters = []
- for morf in morfs:
- file_reporter = self._get_file_reporter(morf)
- file_reporters.append(file_reporter)
- return file_reporters
- def report(
- self, morfs=None, show_missing=None, ignore_errors=None,
- file=None, # pylint: disable=redefined-builtin
- omit=None, include=None, skip_covered=None,
- ):
- """Write a summary report to `file`.
- 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 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.get_data()
- self.config.from_args(
- ignore_errors=ignore_errors, omit=omit, include=include,
- show_missing=show_missing, skip_covered=skip_covered,
- )
- reporter = SummaryReporter(self, self.config)
- return reporter.report(morfs, outfile=file)
- def annotate(
- self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None,
- ):
- """Annotate a list of modules.
- Each module in `morfs` is annotated. The source is written to a new
- file, named with a ",cover" suffix, with each line prefixed with a
- marker to indicate the coverage of the line. Covered lines have ">",
- excluded lines have "-", and missing lines have "!".
- See :meth:`report` for other arguments.
- """
- self.get_data()
- self.config.from_args(
- ignore_errors=ignore_errors, omit=omit, include=include
- )
- reporter = AnnotateReporter(self, self.config)
- reporter.report(morfs, directory=directory)
- def html_report(self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None, extra_css=None, title=None):
- """Generate an HTML report.
- The HTML is written to `directory`. The file "index.html" is the
- overview starting point, with links to more detailed pages for
- individual modules.
- `extra_css` is a path to a file of other CSS to apply on the page.
- It will be copied into the HTML directory.
- `title` is a text string (not HTML) to use as the title of the HTML
- report.
- See :meth:`report` for other arguments.
- Returns a float, the total percentage covered.
- """
- 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,
- )
- reporter = HtmlReporter(self, self.config)
- return reporter.report(morfs)
- def xml_report(
- self, morfs=None, outfile=None, ignore_errors=None,
- omit=None, include=None,
- ):
- """Generate an XML report of coverage results.
- The report is compatible with Cobertura reports.
- Each module in `morfs` is included in the report. `outfile` is the
- path to write the file to, "-" will write to stdout.
- See :meth:`report` for other arguments.
- Returns a float, the total percentage covered.
- """
- self.get_data()
- self.config.from_args(
- ignore_errors=ignore_errors, omit=omit, include=include,
- xml_output=outfile,
- )
- file_to_close = None
- delete_file = False
- if self.config.xml_output:
- if self.config.xml_output == '-':
- outfile = sys.stdout
- else:
- # Ensure that the output directory is created; done here
- # because this report pre-opens the output file.
- # HTMLReport does this using the Report plumbing because
- # its task is more complex, being multiple files.
- output_dir = os.path.dirname(self.config.xml_output)
- if output_dir and not os.path.isdir(output_dir):
- os.makedirs(output_dir)
- 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)
- return reporter.report(morfs, outfile=outfile)
- except CoverageException:
- delete_file = True
- raise
- finally:
- if file_to_close:
- file_to_close.close()
- if delete_file:
- file_be_gone(self.config.xml_output)
- def sys_info(self):
- """Return a list of (key, value) pairs showing internal information."""
- import coverage as covmod
- self._init()
- ft_plugins = []
- for ft in self.plugins.file_tracers:
- ft_name = ft._coverage_plugin_name
- if not ft._coverage_enabled:
- ft_name += " (disabled)"
- ft_plugins.append(ft_name)
- info = [
- ('version', covmod.__version__),
- ('coverage', covmod.__file__),
- ('cover_dirs', self.cover_dirs),
- ('pylib_dirs', self.pylib_dirs),
- ('tracer', self.collector.tracer_name()),
- ('plugins.file_tracers', ft_plugins),
- ('config_files', self.config.attempted_config_files),
- ('configs_read', self.config.config_files),
- ('data_path', self.data_files.filename),
- ('python', sys.version.replace('\n', '')),
- ('platform', platform.platform()),
- ('implementation', platform.python_implementation()),
- ('executable', sys.executable),
- ('cwd', os.getcwd()),
- ('path', sys.path),
- ('environment', sorted(
- ("%s = %s" % (k, v))
- for k, v in iitems(os.environ)
- if k.startswith(("COV", "PY"))
- )),
- ('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))
- 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.
- If the environment variable COVERAGE_PROCESS_START is defined, coverage
- measurement is started. The value of the variable is the config file
- to use.
- There are two ways to configure your Python installation to invoke this
- function when Python starts:
- #. Create or append to sitecustomize.py to add these lines::
- import coverage
- coverage.process_startup()
- #. Create a .pth file in your Python installation containing::
- import coverage; coverage.process_startup()
- Returns the :class:`Coverage` instance that was started, or None if it was
- not started by this call.
- """
- cps = os.environ.get("COVERAGE_PROCESS_START")
- if not cps:
- # No request for coverage, nothing to do.
- return None
- # This function can be called more than once in a process. This happens
- # 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.py has
- # already been started, so we can avoid doing it twice.
- #
- # https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more
- # details.
- if hasattr(process_startup, "coverage"):
- # We've annotated this function before, so we must have already
- # started coverage.py in this process. Nothing to do.
- return None
- cov = Coverage(config_file=cps, auto_data=True)
- process_startup.coverage = cov
- cov.start()
- cov._warn_no_data = False
- cov._warn_unimported_source = False
- return cov
- def _prevent_sub_process_measurement():
- """Stop any subprocess auto-measurement from writing data."""
- auto_created_coverage = getattr(process_startup, "coverage", None)
- if auto_created_coverage is not None:
- auto_created_coverage._auto_data = False
|