From 8b450398ef368157a127ee3375b4714049d87c97 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Sun, 24 May 2026 11:47:23 +0300 Subject: [PATCH 1/5] Fix --sort-reexports crash with non-seekable streams (e.g. stdin) When --sort-reexports was used with stdin, isort crashed with io.UnsupportedOperation because core.process() called seek() on stdout, which is not seekable. Fix: in api.sort_stream(), if sort_reexports is enabled and the output stream is not seekable, swap it for an internal StringIO buffer before passing to core.process(). Fixes #2393 --- isort/api.py | 2 ++ tests/unit/test_regressions.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/isort/api.py b/isort/api.py index abf8bdb1..43dec051 100644 --- a/isort/api.py +++ b/isort/api.py @@ -203,6 +203,8 @@ def sort_stream( if not output_stream.readable(): _internal_output = StringIO() + if config.sort_reexports and not _internal_output.seekable(): + _internal_output = StringIO() try: changed = core.process( input_stream, diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 3291745b..a9aab563 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2012,3 +2012,27 @@ def test_comment_on_opening_line_of_aliased_import_does_not_move(): isort.code(short_line, profile="black") == "from mod import attr as alias # type: ignore[attr-defined] # My comment\n" ) + +def test_sort_reexports_with_non_seekable_stream_issue_2393(): + """Ensure --sort-reexports does not crash when output stream is non-seekable (e.g. stdin). + See: https://github.com/PyCQA/isort/issues/2393 + """ + import sys + from io import StringIO + + code = "from test import B, A\n__all__ = ['B', 'A']\n" + input_stream = StringIO(code) + isort.api.sort_stream( + input_stream=input_stream, + output_stream=sys.stdout, + sort_reexports=True, + ) + input_stream = StringIO(code) + output_stream = StringIO() + isort.api.sort_stream( + input_stream=input_stream, + output_stream=output_stream, + sort_reexports=True, + ) + output_stream.seek(0) + assert "import A, B" in output_stream.read() \ No newline at end of file From 442e3a325736f432b822d10e14c1dcf0b0cb0775 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Thu, 28 May 2026 13:08:20 +0300 Subject: [PATCH 2/5] Raise ValueError when sort_reexports used with non-seekable stream The sort_reexports feature requires seeking backwards in the output stream to rewrite the __all__ section. When used with non-seekable streams (e.g. stdout pipes), the previous code crashed with an internal io.UnsupportedOperation error. Per maintainer feedback, the correct fix is to raise a clear ValueError explaining the limitation rather than silently buffering (which would discard output). Fixes #2393 --- isort/api.py | 5 ++++- tests/unit/test_regressions.py | 35 +++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/isort/api.py b/isort/api.py index 43dec051..b0aa2490 100644 --- a/isort/api.py +++ b/isort/api.py @@ -204,7 +204,10 @@ def sort_stream( _internal_output = StringIO() if config.sort_reexports and not _internal_output.seekable(): - _internal_output = StringIO() + raise ValueError( + "sort_reexports requires a seekable output stream " + "and cannot be used with non-seekable streams such as stdout." + ) try: changed = core.process( input_stream, diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index a9aab563..eb040a65 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2013,26 +2013,35 @@ def test_comment_on_opening_line_of_aliased_import_does_not_move(): == "from mod import attr as alias # type: ignore[attr-defined] # My comment\n" ) + def test_sort_reexports_with_non_seekable_stream_issue_2393(): - """Ensure --sort-reexports does not crash when output stream is non-seekable (e.g. stdin). - See: https://github.com/PyCQA/isort/issues/2393 + """Ensure --sort-reexports raises a clear error when output stream is + issues #2393 """ + import io import sys - from io import StringIO code = "from test import B, A\n__all__ = ['B', 'A']\n" - input_stream = StringIO(code) - isort.api.sort_stream( - input_stream=input_stream, - output_stream=sys.stdout, - sort_reexports=True, - ) - input_stream = StringIO(code) - output_stream = StringIO() + + # Simulate a non-seekable stream (like a pipe/stdout) + class NonSeekableStream(io.StringIO): + def seekable(self): + return False + + with pytest.raises(ValueError, match="sort_reexports"): + isort.api.sort_stream( + input_stream=io.StringIO(code), + output_stream=NonSeekableStream(), + sort_reexports=True, + ) + + # Seekable stream should still work correctly + output_stream = io.StringIO() isort.api.sort_stream( - input_stream=input_stream, + input_stream=io.StringIO(code), output_stream=output_stream, sort_reexports=True, ) output_stream.seek(0) - assert "import A, B" in output_stream.read() \ No newline at end of file + assert "import A, B" in output_stream.read() + From d72a1eb1b479dfc99e5c7e913caaa8b2fda17415 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Mon, 1 Jun 2026 22:36:02 +0300 Subject: [PATCH 3/5] Raise error when --sort-reexports is used with stdin --sort-reexports requires seeking backwards in the output stream to rewrite the __all__ section, which is fundamentally incompatible with non-seekable streams like stdout pipes. Rather than crashing with an internal io.UnsupportedOperation error, raise a clear error at the CLI level when '-' (stdin) is used with --sort-reexports, following the same pattern as the existing 'show_files with streaming input' check in main.py. Fixes #2393 --- isort/api.py | 5 ----- isort/main.py | 2 ++ tests/unit/test_regressions.py | 34 +++++++--------------------------- 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/isort/api.py b/isort/api.py index b0aa2490..abf8bdb1 100644 --- a/isort/api.py +++ b/isort/api.py @@ -203,11 +203,6 @@ def sort_stream( if not output_stream.readable(): _internal_output = StringIO() - if config.sort_reexports and not _internal_output.seekable(): - raise ValueError( - "sort_reexports requires a seekable output stream " - "and cannot be used with non-seekable streams such as stdout." - ) try: changed = core.process( input_stream, diff --git a/isort/main.py b/isort/main.py index 9369ddd1..72d95b97 100644 --- a/isort/main.py +++ b/isort/main.py @@ -1059,6 +1059,8 @@ def main(argv: Sequence[str] | None = None, stdin: TextIOWrapper | None = None) file_path = Path(stream_filename) if stream_filename else None if show_files: sys.exit("Error: can't show files for streaming input.") + if config.sort_reexports: + sys.exit("Error: --sort-reexports is not supported with streaming input (stdin).") input_stream = sys.stdin if stdin is None else stdin if check: diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index eb040a65..088be9eb 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2014,34 +2014,14 @@ def test_comment_on_opening_line_of_aliased_import_does_not_move(): ) -def test_sort_reexports_with_non_seekable_stream_issue_2393(): - """Ensure --sort-reexports raises a clear error when output stream is +def test_sort_reexports_with_stdin_raises_error_issue_2393(): + """Ensure --sort-reexports raises a clear error when used with stdin. issues #2393 """ import io - import sys - - code = "from test import B, A\n__all__ = ['B', 'A']\n" - - # Simulate a non-seekable stream (like a pipe/stdout) - class NonSeekableStream(io.StringIO): - def seekable(self): - return False - - with pytest.raises(ValueError, match="sort_reexports"): - isort.api.sort_stream( - input_stream=io.StringIO(code), - output_stream=NonSeekableStream(), - sort_reexports=True, - ) - - # Seekable stream should still work correctly - output_stream = io.StringIO() - isort.api.sort_stream( - input_stream=io.StringIO(code), - output_stream=output_stream, - sort_reexports=True, - ) - output_stream.seek(0) - assert "import A, B" in output_stream.read() + from isort.main import main as isort_main + fake_stdin = io.TextIOWrapper(io.BytesIO(b"from test import B, A\n")) + with pytest.raises(SystemExit) as exc_info: + isort_main(argv=["--sort-reexports", "-"], stdin=fake_stdin) + assert exc_info.value.code != 0 From a23e62a89b30218c2f4eefbb79a3ed22fe057fda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:33:48 +0200 Subject: [PATCH 4/5] Fix style --- tests/unit/test_regressions.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 088be9eb..4621cc66 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1,10 +1,11 @@ """A growing set of tests designed to ensure isort doesn't have regressions in new versions""" -from io import StringIO +from io import BytesIO, StringIO, TextIOWrapper import pytest import isort +from isort.main import main def test_isort_duplicating_comments_issue_1264(): @@ -2015,13 +2016,8 @@ def test_comment_on_opening_line_of_aliased_import_does_not_move(): def test_sort_reexports_with_stdin_raises_error_issue_2393(): - """Ensure --sort-reexports raises a clear error when used with stdin. - issues #2393 - """ - import io - from isort.main import main as isort_main - + """Ensure --sort-reexports raises a clear error when used with stdin.""" fake_stdin = io.TextIOWrapper(io.BytesIO(b"from test import B, A\n")) with pytest.raises(SystemExit) as exc_info: - isort_main(argv=["--sort-reexports", "-"], stdin=fake_stdin) + main(argv=["--sort-reexports", "-"], stdin=fake_stdin) assert exc_info.value.code != 0 From 5d7a933b51b5d4cc18e359b086e84799dd53df48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Noord?= <13665637+DanielNoord@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:38:47 +0200 Subject: [PATCH 5/5] Fix style properly --- tests/unit/test_regressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4621cc66..965d20a4 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2017,7 +2017,7 @@ def test_comment_on_opening_line_of_aliased_import_does_not_move(): def test_sort_reexports_with_stdin_raises_error_issue_2393(): """Ensure --sort-reexports raises a clear error when used with stdin.""" - fake_stdin = io.TextIOWrapper(io.BytesIO(b"from test import B, A\n")) + fake_stdin = TextIOWrapper(BytesIO(b"from test import B, A\n")) with pytest.raises(SystemExit) as exc_info: main(argv=["--sort-reexports", "-"], stdin=fake_stdin) assert exc_info.value.code != 0