Skip to content

Commit 76e9ff2

Browse files
gh-86427: Replace PyConfig.stdio_encoding with three separate members
PyConfig.stdin_encoding, PyConfig.stdout_encoding and PyConfig.stderr_encoding allow the standard streams to have different encodings. In the legacy Windows stdio mode they are initialized with the encoding of the device the corresponding stream is connected to, instead of the ANSI code page.
1 parent 998b890 commit 76e9ff2

12 files changed

Lines changed: 189 additions & 36 deletions

File tree

Doc/c-api/init_config.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,12 @@ Configuration Options
477477
- :c:member:`skip_source_first_line <PyConfig.skip_source_first_line>`
478478
- ``bool``
479479
- Read-only
480-
* - ``"stdio_encoding"``
481-
- :c:member:`stdio_encoding <PyConfig.stdio_encoding>`
480+
* - ``"stderr_encoding"``
481+
- :c:member:`stderr_encoding <PyConfig.stderr_encoding>`
482+
* - ``"stdin_encoding"``
483+
- :c:member:`stdin_encoding <PyConfig.stdin_encoding>`
484+
* - ``"stdout_encoding"``
485+
- :c:member:`stdout_encoding <PyConfig.stdout_encoding>`
482486
- ``str``
483487
- Read-only
484488
* - ``"stdio_errors"``
@@ -1863,12 +1867,14 @@ PyConfig
18631867
18641868
Default: ``0``.
18651869
1866-
.. c:member:: wchar_t* stdio_encoding
1870+
.. c:member:: wchar_t* stdin_encoding
1871+
.. c:member:: wchar_t* stdout_encoding
1872+
.. c:member:: wchar_t* stderr_encoding
18671873
.. c:member:: wchar_t* stdio_errors
18681874
1869-
Encoding and encoding errors of :data:`sys.stdin`, :data:`sys.stdout` and
1870-
:data:`sys.stderr` (but :data:`sys.stderr` always uses
1871-
``"backslashreplace"`` error handler).
1875+
Encoding of :data:`sys.stdin`, :data:`sys.stdout` and :data:`sys.stderr`
1876+
respectively, and encoding errors of all of them (but :data:`sys.stderr`
1877+
always uses the ``"backslashreplace"`` error handler).
18721878
18731879
Use the :envvar:`PYTHONIOENCODING` environment variable if it is
18741880
non-empty.

Doc/library/sys.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2136,7 +2136,8 @@ always available. Unless explicitly noted otherwise, all variables are read-only
21362136
follows:
21372137

21382138
* The encoding and error handling are initialized from
2139-
:c:member:`PyConfig.stdio_encoding` and :c:member:`PyConfig.stdio_errors`.
2139+
:c:member:`PyConfig.stdin_encoding`, :c:member:`PyConfig.stdout_encoding`,
2140+
:c:member:`PyConfig.stderr_encoding` and :c:member:`PyConfig.stdio_errors`.
21402141

21412142
On Windows, UTF-8 is used for the console device. Non-character
21422143
devices such as disk files and pipes use the system locale

Include/cpython/initconfig.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ typedef struct PyConfig {
171171
int user_site_directory;
172172
int configure_c_stdio;
173173
int buffered_stdio;
174-
wchar_t *stdio_encoding;
174+
wchar_t *stdin_encoding;
175+
wchar_t *stdout_encoding;
176+
wchar_t *stderr_encoding;
175177
wchar_t *stdio_errors;
176178
#ifdef MS_WINDOWS
177179
int legacy_windows_stdio;

Lib/test/test_capi/test_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def test_config_get(self):
8686
("show_ref_count", bool, None),
8787
("site_import", bool, None),
8888
("skip_source_first_line", bool, None),
89-
("stdio_encoding", str, None),
89+
("stderr_encoding", str, None),
90+
("stdin_encoding", str, None),
91+
("stdout_encoding", str, None),
9092
("stdio_errors", str, None),
9193
("stdlib_dir", str | None, "_stdlib_dir"),
9294
("tracemalloc", int, None),

Lib/test/test_cmd_line.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,42 @@ def test_python_legacy_windows_stdio(self):
10671067
support.skip_on_low_desktop_heap_memory_subprocess(p.returncode)
10681068
self.assertEqual(p.returncode, 0)
10691069

1070+
@unittest.skipUnless(support.MS_WINDOWS, 'Test only applicable on Windows')
1071+
def test_python_legacy_windows_stdio_encoding(self):
1072+
# gh-86427: In the legacy mode the encoding of a standard stream is
1073+
# the encoding of the console it is connected to, which can differ
1074+
# for input and output.
1075+
import ctypes
1076+
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
1077+
try:
1078+
fin = open('CONIN$')
1079+
except OSError:
1080+
self.skipTest('no console')
1081+
# We cannot use PIPE, because the standard streams should be
1082+
# connected to the console. So we use the exit code.
1083+
code = ("import sys; sys.exit(sys.stdin.encoding != 'cp850' or "
1084+
"sys.stdout.encoding != 'cp437')")
1085+
env = os.environ.copy()
1086+
env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1087+
env['PYTHONUTF8'] = '0'
1088+
env.pop('PYTHONIOENCODING', None)
1089+
old_cp = kernel32.GetConsoleCP()
1090+
old_output_cp = kernel32.GetConsoleOutputCP()
1091+
with fin, open('CONOUT$', 'w') as fout:
1092+
try:
1093+
if not kernel32.SetConsoleCP(850):
1094+
self.skipTest('cannot set the console input code page')
1095+
if not kernel32.SetConsoleOutputCP(437):
1096+
self.skipTest('cannot set the console output code page')
1097+
proc = subprocess.run([sys.executable, '-c', code], env=env,
1098+
stdin=fin, stdout=fout,
1099+
stderr=subprocess.DEVNULL)
1100+
finally:
1101+
kernel32.SetConsoleCP(old_cp)
1102+
kernel32.SetConsoleOutputCP(old_output_cp)
1103+
support.skip_on_low_desktop_heap_memory_subprocess(proc.returncode)
1104+
self.assertEqual(proc.returncode, 0)
1105+
10701106
@unittest.skipIf("-fsanitize" in sysconfig.get_config_vars().get('PY_CFLAGS', ()),
10711107
"PYTHONMALLOCSTATS doesn't work with ASAN")
10721108
def test_python_malloc_stats(self):

Lib/test/test_embed.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,9 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
767767
'configure_c_stdio': False,
768768
'buffered_stdio': True,
769769

770-
'stdio_encoding': GET_DEFAULT_CONFIG,
770+
'stdin_encoding': GET_DEFAULT_CONFIG,
771+
'stdout_encoding': GET_DEFAULT_CONFIG,
772+
'stderr_encoding': GET_DEFAULT_CONFIG,
771773
'stdio_errors': GET_DEFAULT_CONFIG,
772774

773775
'skip_source_first_line': False,
@@ -934,16 +936,19 @@ def get_expected_config(self, expected_preconfig, expected,
934936
# there is no easy way to get the locale encoding before
935937
# setlocale(LC_CTYPE, "") is called: don't test encodings
936938
for key in ('filesystem_encoding', 'filesystem_errors',
937-
'stdio_encoding', 'stdio_errors'):
939+
'stdin_encoding', 'stdout_encoding',
940+
'stderr_encoding', 'stdio_errors'):
938941
expected[key] = self.IGNORE_CONFIG
939942

940943
if expected_preconfig['utf8_mode'] == 1:
941944
if expected['filesystem_encoding'] is self.GET_DEFAULT_CONFIG:
942945
expected['filesystem_encoding'] = 'utf-8'
943946
if expected['filesystem_errors'] is self.GET_DEFAULT_CONFIG:
944947
expected['filesystem_errors'] = self.UTF8_MODE_ERRORS
945-
if expected['stdio_encoding'] is self.GET_DEFAULT_CONFIG:
946-
expected['stdio_encoding'] = 'utf-8'
948+
for key in ('stdin_encoding', 'stdout_encoding',
949+
'stderr_encoding'):
950+
if expected[key] is self.GET_DEFAULT_CONFIG:
951+
expected[key] = 'utf-8'
947952
if expected['stdio_errors'] is self.GET_DEFAULT_CONFIG:
948953
expected['stdio_errors'] = 'surrogateescape'
949954

@@ -1133,7 +1138,9 @@ def test_init_from_config(self):
11331138
'malloc_stats': True,
11341139
'pymalloc_hugepages': True,
11351140

1136-
'stdio_encoding': 'iso8859-1',
1141+
'stdin_encoding': 'iso8859-1',
1142+
'stdout_encoding': 'iso8859-1',
1143+
'stderr_encoding': 'iso8859-1',
11371144
'stdio_errors': 'replace',
11381145

11391146
'pycache_prefix': 'conf_pycache_prefix',
@@ -1205,7 +1212,9 @@ def test_init_compat_env(self):
12051212
'write_bytecode': False,
12061213
'verbose': 1,
12071214
'buffered_stdio': False,
1208-
'stdio_encoding': 'iso8859-1',
1215+
'stdin_encoding': 'iso8859-1',
1216+
'stdout_encoding': 'iso8859-1',
1217+
'stderr_encoding': 'iso8859-1',
12091218
'stdio_errors': 'replace',
12101219
'user_site_directory': False,
12111220
'faulthandler': True,
@@ -1242,7 +1251,9 @@ def test_init_python_env(self):
12421251
'write_bytecode': False,
12431252
'verbose': 1,
12441253
'buffered_stdio': False,
1245-
'stdio_encoding': 'iso8859-1',
1254+
'stdin_encoding': 'iso8859-1',
1255+
'stdout_encoding': 'iso8859-1',
1256+
'stderr_encoding': 'iso8859-1',
12461257
'stdio_errors': 'replace',
12471258
'user_site_directory': False,
12481259
'faulthandler': True,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Replace :c:member:`!PyConfig.stdio_encoding` with three separate members:
2+
:c:member:`PyConfig.stdin_encoding`, :c:member:`PyConfig.stdout_encoding` and
3+
:c:member:`PyConfig.stderr_encoding`. In the legacy Windows stdio mode they
4+
are initialized with the encoding of the device the corresponding stream is
5+
connected to.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the encoding of the standard streams in the legacy Windows stdio mode
2+
(:envvar:`PYTHONLEGACYWINDOWSSTDIO`). It is now the encoding of the device
3+
the stream is connected to, as in Python 3.7, not the ANSI code page.

Objects/unicodeobject.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15209,9 +15209,14 @@ init_stdio_encoding(PyInterpreterState *interp)
1520915209
{
1521015210
/* Update the stdio encoding to the normalized Python codec name. */
1521115211
PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);
15212-
if (config_get_codec_name(&config->stdio_encoding) < 0) {
15213-
return _PyStatus_ERR("failed to get the Python codec name "
15214-
"of the stdio encoding");
15212+
wchar_t **encodings[] = {&config->stdin_encoding,
15213+
&config->stdout_encoding,
15214+
&config->stderr_encoding};
15215+
for (size_t i = 0; i < Py_ARRAY_LENGTH(encodings); i++) {
15216+
if (config_get_codec_name(encodings[i]) < 0) {
15217+
return _PyStatus_ERR("failed to get the Python codec name "
15218+
"of the stdio encoding");
15219+
}
1521515220
}
1521615221
return _PyStatus_OK();
1521715222
}

Programs/_testembed.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ static void check_stdio_details(const wchar_t *encoding, const wchar_t *errors)
302302
_PyConfig_InitCompatConfig(&config);
303303
/* Force the given IO encoding */
304304
if (encoding) {
305-
config_set_string(&config, &config.stdio_encoding, encoding);
305+
config_set_string(&config, &config.stdin_encoding, encoding);
306+
config_set_string(&config, &config.stdout_encoding, encoding);
307+
config_set_string(&config, &config.stderr_encoding, encoding);
306308
}
307309
if (errors) {
308310
config_set_string(&config, &config.stdio_errors, errors);
@@ -770,7 +772,9 @@ static int test_init_from_config(void)
770772
config.buffered_stdio = 0;
771773

772774
putenv("PYTHONIOENCODING=cp424");
773-
config_set_string(&config, &config.stdio_encoding, L"iso8859-1");
775+
config_set_string(&config, &config.stdin_encoding, L"iso8859-1");
776+
config_set_string(&config, &config.stdout_encoding, L"iso8859-1");
777+
config_set_string(&config, &config.stderr_encoding, L"iso8859-1");
774778
config_set_string(&config, &config.stdio_errors, L"replace");
775779

776780
putenv("PYTHONNOUSERSITE=");

0 commit comments

Comments
 (0)