Skip to content

[3.13] gh-111201: Support pyrepl on Windows (GH-119559) #119850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,10 @@ New Features
A Better Interactive Interpreter
--------------------------------

On Unix-like systems like Linux or macOS, Python now uses a new
:term:`interactive` shell. When the user starts the :term:`REPL` from an
interactive terminal, and both :mod:`curses` and :mod:`readline` are
available, the interactive shell now supports the following new features:
On Unix-like systems like Linux or macOS as well as Windows, Python now
uses a new :term:`interactive` shell. When the user starts the
:term:`REPL` from an interactive terminal the interactive shell now
supports the following new features:

* Colorized prompts.
* Multiline editing with history preservation.
Expand All @@ -174,10 +174,13 @@ available, the interactive shell now supports the following new features:
If the new interactive shell is not desired, it can be disabled via
the :envvar:`PYTHON_BASIC_REPL` environment variable.

The new shell requires :mod:`curses` on Unix-like systems.

For more on interactive mode, see :ref:`tut-interac`.

(Contributed by Pablo Galindo Salgado, Łukasz Langa, and
Lysandros Nikolaou in :gh:`111201` based on code from the PyPy project.)
Lysandros Nikolaou in :gh:`111201` based on code from the PyPy project.
Windows support contributed by Dino Viehland and Anthony Shaw.)

.. _whatsnew313-improved-error-messages:

Expand Down
6 changes: 5 additions & 1 deletion Lib/_pyrepl/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import os
import sys

CAN_USE_PYREPL = sys.platform != "win32"
CAN_USE_PYREPL: bool
if sys.platform != "win32":
CAN_USE_PYREPL = True
else:
CAN_USE_PYREPL = sys.getwindowsversion().build >= 10586 # Windows 10 TH2


def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
Expand Down
30 changes: 28 additions & 2 deletions Lib/_pyrepl/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@

from __future__ import annotations

import sys

from abc import ABC, abstractmethod
from dataclasses import dataclass, field


TYPE_CHECKING = False

if TYPE_CHECKING:
from typing import IO


@dataclass
class Event:
evt: str
Expand All @@ -36,6 +44,25 @@ class Console(ABC):
height: int = 25
width: int = 80

def __init__(
self,
f_in: IO[bytes] | int = 0,
f_out: IO[bytes] | int = 1,
term: str = "",
encoding: str = "",
):
self.encoding = encoding or sys.getdefaultencoding()

if isinstance(f_in, int):
self.input_fd = f_in
else:
self.input_fd = f_in.fileno()

if isinstance(f_out, int):
self.output_fd = f_out
else:
self.output_fd = f_out.fileno()

@abstractmethod
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ...

Expand Down Expand Up @@ -108,5 +135,4 @@ def wait(self) -> None:
...

@abstractmethod
def repaint(self) -> None:
...
def repaint(self) -> None: ...
16 changes: 7 additions & 9 deletions Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,14 +442,13 @@ def get_arg(self, default: int = 1) -> int:
"""
if self.arg is None:
return default
else:
return self.arg
return self.arg

def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
"""Return what should be in the left-hand margin for line
`lineno'."""
if self.arg is not None and cursor_on_line:
prompt = "(arg: %s) " % self.arg
prompt = f"(arg: {self.arg}) "
elif self.paste_mode:
prompt = "(paste) "
elif "\n" in self.buffer:
Expand Down Expand Up @@ -515,12 +514,12 @@ def pos2xy(self) -> tuple[int, int]:
offset = l - 1 if in_wrapped_line else l # need to remove backslash
if offset >= pos:
break

if p + sum(l2) >= self.console.width:
pos -= l - 1 # -1 cause backslash is not in buffer
else:
if p + sum(l2) >= self.console.width:
pos -= l - 1 # -1 cause backslash is not in buffer
else:
pos -= l + 1 # +1 cause newline is in buffer
y += 1
pos -= l + 1 # +1 cause newline is in buffer
y += 1
return p + sum(l2[:pos]), y

def insert(self, text: str | list[str]) -> None:
Expand Down Expand Up @@ -582,7 +581,6 @@ def suspend(self) -> SimpleContextManager:
for arg in ("msg", "ps1", "ps2", "ps3", "ps4", "paste_mode"):
setattr(self, arg, prev_state[arg])
self.prepare()
pass

def finish(self) -> None:
"""Called when a command signals that we're finished."""
Expand Down
11 changes: 9 additions & 2 deletions Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@

from . import commands, historical_reader
from .completing_reader import CompletingReader
from .unix_console import UnixConsole, _error
from .console import Console as ConsoleType

Console: type[ConsoleType]
_error: tuple[type[Exception], ...] | type[Exception]
try:
from .unix_console import UnixConsole as Console, _error
except ImportError:
from .windows_console import WindowsConsole as Console, _error

ENCODING = sys.getdefaultencoding() or "latin1"

Expand Down Expand Up @@ -339,7 +346,7 @@ def __post_init__(self) -> None:

def get_reader(self) -> ReadlineAlikeReader:
if self.reader is None:
console = UnixConsole(self.f_in, self.f_out, encoding=ENCODING)
console = Console(self.f_in, self.f_out, encoding=ENCODING)
self.reader = ReadlineAlikeReader(console=console, config=self.config)
return self.reader

Expand Down
6 changes: 5 additions & 1 deletion Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@
from types import ModuleType

from .readline import _get_reader, multiline_input
from .unix_console import _error

_error: tuple[type[Exception], ...] | type[Exception]
try:
from .unix_console import _error
except ModuleNotFoundError:
from .windows_console import _error

def check() -> str:
"""Returns the error message if there is a problem initializing the state."""
Expand Down
28 changes: 13 additions & 15 deletions Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,7 @@ def __init__(
- term (str): Terminal name.
- encoding (str): Encoding to use for I/O operations.
"""

self.encoding = encoding or sys.getdefaultencoding()

if isinstance(f_in, int):
self.input_fd = f_in
else:
self.input_fd = f_in.fileno()

if isinstance(f_out, int):
self.output_fd = f_out
else:
self.output_fd = f_out.fileno()
super().__init__(f_in, f_out, term, encoding)

self.pollob = poll()
self.pollob.register(self.input_fd, select.POLLIN)
Expand Down Expand Up @@ -592,14 +581,19 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
px_pos = 0
j = 0
for c in oldline:
if j >= px_coord: break
if j >= px_coord:
break
j += wlen(c)
px_pos += 1

# reuse the oldline as much as possible, but stop as soon as we
# encounter an ESCAPE, because it might be the start of an escape
# sequene
while x_coord < minlen and oldline[x_pos] == newline[x_pos] and newline[x_pos] != "\x1b":
while (
x_coord < minlen
and oldline[x_pos] == newline[x_pos]
and newline[x_pos] != "\x1b"
):
x_coord += wlen(newline[x_pos])
x_pos += 1

Expand All @@ -619,7 +613,11 @@ def __write_changed_line(self, y, oldline, newline, px_coord):
self.__posxy = x_coord + character_width, y

# if it's a single character change in the middle of the line
elif x_coord < minlen and oldline[x_pos + 1 :] == newline[x_pos + 1 :] and wlen(oldline[x_pos]) == wlen(newline[x_pos]):
elif (
x_coord < minlen
and oldline[x_pos + 1 :] == newline[x_pos + 1 :]
and wlen(oldline[x_pos]) == wlen(newline[x_pos])
):
character_width = wlen(newline[x_pos])
self.__move(x_coord, y)
self.__write(newline[x_pos])
Expand Down
Loading
Loading