Exemple #1
0
    def flush_buffer(
        self,
        from_python: bool = False,
        final_flush: bool = False,
        in_global_context: bool = False,
    ) -> None:
        if len(self.buffer) == 0 or self.buffer.isspace():
            self.result += self.buffer
            self.buffer = ""
            return

        if not from_python:
            formatted = self.run_black_format_str(self.buffer, self.target_indent)
            if self.target_indent > 0:
                formatted = self.align_strings(formatted, self.target_indent)
        else:
            # Invalid python syntax, eg lone 'else:' between two rules, can occur.
            # Below constructs valid code statements and formats them.
            re_match = contextual_matcher.match(self.buffer)
            if re_match is not None:
                callback_keyword = re_match.group(2)
                used_keyword = (
                    "if" if callback_keyword in {"elif", "else"} else callback_keyword
                )
                condition = re_match.group(3)
                if condition != "":
                    test_substitute = f"{used_keyword}{condition}"
                else:
                    test_substitute = f"{used_keyword} a"
                to_format = (
                    f"{re_match.group(1)}{test_substitute}" f"{re_match.group(4)}pass"
                )
                formatted = self.run_black_format_str(to_format, self.target_indent)
                re_rematch = contextual_matcher.match(formatted)
                if condition != "":
                    callback_keyword += re_rematch.group(3)
                formatted = (
                    f"{re_rematch.group(1)}{callback_keyword}" f"{re_rematch.group(4)}"
                )
                formatted_lines = formatted.splitlines(keepends=True)
                formatted = "".join(formatted_lines[:-1])  # Remove the 'pass' line
            else:
                formatted = self.run_black_format_str(self.buffer, self.target_indent)

            code_indent = self.syntax.code_indent
            if code_indent is not None:
                formatted = textwrap.indent(formatted, f"{TAB * code_indent}")
                if self.syntax.effective_indent == 0:
                    self.syntax.code_indent = 0

        # Re-add newline removed by black for proper parsing of comments
        if self.buffer.endswith("\n\n"):
            if comment_start(self.buffer.rstrip().splitlines()[-1]):
                formatted += "\n"
        # Only stick together separated single-parm keywords when separated by comments
        buffer_is_all_comments = all(map(comment_start, self.buffer.splitlines()))
        if not buffer_is_all_comments:
            self.last_recognised_keyword = ""
        self.add_newlines(self.target_indent, formatted, final_flush, in_global_context)
        self.buffer = ""
Exemple #2
0
    def add_newlines(
        self,
        cur_indent: int,
        formatted_string: str = "",
        final_flush: bool = False,
        in_global_context: bool = False,
        context: Syntax = None,
    ):
        """
        Top-level (indent of 0) rules and python code get two newlines separation
        Indented rules/pycode get one newline separation
        Comments immediately preceding rules/pycode get newlined with them
        """
        comment_matches = 0
        comment_break = 1
        all_lines = formatted_string.splitlines()
        if len(all_lines) > 0:
            for line in reversed(all_lines):
                if not comment_start(line):
                    break
                comment_matches += 1
            comment_break = len(all_lines) - comment_matches

        have_only_comment_lines = comment_break == 0
        if not have_only_comment_lines or final_flush:
            collate_same_singleparamkeyword = (
                context is not None
                and context.keyword_name == self.last_recognised_keyword
                and issubclass(context.__class__, SingleParam)
            )
            if not self.no_formatting_yet and not collate_same_singleparamkeyword:
                if cur_indent == 0:
                    self.result += "\n\n"
                elif in_global_context:
                    self.result += "\n"
        if in_global_context:  # Deal with comments
            if self.lagging_comments != "":
                self.result += self.lagging_comments
                self.lagging_comments = ""

            if len(all_lines) > 0:
                if not have_only_comment_lines:
                    self.result += "\n".join(all_lines[:comment_break]).rstrip() + "\n"
                if comment_matches > 0:
                    self.lagging_comments = "\n".join(all_lines[comment_break:]) + "\n"
                    if final_flush:
                        self.result += self.lagging_comments
        else:
            self.result += formatted_string

        if self.no_formatting_yet:
            if comment_break > 0:
                self.no_formatting_yet = False
Exemple #3
0
    def run_black_format_str(
        self, string: str, target_indent: int, extra_spacing: int = 0
    ) -> str:
        # this initial section deals with comment indenting inside if-else statements
        # that have had snakecode. Somehow the indenting after snakecode inside these
        # nested statements gets messed up
        inside_nested_statement = (
            self.syntax.code_indent is not None and self.syntax.code_indent > 0
        )
        # this checks if we are inside snakecode, within a nested if-else statement
        if inside_nested_statement and self.from_python and self.in_global_context:
            # indent any comments and the first line
            tmpstring = ""
            for i, line in enumerate(string.splitlines(keepends=True)):
                if comment_start(line) or i == 0:
                    line = f"{TAB * self.syntax.code_indent}{line}"
                tmpstring += line
            string = textwrap.dedent(tmpstring)

        # reduce black target line length according to how indented the code is
        current_line_length = target_indent * len(TAB)
        black_mode = copy(self.black_mode)
        black_mode.line_length = max(
            0, black_mode.line_length - current_line_length + extra_spacing
        )
        try:
            fmted = black.format_str(string, mode=black_mode)
        except black.InvalidInput as e:
            err_msg = ""
            # Not clear whether all Black errors start with 'Cannot parse' - it seems to
            # in the tests I ran
            match = re.search(r"(Cannot parse: )(?P<line>\d+)(.*)", str(e))
            try:
                next_token = next(self.snakefile)
                self.snakefile.denext(next_token)
            except StopIteration:
                next_token = None
            if match and next_token is not None:
                # this is the line number within the piece of code that was passed to
                # black, not necessarily the line number within the Snakefile
                line_num = int(match.group("line"))
                context_line_num = next_token.start[0] - len(string.splitlines())
                total_line_num = context_line_num + line_num - 1
                err_msg = match.group(1) + str(total_line_num) + match.group(3)
            else:
                err_msg = str(e) + (
                    "\n\n(Note reported line number may be incorrect, as"
                    " snakefmt could not determine the true line number)"
                )
            err_msg = f"Black error:\n```\n{str(err_msg)}\n```\n"
            raise InvalidPython(err_msg) from None
        return fmted