Skip to content

Commit d9b7390

Browse files
[3.13] gh-87587: Fix os.device_encoding() for any console on Windows (GH-155410) (GH-156597)
It was hard coded to map file descriptors 0, 1 and 2 to the console code page, but a console can be opened as any file descriptor, and other character devices, like NUL, are not consoles. The file handle is now queried. The UTF-8 code page is also now reported as "utf-8" instead of "cp65001". (cherry picked from commit b4aea41)
1 parent 34573c1 commit d9b7390

3 files changed

Lines changed: 85 additions & 16 deletions

File tree

Lib/test/test_os.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3408,6 +3408,45 @@ def test_device_encoding(self):
34083408
self.assertTrue(codecs.lookup(encoding))
34093409

34103410

3411+
@unittest.skipUnless(sys.platform == "win32", "Win32 specific tests")
3412+
class Win32DeviceEncodingTests(unittest.TestCase):
3413+
# gh-87587: any console file descriptor is supported, not only 0, 1 and 2,
3414+
# and other character devices are not consoles.
3415+
3416+
@staticmethod
3417+
def expected_encoding(cp):
3418+
return 'utf-8' if cp == 65001 else 'cp%d' % cp
3419+
3420+
def test_console(self):
3421+
import ctypes
3422+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
3423+
try:
3424+
fin = open('CONIN$')
3425+
except OSError:
3426+
self.skipTest('no console')
3427+
with fin:
3428+
self.assertEqual(os.device_encoding(fin.fileno()),
3429+
self.expected_encoding(kernel32.GetConsoleCP()))
3430+
with open('CONOUT$', 'w') as fout:
3431+
self.assertEqual(
3432+
os.device_encoding(fout.fileno()),
3433+
self.expected_encoding(kernel32.GetConsoleOutputCP()))
3434+
3435+
def test_not_a_console(self):
3436+
with open('NUL', 'w') as f:
3437+
self.assertTrue(os.isatty(f.fileno()))
3438+
self.assertIsNone(os.device_encoding(f.fileno()))
3439+
# Not a console even if it is a standard file descriptor.
3440+
saved = os.dup(1)
3441+
try:
3442+
os.dup2(f.fileno(), 1)
3443+
encoding = os.device_encoding(1)
3444+
finally:
3445+
os.dup2(saved, 1)
3446+
os.close(saved)
3447+
self.assertIsNone(encoding)
3448+
3449+
34113450
@support.requires_subprocess()
34123451
class PidTests(unittest.TestCase):
34133452
@unittest.skipUnless(hasattr(os, 'getppid'), "test needs os.getppid")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`os.device_encoding` on Windows now returns the code page of any
2+
console file descriptor, not only 0, 1 and 2, and returns ``None`` for other
3+
character devices like ``NUL``. The UTF-8 code page is now reported as
4+
``"utf-8"`` instead of ``"cp65001"``.

Python/fileutils.c

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,41 +77,67 @@ get_surrogateescape(_Py_error_handler errors, int *surrogateescape)
7777
PyObject *
7878
_Py_device_encoding(int fd)
7979
{
80-
int valid;
81-
Py_BEGIN_ALLOW_THREADS
80+
#if defined(MS_WINDOWS) && defined(HAVE_WINDOWS_CONSOLE_IO)
81+
HANDLE handle;
82+
DWORD temp;
83+
UINT cp = 0;
84+
8285
_Py_BEGIN_SUPPRESS_IPH
83-
valid = isatty(fd);
86+
handle = (HANDLE)_get_osfhandle(fd);
8487
_Py_END_SUPPRESS_IPH
85-
Py_END_ALLOW_THREADS
86-
if (!valid)
88+
if (handle == INVALID_HANDLE_VALUE) {
8789
Py_RETURN_NONE;
90+
}
91+
92+
Py_BEGIN_ALLOW_THREADS
93+
if (GetFileType(handle) == FILE_TYPE_CHAR) {
94+
/* GetConsoleMode() only succeeds for a console handle. */
95+
if (!GetConsoleMode(handle, &temp)) {
96+
/* Assume that access denied implies an output handle. */
97+
if (GetLastError() == ERROR_ACCESS_DENIED) {
98+
cp = GetConsoleOutputCP();
99+
}
100+
}
101+
else if (GetNumberOfConsoleInputEvents(handle, &temp)) {
102+
cp = GetConsoleCP();
103+
}
104+
else {
105+
cp = GetConsoleOutputCP();
106+
}
107+
}
108+
Py_END_ALLOW_THREADS
88109

89-
#ifdef MS_WINDOWS
90-
#ifdef HAVE_WINDOWS_CONSOLE_IO
91-
UINT cp;
92-
if (fd == 0)
93-
cp = GetConsoleCP();
94-
else if (fd == 1 || fd == 2)
95-
cp = GetConsoleOutputCP();
96-
else
97-
cp = 0;
98110
/* GetConsoleCP() and GetConsoleOutputCP() return 0 if the application
99111
has no console */
112+
if (cp == CP_UTF8) {
113+
_Py_DECLARE_STR(utf_8, "utf-8");
114+
return &_Py_STR(utf_8);
115+
}
100116
if (cp == 0) {
101117
Py_RETURN_NONE;
102118
}
103-
104119
return PyUnicode_FromFormat("cp%u", (unsigned int)cp);
105120
#else
121+
int valid;
122+
Py_BEGIN_ALLOW_THREADS
123+
_Py_BEGIN_SUPPRESS_IPH
124+
valid = isatty(fd);
125+
_Py_END_SUPPRESS_IPH
126+
Py_END_ALLOW_THREADS
127+
if (!valid) {
128+
Py_RETURN_NONE;
129+
}
130+
131+
#ifdef MS_WINDOWS
106132
Py_RETURN_NONE;
107-
#endif /* HAVE_WINDOWS_CONSOLE_IO */
108133
#else
109134
if (_PyRuntime.preconfig.utf8_mode) {
110135
_Py_DECLARE_STR(utf_8, "utf-8");
111136
return &_Py_STR(utf_8);
112137
}
113138
return _Py_GetLocaleEncodingObject();
114139
#endif
140+
#endif /* MS_WINDOWS && HAVE_WINDOWS_CONSOLE_IO */
115141
}
116142

117143

0 commit comments

Comments
 (0)