| 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=sys.stderr) |
| |
| def diff_line(self, line: str) -> None: |
| self.output.write(line) |
| |
| |
| class ColoramaPrinter(BasicPrinter): |
| def __init__(self, output: Optional[TextIO] = None): |
| super().__init__(output=output) |
| |
| # Note: this constants are instance variables instead ofs class variables |
| # because they refer to colorama which might not be installed. |
| self.ERROR = self.style_text("ERROR", colorama.Fore.RED) |
| self.SUCCESS = self.style_text("SUCCESS", colorama.Fore.GREEN) |
| self.ADDED_LINE = colorama.Fore.GREEN |
| self.REMOVED_LINE = colorama.Fore.RED |
| |
| @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) |