Python-checkins
Threads by month
- ----- 2026 -----
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2010 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2009 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2008 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2007 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2006 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2005 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2004 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2003 -----
- December
- November
- October
- September
- August
May 2024
- 1 participants
- 855 discussions
May 31, 2024
https://github.com/python/cpython/commit/b9965ef282d6662145d2e05b080c811132…
commit: b9965ef282d6662145d2e05b080c811132ce6fde
branch: main
author: Joshua Herman <30265+zitterbewegung(a)users.noreply.github.com>
committer: serhiy-storchaka <storchaka(a)gmail.com>
date: 2024-05-31T10:05:09Z
summary:
gh-119189: Fix the power operator for Fraction (GH-119242)
When using the ** operator or pow() with Fraction as the base
and an exponent that is not rational, a float, or a complex, the
fraction is no longer converted to a float.
files:
A Misc/NEWS.d/next/Library/2024-05-20-13-48-37.gh-issue-119189.dhJVs5.rst
M Lib/fractions.py
M Lib/test/test_fractions.py
M Misc/ACKS
diff --git a/Lib/fractions.py b/Lib/fractions.py
index f91b4f35eff370..95adccd86e33a0 100644
--- a/Lib/fractions.py
+++ b/Lib/fractions.py
@@ -877,8 +877,10 @@ def __pow__(a, b, modulo=None):
# A fractional power will generally produce an
# irrational number.
return float(a) ** float(b)
- else:
+ elif isinstance(b, (float, complex)):
return float(a) ** b
+ else:
+ return NotImplemented
def __rpow__(b, a):
"""a ** b"""
diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py
index 28607ee37000f9..3c7780e40db096 100644
--- a/Lib/test/test_fractions.py
+++ b/Lib/test/test_fractions.py
@@ -925,21 +925,21 @@ def testMixedPower(self):
self.assertTypedEquals(Root(4) ** F(2, 1), Root(4, F(1)))
self.assertTypedEquals(Root(4) ** F(-2, 1), Root(4, -F(1)))
self.assertTypedEquals(Root(4) ** F(-2, 3), Root(4, -3.0))
- self.assertEqual(F(3, 2) ** SymbolicReal('X'), SymbolicReal('1.5 ** X'))
+ self.assertEqual(F(3, 2) ** SymbolicReal('X'), SymbolicReal('3/2 ** X'))
self.assertEqual(SymbolicReal('X') ** F(3, 2), SymbolicReal('X ** 1.5'))
- self.assertTypedEquals(F(3, 2) ** Rect(2, 0), Polar(2.25, 0.0))
- self.assertTypedEquals(F(1, 1) ** Rect(2, 3), Polar(1.0, 0.0))
+ self.assertTypedEquals(F(3, 2) ** Rect(2, 0), Polar(F(9,4), 0.0))
+ self.assertTypedEquals(F(1, 1) ** Rect(2, 3), Polar(F(1), 0.0))
self.assertTypedEquals(F(3, 2) ** RectComplex(2, 0), Polar(2.25, 0.0))
self.assertTypedEquals(F(1, 1) ** RectComplex(2, 3), Polar(1.0, 0.0))
self.assertTypedEquals(Polar(4, 2) ** F(3, 2), Polar(8.0, 3.0))
self.assertTypedEquals(Polar(4, 2) ** F(3, 1), Polar(64, 6))
self.assertTypedEquals(Polar(4, 2) ** F(-3, 1), Polar(0.015625, -6))
self.assertTypedEquals(Polar(4, 2) ** F(-3, 2), Polar(0.125, -3.0))
- self.assertEqual(F(3, 2) ** SymbolicComplex('X'), SymbolicComplex('1.5 ** X'))
+ self.assertEqual(F(3, 2) ** SymbolicComplex('X'), SymbolicComplex('3/2 ** X'))
self.assertEqual(SymbolicComplex('X') ** F(3, 2), SymbolicComplex('X ** 1.5'))
- self.assertEqual(F(3, 2) ** Symbolic('X'), Symbolic('1.5 ** X'))
+ self.assertEqual(F(3, 2) ** Symbolic('X'), Symbolic('3/2 ** X'))
self.assertEqual(Symbolic('X') ** F(3, 2), Symbolic('X ** 1.5'))
def testMixingWithDecimal(self):
diff --git a/Misc/ACKS b/Misc/ACKS
index 9c10a76f1df624..2e7e12481bacd7 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -751,6 +751,7 @@ Kasun Herath
Chris Herborth
Ivan Herman
Jürgen Hermann
+Joshua Jay Herman
Gary Herron
Ernie Hershey
Thomas Herve
diff --git a/Misc/NEWS.d/next/Library/2024-05-20-13-48-37.gh-issue-119189.dhJVs5.rst b/Misc/NEWS.d/next/Library/2024-05-20-13-48-37.gh-issue-119189.dhJVs5.rst
new file mode 100644
index 00000000000000..e5cfbcf95a0b81
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-05-20-13-48-37.gh-issue-119189.dhJVs5.rst
@@ -0,0 +1,3 @@
+When using the ``**`` operator or :func:`pow` with :class:`~fractions.Fraction`
+as the base and an exponent that is not rational, a float, or a complex, the
+fraction is no longer converted to a float.
1
0
May 31, 2024
https://github.com/python/cpython/commit/38bf39cb4be279cce6c97da26afcc60859…
commit: 38bf39cb4be279cce6c97da26afcc60859a01571
branch: 3.13
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ambv <lukasz(a)langa.pl>
date: 2024-05-31T11:51:53+02:00
summary:
[3.13] gh-111201: Improve pyrepl auto indentation (GH-119606) (GH-119833)
- auto-indent when editing multi-line block
- ignore comments
(cherry picked from commit dae0375bd97f3821c5db1602a0653a3c5dc53c5b)
Co-authored-by: Arnon Yaari <wiggin15(a)yahoo.com>
files:
M Lib/_pyrepl/readline.py
M Lib/test/test_pyrepl/test_pyrepl.py
M Lib/test/test_pyrepl/test_reader.py
diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py
index ffa14a9ce31a8f..01da926941b256 100644
--- a/Lib/_pyrepl/readline.py
+++ b/Lib/_pyrepl/readline.py
@@ -230,13 +230,24 @@ def _get_first_indentation(buffer: list[str]) -> str | None:
return None
-def _is_last_char_colon(buffer: list[str]) -> bool:
- i = len(buffer)
- while i > 0:
- i -= 1
- if buffer[i] not in " \t\n": # ignore whitespaces
- return buffer[i] == ":"
- return False
+def _should_auto_indent(buffer: list[str], pos: int) -> bool:
+ # check if last character before "pos" is a colon, ignoring
+ # whitespaces and comments.
+ last_char = None
+ while pos > 0:
+ pos -= 1
+ if last_char is None:
+ if buffer[pos] not in " \t\n": # ignore whitespaces
+ last_char = buffer[pos]
+ else:
+ # even if we found a non-whitespace character before
+ # original pos, we keep going back until newline is reached
+ # to make sure we ignore comments
+ if buffer[pos] == "\n":
+ break
+ if buffer[pos] == "#":
+ last_char = None
+ return last_char == ":"
class maybe_accept(commands.Command):
@@ -273,7 +284,7 @@ def _newline_before_pos():
for i in range(prevlinestart, prevlinestart + indent):
r.insert(r.buffer[i])
r.update_last_used_indentation()
- if _is_last_char_colon(r.buffer):
+ if _should_auto_indent(r.buffer, r.pos):
if r.last_used_indentation is not None:
indentation = r.last_used_indentation
else:
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index bdcabf9be05b9e..910e71d6246ac3 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -312,6 +312,14 @@ def test_cursor_position_after_wrap_and_move_up(self):
self.assertEqual(reader.pos, 10)
self.assertEqual(reader.cxy, (1, 1))
+
+class TestPyReplAutoindent(TestCase):
+ def prepare_reader(self, events):
+ console = FakeConsole(events)
+ config = ReadlineConfig(readline_completer=None)
+ reader = ReadlineAlikeReader(console=console, config=config)
+ return reader
+
def test_auto_indent_default(self):
# fmt: off
input_code = (
@@ -372,7 +380,6 @@ def test_auto_indent_prev_block(self):
),
)
-
output_code = (
"def g():\n"
" pass\n"
@@ -385,6 +392,78 @@ def test_auto_indent_prev_block(self):
output2 = multiline_input(reader)
self.assertEqual(output2, output_code)
+ def test_auto_indent_multiline(self):
+ # fmt: off
+ events = itertools.chain(
+ code_to_events(
+ "def f():\n"
+ "pass"
+ ),
+ [
+ # go to the end of the first line
+ Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+ Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
+ # new line should be autoindented
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ ],
+ code_to_events(
+ "pass"
+ ),
+ [
+ # go to end of last line
+ Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
+ Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
+ # double newline to terminate the block
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ ],
+ )
+
+ output_code = (
+ "def f():\n"
+ " pass\n"
+ " pass\n"
+ " "
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
+ def test_auto_indent_with_comment(self):
+ # fmt: off
+ events = code_to_events(
+ "def f(): # foo\n"
+ "pass\n\n"
+ )
+
+ output_code = (
+ "def f(): # foo\n"
+ " pass\n"
+ " "
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
+ def test_auto_indent_ignore_comments(self):
+ # fmt: off
+ events = code_to_events(
+ "pass #:\n"
+ )
+
+ output_code = (
+ "pass #:"
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
class TestPyReplOutput(TestCase):
def prepare_reader(self, events):
diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py
index 7bf7a36d8d7bb9..c9b03d5e711539 100644
--- a/Lib/test/test_pyrepl/test_reader.py
+++ b/Lib/test/test_pyrepl/test_reader.py
@@ -168,8 +168,8 @@ def test_newline_within_block_trailing_whitespace(self):
expected = (
"def foo():\n"
- "\n"
- "\n"
+ " \n"
+ " \n"
" a = 1\n"
" \n"
" " # HistoricalReader will trim trailing whitespace
1
0
May 31, 2024
https://github.com/python/cpython/commit/7dae73b21b500e34ebb070a4d3774e09d8…
commit: 7dae73b21b500e34ebb070a4d3774e09d83d6c1d
branch: 3.13
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: ambv <lukasz(a)langa.pl>
date: 2024-05-31T11:25:39+02:00
summary:
[3.13] gh-97747: Improvements to WASM browser REPL. (GH-97665) (GH-119828)
(cherry picked from commit 010aaa32fb93c5033a698d7213469af02d76fef3)
Co-authored-by: Katie Bell <katie(a)katharos.id.au>
files:
M Tools/wasm/python.html
M Tools/wasm/python.worker.js
diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html
index 17ffa0ea8bfeff..81a035a5c4cd93 100644
--- a/Tools/wasm/python.html
+++ b/Tools/wasm/python.html
@@ -35,11 +35,12 @@
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
<script type="module">
class WorkerManager {
- constructor(workerURL, standardIO, readyCallBack) {
+ constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
this.workerURL = workerURL
this.worker = null
this.standardIO = standardIO
this.readyCallBack = readyCallBack
+ this.finishedCallback = finishedCallback
this.initialiseWorker()
}
@@ -59,6 +60,15 @@
})
}
+ reset() {
+ if (this.worker) {
+ this.worker.terminate()
+ this.worker = null
+ }
+ this.standardIO.message('Worker process terminated.')
+ this.initialiseWorker()
+ }
+
handleStdinData(inputValue) {
if (this.stdinbuffer && this.stdinbufferInt) {
let startingIndex = 1
@@ -92,7 +102,8 @@
this.handleStdinData(inputValue)
})
} else if (type === 'finished') {
- this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
+ this.standardIO.message(`Exited with status: ${event.data.returnCode}`)
+ this.finishedCallback()
}
}
}
@@ -168,9 +179,14 @@
break;
case "\x7F": // BACKSPACE
case "\x08": // CTRL+H
- case "\x04": // CTRL+D
this.handleCursorErase(true);
break;
+ case "\x04": // CTRL+D
+ // Send empty input
+ if (this.input === '') {
+ this.resolveInput('')
+ this.activeInput = false;
+ }
}
} else {
this.handleCursorInsert(data);
@@ -265,9 +281,13 @@
}
}
+const runButton = document.getElementById('run')
const replButton = document.getElementById('repl')
+const stopButton = document.getElementById('stop')
const clearButton = document.getElementById('clear')
+const codeBox = document.getElementById('codebox')
+
window.onload = () => {
const terminal = new WasmTerminal()
terminal.open(document.getElementById('terminal'))
@@ -277,35 +297,72 @@
stderr: (charCode) => { terminal.print(charCode) },
stdin: async () => {
return await terminal.prompt()
+ },
+ message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
+ }
+
+ const programRunning = (isRunning) => {
+ if (isRunning) {
+ replButton.setAttribute('disabled', true)
+ runButton.setAttribute('disabled', true)
+ stopButton.removeAttribute('disabled')
+ } else {
+ replButton.removeAttribute('disabled')
+ runButton.removeAttribute('disabled')
+ stopButton.setAttribute('disabled', true)
}
}
+ runButton.addEventListener('click', (e) => {
+ terminal.clear()
+ programRunning(true)
+ const code = codeBox.value
+ pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
+ })
+
replButton.addEventListener('click', (e) => {
+ terminal.clear()
+ programRunning(true)
// Need to use "-i -" to force interactive mode.
// Looks like isatty always returns false in emscripten
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
})
+ stopButton.addEventListener('click', (e) => {
+ programRunning(false)
+ pythonWorkerManager.reset()
+ })
+
clearButton.addEventListener('click', (e) => {
terminal.clear()
})
const readyCallback = () => {
replButton.removeAttribute('disabled')
+ runButton.removeAttribute('disabled')
clearButton.removeAttribute('disabled')
}
- const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
+ const finishedCallback = () => {
+ programRunning(false)
+ }
+
+ const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback, finishedCallback)
}
</script>
</head>
<body>
<h1>Simple REPL for Python WASM</h1>
- <div id="terminal"></div>
+<textarea id="codebox" cols="108" rows="16">
+print('Welcome to WASM!')
+</textarea>
<div class="button-container">
+ <button id="run" disabled>Run</button>
<button id="repl" disabled>Start REPL</button>
+ <button id="stop" disabled>Stop</button>
<button id="clear" disabled>Clear</button>
</div>
+ <div id="terminal"></div>
<div id="info">
The simple REPL provides a limited Python experience in the browser.
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
diff --git a/Tools/wasm/python.worker.js b/Tools/wasm/python.worker.js
index 1b794608fffe7b..4ce4e16fc0fa19 100644
--- a/Tools/wasm/python.worker.js
+++ b/Tools/wasm/python.worker.js
@@ -19,18 +19,18 @@ class StdinBuffer {
}
stdin = () => {
- if (this.numberOfCharacters + 1 === this.readIndex) {
+ while (this.numberOfCharacters + 1 === this.readIndex) {
if (!this.sentNull) {
// Must return null once to indicate we're done for now.
this.sentNull = true
return null
}
this.sentNull = false
+ // Prompt will reset this.readIndex to 1
this.prompt()
}
const char = this.buffer[this.readIndex]
this.readIndex += 1
- // How do I send an EOF??
return char
}
}
@@ -71,7 +71,11 @@ var Module = {
onmessage = (event) => {
if (event.data.type === 'run') {
- // TODO: Set up files from event.data.files
+ if (event.data.files) {
+ for (const [filename, contents] of Object.entries(event.data.files)) {
+ Module.FS.writeFile(filename, contents)
+ }
+ }
const ret = callMain(event.data.args)
postMessage({
type: 'finished',
1
0
[3.12] gh-103194: Fix Tkinter’s Tcl value type handling for Tcl 8.7/9.0 (GH-103846) (GH-119831)
by serhiy-storchaka May 31, 2024
by serhiy-storchaka May 31, 2024
May 31, 2024
https://github.com/python/cpython/commit/d4680b9e17815140b512a399069400794d…
commit: d4680b9e17815140b512a399069400794dae1f97
branch: 3.12
author: Serhiy Storchaka <storchaka(a)gmail.com>
committer: serhiy-storchaka <storchaka(a)gmail.com>
date: 2024-05-31T09:15:53Z
summary:
[3.12] gh-103194: Fix Tkinter’s Tcl value type handling for Tcl 8.7/9.0 (GH-103846) (GH-119831)
Some of standard Tcl types were renamed, removed, or no longer
registered in Tcl 8.7/9.0. This change fixes automatic conversion of Tcl
values to Python values to avoid returning a Tcl_Obj where the primary
Python types (int, bool, str, bytes) were returned in older Tcl.
(cherry picked from commit 94e9585e99abc2d060cedc77b3c03e06b4a0a9c4)
Co-authored-by: Christopher Chavez <chrischavez(a)gmx.us>
files:
A Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
M Modules/_tkinter.c
diff --git a/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst b/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
new file mode 100644
index 00000000000000..3f70168b81069e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
@@ -0,0 +1,4 @@
+Prepare Tkinter for C API changes in Tcl 8.7/9.0 to avoid
+:class:`_tkinter.Tcl_Obj` being unexpectedly returned
+instead of :class:`bool`, :class:`str`,
+:class:`bytearray`, or :class:`int`.
diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c
index e6d8e741b51ab5..8dca940b3f177a 100644
--- a/Modules/_tkinter.c
+++ b/Modules/_tkinter.c
@@ -322,6 +322,7 @@ typedef struct {
const Tcl_ObjType *ListType;
const Tcl_ObjType *ProcBodyType;
const Tcl_ObjType *StringType;
+ const Tcl_ObjType *UTF32StringType;
} TkappObject;
#define Tkapp_Interp(v) (((TkappObject *) (v))->interp)
@@ -592,15 +593,41 @@ Tkapp_New(const char *screenName, const char *className,
}
v->OldBooleanType = Tcl_GetObjType("boolean");
- v->BooleanType = Tcl_GetObjType("booleanString");
- v->ByteArrayType = Tcl_GetObjType("bytearray");
+ {
+ Tcl_Obj *value;
+ int boolValue;
+
+ /* Tcl 8.5 "booleanString" type is not registered
+ and is renamed to "boolean" in Tcl 9.0.
+ Based on approach suggested at
+ https://core.tcl-lang.org/tcl/info/3bb3bcf2da5b */
+ value = Tcl_NewStringObj("true", -1);
+ Tcl_GetBooleanFromObj(NULL, value, &boolValue);
+ v->BooleanType = value->typePtr;
+ Tcl_DecrRefCount(value);
+
+ // "bytearray" type is not registered in Tcl 9.0
+ value = Tcl_NewByteArrayObj(NULL, 0);
+ v->ByteArrayType = value->typePtr;
+ Tcl_DecrRefCount(value);
+ }
v->DoubleType = Tcl_GetObjType("double");
+ /* TIP 484 suggests retrieving the "int" type without Tcl_GetObjType("int")
+ since it is no longer registered in Tcl 9.0. But even though Tcl 8.7
+ only uses the "wideInt" type on platforms with 32-bit long, it still has
+ a registered "int" type, which FromObj() should recognize just in case. */
v->IntType = Tcl_GetObjType("int");
+ if (v->IntType == NULL) {
+ Tcl_Obj *value = Tcl_NewIntObj(0);
+ v->IntType = value->typePtr;
+ Tcl_DecrRefCount(value);
+ }
v->WideIntType = Tcl_GetObjType("wideInt");
v->BignumType = Tcl_GetObjType("bignum");
v->ListType = Tcl_GetObjType("list");
v->ProcBodyType = Tcl_GetObjType("procbody");
v->StringType = Tcl_GetObjType("string");
+ v->UTF32StringType = Tcl_GetObjType("utf32string");
/* Delete the 'exit' command, which can screw things up */
Tcl_DeleteCommand(v->interp, "exit");
@@ -1130,14 +1157,6 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
return PyFloat_FromDouble(value->internalRep.doubleValue);
}
- if (value->typePtr == tkapp->IntType) {
- long longValue;
- if (Tcl_GetLongFromObj(interp, value, &longValue) == TCL_OK)
- return PyLong_FromLong(longValue);
- /* If there is an error in the long conversion,
- fall through to wideInt handling. */
- }
-
if (value->typePtr == tkapp->IntType ||
value->typePtr == tkapp->WideIntType) {
result = fromWideIntObj(tkapp, value);
@@ -1182,21 +1201,12 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
return result;
}
- if (value->typePtr == tkapp->ProcBodyType) {
- /* fall through: return tcl object. */
- }
-
- if (value->typePtr == tkapp->StringType) {
+ if (value->typePtr == tkapp->StringType ||
+ value->typePtr == tkapp->UTF32StringType)
+ {
return unicodeFromTclObj(value);
}
- if (tkapp->BooleanType == NULL &&
- strcmp(value->typePtr->name, "booleanString") == 0) {
- /* booleanString type is not registered in Tcl */
- tkapp->BooleanType = value->typePtr;
- return fromBoolean(tkapp, value);
- }
-
if (tkapp->BignumType == NULL &&
strcmp(value->typePtr->name, "bignum") == 0) {
/* bignum type is not registered in Tcl */
1
0
https://github.com/python/cpython/commit/dae0375bd97f3821c5db1602a0653a3c5d…
commit: dae0375bd97f3821c5db1602a0653a3c5dc53c5b
branch: main
author: Arnon Yaari <wiggin15(a)yahoo.com>
committer: ambv <lukasz(a)langa.pl>
date: 2024-05-31T11:02:54+02:00
summary:
gh-111201: Improve pyrepl auto indentation (#119606)
- auto-indent when editing multi-line block
- ignore comments
files:
M Lib/_pyrepl/readline.py
M Lib/test/test_pyrepl/test_pyrepl.py
M Lib/test/test_pyrepl/test_reader.py
diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py
index 248f3854a29689..7d811bf41773fe 100644
--- a/Lib/_pyrepl/readline.py
+++ b/Lib/_pyrepl/readline.py
@@ -237,13 +237,24 @@ def _get_first_indentation(buffer: list[str]) -> str | None:
return None
-def _is_last_char_colon(buffer: list[str]) -> bool:
- i = len(buffer)
- while i > 0:
- i -= 1
- if buffer[i] not in " \t\n": # ignore whitespaces
- return buffer[i] == ":"
- return False
+def _should_auto_indent(buffer: list[str], pos: int) -> bool:
+ # check if last character before "pos" is a colon, ignoring
+ # whitespaces and comments.
+ last_char = None
+ while pos > 0:
+ pos -= 1
+ if last_char is None:
+ if buffer[pos] not in " \t\n": # ignore whitespaces
+ last_char = buffer[pos]
+ else:
+ # even if we found a non-whitespace character before
+ # original pos, we keep going back until newline is reached
+ # to make sure we ignore comments
+ if buffer[pos] == "\n":
+ break
+ if buffer[pos] == "#":
+ last_char = None
+ return last_char == ":"
class maybe_accept(commands.Command):
@@ -280,7 +291,7 @@ def _newline_before_pos():
for i in range(prevlinestart, prevlinestart + indent):
r.insert(r.buffer[i])
r.update_last_used_indentation()
- if _is_last_char_colon(r.buffer):
+ if _should_auto_indent(r.buffer, r.pos):
if r.last_used_indentation is not None:
indentation = r.last_used_indentation
else:
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index aa2722095794c9..45114e7315749f 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -312,6 +312,14 @@ def test_cursor_position_after_wrap_and_move_up(self):
self.assertEqual(reader.pos, 10)
self.assertEqual(reader.cxy, (1, 1))
+
+class TestPyReplAutoindent(TestCase):
+ def prepare_reader(self, events):
+ console = FakeConsole(events)
+ config = ReadlineConfig(readline_completer=None)
+ reader = ReadlineAlikeReader(console=console, config=config)
+ return reader
+
def test_auto_indent_default(self):
# fmt: off
input_code = (
@@ -372,7 +380,6 @@ def test_auto_indent_prev_block(self):
),
)
-
output_code = (
"def g():\n"
" pass\n"
@@ -385,6 +392,78 @@ def test_auto_indent_prev_block(self):
output2 = multiline_input(reader)
self.assertEqual(output2, output_code)
+ def test_auto_indent_multiline(self):
+ # fmt: off
+ events = itertools.chain(
+ code_to_events(
+ "def f():\n"
+ "pass"
+ ),
+ [
+ # go to the end of the first line
+ Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+ Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
+ # new line should be autoindented
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ ],
+ code_to_events(
+ "pass"
+ ),
+ [
+ # go to end of last line
+ Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
+ Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
+ # double newline to terminate the block
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ Event(evt="key", data="\n", raw=bytearray(b"\n")),
+ ],
+ )
+
+ output_code = (
+ "def f():\n"
+ " pass\n"
+ " pass\n"
+ " "
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
+ def test_auto_indent_with_comment(self):
+ # fmt: off
+ events = code_to_events(
+ "def f(): # foo\n"
+ "pass\n\n"
+ )
+
+ output_code = (
+ "def f(): # foo\n"
+ " pass\n"
+ " "
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
+ def test_auto_indent_ignore_comments(self):
+ # fmt: off
+ events = code_to_events(
+ "pass #:\n"
+ )
+
+ output_code = (
+ "pass #:"
+ )
+ # fmt: on
+
+ reader = self.prepare_reader(events)
+ output = multiline_input(reader)
+ self.assertEqual(output, output_code)
+
class TestPyReplOutput(TestCase):
def prepare_reader(self, events):
diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py
index 7bf7a36d8d7bb9..c9b03d5e711539 100644
--- a/Lib/test/test_pyrepl/test_reader.py
+++ b/Lib/test/test_pyrepl/test_reader.py
@@ -168,8 +168,8 @@ def test_newline_within_block_trailing_whitespace(self):
expected = (
"def foo():\n"
- "\n"
- "\n"
+ " \n"
+ " \n"
" a = 1\n"
" \n"
" " # HistoricalReader will trim trailing whitespace
1
0
May 31, 2024
https://github.com/python/cpython/commit/8470593a98a5f17d72bea0df8287f43ba4…
commit: 8470593a98a5f17d72bea0df8287f43ba4f7f627
branch: 3.13
author: Miss Islington (bot) <31488909+miss-islington(a)users.noreply.github.com>
committer: pablogsal <Pablogsal(a)gmail.com>
date: 2024-05-31T08:35:21Z
summary:
[3.13] gh-119548: Add a 'clear' command to the REPL (GH-119549) (#119552)
gh-119548: Add a 'clear' command to the REPL (GH-119549)
(cherry picked from commit e3bac04c37f6823cebc74d97feae0e0c25818b31)
Co-authored-by: Pablo Galindo Salgado <Pablogsal(a)gmail.com>
files:
A Misc/NEWS.d/next/Core and Builtins/2024-05-25-16-45-27.gh-issue-119548.pqF9Y6.rst
M Lib/_pyrepl/reader.py
M Lib/_pyrepl/simple_interact.py
diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py
index 0f0ef15f9eb2ea..1c816d5bda5fed 100644
--- a/Lib/_pyrepl/reader.py
+++ b/Lib/_pyrepl/reader.py
@@ -238,6 +238,7 @@ class Reader:
cxy: tuple[int, int] = field(init=False)
lxy: tuple[int, int] = field(init=False)
calc_screen: CalcScreen = field(init=False)
+ scheduled_commands: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
# Enable the use of `insert` without a `prepare` call - necessary to
@@ -557,6 +558,10 @@ def prepare(self) -> None:
self.restore()
raise
+ while self.scheduled_commands:
+ cmd = self.scheduled_commands.pop()
+ self.do_cmd((cmd, []))
+
def last_command_is(self, cls: type) -> bool:
if not self.last_command:
return False
diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py
index 1568a73c1b5ec0..11e831c1d6c5d4 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -57,12 +57,17 @@ def _strip_final_indent(text: str) -> str:
return text
+def _clear_screen():
+ reader = _get_reader()
+ reader.scheduled_commands.append("clear_screen")
+
+
REPL_COMMANDS = {
"exit": _sitebuiltins.Quitter('exit', ''),
"quit": _sitebuiltins.Quitter('quit' ,''),
"copyright": _sitebuiltins._Printer('copyright', sys.copyright),
"help": "help",
- "clear": "clear_screen",
+ "clear": _clear_screen,
}
class InteractiveColoredConsole(code.InteractiveConsole):
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-25-16-45-27.gh-issue-119548.pqF9Y6.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-25-16-45-27.gh-issue-119548.pqF9Y6.rst
new file mode 100644
index 00000000000000..0318790d46f0a3
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-25-16-45-27.gh-issue-119548.pqF9Y6.rst
@@ -0,0 +1 @@
+Add a ``clear`` command to the REPL. Patch by Pablo Galindo
1
0
gh-103194: Fix Tkinter’s Tcl value type handling for Tcl 8.7/9.0 (GH-103846)
by serhiy-storchaka May 31, 2024
by serhiy-storchaka May 31, 2024
May 31, 2024
https://github.com/python/cpython/commit/94e9585e99abc2d060cedc77b3c03e06b4…
commit: 94e9585e99abc2d060cedc77b3c03e06b4a0a9c4
branch: main
author: Christopher Chavez <chrischavez(a)gmx.us>
committer: serhiy-storchaka <storchaka(a)gmail.com>
date: 2024-05-31T11:23:53+03:00
summary:
gh-103194: Fix Tkinter’s Tcl value type handling for Tcl 8.7/9.0 (GH-103846)
Some of standard Tcl types were renamed, removed, or no longer
registered in Tcl 8.7/9.0. This change fixes automatic conversion of Tcl
values to Python values to avoid returning a Tcl_Obj where the primary
Python types (int, bool, str, bytes) were returned in older Tcl.
files:
A Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
M Modules/_tkinter.c
diff --git a/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst b/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
new file mode 100644
index 00000000000000..3f70168b81069e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-24-05-34-23.gh-issue-103194.GwBwWL.rst
@@ -0,0 +1,4 @@
+Prepare Tkinter for C API changes in Tcl 8.7/9.0 to avoid
+:class:`_tkinter.Tcl_Obj` being unexpectedly returned
+instead of :class:`bool`, :class:`str`,
+:class:`bytearray`, or :class:`int`.
diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c
index c7e271faa4cf34..0cff36dd307c39 100644
--- a/Modules/_tkinter.c
+++ b/Modules/_tkinter.c
@@ -318,6 +318,7 @@ typedef struct {
const Tcl_ObjType *BignumType;
const Tcl_ObjType *ListType;
const Tcl_ObjType *StringType;
+ const Tcl_ObjType *UTF32StringType;
} TkappObject;
#define Tkapp_Interp(v) (((TkappObject *) (v))->interp)
@@ -588,14 +589,40 @@ Tkapp_New(const char *screenName, const char *className,
}
v->OldBooleanType = Tcl_GetObjType("boolean");
- v->BooleanType = Tcl_GetObjType("booleanString");
- v->ByteArrayType = Tcl_GetObjType("bytearray");
+ {
+ Tcl_Obj *value;
+ int boolValue;
+
+ /* Tcl 8.5 "booleanString" type is not registered
+ and is renamed to "boolean" in Tcl 9.0.
+ Based on approach suggested at
+ https://core.tcl-lang.org/tcl/info/3bb3bcf2da5b */
+ value = Tcl_NewStringObj("true", -1);
+ Tcl_GetBooleanFromObj(NULL, value, &boolValue);
+ v->BooleanType = value->typePtr;
+ Tcl_DecrRefCount(value);
+
+ // "bytearray" type is not registered in Tcl 9.0
+ value = Tcl_NewByteArrayObj(NULL, 0);
+ v->ByteArrayType = value->typePtr;
+ Tcl_DecrRefCount(value);
+ }
v->DoubleType = Tcl_GetObjType("double");
+ /* TIP 484 suggests retrieving the "int" type without Tcl_GetObjType("int")
+ since it is no longer registered in Tcl 9.0. But even though Tcl 8.7
+ only uses the "wideInt" type on platforms with 32-bit long, it still has
+ a registered "int" type, which FromObj() should recognize just in case. */
v->IntType = Tcl_GetObjType("int");
+ if (v->IntType == NULL) {
+ Tcl_Obj *value = Tcl_NewIntObj(0);
+ v->IntType = value->typePtr;
+ Tcl_DecrRefCount(value);
+ }
v->WideIntType = Tcl_GetObjType("wideInt");
v->BignumType = Tcl_GetObjType("bignum");
v->ListType = Tcl_GetObjType("list");
v->StringType = Tcl_GetObjType("string");
+ v->UTF32StringType = Tcl_GetObjType("utf32string");
/* Delete the 'exit' command, which can screw things up */
Tcl_DeleteCommand(v->interp, "exit");
@@ -1124,14 +1151,6 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
return PyFloat_FromDouble(value->internalRep.doubleValue);
}
- if (value->typePtr == tkapp->IntType) {
- long longValue;
- if (Tcl_GetLongFromObj(interp, value, &longValue) == TCL_OK)
- return PyLong_FromLong(longValue);
- /* If there is an error in the long conversion,
- fall through to wideInt handling. */
- }
-
if (value->typePtr == tkapp->IntType ||
value->typePtr == tkapp->WideIntType) {
result = fromWideIntObj(tkapp, value);
@@ -1176,17 +1195,12 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
return result;
}
- if (value->typePtr == tkapp->StringType) {
+ if (value->typePtr == tkapp->StringType ||
+ value->typePtr == tkapp->UTF32StringType)
+ {
return unicodeFromTclObj(value);
}
- if (tkapp->BooleanType == NULL &&
- strcmp(value->typePtr->name, "booleanString") == 0) {
- /* booleanString type is not registered in Tcl */
- tkapp->BooleanType = value->typePtr;
- return fromBoolean(tkapp, value);
- }
-
if (tkapp->BignumType == NULL &&
strcmp(value->typePtr->name, "bignum") == 0) {
/* bignum type is not registered in Tcl */
1
0
gh-119780: Adjust exception messages in Lib/test/test_format.py (GH-119781)
by serhiy-storchaka May 31, 2024
by serhiy-storchaka May 31, 2024
May 31, 2024
https://github.com/python/cpython/commit/b278c723d79a238b14e99908e83f4b1b6a…
commit: b278c723d79a238b14e99908e83f4b1b6a39ed3d
branch: main
author: Sergey B Kirpichev <skirpichev(a)gmail.com>
committer: serhiy-storchaka <storchaka(a)gmail.com>
date: 2024-05-31T11:07:16+03:00
summary:
gh-119780: Adjust exception messages in Lib/test/test_format.py (GH-119781)
Mismatches were just output to the stdout, without making the test failing.
files:
M Lib/test/test_format.py
diff --git a/Lib/test/test_format.py b/Lib/test/test_format.py
index 8cef621bd716ac..d2026152d8e747 100644
--- a/Lib/test/test_format.py
+++ b/Lib/test/test_format.py
@@ -304,9 +304,9 @@ def test_str_format(self):
test_exc('%c', sys.maxunicode+1, OverflowError,
"%c arg not in range(0x110000)")
#test_exc('%c', 2**128, OverflowError, "%c arg not in range(0x110000)")
- test_exc('%c', 3.14, TypeError, "%c requires int or char")
- test_exc('%c', 'ab', TypeError, "%c requires int or char")
- test_exc('%c', b'x', TypeError, "%c requires int or char")
+ test_exc('%c', 3.14, TypeError, "%c requires an int or a unicode character, not float")
+ test_exc('%c', 'ab', TypeError, "%c requires an int or a unicode character, not a string of length 2")
+ test_exc('%c', b'x', TypeError, "%c requires an int or a unicode character, not bytes")
if maxsize == 2**31-1:
# crashes 2.2.1 and earlier:
@@ -370,11 +370,11 @@ def __bytes__(self):
test_exc(b"%c", 2**128, OverflowError,
"%c arg not in range(256)")
test_exc(b"%c", b"Za", TypeError,
- "%c requires an integer in range(256) or a single byte")
+ "%c requires an integer in range(256) or a single byte, not a bytes object of length 2")
test_exc(b"%c", "Y", TypeError,
- "%c requires an integer in range(256) or a single byte")
+ "%c requires an integer in range(256) or a single byte, not str")
test_exc(b"%c", 3.14, TypeError,
- "%c requires an integer in range(256) or a single byte")
+ "%c requires an integer in range(256) or a single byte, not float")
test_exc(b"%b", "Xc", TypeError,
"%b requires a bytes-like object, "
"or an object that implements __bytes__, not 'str'")
1
0
https://github.com/python/cpython/commit/010aaa32fb93c5033a698d7213469af02d…
commit: 010aaa32fb93c5033a698d7213469af02d76fef3
branch: main
author: Katie Bell <katie(a)katharos.id.au>
committer: ambv <lukasz(a)langa.pl>
date: 2024-05-31T09:58:46+02:00
summary:
gh-97747: Improvements to WASM browser REPL. (#97665)
Improvements to WASM browser REPL.
Adds a text box to write and run code outside the REPL, a stop button, and handling of Ctrl-D for EOF.
files:
M Tools/wasm/python.html
M Tools/wasm/python.worker.js
diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html
index 17ffa0ea8bfeff..81a035a5c4cd93 100644
--- a/Tools/wasm/python.html
+++ b/Tools/wasm/python.html
@@ -35,11 +35,12 @@
<script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
<script type="module">
class WorkerManager {
- constructor(workerURL, standardIO, readyCallBack) {
+ constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
this.workerURL = workerURL
this.worker = null
this.standardIO = standardIO
this.readyCallBack = readyCallBack
+ this.finishedCallback = finishedCallback
this.initialiseWorker()
}
@@ -59,6 +60,15 @@
})
}
+ reset() {
+ if (this.worker) {
+ this.worker.terminate()
+ this.worker = null
+ }
+ this.standardIO.message('Worker process terminated.')
+ this.initialiseWorker()
+ }
+
handleStdinData(inputValue) {
if (this.stdinbuffer && this.stdinbufferInt) {
let startingIndex = 1
@@ -92,7 +102,8 @@
this.handleStdinData(inputValue)
})
} else if (type === 'finished') {
- this.standardIO.stderr(`Exited with status: ${event.data.returnCode}\r\n`)
+ this.standardIO.message(`Exited with status: ${event.data.returnCode}`)
+ this.finishedCallback()
}
}
}
@@ -168,9 +179,14 @@
break;
case "\x7F": // BACKSPACE
case "\x08": // CTRL+H
- case "\x04": // CTRL+D
this.handleCursorErase(true);
break;
+ case "\x04": // CTRL+D
+ // Send empty input
+ if (this.input === '') {
+ this.resolveInput('')
+ this.activeInput = false;
+ }
}
} else {
this.handleCursorInsert(data);
@@ -265,9 +281,13 @@
}
}
+const runButton = document.getElementById('run')
const replButton = document.getElementById('repl')
+const stopButton = document.getElementById('stop')
const clearButton = document.getElementById('clear')
+const codeBox = document.getElementById('codebox')
+
window.onload = () => {
const terminal = new WasmTerminal()
terminal.open(document.getElementById('terminal'))
@@ -277,35 +297,72 @@
stderr: (charCode) => { terminal.print(charCode) },
stdin: async () => {
return await terminal.prompt()
+ },
+ message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
+ }
+
+ const programRunning = (isRunning) => {
+ if (isRunning) {
+ replButton.setAttribute('disabled', true)
+ runButton.setAttribute('disabled', true)
+ stopButton.removeAttribute('disabled')
+ } else {
+ replButton.removeAttribute('disabled')
+ runButton.removeAttribute('disabled')
+ stopButton.setAttribute('disabled', true)
}
}
+ runButton.addEventListener('click', (e) => {
+ terminal.clear()
+ programRunning(true)
+ const code = codeBox.value
+ pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
+ })
+
replButton.addEventListener('click', (e) => {
+ terminal.clear()
+ programRunning(true)
// Need to use "-i -" to force interactive mode.
// Looks like isatty always returns false in emscripten
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
})
+ stopButton.addEventListener('click', (e) => {
+ programRunning(false)
+ pythonWorkerManager.reset()
+ })
+
clearButton.addEventListener('click', (e) => {
terminal.clear()
})
const readyCallback = () => {
replButton.removeAttribute('disabled')
+ runButton.removeAttribute('disabled')
clearButton.removeAttribute('disabled')
}
- const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback)
+ const finishedCallback = () => {
+ programRunning(false)
+ }
+
+ const pythonWorkerManager = new WorkerManager('./python.worker.js', stdio, readyCallback, finishedCallback)
}
</script>
</head>
<body>
<h1>Simple REPL for Python WASM</h1>
- <div id="terminal"></div>
+<textarea id="codebox" cols="108" rows="16">
+print('Welcome to WASM!')
+</textarea>
<div class="button-container">
+ <button id="run" disabled>Run</button>
<button id="repl" disabled>Start REPL</button>
+ <button id="stop" disabled>Stop</button>
<button id="clear" disabled>Clear</button>
</div>
+ <div id="terminal"></div>
<div id="info">
The simple REPL provides a limited Python experience in the browser.
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
diff --git a/Tools/wasm/python.worker.js b/Tools/wasm/python.worker.js
index 1b794608fffe7b..4ce4e16fc0fa19 100644
--- a/Tools/wasm/python.worker.js
+++ b/Tools/wasm/python.worker.js
@@ -19,18 +19,18 @@ class StdinBuffer {
}
stdin = () => {
- if (this.numberOfCharacters + 1 === this.readIndex) {
+ while (this.numberOfCharacters + 1 === this.readIndex) {
if (!this.sentNull) {
// Must return null once to indicate we're done for now.
this.sentNull = true
return null
}
this.sentNull = false
+ // Prompt will reset this.readIndex to 1
this.prompt()
}
const char = this.buffer[this.readIndex]
this.readIndex += 1
- // How do I send an EOF??
return char
}
}
@@ -71,7 +71,11 @@ var Module = {
onmessage = (event) => {
if (event.data.type === 'run') {
- // TODO: Set up files from event.data.files
+ if (event.data.files) {
+ for (const [filename, contents] of Object.entries(event.data.files)) {
+ Module.FS.writeFile(filename, contents)
+ }
+ }
const ret = callMain(event.data.args)
postMessage({
type: 'finished',
1
0
https://github.com/python/cpython/commit/0d07182821fad7b95a043d006f1ce13a2d…
commit: 0d07182821fad7b95a043d006f1ce13a2d22edcb
branch: main
author: Dino Viehland <dinoviehland(a)gmail.com>
committer: ambv <lukasz(a)langa.pl>
date: 2024-05-31T09:49:03+02:00
summary:
gh-111201: Support pyrepl on Windows (#119559)
Co-authored-by: Anthony Shaw <anthony.p.shaw(a)gmail.com>
Co-authored-by: Łukasz Langa <lukasz(a)langa.pl>
files:
A Lib/_pyrepl/windows_console.py
A Lib/test/test_pyrepl/test_windows_console.py
A Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst
M Doc/whatsnew/3.13.rst
M Lib/_pyrepl/__main__.py
M Lib/_pyrepl/console.py
M Lib/_pyrepl/reader.py
M Lib/_pyrepl/readline.py
M Lib/_pyrepl/simple_interact.py
M Lib/_pyrepl/unix_console.py
M Lib/test/test_pyrepl/__init__.py
M Lib/test/test_pyrepl/support.py
M Lib/test/test_pyrepl/test_pyrepl.py
M Lib/test/test_pyrepl/test_unix_console.py
M Lib/test/test_pyrepl/test_unix_eventqueue.py
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 241c07e781af1f..29bb3b81f6323c 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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.
@@ -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:
diff --git a/Lib/_pyrepl/__main__.py b/Lib/_pyrepl/__main__.py
index c598019e7cd4ad..dae4ba6e178b9a 100644
--- a/Lib/_pyrepl/__main__.py
+++ b/Lib/_pyrepl/__main__.py
@@ -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):
diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py
index d7e86e768671dc..fcabf785069ecb 100644
--- a/Lib/_pyrepl/console.py
+++ b/Lib/_pyrepl/console.py
@@ -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
@@ -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: ...
@@ -108,5 +135,4 @@ def wait(self) -> None:
...
@abstractmethod
- def repaint(self) -> None:
- ...
+ def repaint(self) -> None: ...
diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py
index d2960bbb6121b3..0045425cdddb79 100644
--- a/Lib/_pyrepl/reader.py
+++ b/Lib/_pyrepl/reader.py
@@ -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:
@@ -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:
@@ -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."""
diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py
index ffa14a9ce31a8f..248f3854a29689 100644
--- a/Lib/_pyrepl/readline.py
+++ b/Lib/_pyrepl/readline.py
@@ -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"
@@ -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
diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py
index 11e831c1d6c5d4..c624f6e12a7094 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -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."""
diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py
index ec7d0636b9aeb3..4bdb02261982c3 100644
--- a/Lib/_pyrepl/unix_console.py
+++ b/Lib/_pyrepl/unix_console.py
@@ -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)
@@ -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
@@ -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])
diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py
new file mode 100644
index 00000000000000..2277865e3262fc
--- /dev/null
+++ b/Lib/_pyrepl/windows_console.py
@@ -0,0 +1,587 @@
+# Copyright 2000-2004 Michael Hudson-Doyle <micahel(a)gmail.com>
+#
+# All Rights Reserved
+#
+#
+# Permission to use, copy, modify, and distribute this software and
+# its documentation for any purpose is hereby granted without fee,
+# provided that the above copyright notice appear in all copies and
+# that both that copyright notice and this permission notice appear in
+# supporting documentation.
+#
+# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
+# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
+# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from __future__ import annotations
+
+import io
+from multiprocessing import Value
+import os
+import sys
+
+from abc import ABC, abstractmethod
+from collections import deque
+from dataclasses import dataclass, field
+import ctypes
+from ctypes.wintypes import (
+ _COORD,
+ WORD,
+ SMALL_RECT,
+ BOOL,
+ HANDLE,
+ CHAR,
+ DWORD,
+ WCHAR,
+ SHORT,
+)
+from ctypes import Structure, POINTER, Union
+from .console import Event, Console
+from .trace import trace
+from .utils import wlen
+
+try:
+ from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
+except:
+ # Keep MyPy happy off Windows
+ from ctypes import CDLL as WinDLL, cdll as windll
+
+ def GetLastError() -> int:
+ return 42
+
+ class WinError(OSError): # type: ignore[no-redef]
+ def __init__(self, err: int | None, descr: str | None = None) -> None:
+ self.err = err
+ self.descr = descr
+
+
+TYPE_CHECKING = False
+
+if TYPE_CHECKING:
+ from typing import IO
+
+VK_MAP: dict[int, str] = {
+ 0x23: "end", # VK_END
+ 0x24: "home", # VK_HOME
+ 0x25: "left", # VK_LEFT
+ 0x26: "up", # VK_UP
+ 0x27: "right", # VK_RIGHT
+ 0x28: "down", # VK_DOWN
+ 0x2E: "delete", # VK_DELETE
+ 0x70: "f1", # VK_F1
+ 0x71: "f2", # VK_F2
+ 0x72: "f3", # VK_F3
+ 0x73: "f4", # VK_F4
+ 0x74: "f5", # VK_F5
+ 0x75: "f6", # VK_F6
+ 0x76: "f7", # VK_F7
+ 0x77: "f8", # VK_F8
+ 0x78: "f9", # VK_F9
+ 0x79: "f10", # VK_F10
+ 0x7A: "f11", # VK_F11
+ 0x7B: "f12", # VK_F12
+ 0x7C: "f13", # VK_F13
+ 0x7D: "f14", # VK_F14
+ 0x7E: "f15", # VK_F15
+ 0x7F: "f16", # VK_F16
+ 0x79: "f17", # VK_F17
+ 0x80: "f18", # VK_F18
+ 0x81: "f19", # VK_F19
+ 0x82: "f20", # VK_F20
+}
+
+# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-…
+ERASE_IN_LINE = "\x1b[K"
+MOVE_LEFT = "\x1b[{}D"
+MOVE_RIGHT = "\x1b[{}C"
+MOVE_UP = "\x1b[{}A"
+MOVE_DOWN = "\x1b[{}B"
+CLEAR = "\x1b[H\x1b[J"
+
+
+class _error(Exception):
+ pass
+
+
+class WindowsConsole(Console):
+ def __init__(
+ self,
+ f_in: IO[bytes] | int = 0,
+ f_out: IO[bytes] | int = 1,
+ term: str = "",
+ encoding: str = "",
+ ):
+ super().__init__(f_in, f_out, term, encoding)
+
+ SetConsoleMode(
+ OutHandle,
+ ENABLE_WRAP_AT_EOL_OUTPUT
+ | ENABLE_PROCESSED_OUTPUT
+ | ENABLE_VIRTUAL_TERMINAL_PROCESSING,
+ )
+ self.screen: list[str] = []
+ self.width = 80
+ self.height = 25
+ self.__offset = 0
+ self.event_queue: deque[Event] = deque()
+ try:
+ self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
+ except ValueError:
+ # Console I/O is redirected, fallback...
+ self.out = None
+
+ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
+ """
+ Refresh the console screen.
+
+ Parameters:
+ - screen (list): List of strings representing the screen contents.
+ - c_xy (tuple): Cursor position (x, y) on the screen.
+ """
+ cx, cy = c_xy
+
+ while len(self.screen) < min(len(screen), self.height):
+ self._hide_cursor()
+ self._move_relative(0, len(self.screen) - 1)
+ self.__write("\n")
+ self.__posxy = 0, len(self.screen)
+ self.screen.append("")
+
+ px, py = self.__posxy
+ old_offset = offset = self.__offset
+ height = self.height
+
+ # we make sure the cursor is on the screen, and that we're
+ # using all of the screen if we can
+ if cy < offset:
+ offset = cy
+ elif cy >= offset + height:
+ offset = cy - height + 1
+ scroll_lines = offset - old_offset
+
+ # Scrolling the buffer as the current input is greater than the visible
+ # portion of the window. We need to scroll the visible portion and the
+ # entire history
+ self._scroll(scroll_lines, self._getscrollbacksize())
+ self.__posxy = self.__posxy[0], self.__posxy[1] + scroll_lines
+ self.__offset += scroll_lines
+
+ for i in range(scroll_lines):
+ self.screen.append("")
+ elif offset > 0 and len(screen) < offset + height:
+ offset = max(len(screen) - height, 0)
+ screen.append("")
+
+ oldscr = self.screen[old_offset : old_offset + height]
+ newscr = screen[offset : offset + height]
+
+ self.__offset = offset
+
+ self._hide_cursor()
+ for (
+ y,
+ oldline,
+ newline,
+ ) in zip(range(offset, offset + height), oldscr, newscr):
+ if oldline != newline:
+ self.__write_changed_line(y, oldline, newline, px)
+
+ y = len(newscr)
+ while y < len(oldscr):
+ self._move_relative(0, y)
+ self.__posxy = 0, y
+ self._erase_to_end()
+ y += 1
+
+ self._show_cursor()
+
+ self.screen = screen
+ self.move_cursor(cx, cy)
+
+ def __write_changed_line(
+ self, y: int, oldline: str, newline: str, px_coord: int
+ ) -> None:
+ # this is frustrating; there's no reason to test (say)
+ # self.dch1 inside the loop -- but alternative ways of
+ # structuring this function are equally painful (I'm trying to
+ # avoid writing code generators these days...)
+ minlen = min(wlen(oldline), wlen(newline))
+ x_pos = 0
+ x_coord = 0
+
+ px_pos = 0
+ j = 0
+ for c in oldline:
+ 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"
+ ):
+ x_coord += wlen(newline[x_pos])
+ x_pos += 1
+
+ self._hide_cursor()
+ self._move_relative(x_coord, y)
+ if wlen(oldline) > wlen(newline):
+ self._erase_to_end()
+
+ self.__write(newline[x_pos:])
+ if wlen(newline) == self.width:
+ # If we wrapped we want to start at the next line
+ self._move_relative(0, y + 1)
+ self.__posxy = 0, y + 1
+ else:
+ self.__posxy = wlen(newline), y
+
+ if "\x1b" in newline or y != self.__posxy[1]:
+ # ANSI escape characters are present, so we can't assume
+ # anything about the position of the cursor. Moving the cursor
+ # to the left margin should work to get to a known position.
+ self.move_cursor(0, y)
+
+ def _scroll(
+ self, top: int, bottom: int, left: int | None = None, right: int | None = None
+ ) -> None:
+ scroll_rect = SMALL_RECT()
+ scroll_rect.Top = SHORT(top)
+ scroll_rect.Bottom = SHORT(bottom)
+ scroll_rect.Left = SHORT(0 if left is None else left)
+ scroll_rect.Right = SHORT(
+ self.getheightwidth()[1] - 1 if right is None else right
+ )
+ destination_origin = _COORD()
+ fill_info = CHAR_INFO()
+ fill_info.UnicodeChar = " "
+
+ if not ScrollConsoleScreenBuffer(
+ OutHandle, scroll_rect, None, destination_origin, fill_info
+ ):
+ raise WinError(GetLastError())
+
+ def _hide_cursor(self):
+ self.__write("\x1b[?25l")
+
+ def _show_cursor(self):
+ self.__write("\x1b[?25h")
+
+ def _enable_blinking(self):
+ self.__write("\x1b[?12h")
+
+ def _disable_blinking(self):
+ self.__write("\x1b[?12l")
+
+ def __write(self, text: str) -> None:
+ if self.out is not None:
+ self.out.write(text.encode(self.encoding, "replace"))
+ self.out.flush()
+ else:
+ os.write(self.output_fd, text.encode(self.encoding, "replace"))
+
+ @property
+ def screen_xy(self) -> tuple[int, int]:
+ info = CONSOLE_SCREEN_BUFFER_INFO()
+ if not GetConsoleScreenBufferInfo(OutHandle, info):
+ raise WinError(GetLastError())
+ return info.dwCursorPosition.X, info.dwCursorPosition.Y
+
+ def _erase_to_end(self) -> None:
+ self.__write(ERASE_IN_LINE)
+
+ def prepare(self) -> None:
+ trace("prepare")
+ self.screen = []
+ self.height, self.width = self.getheightwidth()
+
+ self.__posxy = 0, 0
+ self.__gone_tall = 0
+ self.__offset = 0
+
+ def restore(self) -> None:
+ pass
+
+ def _move_relative(self, x: int, y: int) -> None:
+ """Moves relative to the current __posxy"""
+ dx = x - self.__posxy[0]
+ dy = y - self.__posxy[1]
+ if dx < 0:
+ self.__write(MOVE_LEFT.format(-dx))
+ elif dx > 0:
+ self.__write(MOVE_RIGHT.format(dx))
+
+ if dy < 0:
+ self.__write(MOVE_UP.format(-dy))
+ elif dy > 0:
+ self.__write(MOVE_DOWN.format(dy))
+
+ def move_cursor(self, x: int, y: int) -> None:
+ if x < 0 or y < 0:
+ raise ValueError(f"Bad cursor position {x}, {y}")
+
+ if y < self.__offset or y >= self.__offset + self.height:
+ self.event_queue.insert(0, Event("scroll", ""))
+ else:
+ self._move_relative(x, y)
+ self.__posxy = x, y
+
+ def set_cursor_vis(self, visible: bool) -> None:
+ if visible:
+ self._show_cursor()
+ else:
+ self._hide_cursor()
+
+ def getheightwidth(self) -> tuple[int, int]:
+ """Return (height, width) where height and width are the height
+ and width of the terminal window in characters."""
+ info = CONSOLE_SCREEN_BUFFER_INFO()
+ if not GetConsoleScreenBufferInfo(OutHandle, info):
+ raise WinError(GetLastError())
+ return (
+ info.srWindow.Bottom - info.srWindow.Top + 1,
+ info.srWindow.Right - info.srWindow.Left + 1,
+ )
+
+ def _getscrollbacksize(self) -> int:
+ info = CONSOLE_SCREEN_BUFFER_INFO()
+ if not GetConsoleScreenBufferInfo(OutHandle, info):
+ raise WinError(GetLastError())
+
+ return info.srWindow.Bottom # type: ignore[no-any-return]
+
+ def _read_input(self) -> INPUT_RECORD | None:
+ rec = INPUT_RECORD()
+ read = DWORD()
+ if not ReadConsoleInput(InHandle, rec, 1, read):
+ raise WinError(GetLastError())
+
+ if read.value == 0:
+ return None
+
+ return rec
+
+ def get_event(self, block: bool = True) -> Event | None:
+ """Return an Event instance. Returns None if |block| is false
+ and there is no event pending, otherwise waits for the
+ completion of an event."""
+ if self.event_queue:
+ return self.event_queue.pop()
+
+ while True:
+ rec = self._read_input()
+ if rec is None:
+ if block:
+ continue
+ return None
+
+ if rec.EventType == WINDOW_BUFFER_SIZE_EVENT:
+ return Event("resize", "")
+
+ if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown:
+ # Only process keys and keydown events
+ if block:
+ continue
+ return None
+
+ key = rec.Event.KeyEvent.uChar.UnicodeChar
+
+ if rec.Event.KeyEvent.uChar.UnicodeChar == "\r":
+ # Make enter make unix-like
+ return Event(evt="key", data="\n", raw=b"\n")
+ elif rec.Event.KeyEvent.wVirtualKeyCode == 8:
+ # Turn backspace directly into the command
+ return Event(
+ evt="key",
+ data="backspace",
+ raw=rec.Event.KeyEvent.uChar.UnicodeChar,
+ )
+ elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00":
+ # Handle special keys like arrow keys and translate them into the appropriate command
+ code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode)
+ if code:
+ return Event(
+ evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar
+ )
+ if block:
+ continue
+
+ return None
+
+ return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
+
+ def push_char(self, char: int | bytes) -> None:
+ """
+ Push a character to the console event queue.
+ """
+ raise NotImplementedError("push_char not supported on Windows")
+
+ def beep(self) -> None:
+ self.__write("\x07")
+
+ def clear(self) -> None:
+ """Wipe the screen"""
+ self.__write(CLEAR)
+ self.__posxy = 0, 0
+ self.screen = [""]
+
+ def finish(self) -> None:
+ """Move the cursor to the end of the display and otherwise get
+ ready for end. XXX could be merged with restore? Hmm."""
+ y = len(self.screen) - 1
+ while y >= 0 and not self.screen[y]:
+ y -= 1
+ self._move_relative(0, min(y, self.height + self.__offset - 1))
+ self.__write("\r\n")
+
+ def flushoutput(self) -> None:
+ """Flush all output to the screen (assuming there's some
+ buffering going on somewhere).
+
+ All output on Windows is unbuffered so this is a nop"""
+ pass
+
+ def forgetinput(self) -> None:
+ """Forget all pending, but not yet processed input."""
+ while self._read_input() is not None:
+ pass
+
+ def getpending(self) -> Event:
+ """Return the characters that have been typed but not yet
+ processed."""
+ return Event("key", "", b"")
+
+ def wait(self) -> None:
+ """Wait for an event."""
+ raise NotImplementedError("No wait support")
+
+ def repaint(self) -> None:
+ raise NotImplementedError("No repaint support")
+
+
+# Windows interop
+class CONSOLE_SCREEN_BUFFER_INFO(Structure):
+ _fields_ = [
+ ("dwSize", _COORD),
+ ("dwCursorPosition", _COORD),
+ ("wAttributes", WORD),
+ ("srWindow", SMALL_RECT),
+ ("dwMaximumWindowSize", _COORD),
+ ]
+
+
+class CONSOLE_CURSOR_INFO(Structure):
+ _fields_ = [
+ ("dwSize", DWORD),
+ ("bVisible", BOOL),
+ ]
+
+
+class CHAR_INFO(Structure):
+ _fields_ = [
+ ("UnicodeChar", WCHAR),
+ ("Attributes", WORD),
+ ]
+
+
+class Char(Union):
+ _fields_ = [
+ ("UnicodeChar", WCHAR),
+ ("Char", CHAR),
+ ]
+
+
+class KeyEvent(ctypes.Structure):
+ _fields_ = [
+ ("bKeyDown", BOOL),
+ ("wRepeatCount", WORD),
+ ("wVirtualKeyCode", WORD),
+ ("wVirtualScanCode", WORD),
+ ("uChar", Char),
+ ("dwControlKeyState", DWORD),
+ ]
+
+
+class WindowsBufferSizeEvent(ctypes.Structure):
+ _fields_ = [("dwSize", _COORD)]
+
+
+class ConsoleEvent(ctypes.Union):
+ _fields_ = [
+ ("KeyEvent", KeyEvent),
+ ("WindowsBufferSizeEvent", WindowsBufferSizeEvent),
+ ]
+
+
+class INPUT_RECORD(Structure):
+ _fields_ = [("EventType", WORD), ("Event", ConsoleEvent)]
+
+
+KEY_EVENT = 0x01
+FOCUS_EVENT = 0x10
+MENU_EVENT = 0x08
+MOUSE_EVENT = 0x02
+WINDOW_BUFFER_SIZE_EVENT = 0x04
+
+ENABLE_PROCESSED_OUTPUT = 0x01
+ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
+ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
+
+STD_INPUT_HANDLE = -10
+STD_OUTPUT_HANDLE = -11
+
+if sys.platform == "win32":
+ _KERNEL32 = WinDLL("kernel32", use_last_error=True)
+
+ GetStdHandle = windll.kernel32.GetStdHandle
+ GetStdHandle.argtypes = [DWORD]
+ GetStdHandle.restype = HANDLE
+
+ GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo
+ GetConsoleScreenBufferInfo.argtypes = [
+ HANDLE,
+ ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
+ ]
+ GetConsoleScreenBufferInfo.restype = BOOL
+
+ ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW
+ ScrollConsoleScreenBuffer.argtypes = [
+ HANDLE,
+ POINTER(SMALL_RECT),
+ POINTER(SMALL_RECT),
+ _COORD,
+ POINTER(CHAR_INFO),
+ ]
+ ScrollConsoleScreenBuffer.restype = BOOL
+
+ SetConsoleMode = _KERNEL32.SetConsoleMode
+ SetConsoleMode.argtypes = [HANDLE, DWORD]
+ SetConsoleMode.restype = BOOL
+
+ ReadConsoleInput = _KERNEL32.ReadConsoleInputW
+ ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)]
+ ReadConsoleInput.restype = BOOL
+
+ OutHandle = GetStdHandle(STD_OUTPUT_HANDLE)
+ InHandle = GetStdHandle(STD_INPUT_HANDLE)
+else:
+
+ def _win_only(*args, **kwargs):
+ raise NotImplementedError("Windows only")
+
+ GetStdHandle = _win_only
+ GetConsoleScreenBufferInfo = _win_only
+ ScrollConsoleScreenBuffer = _win_only
+ SetConsoleMode = _win_only
+ ReadConsoleInput = _win_only
+ OutHandle = 0
+ InHandle = 0
diff --git a/Lib/test/test_pyrepl/__init__.py b/Lib/test/test_pyrepl/__init__.py
index fa38b86b847dd9..8359d9844623c2 100644
--- a/Lib/test/test_pyrepl/__init__.py
+++ b/Lib/test/test_pyrepl/__init__.py
@@ -1,12 +1,14 @@
import os
+import sys
from test.support import requires, load_package_tests
from test.support.import_helper import import_module
-# Optionally test pyrepl. This currently requires that the
-# 'curses' resource be given on the regrtest command line using the -u
-# option. Additionally, we need to attempt to import curses and readline.
-requires("curses")
-curses = import_module("curses")
+if sys.platform != "win32":
+ # On non-Windows platforms, testing pyrepl currently requires that the
+ # 'curses' resource be given on the regrtest command line using the -u
+ # option. Additionally, we need to attempt to import curses and readline.
+ requires("curses")
+ curses = import_module("curses")
def load_tests(*args):
diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py
index 75539049d43c2a..d2f5429aea7a11 100644
--- a/Lib/test/test_pyrepl/support.py
+++ b/Lib/test/test_pyrepl/support.py
@@ -55,7 +55,7 @@ def get_prompt(lineno, cursor_on_line) -> str:
return reader
-def prepare_console(events: Iterable[Event], **kwargs):
+def prepare_console(events: Iterable[Event], **kwargs) -> MagicMock | Console:
console = MagicMock()
console.get_event.side_effect = events
console.height = 100
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py
index bdcabf9be05b9e..aa2722095794c9 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -508,14 +508,15 @@ def prepare_reader(self, events, namespace):
reader = ReadlineAlikeReader(console=console, config=config)
return reader
+ @patch("rlcompleter._readline_available", False)
def test_simple_completion(self):
- events = code_to_events("os.geten\t\n")
+ events = code_to_events("os.getpid\t\n")
namespace = {"os": os}
reader = self.prepare_reader(events, namespace)
output = multiline_input(reader, namespace)
- self.assertEqual(output, "os.getenv")
+ self.assertEqual(output, "os.getpid()")
def test_completion_with_many_options(self):
# Test with something that initially displays many options
diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py
index e1faa00caafc27..d0b98f17ade094 100644
--- a/Lib/test/test_pyrepl/test_unix_console.py
+++ b/Lib/test/test_pyrepl/test_unix_console.py
@@ -1,12 +1,16 @@
import itertools
+import sys
+import unittest
from functools import partial
from unittest import TestCase
from unittest.mock import MagicMock, call, patch, ANY
from .support import handle_all_events, code_to_events
-from _pyrepl.console import Event
-from _pyrepl.unix_console import UnixConsole
-
+try:
+ from _pyrepl.console import Event
+ from _pyrepl.unix_console import UnixConsole
+except ImportError:
+ pass
def unix_console(events, **kwargs):
console = UnixConsole()
@@ -67,6 +71,7 @@ def unix_console(events, **kwargs):
}
+(a)unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows")
@patch("_pyrepl.curses.tigetstr", lambda s: TERM_CAPABILITIES.get(s))
@patch(
"_pyrepl.curses.tparm",
diff --git a/Lib/test/test_pyrepl/test_unix_eventqueue.py b/Lib/test/test_pyrepl/test_unix_eventqueue.py
index c06536b4a86a04..301f79927a741f 100644
--- a/Lib/test/test_pyrepl/test_unix_eventqueue.py
+++ b/Lib/test/test_pyrepl/test_unix_eventqueue.py
@@ -1,11 +1,15 @@
import tempfile
import unittest
+import sys
from unittest.mock import patch
-from _pyrepl.console import Event
-from _pyrepl.unix_eventqueue import EventQueue
-
+try:
+ from _pyrepl.console import Event
+ from _pyrepl.unix_eventqueue import EventQueue
+except ImportError:
+ pass
+(a)unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows")
@patch("_pyrepl.curses.tigetstr", lambda x: b"")
class TestUnixEventQueue(unittest.TestCase):
def setUp(self):
diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py
new file mode 100644
index 00000000000000..e87dfe99b1a17d
--- /dev/null
+++ b/Lib/test/test_pyrepl/test_windows_console.py
@@ -0,0 +1,331 @@
+import itertools
+import sys
+import unittest
+from _pyrepl.console import Event, Console
+from _pyrepl.windows_console import (
+ MOVE_LEFT,
+ MOVE_RIGHT,
+ MOVE_UP,
+ MOVE_DOWN,
+ ERASE_IN_LINE,
+)
+from functools import partial
+from typing import Iterable
+from unittest import TestCase, main
+from unittest.mock import MagicMock, call, patch, ANY
+
+from .support import handle_all_events, code_to_events
+
+try:
+ from _pyrepl.console import Event
+ from _pyrepl.windows_console import WindowsConsole
+except ImportError:
+ pass
+
+
+(a)unittest.skipIf(sys.platform != "win32", "Test class specifically for Windows")
+class WindowsConsoleTests(TestCase):
+ def console(self, events, **kwargs) -> Console:
+ console = WindowsConsole()
+ console.get_event = MagicMock(side_effect=events)
+ console._scroll = MagicMock()
+ console._hide_cursor = MagicMock()
+ console._show_cursor = MagicMock()
+ console._getscrollbacksize = MagicMock(42)
+ console.out = MagicMock()
+
+ height = kwargs.get("height", 25)
+ width = kwargs.get("width", 80)
+ console.getheightwidth = MagicMock(side_effect=lambda: (height, width))
+
+ console.prepare()
+ for key, val in kwargs.items():
+ setattr(console, key, val)
+ return console
+
+ def handle_events(self, events: Iterable[Event], **kwargs):
+ return handle_all_events(events, partial(self.console, **kwargs))
+
+ def handle_events_narrow(self, events):
+ return self.handle_events(events, width=5)
+
+ def handle_events_short(self, events):
+ return self.handle_events(events, height=1)
+
+ def handle_events_height_3(self, events):
+ return self.handle_events(events, height=3)
+
+ def test_simple_addition(self):
+ code = "12+34"
+ events = code_to_events(code)
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(b"1")
+ con.out.write.assert_any_call(b"2")
+ con.out.write.assert_any_call(b"+")
+ con.out.write.assert_any_call(b"3")
+ con.out.write.assert_any_call(b"4")
+ con.restore()
+
+ def test_wrap(self):
+ code = "12+34"
+ events = code_to_events(code)
+ _, con = self.handle_events_narrow(events)
+ con.out.write.assert_any_call(b"1")
+ con.out.write.assert_any_call(b"2")
+ con.out.write.assert_any_call(b"+")
+ con.out.write.assert_any_call(b"3")
+ con.out.write.assert_any_call(b"\\")
+ con.out.write.assert_any_call(b"\n")
+ con.out.write.assert_any_call(b"4")
+ con.restore()
+
+ def test_resize_wider(self):
+ code = "1234567890"
+ events = code_to_events(code)
+ reader, console = self.handle_events_narrow(events)
+
+ console.height = 20
+ console.width = 80
+ console.getheightwidth = MagicMock(lambda _: (20, 80))
+
+ def same_reader(_):
+ return reader
+
+ def same_console(events):
+ console.get_event = MagicMock(side_effect=events)
+ return console
+
+ _, con = handle_all_events(
+ [Event(evt="resize", data=None)],
+ prepare_reader=same_reader,
+ prepare_console=same_console,
+ )
+
+ con.out.write.assert_any_call(self.move_right(2))
+ con.out.write.assert_any_call(self.move_up(2))
+ con.out.write.assert_any_call(b"567890")
+
+ con.restore()
+
+ def test_resize_narrower(self):
+ code = "1234567890"
+ events = code_to_events(code)
+ reader, console = self.handle_events(events)
+
+ console.height = 20
+ console.width = 4
+ console.getheightwidth = MagicMock(lambda _: (20, 4))
+
+ def same_reader(_):
+ return reader
+
+ def same_console(events):
+ console.get_event = MagicMock(side_effect=events)
+ return console
+
+ _, con = handle_all_events(
+ [Event(evt="resize", data=None)],
+ prepare_reader=same_reader,
+ prepare_console=same_console,
+ )
+
+ con.out.write.assert_any_call(b"456\\")
+ con.out.write.assert_any_call(b"789\\")
+
+ con.restore()
+
+ def test_cursor_left(self):
+ code = "1"
+ events = itertools.chain(
+ code_to_events(code),
+ [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
+ )
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(self.move_left())
+ con.restore()
+
+ def test_cursor_left_right(self):
+ code = "1"
+ events = itertools.chain(
+ code_to_events(code),
+ [
+ Event(evt="key", data="left", raw=bytearray(b"\x1bOD")),
+ Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
+ ],
+ )
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(self.move_left())
+ con.out.write.assert_any_call(self.move_right())
+ con.restore()
+
+ def test_cursor_up(self):
+ code = "1\n2+3"
+ events = itertools.chain(
+ code_to_events(code),
+ [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))],
+ )
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(self.move_up())
+ con.restore()
+
+ def test_cursor_up_down(self):
+ code = "1\n2+3"
+ events = itertools.chain(
+ code_to_events(code),
+ [
+ Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+ Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
+ ],
+ )
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(self.move_up())
+ con.out.write.assert_any_call(self.move_down())
+ con.restore()
+
+ def test_cursor_back_write(self):
+ events = itertools.chain(
+ code_to_events("1"),
+ [Event(evt="key", data="left", raw=bytearray(b"\x1bOD"))],
+ code_to_events("2"),
+ )
+ _, con = self.handle_events(events)
+ con.out.write.assert_any_call(b"1")
+ con.out.write.assert_any_call(self.move_left())
+ con.out.write.assert_any_call(b"21")
+ con.restore()
+
+ def test_multiline_function_move_up_short_terminal(self):
+ # fmt: off
+ code = (
+ "def f():\n"
+ " foo"
+ )
+ # fmt: on
+
+ events = itertools.chain(
+ code_to_events(code),
+ [
+ Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+ Event(evt="scroll", data=None),
+ ],
+ )
+ _, con = self.handle_events_short(events)
+ con.out.write.assert_any_call(self.move_left(5))
+ con.out.write.assert_any_call(self.move_up())
+ con.restore()
+
+ def test_multiline_function_move_up_down_short_terminal(self):
+ # fmt: off
+ code = (
+ "def f():\n"
+ " foo"
+ )
+ # fmt: on
+
+ events = itertools.chain(
+ code_to_events(code),
+ [
+ Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
+ Event(evt="scroll", data=None),
+ Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
+ Event(evt="scroll", data=None),
+ ],
+ )
+ _, con = self.handle_events_short(events)
+ con.out.write.assert_any_call(self.move_left(8))
+ con.out.write.assert_any_call(self.erase_in_line())
+ con.restore()
+
+ def test_resize_bigger_on_multiline_function(self):
+ # fmt: off
+ code = (
+ "def f():\n"
+ " foo"
+ )
+ # fmt: on
+
+ events = itertools.chain(code_to_events(code))
+ reader, console = self.handle_events_short(events)
+
+ console.height = 2
+ console.getheightwidth = MagicMock(lambda _: (2, 80))
+
+ def same_reader(_):
+ return reader
+
+ def same_console(events):
+ console.get_event = MagicMock(side_effect=events)
+ return console
+
+ _, con = handle_all_events(
+ [Event(evt="resize", data=None)],
+ prepare_reader=same_reader,
+ prepare_console=same_console,
+ )
+ con.out.write.assert_has_calls(
+ [
+ call(self.move_left(5)),
+ call(self.move_up()),
+ call(b"def f():"),
+ call(self.move_left(3)),
+ call(self.move_down()),
+ ]
+ )
+ console.restore()
+ con.restore()
+
+ def test_resize_smaller_on_multiline_function(self):
+ # fmt: off
+ code = (
+ "def f():\n"
+ " foo"
+ )
+ # fmt: on
+
+ events = itertools.chain(code_to_events(code))
+ reader, console = self.handle_events_height_3(events)
+
+ console.height = 1
+ console.getheightwidth = MagicMock(lambda _: (1, 80))
+
+ def same_reader(_):
+ return reader
+
+ def same_console(events):
+ console.get_event = MagicMock(side_effect=events)
+ return console
+
+ _, con = handle_all_events(
+ [Event(evt="resize", data=None)],
+ prepare_reader=same_reader,
+ prepare_console=same_console,
+ )
+ con.out.write.assert_has_calls(
+ [
+ call(self.move_left(5)),
+ call(self.move_up()),
+ call(self.erase_in_line()),
+ call(b" foo"),
+ ]
+ )
+ console.restore()
+ con.restore()
+
+ def move_up(self, lines=1):
+ return MOVE_UP.format(lines).encode("utf8")
+
+ def move_down(self, lines=1):
+ return MOVE_DOWN.format(lines).encode("utf8")
+
+ def move_left(self, cols=1):
+ return MOVE_LEFT.format(cols).encode("utf8")
+
+ def move_right(self, cols=1):
+ return MOVE_RIGHT.format(cols).encode("utf8")
+
+ def erase_in_line(self):
+ return ERASE_IN_LINE.encode("utf8")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst b/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst
new file mode 100644
index 00000000000000..f3918ed633d78c
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2024-05-25-18-43-10.gh-issue-111201.SLPJIx.rst
@@ -0,0 +1 @@
+Add support for new pyrepl on Windows
1
0