Example #1
0
    def run(self):
        messages = []

        for code_file in self._code_files:
            try:
                tree = ast.parse(
                    open(code_file, 'r').read(),
                    filename=code_file,
                )
            except (SyntaxError, TypeError):
                location = Location(
                    path=code_file,
                    module=None,
                    function=None,
                    line=1,
                    character=0,
                )
                message = Message(
                    source='mccabe',
                    code='MC0000',
                    location=location,
                    message='Could not parse file',
                )
                messages.append(message)
                continue

            visitor = PathGraphingAstVisitor()
            visitor.preorder(tree, visitor)

            for graph in visitor.graphs.values():
                complexity = graph.complexity()
                if complexity > self.max_complexity:
                    location = Location(
                        path=code_file,
                        module=None,
                        function=graph.entity,
                        line=graph.lineno,
                        character=0,
                    )
                    message = Message(
                        source='mccabe',
                        code='MC0001',
                        location=location,
                        message='%s is too complex (%s)' % (
                            graph.entity,
                            complexity,
                        ),
                    )
                    messages.append(message)

        return self.filter_messages(messages)
Example #2
0
    def run(self, found_files):

        warnings = []
        for filepath in found_files.iter_file_paths():
            mimetype = mimetypes.guess_type(filepath)
            if mimetype[0] is None or not mimetype[0].startswith("text/") or mimetype[1] is not None:
                continue
            try:
                contents = read_py_file(filepath)
            except CouldNotHandleEncoding:
                continue
            for line, code, message in check_file_contents(contents):
                warnings.append({"line": line, "code": code, "message": message, "path": filepath})

        messages = []
        for warning in warnings:
            path = warning["path"]
            prefix = os.path.commonprefix([found_files.rootpath, path])
            loc = Location(
                path,
                module_from_path(path[len(prefix) :]),
                "",
                warning["line"],
                0,
                absolute_path=True,
            )
            msg = Message("dodgy", warning["code"], loc, warning["message"])
            messages.append(msg)

        return messages
Example #3
0
    def run(self, found_files):

        warnings = []
        for filepath in found_files.iter_file_paths():
            mimetype = mimetypes.guess_type(filepath)
            if mimetype[0] is None or not mimetype[0].startswith('text/'):
                continue
            for line, code, message in check_file(filepath):
                warnings.append({
                    'line': line,
                    'code': code,
                    'message': message,
                    'path': filepath
                })

        messages = []
        for warning in warnings:
            path = warning['path']
            prefix = os.path.commonprefix([found_files.rootpath, path])
            loc = Location(path,
                           module_from_path(path[len(prefix):]),
                           '',
                           warning['line'],
                           0,
                           absolute_path=True)
            msg = Message('dodgy', warning['code'], loc, warning['message'])
            messages.append(msg)

        return messages
Example #4
0
    def get_messages(self):
        all_items = (
            ("unused-function", "Unused function %s", self.unused_funcs),
            ("unused-property", "Unused property %s", self.unused_props),
            ("unused-variable", "Unused variable %s", self.unused_vars),
            ("unused-attribute", "Unused attribute %s", self.unused_attrs),
        )

        vulture_messages = []
        for code, template, items in all_items:
            for item in items:
                try:
                    filename = item.file
                except AttributeError:
                    filename = item.filename
                if hasattr(item, "lineno"):
                    lineno = item.lineno  # for older versions of vulture
                else:
                    lineno = item.first_lineno
                loc = Location(filename, None, None, lineno, -1)
                message_text = template % item
                message = Message("vulture", code, loc, message_text)
                vulture_messages.append(message)

        return self._internal_messages + vulture_messages
    def run(self, found_files):
        messages = []

        for code_file in found_files.iter_module_paths():
            try:
                contents = read_py_file(code_file)
                tree = ast.parse(
                    contents,
                    filename=code_file,
                )
            except CouldNotHandleEncoding as err:
                messages.append(
                    make_tool_error_message(
                        code_file,
                        'mccabe',
                        'MC0000',
                        message='Could not handle the encoding of this file: %s'
                        % err.encoding))
                continue
            except SyntaxError as err:
                messages.append(
                    make_tool_error_message(code_file,
                                            'mccabe',
                                            'MC0000',
                                            line=err.lineno,
                                            character=err.offset,
                                            message='Syntax Error'))
                continue
            except TypeError:
                messages.append(
                    make_tool_error_message(code_file,
                                            'mccabe',
                                            'MC0000',
                                            message='Unable to parse file'))
                continue

            visitor = PathGraphingAstVisitor()
            visitor.preorder(tree, visitor)

            for graph in visitor.graphs.values():
                complexity = graph.complexity()
                if complexity > self.max_complexity:
                    location = Location(path=code_file,
                                        module=None,
                                        function=graph.entity,
                                        line=graph.lineno,
                                        character=0,
                                        absolute_path=True)
                    message = Message(
                        source='mccabe',
                        code='MC0001',
                        location=location,
                        message='%s is too complex (%s)' % (
                            graph.entity,
                            complexity,
                        ),
                    )
                    messages.append(message)

        return self.filter_messages(messages)
Example #6
0
    def record_message(self,
                       filename=None,
                       line=None,
                       character=None,
                       code=None,
                       message=None):

        code = code or 'FL0000'
        if code in self.ignore:
            return

        location = Location(
            path=filename,
            module=None,
            function=None,
            line=line,
            character=character,
        )
        message = Message(
            source='pyflakes',
            code=code,
            location=location,
            message=message,
        )
        self._messages.append(message)
Example #7
0
    def run(self, found_files):
        paths = [path for path in found_files.iter_module_paths()]
        paths.extend(self.options)
        result = self.checker.run(paths)
        report, _ = result[0], result[1:]
        messages = []

        for message in report.splitlines():
            iter_message = iter(message.split(':'))
            (path, line, char, err_type), err_msg = islice(iter_message, 4), list(message)
            location = Location(
                path=path,
                module=None,
                function=None,
                line=line,
                character=char,
                absolute_path=True
            )
            message = Message(
                source='mypy',
                code=err_type,
                location=location,
                message=''.join(err_msg).strip()
            )
            messages.append(message)

        return messages
Example #8
0
    def error(self, line_number, offset, text, check):
        code = super().error(
            line_number,
            offset,
            text,
            check,
        )
        if code is None:
            # The error pycodestyle found is being ignored, let's move on.
            return

        # Get a clean copy of the message text without the code embedded.
        text = text[5:]

        # mixed indentation (E101) is a file global message
        if code == "E101":
            line_number = None

        # Record the message using prospector's data structures.
        location = Location(
            path=self.filename,
            module=None,
            function=None,
            line=line_number,
            character=(offset + 1),
        )
        message = Message(
            # TODO: legacy output naming
            source="pycodestyle",
            code=code,
            location=location,
            message=text,
        )

        self._prospector_messages.append(message)
Example #9
0
def format_message(message):
    try:
        (path, line, char, err_type, err_msg) = message.split(":", 4)
        line = int(line)
        character = int(char)
    except ValueError:
        try:
            (path, line, err_type, err_msg) = message.split(":", 3)
            line = int(line)
            character = None
        except ValueError:
            (path, err_type, err_msg) = message.split(":", 2)
            line = 0
            character = None
    location = Location(
        path=path,
        module=None,
        function=None,
        line=line,
        character=character,
    )
    return Message(
        source="mypy",
        code=err_type.lstrip(" "),
        location=location,
        message=err_msg.lstrip(" "),
    )
Example #10
0
def _run_markdownlint(matched_filenames, show_lint_files):
    """Run markdownlint on matched_filenames."""
    from prospector.message import Message, Location

    for filename in matched_filenames:
        _debug_linter_status("mdl", filename, show_lint_files)

    try:
        proc = subprocess.Popen(["mdl"] + matched_filenames,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        lines = proc.communicate()[0].decode().splitlines()
    except OSError as error:
        if error.errno == errno.ENOENT:
            return []

    lines = [
        re.match(r"([\w\-.\/\\ ]+)\:([0-9]+)\: (\w+) (.+)", l).groups(1)
        for l in lines
    ]
    return_dict = dict()
    for filename, lineno, code, msg in lines:
        key = _Key(filename, int(lineno), code)
        loc = Location(filename, None, None, int(lineno), 0)
        return_dict[key] = Message("markdownlint", code, loc, msg)

    return return_dict
Example #11
0
    def _combine_w0614(self, messages):
        """
        For the "unused import from wildcard import" messages,
        we want to combine all warnings about the same line into
        a single message.
        """
        by_loc = defaultdict(list)
        out = []

        for message in messages:
            if message.code == 'unused-wildcard-import':
                by_loc[message.location].append(message)
            else:
                out.append(message)

        for location, message_list in by_loc.items():
            names = []
            for msg in message_list:
                names.append(
                    _UNUSED_WILDCARD_IMPORT_RE.match(msg.message).group(1))

            msgtxt = 'Unused imports from wildcard import: %s' % ', '.join(
                names)
            combined_message = Message('pylint', 'unused-wildcard-import',
                                       location, msgtxt)
            out.append(combined_message)

        return out
Example #12
0
 def _custom_reporter(error, file_path):
     """Reporter for polysquare-generic-file-linter."""
     line = error.line_offset + 1
     key = _Key(file_path, line, "file/spelling_error")
     loc = Location(file_path, None, None, line, 0)
     # suppress(protected-access)
     desc = lint._SPELLCHECK_MESSAGES[error.error_type].format(error.word)
     return_dict[key] = Message("spellcheck-linter", "file/spelling_error",
                                loc, desc)
Example #13
0
    def run(self, found_files):
        messages = []

        checker = PEP257Checker()

        for code_file in found_files.iter_module_paths():
            try:
                for error in checker.check_source(
                        open(code_file, 'r').read(),
                        code_file,
                ):
                    location = Location(
                        path=code_file,
                        module=None,
                        function='',
                        line=error.line,
                        character=0,
                        absolute_path=True,
                    )
                    message = Message(
                        source='pep257',
                        code=error.code,
                        location=location,
                        message=error.message.partition(':')[2].strip(),
                    )
                    messages.append(message)
            except AllError as exc:
                location = Location(
                    path=code_file,
                    module=None,
                    function=None,
                    line=1,
                    character=0,
                    absolute_path=True,
                )
                message = Message(
                    source='pep257',
                    code='D000',
                    location=location,
                    message=exc.message,
                )
                messages.append(message)

        return self.filter_messages(messages)
Example #14
0
        def error(self, line, offset, text, check):
            """Record error and store in return_dict."""
            code = super(Flake8MergeReporter,
                         self).error(line, offset, text, check)

            key = _Key(self._current_file, line, code)
            return_dict[key] = Message(
                code, code,
                Location(self._current_file, None, None, line, offset),
                text[5:])
Example #15
0
 def add_message(code, message, setting):
     if code in self.ignore_codes:
         return
     line = -1
     for number, fileline in enumerate(raw_contents):
         if setting in fileline:
             line = number + 1
             break
     location = Location(relative_filepath, None, None, line, 0, False)
     message = Message('profile-validator', code, location, message)
     messages.append(message)
Example #16
0
 def test_format_message_without_character(self):
     location = Location(path="file.py",
                         module=None,
                         function=None,
                         line=17,
                         character=None)
     expected = Message(source="mypy",
                        code="error",
                        location=location,
                        message="Important error")
     self.assertEqual(format_message("file.py:17: error: Important error"),
                      expected)
Example #17
0
 def test_format_message_without_character_and_columns_in_message(self):
     location = Location(path="file.py",
                         module=None,
                         function=None,
                         line=17,
                         character=None)
     expected = Message(source="mypy",
                        code="note",
                        location=location,
                        message="Important error")
     self.assertEqual(
         format_message('file.py:17: note: unused "type: ignore" comment'),
         expected)
Example #18
0
    def run(self, found_files):
        messages = []

        checker = ConventionChecker()

        for code_file in found_files.iter_module_paths():
            try:
                for error in checker.check_source(read_py_file(code_file),
                                                  code_file, None):

                    location = Location(
                        path=code_file,
                        module=None,
                        function="",
                        line=error.line,
                        character=0,
                        absolute_path=True,
                    )
                    message = Message(
                        source="pydocstyle",
                        code=error.code,
                        location=location,
                        message=error.message.partition(":")[2].strip(),
                    )
                    messages.append(message)
            except CouldNotHandleEncoding as err:
                messages.append(
                    make_tool_error_message(
                        code_file,
                        "pydocstyle",
                        "D000",
                        message=
                        f"Could not handle the encoding of this file: {err.encoding}",
                    ))
                continue
            except AllError as exc:
                # pydocstyle's Parser.parse_all method raises AllError when an
                # attempt to analyze the __all__ definition has failed.  This
                # occurs when __all__ is too complex to be parsed.
                messages.append(
                    make_tool_error_message(
                        code_file,
                        "pydocstyle",
                        "D000",
                        line=1,
                        character=0,
                        message=exc.args[0],
                    ))
                continue

        return self.filter_messages(messages)
Example #19
0
    def execute(self):
        summary = {
            'started': datetime.now(),
            'libraries': self.libraries,
            'strictness': self.config.strictness,
            'profiles': self.profiles,
            'adaptors': [adaptor.name for adaptor in self.adaptors],
            'tools': self.config.tools,
        }

        # Prep the tools.
        for tool in self.tool_runners:
            tool.prepare(self.path, self.ignores, self.config, self.adaptors)

        # Run the tools
        messages = []
        for tool in self.tool_runners:
            try:
                messages += tool.run()
            except Exception:  # pylint: disable=W0703
                if self.config.die_on_tool_error:
                    raise
                else:
                    for name, cls in tools.TOOLS.items():
                        if cls == tool.__class__:
                            toolname = name
                            break
                    else:
                        toolname = 'Unknown'

                    loc = Location(self.path, None, None, None, None)
                    msg = 'Tool %s failed to run (exception was raised)' % (
                        toolname, )
                    message = Message(
                        toolname,
                        'failure',
                        loc,
                        message=msg,
                    )
                    messages.append(message)

        messages = self.process_messages(messages)

        summary['message_count'] = len(messages)
        summary['completed'] = datetime.now()
        delta = (summary['completed'] - summary['started'])
        summary['time_taken'] = '%0.2f' % delta.total_seconds()

        return summary, messages
Example #20
0
    def run(self, found_files):
        self.manager.files_list = sorted(found_files.iter_file_paths())
        self.manager.exclude_files = []

        if not self.manager.b_ts.tests:
            raise ValueError("No test will run for bandit")

        self.manager.run_tests()
        results = self.manager.get_issue_list(sev_level=RANKING[self.severity], conf_level=RANKING[self.confidence])
        messages = []
        for result in results:
            loc = Location(os.path.abspath(result.fname), None, "", int(result.lineno), 0)
            msg = Message("bandit", result.test_id, loc, result.text)
            messages.append(msg)
        return messages
Example #21
0
    def run(self):
        warnings = run_checks(self.rootpath, self.ignore)
        messages = []

        for warning in warnings:
            path = warning['path']
            loc = Location(path,
                           module_from_path(path),
                           '',
                           warning['line'],
                           0,
                           absolute_path=False)
            msg = Message('dodgy', warning['code'], loc, warning['message'])
            messages.append(msg)

        return messages
Example #22
0
    def get_messages(self):
        all_items = (
            ('unused-function', 'Unused function %s', self.unused_funcs),
            ('unused-property', 'Unused property %s', self.unused_props),
            ('unused-variable', 'Unused variable %s', self.unused_vars),
            ('unused-attribute', 'Unused attribute %s', self.unused_attrs)
        )

        messages = []
        for code, template, items in all_items:
            for item in items:
                loc = Location(item.file, None, None, item.lineno, -1)
                message_text = template % item
                message = Message('vulture', code, loc, message_text)
                messages.append(message)

        return messages
Example #23
0
    def add_message(self, msg_id, location, msg):
        # (* magic is acceptable here)
        loc = Location(*location)
        # At this point pylint will give us the code but we want the
        # more user-friendly symbol
        try:
            msg_data = self._message_store.get_message_definitions(msg_id)
        except UnknownMessageError:
            # this shouldn't happen, as all pylint errors should be
            # in the message store, but just in case we'll fall back
            # to using the code.
            msg_symbol = msg_id
        else:
            msg_symbol = msg_data[0].symbol

        message = Message("pylint", msg_symbol, loc, msg)
        self._messages.append(message)
Example #24
0
 def test_format_dupplicated_module_linux(self):
     location = Location(path="file.py",
                         module=None,
                         function=None,
                         line=0,
                         character=None)
     expected = Message(
         source="mypy",
         code="error",
         location=location,
         message=
         "Duplicate module named 'file' (also at '/Repositories/file.py')",
     )
     self.assertEqual(
         format_message(
             "file.py: error: Duplicate module named 'file' (also at '/Repositories/file.py')"
         ),
         expected,
     )
Example #25
0
def _run_pyroma(setup_file, show_lint_files):
    """Run pyroma."""
    from pyroma import projectdata, ratings
    from prospector.message import Message, Location

    _debug_linter_status("pyroma", setup_file, show_lint_files)

    return_dict = dict()

    data = projectdata.get_data(os.getcwd())
    all_tests = ratings.ALL_TESTS
    for test in [mod() for mod in [t.__class__ for t in all_tests]]:
        if test.test(data) is False:
            class_name = test.__class__.__name__
            key = _Key(setup_file, 0, class_name)
            loc = Location(setup_file, None, None, 0, 0)
            msg = test.message()
            return_dict[key] = Message("pyroma", class_name, loc, msg)

    return return_dict
Example #26
0
    def get_messages(self):
        all_items = (
            ('unused-function', 'Unused function %s', self.unused_funcs),
            ('unused-property', 'Unused property %s', self.unused_props),
            ('unused-variable', 'Unused variable %s', self.unused_vars),
            ('unused-attribute', 'Unused attribute %s', self.unused_attrs)
        )

        vulture_messages = []
        for code, template, items in all_items:
            for item in items:
                try:
                    filename = item.file
                except AttributeError:
                    filename = item.filename
                loc = Location(filename, None, None, item.lineno, -1)
                message_text = template % item
                message = Message('vulture', code, loc, message_text)
                vulture_messages.append(message)

        return self._internal_messages + vulture_messages
Example #27
0
    def run(self, found_files):
        messages = []
        for module in found_files.iter_module_paths(include_ignored=True):
            dirname, filename = os.path.split(module)
            if filename != 'setup.py':
                continue

            data = projectdata.get_data(dirname)

            all_tests = [m() for m in PYROMA_TEST_CLASSES]
            for test in all_tests:
                code = PYROMA_CODES.get(test.__class__, 'PYRUNKNOWN')

                if code in self.ignore_codes:
                    continue

                passed = test.test(data)
                if passed is False:  # passed can be True, False or None...
                    loc = Location(module, 'setup', None, -1, -1)
                    msg = Message('pyroma', code, loc, test.message())
                    messages.append(msg)

        return messages
Example #28
0
 def _error_message(self, filepath, message):
     location = Location(filepath, None, None, 0, 0)
     return Message('prospector', 'config-problem', location, message)
Example #29
0
    def execute(self):

        deprecated_names = self.config.replace_deprecated_tool_names()

        summary = {
            "started": datetime.now(),
        }
        summary.update(self.config.get_summary_information())

        found_files = find_python(
            self.config.ignores,
            self.config.paths,
            self.config.explicit_file_mode,
            self.config.workdir,
        )

        messages = []

        # see if any old tool names are run
        for deprecated_name in deprecated_names:
            loc = Location(self.config.workdir, None, None, None, None)
            new_name = DEPRECATED_TOOL_NAMES[deprecated_name]
            msg = (
                f"Tool {deprecated_name} has been renamed to {new_name}. "
                f"The old name {deprecated_name} is now deprecated and will be removed in Prospector 2.0. "
                f"Please update your prospector configuration.")

            message = Message(
                "prospector",
                "Deprecation",
                loc,
                message=msg,
            )
            messages.append(message)
            warnings.warn(msg, category=DeprecationWarning)

        # Run the tools
        for tool in self.config.get_tools(found_files):
            for name, cls in tools.TOOLS.items():
                if cls == tool.__class__:
                    toolname = name
                    break
            else:
                toolname = "Unknown"

            try:
                # Tools can output to stdout/stderr in unexpected places, for example,
                # pydocstyle emits warnings about __all__ and as pyroma exec's the setup.py
                # file, it will execute any print statements in that, etc etc...
                with CaptureOutput(
                        hide=not self.config.direct_tool_stdout) as capture:
                    messages += tool.run(found_files)

                    if self.config.include_tool_stdout:
                        loc = Location(self.config.workdir, None, None, None,
                                       None)

                        if capture.get_hidden_stderr():
                            msg = f"stderr from {toolname}:\n{capture.get_hidden_stderr()}"
                            messages.append(
                                Message(toolname,
                                        "hidden-output",
                                        loc,
                                        message=msg))
                        if capture.get_hidden_stdout():
                            msg = f"stdout from {toolname}:\n{capture.get_hidden_stdout()}"
                            messages.append(
                                Message(toolname,
                                        "hidden-output",
                                        loc,
                                        message=msg))

            except FatalProspectorException as fatal:
                sys.stderr.write(fatal.message)
                sys.exit(2)

            except Exception as ex:  # pylint:disable=broad-except
                if self.config.die_on_tool_error:
                    raise FatalProspectorException from ex

                loc = Location(self.config.workdir, None, None, None, None)
                msg = (
                    f"Tool {toolname} failed to run "
                    f"(exception was raised, re-run prospector with -X to see the stacktrace)"
                )
                message = Message(
                    toolname,
                    "failure",
                    loc,
                    message=msg,
                )
                messages.append(message)

        messages = self.process_messages(found_files, messages)

        summary["message_count"] = len(messages)
        summary["completed"] = datetime.now()

        # Timedelta.total_seconds() is not available
        # on Python<=2.6 so we calculate it ourselves
        # See issue #60 and http://stackoverflow.com/a/3694895
        delta = summary["completed"] - summary["started"]
        total_seconds = (delta.microseconds +
                         (delta.seconds + delta.days * 24 * 3600) * 1e6) / 1e6
        summary["time_taken"] = "%0.2f" % total_seconds

        external_config = []
        for tool, configured_by in self.config.configured_by.items():
            if configured_by is not None:
                external_config.append((tool, configured_by))
        if len(external_config) > 0:
            summary["external_config"] = ", ".join(
                ["%s: %s" % info for info in external_config])

        self.summary = summary
        self.messages = self.messages + messages
Example #30
0
    def execute(self):

        summary = {
            'started': datetime.now(),
        }
        summary.update(self.config.get_summary_information())

        found_files = find_python(self.config.ignores, self.config.paths,
                                  self.config.explicit_file_mode,
                                  self.config.workdir)

        # Run the tools
        messages = []
        for tool in self.config.get_tools(found_files):
            for name, cls in tools.TOOLS.items():
                if cls == tool.__class__:
                    toolname = name
                    break
            else:
                toolname = 'Unknown'

            try:
                # Tools can output to stdout/stderr in unexpected places, for example,
                # pep257 emits warnings about __all__ and as pyroma exec's the setup.py
                # file, it will execute any print statements in that, etc etc...
                with capture_output(
                        hide=not self.config.direct_tool_stdout) as capture:
                    messages += tool.run(found_files)

                    if self.config.include_tool_stdout:
                        loc = Location(self.config.workdir, None, None, None,
                                       None)

                        if capture.get_hidden_stderr():
                            msg = 'stderr from %s:\n%s' % (
                                toolname, capture.get_hidden_stderr())
                            messages.append(
                                Message(toolname,
                                        'hidden-output',
                                        loc,
                                        message=msg))
                        if capture.get_hidden_stdout():
                            msg = 'stdout from %s:\n%s' % (
                                toolname, capture.get_hidden_stdout())
                            messages.append(
                                Message(toolname,
                                        'hidden-output',
                                        loc,
                                        message=msg))

            except FatalProspectorException as fatal:
                sys.stderr.write(fatal.message)
                sys.exit(2)

            except Exception:  # pylint: disable=broad-except
                if self.config.die_on_tool_error:
                    raise
                else:
                    loc = Location(self.config.workdir, None, None, None, None)
                    msg = 'Tool %s failed to run (exception was raised)' % (
                        toolname, )
                    message = Message(
                        toolname,
                        'failure',
                        loc,
                        message=msg,
                    )
                    messages.append(message)

        messages = self.process_messages(found_files, messages)

        summary['message_count'] = len(messages)
        summary['completed'] = datetime.now()

        # Timedelta.total_seconds() is not available
        # on Python<=2.6 so we calculate it ourselves
        # See issue #60 and http://stackoverflow.com/a/3694895
        delta = (summary['completed'] - summary['started'])
        total_seconds = (delta.microseconds +
                         (delta.seconds + delta.days * 24 * 3600) * 1e6) / 1e6
        summary['time_taken'] = '%0.2f' % total_seconds

        external_config = []
        for tool, configured_by in self.config.configured_by.items():
            if configured_by is not None:
                external_config.append((tool, configured_by))
        if len(external_config) > 0:
            summary['external_config'] = ', '.join(
                ['%s: %s' % info for info in external_config])

        self.summary = summary
        self.messages = self.messages + messages