From 276a88e76c17aa3bc49af662b7eac2244ed49a94 Mon Sep 17 00:00:00 2001 From: MilesCranmerBot Date: Wed, 8 Jul 2026 08:39:57 +0000 Subject: [PATCH 1/2] fix: fall back from incompatible stdin Co-authored-by: Miles Cranmer --- pysr/sr.py | 31 ++++++++++++++++++++++++++----- pysr/test/test_main.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/pysr/sr.py b/pysr/sr.py index f09b8bc50..73a08ade2 100644 --- a/pysr/sr.py +++ b/pysr/sr.py @@ -81,6 +81,26 @@ pysr_logger = logging.getLogger(__name__) +def _stdin_is_compatible() -> bool: + """Return whether Python's stdin is a usable interactive stream.""" + stdin = sys.stdin + if stdin is None or stdin.closed: + return False + try: + if not stdin.isatty(): + return False + os.fstat(stdin.fileno()) + except (AttributeError, OSError, ValueError): + return False + return True + + +def _resolve_input_stream(input_stream: str) -> AnyValue: + if input_stream == "stdin" and not _stdin_is_compatible(): + return jl.seval("devnull") + return jl.seval(input_stream) + + def _process_constraints( operators: dict[int, list[str]], constraints: dict[str, int | tuple[int, ...]], @@ -780,10 +800,11 @@ class PySRRegressor(MultiOutputMixin, RegressorMixin, BaseEstimator): `TensorBoardLoggerSpec`. Default is `None`. input_stream : str - The stream to read user input from. By default, this is `"stdin"`. - If you encounter issues with reading from `stdin`, like a hang, - you can simply pass `"devnull"` to this argument. You can also - reference an arbitrary Julia object in the `Main` namespace. + The stream to read user input from. By default, this is `"stdin"`, + which falls back to `"devnull"` when Python's stdin is not an + interactive terminal. You can pass `"devnull"` to explicitly disable + reading from stdin, or reference an arbitrary Julia object in the + `Main` namespace. Default is `"stdin"`. run_id : str A unique identifier for the run. Will be generated using the @@ -2152,7 +2173,7 @@ def _run( else "nothing" ) - input_stream = jl.seval(self.input_stream) + input_stream = _resolve_input_stream(self.input_stream) load_required_packages( turbo=self.turbo, diff --git a/pysr/test/test_main.py b/pysr/test/test_main.py index b703ff752..9442e24bd 100644 --- a/pysr/test/test_main.py +++ b/pysr/test/test_main.py @@ -40,6 +40,8 @@ from pysr.sr import ( _check_assertions, _process_constraints, + _resolve_input_stream, + _stdin_is_compatible, _suggest_keywords, _validate_elementwise_loss, _validate_export_mappings, @@ -63,6 +65,40 @@ from juliacall import JuliaError # type: ignore +class TestInputStream(unittest.TestCase): + def test_stdin_resolves_to_devnull_when_not_interactive(self): + stdin = mock.Mock(closed=False) + stdin.isatty.return_value = False + seval = mock.Mock(return_value="DEVNULL") + with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( + "pysr.sr.jl", mock.Mock(seval=seval) + ): + self.assertEqual(_resolve_input_stream("stdin"), "DEVNULL") + seval.assert_called_once_with("devnull") + + def test_stdin_resolves_to_stdin_when_interactive(self): + stdin = mock.Mock(closed=False) + stdin.isatty.return_value = True + stdin.fileno.return_value = 0 + seval = mock.Mock(return_value="STDIN") + with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( + "pysr.sr.os.fstat" + ), mock.patch("pysr.sr.jl", mock.Mock(seval=seval)): + self.assertTrue(_stdin_is_compatible()) + self.assertEqual(_resolve_input_stream("stdin"), "STDIN") + seval.assert_called_once_with("stdin") + + def test_custom_input_stream_is_preserved(self): + stdin = mock.Mock(closed=False) + stdin.isatty.return_value = False + seval = mock.Mock(return_value="STREAM") + with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( + "pysr.sr.jl", mock.Mock(seval=seval) + ): + self.assertEqual(_resolve_input_stream("Main.my_stream"), "STREAM") + seval.assert_called_once_with("Main.my_stream") + + class TestPipeline(unittest.TestCase): def setUp(self): # Using inspect, From d4a26738f4895971057885e0be3ebaad7ada40f3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:40:47 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pysr/test/test_main.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pysr/test/test_main.py b/pysr/test/test_main.py index 9442e24bd..387dc2fc0 100644 --- a/pysr/test/test_main.py +++ b/pysr/test/test_main.py @@ -70,8 +70,9 @@ def test_stdin_resolves_to_devnull_when_not_interactive(self): stdin = mock.Mock(closed=False) stdin.isatty.return_value = False seval = mock.Mock(return_value="DEVNULL") - with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( - "pysr.sr.jl", mock.Mock(seval=seval) + with ( + mock.patch("pysr.sr.sys.stdin", stdin), + mock.patch("pysr.sr.jl", mock.Mock(seval=seval)), ): self.assertEqual(_resolve_input_stream("stdin"), "DEVNULL") seval.assert_called_once_with("devnull") @@ -81,9 +82,11 @@ def test_stdin_resolves_to_stdin_when_interactive(self): stdin.isatty.return_value = True stdin.fileno.return_value = 0 seval = mock.Mock(return_value="STDIN") - with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( - "pysr.sr.os.fstat" - ), mock.patch("pysr.sr.jl", mock.Mock(seval=seval)): + with ( + mock.patch("pysr.sr.sys.stdin", stdin), + mock.patch("pysr.sr.os.fstat"), + mock.patch("pysr.sr.jl", mock.Mock(seval=seval)), + ): self.assertTrue(_stdin_is_compatible()) self.assertEqual(_resolve_input_stream("stdin"), "STDIN") seval.assert_called_once_with("stdin") @@ -92,8 +95,9 @@ def test_custom_input_stream_is_preserved(self): stdin = mock.Mock(closed=False) stdin.isatty.return_value = False seval = mock.Mock(return_value="STREAM") - with mock.patch("pysr.sr.sys.stdin", stdin), mock.patch( - "pysr.sr.jl", mock.Mock(seval=seval) + with ( + mock.patch("pysr.sr.sys.stdin", stdin), + mock.patch("pysr.sr.jl", mock.Mock(seval=seval)), ): self.assertEqual(_resolve_input_stream("Main.my_stream"), "STREAM") seval.assert_called_once_with("Main.my_stream")