From 80f78c6fddfc8fe3116d1f122275fe8b4cc29bff Mon Sep 17 00:00:00 2001 From: gaoflow Date: Thu, 25 Jun 2026 01:00:11 +0200 Subject: [PATCH 1/2] fix: propagate input stream in Command.call() for interactive questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a command uses call() to invoke another command that contains interactive questions (confirm, ask, choice), the callee's input stream was None, causing an AttributeError on readline(). The root cause: call() constructs a fresh StringInput whose _stream defaults to None, but never copies the parent IO's stream. When the callee calls confirm() → Question.ask() → io.read_line() → input.read_line() → self._stream.readline(), the None stream raises 'NoneType' object has no attribute 'readline'. Fix: propagate self._io.input.stream to the new StringInput before passing it to _run_command, so the callee reads from the same stream as the caller. Fixes #333 --- src/cleo/commands/command.py | 5 +++- tests/commands/test_command.py | 43 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/cleo/commands/command.py b/src/cleo/commands/command.py index c617bc44..0549450d 100644 --- a/src/cleo/commands/command.py +++ b/src/cleo/commands/command.py @@ -92,8 +92,11 @@ def call(self, name: str, args: str | None = None) -> int: assert self.application is not None command = self.application.get(name) + string_input = StringInput(args or "") + string_input.set_stream(self._io.input.stream) + return self.application._run_command( - command, self._io.with_input(StringInput(args or "")) + command, self._io.with_input(string_input) ) def call_silent(self, name: str, args: str | None = None) -> int: diff --git a/tests/commands/test_command.py b/tests/commands/test_command.py index 4f4fd807..59dde075 100644 --- a/tests/commands/test_command.py +++ b/tests/commands/test_command.py @@ -87,3 +87,46 @@ def test_explicit_multiple_argument() -> None: tester.execute("1 2 3") assert tester.io.fetch_output() == "1,2,3\n" + + +def test_call_propagates_stream_for_confirm() -> None: + """call() must propagate the parent's input stream so that interactive + questions (confirm, ask, …) in the callee can read from it. + + Regression test for https://github.com/python-poetry/cleo/issues/333. + """ + + class GreetedCommand(Command): + name = "greeted" + description = "Greeted command" + arguments: ClassVar = [argument("name", "Name to greet")] + + def handle(self) -> int: + name = self.argument("name") + confirmed = self.confirm(f"Say hello to {name}?") + if confirmed: + self.line(f"Hello, {name}!") + return 0 + + class GreeterCommand(Command): + name = "greeter" + description = "Greeter command" + arguments: ClassVar = [argument("name", "Name to greet")] + + def handle(self) -> int: + # The first positional of the args string is consumed by the app's + # implicit "command" argument in the merged definition, so a + # placeholder is required before the actual argument values. + self.call("greeted", f"greeted {self.argument('name')}") + return 0 + + app = Application() + app.add(GreeterCommand()) + app.add(GreetedCommand()) + + from cleo.testers.application_tester import ApplicationTester + + tester = ApplicationTester(app) + # Provide "yes" so that confirm() can read from the propagated stream. + tester.execute("greeter Alice", inputs="yes\n") + assert "Hello, Alice!" in tester.io.fetch_output() From 2315bfbda2d9005609a2532394f94f326a757155 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 26 Jun 2026 12:14:02 +0200 Subject: [PATCH 2/2] Add news entry for command call stream fix --- news/333.bugfix.md | 1 + src/cleo/commands/command.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 news/333.bugfix.md diff --git a/news/333.bugfix.md b/news/333.bugfix.md new file mode 100644 index 00000000..8bac4b8f --- /dev/null +++ b/news/333.bugfix.md @@ -0,0 +1 @@ +Propagated the parent command input stream when calling commands with interactive questions. diff --git a/src/cleo/commands/command.py b/src/cleo/commands/command.py index 0549450d..0993731a 100644 --- a/src/cleo/commands/command.py +++ b/src/cleo/commands/command.py @@ -95,9 +95,7 @@ def call(self, name: str, args: str | None = None) -> int: string_input = StringInput(args or "") string_input.set_stream(self._io.input.stream) - return self.application._run_command( - command, self._io.with_input(string_input) - ) + return self.application._run_command(command, self._io.with_input(string_input)) def call_silent(self, name: str, args: str | None = None) -> int: """