Skip to content

Commit 4310f6b

Browse files
Merge branch 'main' into curses-wide-by-capability
2 parents 9121250 + 0ca0107 commit 4310f6b

12 files changed

Lines changed: 139 additions & 25 deletions

File tree

Doc/library/msvcrt.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,23 @@ Console I/O
135135

136136
.. function:: putch(char)
137137

138-
Print the byte string *char* to the console without buffering.
138+
Print the byte string *char* to the console without buffering. Raises
139+
:exc:`OSError` on failure, for example when the process has no console
140+
attached.
141+
142+
.. versionchanged:: next
143+
Failures are now reported by raising :exc:`OSError` instead of being
144+
silently ignored.
139145

140146

141147
.. function:: putwch(unicode_char)
142148

143149
Wide char variant of :func:`putch`, accepting a Unicode value.
144150

151+
.. versionchanged:: next
152+
Failures are now reported by raising :exc:`OSError` instead of being
153+
silently ignored.
154+
145155

146156
.. function:: ungetch(char)
147157

Lib/test/test_capi/test_mem.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ def check(self, code):
2424
out = assert_python_failure(
2525
'-c', code,
2626
PYTHONMALLOC=self.PYTHONMALLOC,
27-
# FreeBSD: instruct jemalloc to not fill freed() memory
28-
# with junk byte 0x5a, see JEMALLOC(3)
27+
# Instruct the system allocator to not fill freed() memory
28+
# with junk bytes:
29+
# FreeBSD: jemalloc, see JEMALLOC(3).
2930
MALLOC_CONF="junk:false",
31+
# OpenBSD: see MALLOC.CONF(5).
32+
MALLOC_OPTIONS="j",
3033
)
3134
stderr = out.err
3235
return stderr.decode('ascii', 'replace')
@@ -102,7 +105,9 @@ def check_pyobject_is_freed(self, func_name):
102105
assert_python_ok(
103106
'-c', code,
104107
PYTHONMALLOC=self.PYTHONMALLOC,
108+
# See the comment in check() above.
105109
MALLOC_CONF="junk:false",
110+
MALLOC_OPTIONS="j",
106111
)
107112

108113
def test_pyobject_null_is_freed(self):

Lib/test/test_concurrent_futures/test_init.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import logging
3+
import multiprocessing
34
import queue
45
import time
56
import unittest
@@ -147,8 +148,8 @@ def test_spawn(self):
147148
self._test(ProcessPoolSpawnFailingInitializerTest)
148149

149150
@support.skip_if_sanitizer("TSAN doesn't support threads after fork", thread=True)
150-
@unittest.skipIf(sys.platform == "cygwin",
151-
"Forkserver is not available on Cygwin")
151+
@unittest.skipUnless("forkserver" in multiprocessing.get_all_start_methods(),
152+
"forkserver start method is not available")
152153
def test_forkserver(self):
153154
self._test(ProcessPoolForkserverFailingInitializerTest)
154155

Lib/test/test_curses.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1997,12 +1997,15 @@ def test_dynamic_color_pairs(self):
19971997
pair = curses.alloc_pair(fg, bg)
19981998
self.assertGreater(pair, 0)
19991999
self.assertEqual(curses.pair_content(pair), (fg, bg))
2000-
# The same combination of colors reuses the same pair.
2001-
self.assertEqual(curses.alloc_pair(fg, bg), pair)
2002-
self.assertEqual(curses.find_pair(fg, bg), pair)
2003-
# Once freed, the pair is no longer found.
2004-
self.assertIsNone(curses.free_pair(pair))
2005-
self.assertEqual(curses.find_pair(fg, bg), -1)
2000+
if getattr(curses, 'ncurses_version', (6, 3)) >= (6, 3):
2001+
# The same combination of colors reuses the same pair.
2002+
self.assertEqual(curses.alloc_pair(fg, bg), pair)
2003+
self.assertEqual(curses.find_pair(fg, bg), pair)
2004+
# Once freed, the pair is no longer found.
2005+
self.assertIsNone(curses.free_pair(pair))
2006+
self.assertEqual(curses.find_pair(fg, bg), -1)
2007+
else:
2008+
self.assertIsNone(curses.free_pair(pair))
20062009

20072010
# Error paths.
20082011
for color in self.bad_colors2():

Lib/test/test_msvcrt.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ def test_get_osfhandle(self):
6161
c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded char for this CJK char
6262

6363

64+
def has_console():
65+
# A process created without a console (for example by pythonw.exe, or with
66+
# the DETACHED_PROCESS creation flag) cannot write to the console.
67+
try:
68+
with open('CONOUT$', 'w'):
69+
return True
70+
except OSError:
71+
return False
72+
73+
74+
requires_console = unittest.skipUnless(has_console(), 'requires a console')
75+
76+
6477
class TestConsoleIO(unittest.TestCase):
6578
# CREATE_NEW_CONSOLE creates a "popup" window.
6679
@requires_resource('gui')
@@ -106,12 +119,36 @@ def test_getche(self):
106119
def test_getwche(self):
107120
self.check_getwch('getwche')
108121

122+
@requires_console
109123
def test_putch(self):
110124
msvcrt.putch(b'c')
111125

126+
@requires_console
112127
def test_putwch(self):
113128
msvcrt.putwch(c)
114129

130+
def test_putch_without_console(self):
131+
# gh-69573: putch() and putwch() must report the error instead of
132+
# silently ignoring it when the process has no console attached.
133+
code = dedent('''
134+
import msvcrt
135+
import sys
136+
137+
for name, arg in (('putch', b'c'), ('putwch', 'c')):
138+
func = getattr(msvcrt, name)
139+
try:
140+
func(arg)
141+
except OSError:
142+
pass
143+
else:
144+
sys.exit(f'msvcrt.{name}() did not raise OSError')
145+
''')
146+
# DETACHED_PROCESS: the child process is created without a console.
147+
proc = subprocess.run([sys.executable, '-c', code],
148+
creationflags=subprocess.DETACHED_PROCESS,
149+
capture_output=True, text=True)
150+
self.assertEqual(proc.returncode, 0, proc.stderr)
151+
115152

116153
class TestOther(unittest.TestCase):
117154
def test_heap_min(self):

Lib/test/test_profiling/test_tracing_profiler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Test suite for the cProfile module."""
2+
import multiprocessing
23
import sys
34
import unittest
45

@@ -220,8 +221,8 @@ def test_process_spawn_pickle(self):
220221
# gh-140729: test use Process in cProfile.
221222
self._test_process_run_pickle('spawn')
222223

223-
@unittest.skipIf(sys.platform == 'win32',
224-
"No 'forkserver' start method on Windows")
224+
@unittest.skipUnless("forkserver" in multiprocessing.get_all_start_methods(),
225+
"forkserver start method is not available")
225226
def test_process_forkserver_pickle(self):
226227
# gh-140729: test use Process in cProfile.
227228
self._test_process_run_pickle('forkserver')

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import tempfile
1414
from functools import partial
1515
from pkgutil import ModuleInfo
16-
from unittest import TestCase, skipUnless, skipIf, SkipTest
16+
from unittest import TestCase, skipUnless, SkipTest
1717
from unittest.mock import Mock, patch
1818
import warnings
1919
from test.support import (
@@ -37,6 +37,7 @@
3737
code_to_events,
3838
)
3939
from _colorize import ANSIColors, get_theme
40+
from _pyrepl import terminfo
4041
from _pyrepl.console import Event
4142
from _pyrepl.completing_reader import stripcolor
4243
from _pyrepl._module_completer import (
@@ -1998,8 +1999,20 @@ def test_dumb_terminal_exits_cleanly(self):
19981999
self.assertNotIn("Traceback", output)
19992000

20002001

2002+
def supports_pyrepl():
2003+
# pyrepl falls back to the basic REPL if the terminal lacks any of the
2004+
# capabilities which UnixConsole requires. This covers an unset or
2005+
# "dumb" TERM as well, they are resolved to the "dumb" capabilities.
2006+
try:
2007+
info = terminfo.TermInfo(None)
2008+
except Exception:
2009+
return False
2010+
return all(info.get(cap) is not None
2011+
for cap in ("bel", "clear", "cup", "el"))
2012+
2013+
20012014
@skipUnless(pty, "requires pty")
2002-
@skipIf((os.environ.get("TERM") or "dumb") == "dumb", "can't use pyrepl in dumb terminal")
2015+
@skipUnless(supports_pyrepl(), "can't use pyrepl in this terminal")
20032016
class TestMain(ReplTestCase):
20042017
def setUp(self):
20052018
# Cleanup from PYTHON* variables to isolate from local
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Slightly speed up deallocation of objects that are not tracked by the garbage
2+
collector, such as :class:`int`, :class:`float` and :class:`str`.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of
2+
the underlying ``_putch()`` and ``_putwch()`` C functions and raise
3+
:exc:`OSError` on failure, for example when the process has no console
4+
attached, instead of silently ignoring the error.

Modules/_cursesmodule.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,9 @@ curses_getcchar(const cchar_t *wcval, wchar_t *wstr, attr_t *attrs, int *pair)
763763
/* getcchar() is not guaranteed to write the text of an empty cell, so make
764764
the output an empty string by default. */
765765
wstr[0] = L'\0';
766-
#if _NCURSES_EXTENDED_COLOR_FUNCS
766+
/* getcchar()'s opts slot returns the extended color pair, but ncurses
767+
returned ERR for a non-NULL opts until 6.3 (patch 20210116). */
768+
#if _NCURSES_EXTENDED_COLOR_FUNCS && NCURSES_VERSION_PATCH+0 >= 20210116
767769
int rtn = getcchar(wcval, wstr, attrs, &spair, pair);
768770
#else
769771
int rtn = getcchar(wcval, wstr, attrs, &spair, NULL);

0 commit comments

Comments
 (0)