diff options
Diffstat (limited to 'coverage/control.py')
-rw-r--r-- | coverage/control.py | 226 |
1 files changed, 140 insertions, 86 deletions
diff --git a/coverage/control.py b/coverage/control.py index 54e6d3f9..28d084bf 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -3,21 +3,22 @@ import atexit, os, random, socket, sys from coverage.annotate import AnnotateReporter -from coverage.backward import string_class +from coverage.backward import string_class, iitems from coverage.codeunit import code_unit_factory, CodeUnit from coverage.collector import Collector from coverage.config import CoverageConfig from coverage.data import CoverageData from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher -from coverage.files import find_python_files +from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.html import HtmlReporter -from coverage.misc import CoverageException, bool_or_none +from coverage.misc import CoverageException, bool_or_none, join_regex +from coverage.misc import file_be_gone from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter class coverage(object): - """Programmatic access to Coverage. + """Programmatic access to coverage.py. To use:: @@ -25,7 +26,7 @@ class coverage(object): cov = coverage() cov.start() - #.. blah blah (run your code) blah blah .. + #.. call your code .. cov.stop() cov.html_report(directory='covhtml') @@ -64,7 +65,8 @@ class coverage(object): measured. `include` and `omit` are lists of filename patterns. Files that match - `include` will be measured, files that match `omit` will not. + `include` will be measured, files that match `omit` will not. Each + will also accept a single string argument. """ from coverage import __version__ @@ -104,8 +106,10 @@ class coverage(object): self.auto_data = auto_data self.atexit_registered = False - self.exclude_re = "" - self._compile_exclude() + # _exclude_re is a dict mapping exclusion list names to compiled + # regexes. + self._exclude_re = {} + self._exclude_regex_stale() self.file_locator = FileLocator() @@ -118,8 +122,8 @@ class coverage(object): else: self.source_pkgs.append(src) - self.omit = self._abs_files(self.config.omit) - self.include = self._abs_files(self.config.include) + self.omit = prep_patterns(self.config.omit) + self.include = prep_patterns(self.config.include) self.collector = Collector( self._should_trace, timid=self.config.timid, @@ -157,7 +161,7 @@ class coverage(object): # we've imported, and take all the different ones. for m in (atexit, os, random, socket): if hasattr(m, "__file__"): - m_dir = self._canonical_dir(m.__file__) + m_dir = self._canonical_dir(m) if m_dir not in self.pylib_dirs: self.pylib_dirs.append(m_dir) @@ -176,17 +180,9 @@ class coverage(object): # Set the reporting precision. Numbers.set_precision(self.config.precision) - # When tearing down the coverage object, modules can become None. - # Saving the modules as object attributes avoids problems, but it is - # quite ad-hoc which modules need to be saved and which references - # need to use the object attributes. - self.socket = socket - self.os = os - self.random = random - - def _canonical_dir(self, f): - """Return the canonical directory of the file `f`.""" - return os.path.split(self.file_locator.canonical_filename(f))[0] + def _canonical_dir(self, morf): + """Return the canonical directory of the module or file `morf`.""" + return os.path.split(CodeUnit(morf, self.file_locator).filename)[0] def _source_for_file(self, filename): """Return the source file for `filename`.""" @@ -207,9 +203,6 @@ class coverage(object): should not. """ - if os is None: - return False - if filename.startswith('<'): # Lots of non-file execution is represented with artificial # filenames like "<string>", "<doctest readme.txt[0]>", or @@ -217,19 +210,11 @@ class coverage(object): # can't do anything with the data later anyway. return False - if filename.endswith(".html"): - # Jinja and maybe other templating systems compile templates into - # Python code, but use the template filename as the filename in - # the compiled code. Of course, those filenames are useless later - # so don't bother collecting. TODO: How should we really separate - # out good file extensions from bad? - return False - self._check_for_packages() # Compiled Python files have two filenames: frame.f_code.co_filename is - # the filename at the time the .pyc was compiled. The second name - # is __file__, which is where the .pyc was actually loaded from. Since + # the filename 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. @@ -243,12 +228,16 @@ class coverage(object): canonical = self.file_locator.canonical_filename(filename) - # If the user specified source, then that's authoritative about what to - # measure. If they didn't, then we have to exclude the stdlib and - # coverage.py directories. + # 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 not self.source_match.match(canonical): return False + elif self.include_match: + if not self.include_match.match(canonical): + return False 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. @@ -260,9 +249,7 @@ class coverage(object): if self.cover_match and self.cover_match.match(canonical): return False - # Check the file against the include and omit patterns. - if self.include_match and not self.include_match.match(canonical): - return False + # Check the file against the omit pattern. if self.omit_match and self.omit_match.match(canonical): return False @@ -271,7 +258,7 @@ class coverage(object): # To log what should_trace returns, change this to "if 1:" if 0: _real_should_trace = _should_trace - def _should_trace(self, filename, frame): # pylint: disable-msg=E0102 + def _should_trace(self, filename, frame): # pylint: disable=E0102 """A logging decorator around the real _should_trace function.""" ret = self._real_should_trace(filename, frame) print("should_trace: %r -> %r" % (filename, ret)) @@ -282,11 +269,6 @@ class coverage(object): self._warnings.append(msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) - def _abs_files(self, files): - """Return a list of absolute file names for the names in `files`.""" - files = files or [] - return [self.file_locator.abs_file(f) for f in files] - def _check_for_packages(self): """Update the source_match matcher with latest imported packages.""" # Our self.source_pkgs attribute is a list of package names we want to @@ -306,7 +288,7 @@ class coverage(object): try: pkg_file = mod.__file__ except AttributeError: - self._warn("Module %s has no python source." % pkg) + pkg_file = None else: d, f = os.path.split(pkg_file) if f.startswith('__init__'): @@ -315,8 +297,14 @@ class coverage(object): else: pkg_file = self._source_for_file(pkg_file) pkg_file = self.file_locator.canonical_filename(pkg_file) + if not os.path.exists(pkg_file): + pkg_file = None + + if pkg_file: self.source.append(pkg_file) self.source_match.add(pkg_file) + else: + self._warn("Module %s has no Python source." % pkg) for pkg in found: self.source_pkgs.remove(pkg) @@ -335,7 +323,15 @@ class coverage(object): self.data.read() def start(self): - """Start measuring code coverage.""" + """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. + + Once you invoke `start`, you must also call `stop` eventually, or your + process might not shut down cleanly. + + """ 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. @@ -366,7 +362,6 @@ class coverage(object): def stop(self): """Stop measuring code coverage.""" self.collector.stop() - self._harvest_data() def erase(self): """Erase previously-collected coverage data. @@ -378,30 +373,49 @@ class coverage(object): self.collector.reset() self.data.erase() - def clear_exclude(self): + def clear_exclude(self, which='exclude'): """Clear the exclude list.""" - self.config.exclude_list = [] - self.exclude_re = "" + setattr(self.config, which + "_list", []) + self._exclude_regex_stale() - def exclude(self, regex): + def exclude(self, regex, which='exclude'): """Exclude source lines from execution consideration. - `regex` is a regular expression. Lines matching this expression are - not considered executable when reporting code coverage. A list of - regexes is maintained; this function adds a new regex to the list. - Matching any of the regexes excludes a source line. + 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.config.exclude_list.append(regex) - self._compile_exclude() + 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 _compile_exclude(self): - """Build the internal usable form of the exclude list.""" - self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")" + def get_exclude_list(self, which='exclude'): + """Return a list of excluded regex patterns. - def get_exclude_list(self): - """Return the list of excluded regex patterns.""" - return self.config.exclude_list + `which` indicates which list is desired. See `exclude` for the lists + that are available, and their meaning. + + """ + return getattr(self.config, which + "_list") def save(self): """Save the collected coverage data to the data file.""" @@ -412,8 +426,8 @@ class coverage(object): # `save()` at the last minute so that the pid will be correct even # if the process forks. data_suffix = "%s.%s.%06d" % ( - self.socket.gethostname(), self.os.getpid(), - self.random.randint(0, 99999) + socket.gethostname(), os.getpid(), + random.randint(0, 99999) ) self._harvest_data() @@ -427,7 +441,14 @@ class coverage(object): current measurements. """ - self.data.combine_parallel_data() + aliases = None + if self.config.paths: + aliases = PathAliases(self.file_locator) + 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) def _harvest_data(self): """Get the collected data and reset the collector. @@ -443,7 +464,7 @@ class coverage(object): # If there are still entries in the source_pkgs list, then we never # encountered those packages. for pkg in self.source_pkgs: - self._warn("Source module %s was never encountered." % pkg) + self._warn("Module %s was never imported." % pkg) # Find out if we got any data. summary = self.data.summary() @@ -453,6 +474,7 @@ class coverage(object): # 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) self.data.touch_file(py_file) self._harvested = True @@ -492,13 +514,14 @@ class coverage(object): Returns an `Analysis` object. """ + self._harvest_data() if not isinstance(it, CodeUnit): it = code_unit_factory(it, self.file_locator)[0] return Analysis(self, it) def report(self, morfs=None, show_missing=True, ignore_errors=None, - file=None, # pylint: disable-msg=W0622 + file=None, # pylint: disable=W0622 omit=None, include=None ): """Write a summary report to `file`. @@ -510,14 +533,16 @@ class coverage(object): match those patterns will be included in the report. Modules matching `omit` will not be included in the report. + Returns a float, the total percentage covered. + """ + self._harvest_data() self.config.from_args( - ignore_errors=ignore_errors, omit=omit, include=include - ) - reporter = SummaryReporter( - self, show_missing, self.config.ignore_errors + ignore_errors=ignore_errors, omit=omit, include=include, + show_missing=show_missing, ) - reporter.report(morfs, outfile=file, config=self.config) + 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): @@ -531,25 +556,39 @@ class coverage(object): See `coverage.report()` for other arguments. """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include ) - reporter = AnnotateReporter(self, self.config.ignore_errors) - reporter.report(morfs, config=self.config, directory=directory) + 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): + 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 `coverage.report()` for other arguments. + Returns a float, the total percentage covered. + """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, - html_dir=directory, + html_dir=directory, extra_css=extra_css, html_title=title, ) - reporter = HtmlReporter(self, self.config.ignore_errors) - reporter.report(morfs, config=self.config) + reporter = HtmlReporter(self, self.config) + return reporter.report(morfs) def xml_report(self, morfs=None, outfile=None, ignore_errors=None, omit=None, include=None): @@ -562,12 +601,16 @@ class coverage(object): See `coverage.report()` for other arguments. + Returns a float, the total percentage covered. + """ + self._harvest_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 @@ -575,11 +618,16 @@ class coverage(object): outfile = open(self.config.xml_output, "w") file_to_close = outfile try: - reporter = XmlReporter(self, self.config.ignore_errors) - reporter.report(morfs, outfile=outfile, config=self.config) + 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 sysinfo(self): """Return a list of (key, value) pairs showing internal information.""" @@ -587,6 +635,11 @@ class coverage(object): import coverage as covmod import platform, re + try: + implementation = platform.python_implementation() + except AttributeError: + implementation = "unknown" + info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), @@ -596,11 +649,12 @@ class coverage(object): ('data_path', self.data.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), + ('implementation', implementation), ('cwd', os.getcwd()), ('path', sys.path), ('environment', [ - ("%s = %s" % (k, v)) for k, v in os.environ.items() - if re.search("^COV|^PY", k) + ("%s = %s" % (k, v)) for k, v in iitems(os.environ) + if re.search(r"^COV|^PY", k) ]), ] return info |