summaryrefslogtreecommitdiff
path: root/isort/format.py
blob: 67c8c5b1cbcd45a9d9346ae462fc10b2b0057f87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import re
import sys
from datetime import datetime
from difflib import unified_diff
from pathlib import Path
from typing import Optional, TextIO

try:
    import colorama
except ImportError:
    colorama_unavailable = True
else:
    colorama_unavailable = False
    colorama.init()


ADDED_LINE_PATTERN = re.compile(r"\+[^+]")
REMOVED_LINE_PATTERN = re.compile(r"-[^-]")


def format_simplified(import_line: str) -> str:
    import_line = import_line.strip()
    if import_line.startswith("from "):
        import_line = import_line.replace("from ", "")
        import_line = import_line.replace(" import ", ".")
    elif import_line.startswith("import "):
        import_line = import_line.replace("import ", "")

    return import_line


def format_natural(import_line: str) -> str:
    import_line = import_line.strip()
    if not import_line.startswith("from ") and not import_line.startswith("import "):
        if "." not in import_line:
            return f"import {import_line}"
        parts = import_line.split(".")
        end = parts.pop(-1)
        return f"from {'.'.join(parts)} import {end}"

    return import_line


def show_unified_diff(
    *,
    file_input: str,
    file_output: str,
    file_path: Optional[Path],
    output: Optional[TextIO] = None,
    color_output: bool = False,
):
    """Shows a unified_diff for the provided input and output against the provided file path.

    - **file_input**: A string that represents the contents of a file before changes.
    - **file_output**: A string that represents the contents of a file after changes.
    - **file_path**: A Path object that represents the file path of the file being changed.
    - **output**: A stream to output the diff to. If non is provided uses sys.stdout.
    - **color_output**: Use color in output if True.
    """
    printer = create_terminal_printer(color_output, output)
    file_name = "" if file_path is None else str(file_path)
    file_mtime = str(
        datetime.now() if file_path is None else datetime.fromtimestamp(file_path.stat().st_mtime)
    )
    unified_diff_lines = unified_diff(
        file_input.splitlines(keepends=True),
        file_output.splitlines(keepends=True),
        fromfile=file_name + ":before",
        tofile=file_name + ":after",
        fromfiledate=file_mtime,
        tofiledate=str(datetime.now()),
    )
    for line in unified_diff_lines:
        printer.diff_line(line)


def ask_whether_to_apply_changes_to_file(file_path: str) -> bool:
    answer = None
    while answer not in ("yes", "y", "no", "n", "quit", "q"):
        answer = input(f"Apply suggested changes to '{file_path}' [y/n/q]? ")  # nosec
        answer = answer.lower()
        if answer in ("no", "n"):
            return False
        if answer in ("quit", "q"):
            sys.exit(1)
    return True


def remove_whitespace(content: str, line_separator: str = "\n") -> str:
    content = content.replace(line_separator, "").replace(" ", "").replace("\x0c", "")
    return content


class BasicPrinter:
    ERROR = "ERROR"
    SUCCESS = "SUCCESS"

    def __init__(self, output: Optional[TextIO] = None):
        self.output = output or sys.stdout

    def success(self, message: str) -> None:
        print(f"{self.SUCCESS}: {message}", file=self.output)

    def error(self, message: str) -> None:
        print(
            f"{self.ERROR}: {message}",
            file=self.output,
            # TODO this should print to stderr, but don't want to make it backward incompatible now
            # file=sys.stderr
        )

    def diff_line(self, line: str) -> None:
        self.output.write(line)


class ColoramaPrinter(BasicPrinter):
    ADDED_LINE = colorama.Fore.GREEN
    REMOVED_LINE = colorama.Fore.RED

    def __init__(self, output: Optional[TextIO] = None):
        self.output = output or sys.stdout
        self.ERROR = self.style_text("ERROR", colorama.Fore.RED)
        self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN)

    @staticmethod
    def style_text(text: str, style: Optional[str] = None) -> str:
        if style is None:
            return text
        return style + text + colorama.Style.RESET_ALL

    def diff_line(self, line: str) -> None:
        style = None
        if re.match(ADDED_LINE_PATTERN, line):
            style = self.ADDED_LINE
        elif re.match(REMOVED_LINE_PATTERN, line):
            style = self.REMOVED_LINE
        self.output.write(self.style_text(line, style))


def create_terminal_printer(color: bool, output: Optional[TextIO] = None):
    if color and colorama_unavailable:
        no_colorama_message = (
            "\n"
            "Sorry, but to use --color (color_output) the colorama python package is required.\n\n"
            "Reference: https://pypi.org/project/colorama/\n\n"
            "You can either install it separately on your system or as the colors extra "
            "for isort. Ex: \n\n"
            "$ pip install isort[colors]\n"
        )
        print(no_colorama_message, file=sys.stderr)
        sys.exit(1)

    return ColoramaPrinter(output) if color else BasicPrinter(output)