mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
db33af9c3e
commit
6e82636bee
3 files changed
+109
-13
No files matched your search
Vendored
+3
-13
@@ -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):
|
||||
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user