Skip to content

Commit

Permalink
pythongh-111201: Support pyrepl on Windows (python#119559)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Shaw <anthony.p.shaw@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
  • Loading branch information
3 people committed May 31, 2024
1 parent 13a5fdc commit 0d07182
Show file tree
Hide file tree
Showing 15 changed files with 1,020 additions and 49 deletions.
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 @@ -328,7 +335,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

0 comments on commit 0d07182

Please sign in to comment.