diff --git a/isort/api.py b/isort/api.py index f59bc6d91..ad7ea236f 100644 --- a/isort/api.py +++ b/isort/api.py @@ -13,6 +13,7 @@ FileSkipComment, FileSkipSetting, IntroducedSyntaxErrors, + UnsupportedEncodingError, ) from .format import ask_whether_to_apply_changes_to_file, create_terminal_printer, show_unified_diff from .io import Empty @@ -260,16 +261,20 @@ def check_file( - **extension**: The file extension that contains imports. Defaults to filename extension or py. - ****config_kwargs**: Any config modifications. """ - with io.File.read(filename) as source_file: - return check_stream( - source_file.stream, - show_diff=show_diff, - extension=extension, - config=config, - file_path=file_path or source_file.path, - disregard_skip=disregard_skip, - **config_kwargs, - ) + try: + with io.File.read(filename) as source_file: + return check_stream( + source_file.stream, + show_diff=show_diff, + extension=extension, + config=config, + file_path=file_path or source_file.path, + disregard_skip=disregard_skip, + **config_kwargs, + ) + + except UnsupportedEncodingError: + raise UnsupportedEncodingError(filename) def sort_file( @@ -297,70 +302,76 @@ def sort_file( - **write_to_stdout**: If `True`, write to stdout instead of the input file. - ****config_kwargs**: Any config modifications. """ - with io.File.read(filename) as source_file: - actual_file_path = file_path or source_file.path - config = _config(path=actual_file_path, config=config, **config_kwargs) - changed: bool = False - try: - if write_to_stdout: - changed = sort_stream( - input_stream=source_file.stream, - output_stream=sys.stdout, - config=config, - file_path=actual_file_path, - disregard_skip=disregard_skip, - extension=extension, - ) - else: - tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") - try: - with tmp_file.open( - "w", encoding=source_file.encoding, newline="" - ) as output_stream: - shutil.copymode(filename, tmp_file) - changed = sort_stream( - input_stream=source_file.stream, - output_stream=output_stream, - config=config, - file_path=actual_file_path, - disregard_skip=disregard_skip, - extension=extension, - ) - if changed: - if show_diff or ask_to_apply: - source_file.stream.seek(0) - with tmp_file.open( - encoding=source_file.encoding, newline="" - ) as tmp_out: - show_unified_diff( - file_input=source_file.stream.read(), - file_output=tmp_out.read(), - file_path=actual_file_path, - output=None if show_diff is True else cast(TextIO, show_diff), - color_output=config.color_output, - ) - if show_diff or ( - ask_to_apply - and not ask_whether_to_apply_changes_to_file( - str(source_file.path) + try: + with io.File.read(filename) as source_file: + actual_file_path = file_path or source_file.path + config = _config(path=actual_file_path, config=config, **config_kwargs) + changed: bool = False + try: + if write_to_stdout: + changed = sort_stream( + input_stream=source_file.stream, + output_stream=sys.stdout, + config=config, + file_path=actual_file_path, + disregard_skip=disregard_skip, + extension=extension, + ) + else: + tmp_file = source_file.path.with_suffix(source_file.path.suffix + ".isorted") + try: + with tmp_file.open( + "w", encoding=source_file.encoding, newline="" + ) as output_stream: + shutil.copymode(filename, tmp_file) + changed = sort_stream( + input_stream=source_file.stream, + output_stream=output_stream, + config=config, + file_path=actual_file_path, + disregard_skip=disregard_skip, + extension=extension, + ) + if changed: + if show_diff or ask_to_apply: + source_file.stream.seek(0) + with tmp_file.open( + encoding=source_file.encoding, newline="" + ) as tmp_out: + show_unified_diff( + file_input=source_file.stream.read(), + file_output=tmp_out.read(), + file_path=actual_file_path, + output=None + if show_diff is True + else cast(TextIO, show_diff), + color_output=config.color_output, ) - ): - return False - source_file.stream.close() - tmp_file.replace(source_file.path) - if not config.quiet: - print(f"Fixing {source_file.path}") - finally: - try: # Python 3.8+: use `missing_ok=True` instead of try except. - tmp_file.unlink() - except FileNotFoundError: - pass - except ExistingSyntaxErrors: - warn(f"{actual_file_path} unable to sort due to existing syntax errors") - except IntroducedSyntaxErrors: # pragma: no cover - warn(f"{actual_file_path} unable to sort as isort introduces new syntax errors") - - return changed + if show_diff or ( + ask_to_apply + and not ask_whether_to_apply_changes_to_file( + str(source_file.path) + ) + ): + return False + source_file.stream.close() + tmp_file.replace(source_file.path) + if not config.quiet: + print(f"Fixing {source_file.path}") + finally: + try: # Python 3.8+: use `missing_ok=True` instead of try except. + tmp_file.unlink() + except FileNotFoundError: + pass + except ExistingSyntaxErrors: + warn(f"{actual_file_path} unable to sort due to existing syntax errors") + except IntroducedSyntaxErrors: # pragma: no cover + warn(f"{actual_file_path} unable to sort as isort introduces new syntax errors") + + return changed + + except UnsupportedEncodingError: + raise UnsupportedEncodingError(filename) def _config( diff --git a/isort/exceptions.py b/isort/exceptions.py index 265928e98..a780f0eed 100644 --- a/isort/exceptions.py +++ b/isort/exceptions.py @@ -1,5 +1,6 @@ """All isort specific exception classes should be defined here""" -from typing import Any, Dict +from pathlib import Path +from typing import Any, Dict, Union from .profiles import profiles @@ -157,3 +158,13 @@ def __init__(self, unsupported_settings: Dict[str, Dict[str, str]]): "https://pycqa.github.io/isort/docs/configuration/options/.\n" ) self.unsupported_settings = unsupported_settings + + +class UnsupportedEncodingError(ISortError): + """Raised when isort has introduced an encoding error while reading a file""" + + def __init__( + self, + filename: Union[str, Path], + ): + super().__init__(f"Unknown encoding in {filename}") diff --git a/isort/io.py b/isort/io.py index a0357347b..b4c1b191c 100644 --- a/isort/io.py +++ b/isort/io.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Iterator, NamedTuple, TextIO, Union +from .exceptions import UnsupportedEncodingError + _ENCODING_PATTERN = re.compile(br"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)") @@ -37,7 +39,7 @@ def _open(filename): return text except Exception: buffer.close() - raise + raise UnsupportedEncodingError(filename) @staticmethod @contextmanager diff --git a/isort/main.py b/isort/main.py index fb5f3200e..b657c1616 100644 --- a/isort/main.py +++ b/isort/main.py @@ -10,7 +10,7 @@ from warnings import warn from . import __version__, api, sections -from .exceptions import FileSkipped +from .exceptions import FileSkipped, UnsupportedEncodingError from .format import create_terminal_printer from .logo import ASCII_ART from .profiles import profiles @@ -67,9 +67,12 @@ class SortAttempt: - def __init__(self, incorrectly_sorted: bool, skipped: bool) -> None: + def __init__( + self, incorrectly_sorted: bool, skipped: bool, supported_encoding: bool = True + ) -> None: self.incorrectly_sorted = incorrectly_sorted self.skipped = skipped + self.supported_encoding = supported_encoding def sort_imports( @@ -101,6 +104,11 @@ def sort_imports( except FileSkipped: skipped = True return SortAttempt(incorrectly_sorted, skipped) + + except UnsupportedEncodingError: + if config.verbose: + warn(f"Encoding not supported for {file_name}") + return SortAttempt(incorrectly_sorted, skipped, False) except (OSError, ValueError) as error: warn(f"Unable to parse file {file_name} due to {error}") return None @@ -843,6 +851,7 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = remapped_deprecated_args = config_dict.pop("remapped_deprecated_args", False) wrong_sorted_files = False all_attempt_broken = False + no_valid_encodings = False if "src_paths" in config_dict: config_dict["src_paths"] = { @@ -918,9 +927,18 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = # If any files passed in are missing considered as error, should be removed is_no_attempt = True + unsupported_encodings = 0 + total_files_scanned = 0 for sort_attempt in attempt_iterator: if not sort_attempt: continue # pragma: no cover - shouldn't happen, satisfies type constraint + + total_files_scanned += 1 + + if not sort_attempt.supported_encoding: + unsupported_encodings += 1 + continue + incorrectly_sorted = sort_attempt.incorrectly_sorted if arguments.get("check", False) and incorrectly_sorted: wrong_sorted_files = True @@ -930,6 +948,9 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = ) is_no_attempt = False + if total_files_scanned and unsupported_encodings == total_files_scanned: + no_valid_encodings = True + num_skipped += len(skipped) if num_skipped and not arguments.get("quiet", False): if config.verbose: @@ -972,6 +993,11 @@ def main(argv: Optional[Sequence[str]] = None, stdin: Optional[TextIOWrapper] = if all_attempt_broken: sys.exit(1) + if no_valid_encodings: + printer = create_terminal_printer(color=config.color_output) + printer.error("No valid encodings.") + sys.exit(1) + if __name__ == "__main__": main() diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 44e1f7efc..652a2ebd6 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -316,6 +316,37 @@ def test_main(capsys, tmpdir): main.main(["-sp", str(config_file), "-"], stdin=input_content) +def test_unsupported_encodings(tmpdir, capsys): + tmp_file = tmpdir.join("file.py") + # fmt: off + tmp_file.write( + u''' +# [syntax-error]\ +# -*- coding: IBO-8859-1 -*- +""" check correct unknown encoding declaration +""" +__revision__ = 'יייי' +''' + ) + # fmt: on + + # should throw an error if only unsupported encoding provided + with pytest.raises(SystemExit): + main.main([str(tmp_file)]) + out, error = capsys.readouterr() + + assert "No valid encodings." in error + + # should not throw an error if at least one valid encoding found + normal_file = tmpdir.join("file1.py") + normal_file.write("import os\nimport sys") + + main.main([str(tmp_file), str(normal_file)]) + out, error = capsys.readouterr() + + assert not error + + def test_isort_command(): """Ensure ISortCommand got registered, otherwise setuptools error must have occurred""" assert main.ISortCommand