diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c7ef562c..33e394eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Find out more about isort's release policy [here](https://pycqa.github.io/isort/ - Implemented #1737: Support for using action comments to avoid adding imports to individual files. - Implemented #1750: Ability to customize output format lines. - Implemented #1732: Support for custom sort functions. + - Implemented #1722: Improved behavior for running isort in atomic mode over Cython source files. - Fixed (https://github.com/PyCQA/isort/pull/1695): added imports being added to doc string in some cases. - Fixed (https://github.com/PyCQA/isort/pull/1714): in rare cases line continuation combined with tabs can output invalid code. - Fixed (https://github.com/PyCQA/isort/pull/1726): isort ignores reverse_sort when force_sort_within_sections is true. diff --git a/isort/api.py b/isort/api.py index fcc0bd231..31e433cfa 100644 --- a/isort/api.py +++ b/isort/api.py @@ -37,7 +37,7 @@ from .io import Empty, File from .place import module as place_module # noqa: F401 from .place import module_with_reason as place_module_with_reason # noqa: F401 -from .settings import DEFAULT_CONFIG, Config +from .settings import CYTHON_EXTENSIONS, DEFAULT_CONFIG, Config class ImportKey(Enum): @@ -193,9 +193,14 @@ def sort_stream( try: file_content = input_stream.read() compile(file_content, content_source, "exec", 0, 1) - input_stream = StringIO(file_content) except SyntaxError: - raise ExistingSyntaxErrors(content_source) + if extension not in CYTHON_EXTENSIONS: + raise ExistingSyntaxErrors(content_source) + elif config.verbose: + warn( + f"{content_source} Python AST errors found but ignored due to Cython extension" + ) + input_stream = StringIO(file_content) if not output_stream.readable(): _internal_output = StringIO() @@ -216,10 +221,15 @@ def sort_stream( try: compile(_internal_output.read(), content_source, "exec", 0, 1) _internal_output.seek(0) - if _internal_output != output_stream: - output_stream.write(_internal_output.read()) except SyntaxError: # pragma: no cover - raise IntroducedSyntaxErrors(content_source) + if extension not in CYTHON_EXTENSIONS: + raise IntroducedSyntaxErrors(content_source) + elif config.verbose: + warn( + f"{content_source} Python AST errors found but ignored due to Cython extension" + ) + if _internal_output != output_stream: + output_stream.write(_internal_output.read()) return changed diff --git a/isort/settings.py b/isort/settings.py index 9eafb8644..b17488bf2 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -47,7 +47,8 @@ from .wrap_modes import from_string as wrap_mode_from_string _SHEBANG_RE = re.compile(br"^#!.*\bpython[23w]?\b") -SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", "pyx", "pxd"}) +CYTHON_EXTENSIONS = frozenset({"pyx", "pxd"}) +SUPPORTED_EXTENSIONS = frozenset({"py", "pyi", *CYTHON_EXTENSIONS}) BLOCKED_EXTENSIONS = frozenset({"pex"}) FILE_SKIP_COMMENTS: Tuple[str, ...] = ( "isort:" + "skip_file", diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 0599925e7..c90f40d29 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -1408,6 +1408,17 @@ def test_atomic_mode() -> None: with pytest.raises(ExistingSyntaxErrors): isort.code(test_input, atomic=True) + # unless file is for Cython which doesn't yet provide a public AST parsing API + assert ( + isort.code(test_input, extension="pyx", atomic=True, verbose=True) + == isort.code(test_input, extension="pyx", atomic=True) + == """from a import e, f +from b import c, d + +while True print 'Hello world' +""" + ) + # ensure atomic works with streams test_input = as_stream("from b import d, c\nfrom a import f, e\n") test_output = UnreadableStream()