import locale import os import re import sys from pathlib import Path from typing import Any, Optional, Tuple from isort import settings from isort.format import ask_whether_to_apply_changes_to_file, show_unified_diff from isort.isort import _SortImports def determine_file_encoding(file_path: Path, default: str = 'utf-8') -> str: # see https://www.python.org/dev/peps/pep-0263/ pattern = re.compile(br'coding[:=]\s*([-\w.]+)') coding = default with file_path.open('rb') as f: for line_number, line in enumerate(f, 1): groups = re.findall(pattern, line) if groups: coding = groups[0].decode('ascii') break if line_number > 2: break return coding def read_file_contents(file_path: Path, encoding: str, fallback_encoding: str) -> Tuple[Optional[str], Optional[str]]: with file_path.open(encoding=encoding, newline='') as file_to_import_sort: try: file_contents = file_to_import_sort.read() return file_contents, encoding except UnicodeDecodeError: pass with file_path.open(encoding=fallback_encoding, newline='') as file_to_import_sort: try: file_contents = file_to_import_sort.read() return file_contents, fallback_encoding except UnicodeDecodeError: return None, None def resolve(path: Path) -> Path: if sys.version_info[:2] >= (3, 6): return path.resolve() else: return Path(os.path.abspath(str(path))) def get_settings_path(settings_path: Optional[Path], current_file_path: Optional[Path]) -> Path: if settings_path: return settings_path if current_file_path: return resolve(current_file_path).parent else: return Path.cwd() class SortImports(object): incorrectly_sorted = False skipped = False def __init__( self, file_path: Optional[str] = None, file_contents: Optional[str] = None, write_to_stdout: bool = False, check: bool = False, show_diff: bool = False, settings_path: Optional[str] = None, ask_to_apply: bool = False, run_path: str = '', check_skip: bool = True, **setting_overrides: Any ): file_path = None if file_path is None else Path(file_path) settings_path = None if settings_path is None else Path(settings_path) self.config = settings.prepare_config(get_settings_path(settings_path, file_path), **setting_overrides) self.output = None file_encoding = 'utf-8' self.file_path = None if file_path: self.file_path = file_path # raw file path (unresolved) ? absolute_file_path = resolve(file_path) if check_skip: if run_path and run_path in absolute_file_path.parents: file_name = os.path.relpath(absolute_file_path, run_path) else: file_name = str(absolute_file_path) run_path = '' if settings.file_should_be_skipped(file_name, self.config, run_path): self.skipped = True if self.config['verbose']: print("WARNING: {0} was skipped as it's listed in 'skip' setting" " or matches a glob in 'skip_glob' setting".format(absolute_file_path)) file_contents = None if not self.skipped and not file_contents: preferred_encoding = determine_file_encoding(absolute_file_path) # default encoding for open(mode='r') on the system fallback_encoding = locale.getpreferredencoding(False) file_contents, used_encoding = read_file_contents(absolute_file_path, encoding=preferred_encoding, fallback_encoding=fallback_encoding) if used_encoding is None: self.skipped = True if self.config['verbose']: print("WARNING: {} was skipped as it couldn't be opened with the given " "{} encoding or {} fallback encoding".format(str(absolute_file_path), file_encoding, fallback_encoding)) else: file_encoding = used_encoding if file_contents is None or ("isort:" + "skip_file") in file_contents: self.skipped = True if write_to_stdout and file_contents: sys.stdout.write(file_contents) return self.sorted_imports = _SortImports(file_contents=file_contents, config=self.config) self.output = self.sorted_imports.output if self.config['atomic']: logging_file_path = str(self.file_path or '') try: out_lines_without_top_comment = self.sorted_imports.get_out_lines_without_top_comment() compile(out_lines_without_top_comment, logging_file_path, 'exec', 0, 1) except SyntaxError: self.output = file_contents self.incorrectly_sorted = True try: in_lines_without_top_comment = self.sorted_imports.get_in_lines_without_top_comment() compile(in_lines_without_top_comment, logging_file_path, 'exec', 0, 1) print("ERROR: {0} isort would have introduced syntax errors, please report to the project!". format(logging_file_path)) except SyntaxError: print("ERROR: {0} File contains syntax errors.".format(logging_file_path)) return if check: check_output = self.output check_against = file_contents if self.config['ignore_whitespace']: check_output = self.sorted_imports.remove_whitespaces(check_output) check_against = self.sorted_imports.remove_whitespaces(check_against) current_input_sorted_correctly = (self.sorted_imports .check_if_input_already_sorted(check_output, check_against, logging_file_path=str(self.file_path or ''))) if current_input_sorted_correctly: return else: self.incorrectly_sorted = True if show_diff or self.config['show_diff']: show_unified_diff(file_input=file_contents, file_output=self.output, file_path=self.file_path) elif write_to_stdout: sys.stdout.write(self.output) elif self.file_path and not check: # if file_name resolves to True, file_path never None or '' if self.output == file_contents: return if ask_to_apply: show_unified_diff(file_input=file_contents, file_output=self.output, file_path=self.file_path) apply_changes = ask_whether_to_apply_changes_to_file(str(self.file_path)) if not apply_changes: return with self.file_path.open('w', encoding=file_encoding, newline='') as output_file: if not self.config['quiet']: print("Fixing {0}".format(self.file_path)) output_file.write(self.output) @property def sections(self): return self.sorted_imports.sections @property def length_change(self) -> int: return self.sorted_imports.length_change