@@ -61,6 +61,19 @@ def test_get_osfhandle(self):
6161c_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+
6477class 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
116153class TestOther (unittest .TestCase ):
117154 def test_heap_min (self ):
0 commit comments