From c9a586113b60ffda951c04fe5b5fe040c3e43efc Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Sep 2024 11:33:59 +0300 Subject: [PATCH 1/6] gh-123803: Support arbitrary code page encodings on Windows If the cpXXX encoding is not directly implemented in Python, fall back to use the Windows-specific API codecs.code_page_encode() and codecs.code_page_decode(). --- Lib/encodings/__init__.py | 61 +++++++++++++++----- Lib/test/test_codecs.py | 118 +++++++++++++++++++++++++++++++------- 2 files changed, 143 insertions(+), 36 deletions(-) diff --git a/Lib/encodings/__init__.py b/Lib/encodings/__init__.py index f9075b8f0d98acf..9fa31f6b199cd16 100644 --- a/Lib/encodings/__init__.py +++ b/Lib/encodings/__init__.py @@ -156,19 +156,50 @@ def search_function(encoding): codecs.register(search_function) if sys.platform == 'win32': - # bpo-671666, bpo-46668: If Python does not implement a codec for current - # Windows ANSI code page, use the "mbcs" codec instead: - # WideCharToMultiByte() and MultiByteToWideChar() functions with CP_ACP. - # Python does not support custom code pages. - def _alias_mbcs(encoding): + def _code_page_search_function(encoding): + encoding = encoding.lower() + if not encoding.startswith('cp'): + return None try: - import _winapi - ansi_code_page = "cp%s" % _winapi.GetACP() - if encoding == ansi_code_page: - import encodings.mbcs - return encodings.mbcs.getregentry() - except ImportError: - # Imports may fail while we are shutting down - pass - - codecs.register(_alias_mbcs) + cp = int(encoding[2:]) + except ValueError: + return None + # Test if the code page is supported + try: + codecs.code_page_encode(cp, 'x') + except (OverflowError, OSError): + return None + + def encode(input, errors='strict'): + return codecs.code_page_encode(cp, input, errors) + + def decode(input, errors='strict'): + return codecs.code_page_decode(cp, input, errors, True) + + class IncrementalEncoder(codecs.IncrementalEncoder): + def encode(self, input, final=False): + return codecs.code_page_encode(cp, input, self.errors)[0] + + class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + def _buffer_decode(self, input, errors, final): + return codecs.code_page_decode(cp, input, errors, final) + + class StreamWriter(codecs.StreamWriter): + def encode(self, input, errors='strict'): + return codecs.code_page_encode(cp, input, errors) + + class StreamReader(codecs.StreamReader): + def decode(self, input, errors, final): + return codecs.code_page_decode(cp, input, errors, final) + + return codecs.CodecInfo( + name=f'cp{cp}', + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamreader=StreamReader, + streamwriter=StreamWriter, + ) + + codecs.register(_code_page_search_function) diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index 428036e1765b8f4..5bb7813ffd0614e 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -3263,7 +3263,11 @@ def test_code_page_name(self): codecs.code_page_decode, self.CP_UTF8, b'\xff', 'strict', True) def check_decode(self, cp, tests): - for raw, errors, expected in tests: + for raw, errors, expected, *rest in tests: + if rest: + altexpected, = rest + else: + altexpected = expected if expected is not None: try: decoded = codecs.code_page_decode(cp, raw, errors, True) @@ -3280,8 +3284,21 @@ def check_decode(self, cp, tests): self.assertRaises(UnicodeDecodeError, codecs.code_page_decode, cp, raw, errors, True) + if altexpected is not None: + decoded = raw.decode(f'cp{cp}', errors) + self.assertEqual(decoded, altexpected, + '%a.decode("cp%s", %r)=%a != %a' + % (raw, cp, errors, decoded, altexpected)) + else: + self.assertRaises(UnicodeDecodeError, + raw.decode, f'cp{cp}', errors) + def check_encode(self, cp, tests): - for text, errors, expected in tests: + for text, errors, expected, *rest in tests: + if rest: + altexpected, = rest + else: + altexpected = expected if expected is not None: try: encoded = codecs.code_page_encode(cp, text, errors) @@ -3292,18 +3309,26 @@ def check_encode(self, cp, tests): '%a.encode("cp%s", %r)=%a != %a' % (text, cp, errors, encoded[0], expected)) self.assertEqual(encoded[1], len(text)) + + encoded = text.encode(f'cp{cp}', errors) + self.assertEqual(encoded, altexpected, + '%a.encode("cp%s", %r)=%a != %a' + % (text, cp, errors, encoded, altexpected)) else: self.assertRaises(UnicodeEncodeError, codecs.code_page_encode, cp, text, errors) + self.assertRaises(UnicodeEncodeError, + text.encode, f'cp{cp}', errors) def test_cp932(self): self.check_encode(932, ( ('abc', 'strict', b'abc'), ('\uff44\u9a3e', 'strict', b'\x82\x84\xe9\x80'), + ('\uf8f3', 'strict', b'\xff'), # test error handlers ('\xff', 'strict', None), ('[\xff]', 'ignore', b'[]'), - ('[\xff]', 'replace', b'[y]'), + ('[\xff]', 'replace', b'[y]', b'[?]'), ('[\u20ac]', 'replace', b'[?]'), ('[\xff]', 'backslashreplace', b'[\\xff]'), ('[\xff]', 'namereplace', @@ -3317,12 +3342,12 @@ def test_cp932(self): (b'abc', 'strict', 'abc'), (b'\x82\x84\xe9\x80', 'strict', '\uff44\u9a3e'), # invalid bytes - (b'[\xff]', 'strict', None), - (b'[\xff]', 'ignore', '[]'), - (b'[\xff]', 'replace', '[\ufffd]'), - (b'[\xff]', 'backslashreplace', '[\\xff]'), - (b'[\xff]', 'surrogateescape', '[\udcff]'), - (b'[\xff]', 'surrogatepass', None), + (b'[\xff]', 'strict', None, '[\uf8f3]'), + (b'[\xff]', 'ignore', '[]', '[\uf8f3]'), + (b'[\xff]', 'replace', '[\ufffd]', '[\uf8f3]'), + (b'[\xff]', 'backslashreplace', '[\\xff]', '[\uf8f3]'), + (b'[\xff]', 'surrogateescape', '[\udcff]', '[\uf8f3]'), + (b'[\xff]', 'surrogatepass', None, '[\uf8f3]'), (b'\x81\x00abc', 'strict', None), (b'\x81\x00abc', 'ignore', '\x00abc'), (b'\x81\x00abc', 'replace', '\ufffd\x00abc'), @@ -3337,7 +3362,7 @@ def test_cp1252(self): # test error handlers ('\u0141', 'strict', None), ('\u0141', 'ignore', b''), - ('\u0141', 'replace', b'L'), + ('\u0141', 'replace', b'L', b'?'), ('\udc98', 'surrogateescape', b'\x98'), ('\udc98', 'surrogatepass', None), )) @@ -3347,6 +3372,59 @@ def test_cp1252(self): (b'\xff', 'strict', '\xff'), )) + def test_cp708(self): + self.check_encode(708, ( + ('abc2%', 'strict', b'abc2%'), + ('\u060c\u0621\u064a', 'strict', b'\xac\xc1\xea'), + ('\u2562\xe7\xa0', 'strict', b'\x86\x87\xff'), + ('\x9a\x9f', 'strict', b'\x9a\x9f'), + ('\u256b', 'strict', b'\xc0'), + # test error handlers + ('[\u0662]', 'strict', None), + ('[\u0662]', 'ignore', b'[]'), + ('[\u0662]', 'replace', b'[?]'), + ('\udca0', 'surrogateescape', b'\xa0'), + ('\udca0', 'surrogatepass', None), + )) + self.check_decode(708, ( + (b'abc2%', 'strict', 'abc2%'), + (b'\xac\xc1\xea', 'strict', '\u060c\u0621\u064a'), + (b'\x86\x87\xff', 'strict', '\u2562\xe7\xa0'), + (b'\x9a\x9f', 'strict', '\x9a\x9f'), + (b'\xc0', 'strict', '\u256b'), + # test error handlers + (b'\xa0', 'strict', None), + (b'[\xa0]', 'ignore', '[]'), + (b'[\xa0]', 'replace', '[\ufffd]'), + (b'[\xa0]', 'backslashreplace', '[\\xa0]'), + (b'[\xa0]', 'surrogateescape', '[\udca0]'), + (b'[\xa0]', 'surrogatepass', None), + )) + + def test_cp20106(self): + self.check_encode(20106, ( + ('abc', 'strict', b'abc'), + ('\xa7\xc4\xdf', 'strict', b'@[~'), + # test error handlers + ('@', 'strict', None), + ('@', 'ignore', b''), + ('@', 'replace', b'?'), + ('\udcbf', 'surrogateescape', b'\xbf'), + ('\udcbf', 'surrogatepass', None), + )) + self.check_decode(20106, ( + (b'abc', 'strict', 'abc'), + (b'@[~', 'strict', '\xa7\xc4\xdf'), + (b'\xe1\xfe', 'strict', 'a\xdf'), + # test error handlers + (b'(\xbf)', 'strict', None), + (b'(\xbf)', 'ignore', '()'), + (b'(\xbf)', 'replace', '(\ufffd)'), + (b'(\xbf)', 'backslashreplace', '(\\xbf)'), + (b'(\xbf)', 'surrogateescape', '(\udcbf)'), + (b'(\xbf)', 'surrogatepass', None), + )) + def test_cp_utf7(self): cp = 65000 self.check_encode(cp, ( @@ -3419,17 +3497,15 @@ def test_incremental(self): False) self.assertEqual(decoded, ('abc', 3)) - def test_mbcs_alias(self): - # Check that looking up our 'default' codepage will return - # mbcs when we don't have a more specific one available - code_page = 99_999 - name = f'cp{code_page}' - with mock.patch('_winapi.GetACP', return_value=code_page): - try: - codec = codecs.lookup(name) - self.assertEqual(codec.name, 'mbcs') - finally: - codecs.unregister(name) + def test_mbcs_code_page(self): + # Check that codec for the current Windows (ANSII) code page is + # always available. + try: + from _winapi import GetACP + except ImportError: + self.skipTest('requires _winapi.GetACP') + cp = GetACP() + codecs.lookup(f'cp{cp}') @support.bigmemtest(size=2**31, memuse=7, dry_run=False) def test_large_input(self, size): From ebf8ed154658c1fcd954c37782ed7e37147dfc46 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 7 Sep 2024 15:31:35 +0300 Subject: [PATCH 2/6] Update the documentation. --- Doc/library/codecs.rst | 7 +++++++ Doc/whatsnew/3.14.rst | 2 ++ .../Windows/2024-09-07-15-16-24.gh-issue-123803.J9VNQU.rst | 1 + 3 files changed, 10 insertions(+) create mode 100644 Misc/NEWS.d/next/Windows/2024-09-07-15-16-24.gh-issue-123803.J9VNQU.rst diff --git a/Doc/library/codecs.rst b/Doc/library/codecs.rst index 2cfd8a1eaee806f..a129a26190ba991 100644 --- a/Doc/library/codecs.rst +++ b/Doc/library/codecs.rst @@ -1042,6 +1042,10 @@ is meant to be exhaustive. Notice that spelling alternatives that only differ in case or use a hyphen instead of an underscore are also valid aliases; therefore, e.g. ``'utf-8'`` is a valid alias for the ``'utf_8'`` codec. +On Windows, ``cpXXX`` codecs are available for all code pages. +But only codecs listed in the following table are guarantead to exist on +other platforms. + .. impl-detail:: Some common encodings can bypass the codecs lookup machinery to @@ -1307,6 +1311,9 @@ particular, the following variants typically exist: .. versionchanged:: 3.8 ``cp65001`` is now an alias to ``utf_8``. +.. versionchanged:: 3.14 + On Windows, ``cpXXX`` codecs are now available for all code pages. + Python Specific Encodings ------------------------- diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index e1bd52370d776c4..ee75b97f62bad54 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -90,6 +90,8 @@ Other Language Changes They raise an error if the argument is a string. (Contributed by Serhiy Storchaka in :gh:`84978`.) +* All Windows code pages are now supported as "cpXXX" codecs on Windows. + (Contributed by Serhiy Storchaka in :gh:`123803`.) New Modules =========== diff --git a/Misc/NEWS.d/next/Windows/2024-09-07-15-16-24.gh-issue-123803.J9VNQU.rst b/Misc/NEWS.d/next/Windows/2024-09-07-15-16-24.gh-issue-123803.J9VNQU.rst new file mode 100644 index 000000000000000..3ad4d12eb0dc3ed --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2024-09-07-15-16-24.gh-issue-123803.J9VNQU.rst @@ -0,0 +1 @@ +All Windows code pages are now supported as "cpXXX" codecs on Windows. From b3f8ee4705c9c46f6f6249dcb11592f430ce8178 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 6 Nov 2024 10:02:47 +0200 Subject: [PATCH 3/6] Address review comments. --- Lib/encodings/__init__.py | 38 +++++---------------------------- Lib/encodings/_win_cp_codecs.py | 36 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 Lib/encodings/_win_cp_codecs.py diff --git a/Lib/encodings/__init__.py b/Lib/encodings/__init__.py index 9fa31f6b199cd16..3e5e0b1882c633d 100644 --- a/Lib/encodings/__init__.py +++ b/Lib/encodings/__init__.py @@ -156,7 +156,9 @@ def search_function(encoding): codecs.register(search_function) if sys.platform == 'win32': - def _code_page_search_function(encoding): + from _win_cp_codecs import create_win32_code_page_codec + + def win32_code_page_search_function(encoding): encoding = encoding.lower() if not encoding.startswith('cp'): return None @@ -170,36 +172,6 @@ def _code_page_search_function(encoding): except (OverflowError, OSError): return None - def encode(input, errors='strict'): - return codecs.code_page_encode(cp, input, errors) - - def decode(input, errors='strict'): - return codecs.code_page_decode(cp, input, errors, True) - - class IncrementalEncoder(codecs.IncrementalEncoder): - def encode(self, input, final=False): - return codecs.code_page_encode(cp, input, self.errors)[0] - - class IncrementalDecoder(codecs.BufferedIncrementalDecoder): - def _buffer_decode(self, input, errors, final): - return codecs.code_page_decode(cp, input, errors, final) - - class StreamWriter(codecs.StreamWriter): - def encode(self, input, errors='strict'): - return codecs.code_page_encode(cp, input, errors) - - class StreamReader(codecs.StreamReader): - def decode(self, input, errors, final): - return codecs.code_page_decode(cp, input, errors, final) - - return codecs.CodecInfo( - name=f'cp{cp}', - encode=encode, - decode=decode, - incrementalencoder=IncrementalEncoder, - incrementaldecoder=IncrementalDecoder, - streamreader=StreamReader, - streamwriter=StreamWriter, - ) + return create_win32_code_page_codec(cp) - codecs.register(_code_page_search_function) + codecs.register(win32_code_page_search_function) diff --git a/Lib/encodings/_win_cp_codecs.py b/Lib/encodings/_win_cp_codecs.py new file mode 100644 index 000000000000000..a3e8f17390b0473 --- /dev/null +++ b/Lib/encodings/_win_cp_codecs.py @@ -0,0 +1,36 @@ +import codecs + +def create_win32_code_page_codec(cp): + from codecs import code_page_decode, code_page_decode + + def encode(input, errors='strict'): + return code_page_encode(cp, input, errors) + + def decode(input, errors='strict'): + return code_page_decode(cp, input, errors, True) + + class IncrementalEncoder(codecs.IncrementalEncoder): + def encode(self, input, final=False): + return code_page_encode(cp, input, self.errors)[0] + + class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + def _buffer_decode(self, input, errors, final): + return code_page_decode(cp, input, errors, final) + + class StreamWriter(codecs.StreamWriter): + def encode(self, input, errors='strict'): + return code_page_encode(cp, input, errors) + + class StreamReader(codecs.StreamReader): + def decode(self, input, errors, final): + return code_page_decode(cp, input, errors, final) + + return codecs.CodecInfo( + name=f'cp{cp}', + encode=encode, + decode=decode, + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamreader=StreamReader, + streamwriter=StreamWriter, + ) From 1a98b24982ff48936fa6688b96317b24f6d355db Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 6 Nov 2024 10:41:26 +0200 Subject: [PATCH 4/6] Fix imports. --- Lib/encodings/__init__.py | 2 +- Lib/encodings/_win_cp_codecs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/encodings/__init__.py b/Lib/encodings/__init__.py index 3e5e0b1882c633d..298177eb8003a7a 100644 --- a/Lib/encodings/__init__.py +++ b/Lib/encodings/__init__.py @@ -156,7 +156,7 @@ def search_function(encoding): codecs.register(search_function) if sys.platform == 'win32': - from _win_cp_codecs import create_win32_code_page_codec + from ._win_cp_codecs import create_win32_code_page_codec def win32_code_page_search_function(encoding): encoding = encoding.lower() diff --git a/Lib/encodings/_win_cp_codecs.py b/Lib/encodings/_win_cp_codecs.py index a3e8f17390b0473..4f8eb886794404f 100644 --- a/Lib/encodings/_win_cp_codecs.py +++ b/Lib/encodings/_win_cp_codecs.py @@ -1,7 +1,7 @@ import codecs def create_win32_code_page_codec(cp): - from codecs import code_page_decode, code_page_decode + from codecs import code_page_encode, code_page_decode def encode(input, errors='strict'): return code_page_encode(cp, input, errors) From 57ad6d2a928e821cbb42e3a9538491808e4a9925 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 6 Nov 2024 11:13:27 +0200 Subject: [PATCH 5/6] Change error message. --- Lib/test/test_codecs.py | 2 +- Lib/test/test_io.py | 4 ++-- Lib/tokenize.py | 5 ++--- Python/codecs.c | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index e51f7e0ee12b1f3..6799948478aa2ae 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -3196,7 +3196,7 @@ def test_multiple_args(self): # http://bugs.python.org/issue19609 def test_codec_lookup_failure(self): - msg = "^unknown encoding: {}$".format(self.codec_name) + msg = f"^encoding '{self.codec_name}' is not registered$" with self.assertRaisesRegex(LookupError, msg): "str input".encode(self.codec_name) with self.assertRaisesRegex(LookupError, msg): diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index aa1b8268592ff78..cb93be703ba8b20 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -3939,7 +3939,7 @@ def _to_memoryview(buf): class CTextIOWrapperTest(TextIOWrapperTest): io = io - shutdown_error = "LookupError: unknown encoding: ascii" + shutdown_error = "LookupError: encoding 'ascii' is not registered" def test_initialization(self): r = self.BytesIO(b"\xc3\xa9\n\n") @@ -4039,7 +4039,7 @@ def write(self, data): class PyTextIOWrapperTest(TextIOWrapperTest): io = pyio - shutdown_error = "LookupError: unknown encoding: ascii" + shutdown_error = "LookupError: encoding 'ascii' is not registered" class IncrementalNewlineDecoderTest(unittest.TestCase): diff --git a/Lib/tokenize.py b/Lib/tokenize.py index 7ece4e9b70d31b3..41ae0ed3e039150 100644 --- a/Lib/tokenize.py +++ b/Lib/tokenize.py @@ -398,10 +398,9 @@ def find_cookie(line): except LookupError: # This behaviour mimics the Python interpreter if filename is None: - msg = "unknown encoding: " + encoding + msg = f"encoding '{encoding}' is not registered" else: - msg = "unknown encoding for {!r}: {}".format(filename, - encoding) + msg = f"encoding '{encoding}' is not registered for {filename!r}" raise SyntaxError(msg) if bom_found: diff --git a/Python/codecs.c b/Python/codecs.c index 2cb3875db350588..fbc6a1996ee5308 100644 --- a/Python/codecs.c +++ b/Python/codecs.c @@ -204,7 +204,7 @@ PyObject *_PyCodec_Lookup(const char *encoding) if (result == NULL) { /* XXX Perhaps we should cache misses too ? */ PyErr_Format(PyExc_LookupError, - "unknown encoding: %s", encoding); + "encoding '%s' is not registered", encoding); goto onError; } From c999d014c136fb2964ff4ddeff682814f49330ec Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 18 Nov 2024 19:18:28 +0200 Subject: [PATCH 6/6] Revert "Change error message." This reverts commit 57ad6d2a928e821cbb42e3a9538491808e4a9925. --- Lib/test/test_codecs.py | 2 +- Lib/test/test_io.py | 4 ++-- Lib/tokenize.py | 5 +++-- Python/codecs.c | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index 6799948478aa2ae..e51f7e0ee12b1f3 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -3196,7 +3196,7 @@ def test_multiple_args(self): # http://bugs.python.org/issue19609 def test_codec_lookup_failure(self): - msg = f"^encoding '{self.codec_name}' is not registered$" + msg = "^unknown encoding: {}$".format(self.codec_name) with self.assertRaisesRegex(LookupError, msg): "str input".encode(self.codec_name) with self.assertRaisesRegex(LookupError, msg): diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index cb93be703ba8b20..aa1b8268592ff78 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -3939,7 +3939,7 @@ def _to_memoryview(buf): class CTextIOWrapperTest(TextIOWrapperTest): io = io - shutdown_error = "LookupError: encoding 'ascii' is not registered" + shutdown_error = "LookupError: unknown encoding: ascii" def test_initialization(self): r = self.BytesIO(b"\xc3\xa9\n\n") @@ -4039,7 +4039,7 @@ def write(self, data): class PyTextIOWrapperTest(TextIOWrapperTest): io = pyio - shutdown_error = "LookupError: encoding 'ascii' is not registered" + shutdown_error = "LookupError: unknown encoding: ascii" class IncrementalNewlineDecoderTest(unittest.TestCase): diff --git a/Lib/tokenize.py b/Lib/tokenize.py index 41ae0ed3e039150..7ece4e9b70d31b3 100644 --- a/Lib/tokenize.py +++ b/Lib/tokenize.py @@ -398,9 +398,10 @@ def find_cookie(line): except LookupError: # This behaviour mimics the Python interpreter if filename is None: - msg = f"encoding '{encoding}' is not registered" + msg = "unknown encoding: " + encoding else: - msg = f"encoding '{encoding}' is not registered for {filename!r}" + msg = "unknown encoding for {!r}: {}".format(filename, + encoding) raise SyntaxError(msg) if bom_found: diff --git a/Python/codecs.c b/Python/codecs.c index fbc6a1996ee5308..2cb3875db350588 100644 --- a/Python/codecs.c +++ b/Python/codecs.c @@ -204,7 +204,7 @@ PyObject *_PyCodec_Lookup(const char *encoding) if (result == NULL) { /* XXX Perhaps we should cache misses too ? */ PyErr_Format(PyExc_LookupError, - "encoding '%s' is not registered", encoding); + "unknown encoding: %s", encoding); goto onError; }