From aaf392c6efd32a6025c26847d02f952dcf700ba7 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Sun, 24 May 2026 11:47:23 +0300 Subject: [PATCH 1/3] 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 bd0e538d..34920867 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1985,3 +1985,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 df3d9b836197318af6349abca827e2c6f2f5aa91 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Thu, 28 May 2026 13:08:20 +0300 Subject: [PATCH 2/3] 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 34920867..f602c306 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1986,26 +1986,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 9d406466c90c637fa630bfe3e77bfa5459b18413 Mon Sep 17 00:00:00 2001 From: Abdu Ahmed Date: Mon, 1 Jun 2026 22:36:02 +0300 Subject: [PATCH 3/3] 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 f602c306..1f6509fd 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -1987,34 +1987,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