Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Doc/library/msvcrt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,23 @@ Console I/O

.. function:: putch(char)

Print the byte string *char* to the console without buffering.
Print the byte string *char* to the console without buffering. Raises
:exc:`OSError` on failure, for example when the process has no console
attached.

.. versionchanged:: next
Failures are now reported by raising :exc:`OSError` instead of being
silently ignored.


.. function:: putwch(unicode_char)

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

.. versionchanged:: next
Failures are now reported by raising :exc:`OSError` instead of being
silently ignored.


.. function:: ungetch(char)

Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_msvcrt.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ def test_get_osfhandle(self):
c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded char for this CJK char


def has_console():
# A process created without a console (for example by pythonw.exe, or with
# the DETACHED_PROCESS creation flag) cannot write to the console.
try:
with open('CONOUT$', 'w'):
return True
except OSError:
return False


requires_console = unittest.skipUnless(has_console(), 'requires a console')


class TestConsoleIO(unittest.TestCase):
# CREATE_NEW_CONSOLE creates a "popup" window.
@requires_resource('gui')
Expand Down Expand Up @@ -106,12 +119,36 @@ def test_getche(self):
def test_getwche(self):
self.check_getwch('getwche')

@requires_console
def test_putch(self):
msvcrt.putch(b'c')

@requires_console
def test_putwch(self):
msvcrt.putwch(c)

def test_putch_without_console(self):
# gh-69573: putch() and putwch() must report the error instead of
# silently ignoring it when the process has no console attached.
code = dedent('''
import msvcrt
import sys

for name, arg in (('putch', b'c'), ('putwch', 'c')):
func = getattr(msvcrt, name)
try:
func(arg)
except OSError:
pass
else:
sys.exit(f'msvcrt.{name}() did not raise OSError')
''')
# DETACHED_PROCESS: the child process is created without a console.
proc = subprocess.run([sys.executable, '-c', code],
creationflags=subprocess.DETACHED_PROCESS,
capture_output=True, text=True)
self.assertEqual(proc.returncode, 0, proc.stderr)


class TestOther(unittest.TestCase):
def test_heap_min(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of
the underlying ``_putch()`` and ``_putwch()`` C functions and raise
:exc:`OSError` on failure, for example when the process has no console
attached, instead of silently ignoring the error.
35 changes: 32 additions & 3 deletions PC/msvcrtmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,22 @@ msvcrt_getwche_impl(PyObject *module)

#endif /* MS_WINDOWS_DESKTOP */

/* Raise an OSError for a failed _putch()/_putwch() call.

These functions fail, for example, when the process has no console
attached, but the CRT reports the failure without setting errno (and
without setting the Windows last error either), so fall back to a
generic error message in that case. */
static PyObject *
set_console_write_error(void)
{
if (errno != 0) {
return PyErr_SetFromErrno(PyExc_OSError);
}
PyErr_SetString(PyExc_OSError, "write to console failed");
return NULL;
}

/*[clinic input]
msvcrt.putch

Expand All @@ -330,9 +346,16 @@ static PyObject *
msvcrt_putch_impl(PyObject *module, char char_value)
/*[clinic end generated code: output=92ec9b81012d8f60 input=ec078dd10cb054d6]*/
{
int res;

_Py_BEGIN_SUPPRESS_IPH
_putch(char_value);
errno = 0;
res = _putch(char_value);
_Py_END_SUPPRESS_IPH

if (res == EOF) {
return set_console_write_error();
}
Py_RETURN_NONE;
}

Expand All @@ -351,11 +374,17 @@ static PyObject *
msvcrt_putwch_impl(PyObject *module, int unicode_char)
/*[clinic end generated code: output=a3bd1a8951d28eee input=996ccd0bbcbac4c3]*/
{
wint_t res;

_Py_BEGIN_SUPPRESS_IPH
_putwch(unicode_char);
errno = 0;
res = _putwch(unicode_char);
_Py_END_SUPPRESS_IPH
Py_RETURN_NONE;

if (res == WEOF) {
return set_console_write_error();
}
Py_RETURN_NONE;
}

#endif /* MS_WINDOWS_DESKTOP */
Expand Down
Loading