From 3fa01f25f9f87f8a078ed48395e9123a5b5fa65c Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 26 Sep 2025 20:08:34 +0800 Subject: [PATCH 1/3] fix: init dataclass Console init with value ignore repr error Signed-off-by: yihong0618 --- Lib/_pyrepl/console.py | 4 ++-- Lib/test/test_pyrepl/test_pyrepl.py | 20 +++++++++++++++++++ ...-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index e0535d50396316..5cd8d2caf72e7f 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -47,8 +47,8 @@ class Event: @dataclass class Console(ABC): - posxy: tuple[int, int] - screen: list[str] = field(default_factory=list) + posxy: tuple[int, int] = field(default=(0, 0), repr=False) + screen: list[str] = field(default_factory=list, repr=False) height: int = 25 width: int = 80 diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 47d384a209e9ac..35eb5412a8a20b 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1414,6 +1414,26 @@ def test_dumb_terminal_exits_cleanly(self): self.assertNotIn("Traceback", output) +class TestConsoleRepr(TestCase): + + def test_console_repr_with_missing_attributes(self): + from _pyrepl.unix_console import UnixConsole + console = UnixConsole() + + repr_str = repr(console) + + self.assertIsInstance(repr_str, str) + self.assertIn("UnixConsole", repr_str) + + def test_readline_wrapper_repr_after_import(self): + import _pyrepl.readline + + wrapper = _pyrepl.readline._wrapper + if wrapper is not None and wrapper.reader is not None: + repr_str = repr(wrapper.reader) + self.assertIsInstance(repr_str, str) + + @skipUnless(pty, "requires pty") @skipIf((os.environ.get("TERM") or "dumb") == "dumb", "can't use pyrepl in dumb terminal") class TestMain(ReplTestCase): diff --git a/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst b/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst new file mode 100644 index 00000000000000..4c274bb294a6b9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst @@ -0,0 +1,2 @@ +Fix: make dataclass Console init right to avoid has no attribute 'posxy' +error From b2f9f6c77fd3a480093b0dd062f9a6c0f7f80866 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 26 Sep 2025 22:53:18 +0800 Subject: [PATCH 2/3] fix: address comments Signed-off-by: yihong0618 --- Lib/_pyrepl/console.py | 4 +- Lib/_pyrepl/reader.py | 2 +- Lib/test/test_pyrepl/test_pyrepl.py | 95 ++++++++++++++++--- ...-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst | 4 +- 4 files changed, 88 insertions(+), 17 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 5cd8d2caf72e7f..e0535d50396316 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -47,8 +47,8 @@ class Event: @dataclass class Console(ABC): - posxy: tuple[int, int] = field(default=(0, 0), repr=False) - screen: list[str] = field(default_factory=list, repr=False) + posxy: tuple[int, int] + screen: list[str] = field(default_factory=list) height: int = 25 width: int = 80 diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 0ebd9162eca4bb..d794290fc68a26 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -186,7 +186,7 @@ class Reader: that we're done. """ - console: console.Console + console: console.Console = field(repr=False) ## state buffer: list[str] = field(default_factory=list) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 35eb5412a8a20b..769790c22f0415 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -26,7 +26,9 @@ multiline_input, code_to_events, ) -from _pyrepl.console import Event +from typing import IO + +from _pyrepl.console import Console, Event from _pyrepl._module_completer import ( ImportParser, ModuleCompleter, @@ -1416,22 +1418,91 @@ def test_dumb_terminal_exits_cleanly(self): class TestConsoleRepr(TestCase): - def test_console_repr_with_missing_attributes(self): - from _pyrepl.unix_console import UnixConsole - console = UnixConsole() + class _StubConsole(Console): + def __init__( + self, + f_in: int | IO[bytes] = 0, + f_out: int | IO[bytes] = 1, + term: str = "", + encoding: str = "", + ) -> None: + super().__init__(f_in, f_out, term, encoding) + self.height = 25 + self.width = 80 + self.screen = [] + + def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: + pass + + def prepare(self) -> None: + pass + + def restore(self) -> None: + pass + + def move_cursor(self, x: int, y: int) -> None: + pass + + def set_cursor_vis(self, visible: bool) -> None: + pass + + def getheightwidth(self) -> tuple[int, int]: + return self.height, self.width + + def get_event(self, block: bool = True) -> Event | None: + return None + + def push_char(self, char: int | bytes) -> None: + pass + + def beep(self) -> None: + pass + + def clear(self) -> None: + pass + + def finish(self) -> None: + pass + + def flushoutput(self) -> None: + pass + + def forgetinput(self) -> None: + pass + + def getpending(self) -> Event: + return Event("key", "", b"") + + def wait(self, timeout: float | None = None) -> bool: + return False + + def repaint(self) -> None: + pass + + @property + def input_hook(self): + return None + + def test_reader_repr_avoids_console_field(self): + console = self._StubConsole() + config = ReadlineConfig(readline_completer=None) + + reader = ReadlineAlikeReader(console=console, config=config) - repr_str = repr(console) + repr_str = repr(reader) self.assertIsInstance(repr_str, str) - self.assertIn("UnixConsole", repr_str) + self.assertNotIn("console=", repr_str) - def test_readline_wrapper_repr_after_import(self): - import _pyrepl.readline + def test_wrapper_reader_repr_before_prepare(self): + with patch("_pyrepl.readline.Console", self._StubConsole), patch( + "_pyrepl.readline.os.dup", + side_effect=lambda fd: fd, + ): + wrapper = _ReadlineWrapper() + reader = wrapper.get_reader() - wrapper = _pyrepl.readline._wrapper - if wrapper is not None and wrapper.reader is not None: - repr_str = repr(wrapper.reader) - self.assertIsInstance(repr_str, str) + self.assertIsInstance(repr(reader), str) @skipUnless(pty, "requires pty") diff --git a/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst b/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst index 4c274bb294a6b9..fb0dc5f765a413 100644 --- a/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst +++ b/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst @@ -1,2 +1,2 @@ -Fix: make dataclass Console init right to avoid has no attribute 'posxy' -error +Avoid an ``AttributeError`` when calling :func:`repr` on +``_pyrepl.readline._wrapper.reader`` before the console has been prepared. From 7b581e2dd5c50b17379bbdbfd027da7cc99c2af3 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Fri, 26 Sep 2025 23:06:14 +0800 Subject: [PATCH 3/3] fix: skip news Signed-off-by: yihong0618 --- .../next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst diff --git a/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst b/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst deleted file mode 100644 index fb0dc5f765a413..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-09-26-20-07-59.gh-issue-139349.ZJaeCq.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid an ``AttributeError`` when calling :func:`repr` on -``_pyrepl.readline._wrapper.reader`` before the console has been prepared.