Add Python 3.15 encoding normalization helper (#9115)

Extract _codecs._normalize_encoding and delegate encodings.normalize_encoding
to it using the final CPython 3.15 library source. Preserve case and dots,
validate UTF-8 before NUL truncation, and retain wrapper warnings and bytes
conversion. Add bounded regression assertions without changing registry keys.

Lib/encodings/__init__.py was updated via scripts/update_lib migrate from
CPython d220409a13b77de7a6c30abe16ea90db01528edc, with byte-identical output.

AI assistance: OpenAI Codex implemented and source-reviewed this split,
ran update_lib, prepared bounded regressions and performed focused automated
validation. No human manual testing is claimed.

Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-15 00:53:59 +09:00
1 parent db33af9c3e
commit 6e82636bee
3 files changed
+109 -13

No files matched your search

+3 -13
View File
@@ -26,10 +26,11 @@ Written by Marc-Andre Lemburg (mal@lemburg.com).
(c) Copyright CNRI, All Rights Reserved. NO WARRANTY.
"""#"
"""
import codecs
import sys
from _codecs import _normalize_encoding
from . import aliases
_cache = {}
@@ -62,18 +63,7 @@ def normalize_encoding(encoding):
"Support for non-ascii encoding names will be removed in 3.17",
DeprecationWarning, stacklevel=2)
chars = []
punct = False
for c in encoding:
if c.isalnum() or c == '.':
if punct and chars:
chars.append('_')
if c.isascii():
chars.append(c)
punct = False
else:
punct = True
return ''.join(chars)
return _normalize_encoding(encoding)
def search_function(encoding):
+30
View File
@@ -20,6 +20,36 @@ mod _codecs {
function::ArgBytesLike,
};
#[derive(FromArgs)]
struct NormalizeEncodingArgs {
#[pyarg(any)]
encoding: PyStrRef,
}
#[pyfunction]
fn _normalize_encoding(args: NormalizeEncodingArgs, vm: &VirtualMachine) -> PyResult<String> {
let encoding = args.encoding;
// CPython converts the entire input to UTF-8 before scanning for NUL.
if !encoding.is_utf8() {
let ctx = PyEncodeContext::new(encodings::utf8::ENCODING_NAME, &encoding, vm);
encodings::utf8::encode(ctx, &encodings::errors::Strict)?;
}
let mut normalized = String::with_capacity(encoding.byte_len());
let mut punct = false;
for byte in encoding.as_bytes().iter().copied().take_while(|b| *b != 0) {
if byte.is_ascii_alphanumeric() || byte == b'.' {
if punct && !normalized.is_empty() {
normalized.push('_');
}
normalized.push(char::from(byte));
punct = false;
} else {
punct = true;
}
}
Ok(normalized)
}
#[pyfunction]
fn register(search_function: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
vm.state.codec_registry.register(search_function, vm)
@@ -64,3 +64,79 @@ try:
assert seen_encoding_names[-1] == normalized
finally:
codecs.unregister(record_unknown_encoding)
# The encodings helper preserves case and dots, unlike registry lookup keys.
import encodings
import warnings
class EncodingName(str):
pass
for encoding, expected in (
("", ""),
("UTF-8", "UTF_8"),
(" .UTF--8. ", ".UTF_8."),
("---", ""),
("A__-- B", "A_B"),
("AéB", "A_B"),
("éUTF-8Ω", "UTF_8"),
("A😀B", "A_B"),
("UTF-8\x00ignored", "UTF_8"),
("\x00UTF-8", ""),
):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
assert _codecs._normalize_encoding(encoding) == expected
assert _codecs._normalize_encoding(encoding=EncodingName(encoding)) == expected
assert not caught
for encoding in (b"UTF-8", bytearray(b"UTF-8"), None, 1):
try:
_codecs._normalize_encoding(encoding)
except TypeError:
pass
else:
assert False, "non-string encoding was accepted"
# UTF-8 validation precedes scanning, including a surrogate after NUL.
for encoding in ("A\ud800B", "A\x00\ud800B"):
try:
_codecs._normalize_encoding(encoding)
except UnicodeEncodeError as error:
assert error.encoding == "utf-8"
assert error.object == encoding
assert error.start == encoding.index("\ud800")
assert error.end == error.start + 1
else:
assert False, "surrogate encoding was accepted"
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
assert encodings.normalize_encoding(b"UTF-8") == "UTF_8"
assert not caught
assert encodings.normalize_encoding("AéB") == "A_B"
assert len(caught) == 1
assert caught[0].category is DeprecationWarning
try:
encodings.normalize_encoding(b"UTF-\xff")
except UnicodeDecodeError:
pass
else:
assert False, "non-ASCII bytes encoding was accepted"
for args, kwargs in (
((), {}),
(("UTF-8", "ASCII"), {}),
(("UTF-8",), {"encoding": "ASCII"}),
((), {"name": "UTF-8"}),
):
try:
_codecs._normalize_encoding(*args, **kwargs)
except TypeError:
pass
else:
assert False, "invalid normalization arguments were accepted"