Align merged runtime contracts with Python 3.15 final

Restrict mappingproxy ordering, use final codec registry normalization, and prefer index conversion before float conversion for sleep. Retain universal Python assertions and import the matching final codec/time canonical test hunks plus the non-ASCII codec-name deprecation warning.

Upstream source: CPython d220409a13b77de7a6c30abe16ea90db01528edc. The existing codec normalization implementation and bytecode contract remain unchanged.

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
youknowdot committed 2026-10-11 07:36:19 +09:00
1 parent 73ea4179d1
commit d7b4be2978
9 files changed
+119 -80

No files matched your search

+6
View File
@@ -56,6 +56,12 @@ def normalize_encoding(encoding):
if isinstance(encoding, bytes):
encoding = str(encoding, "ascii")
if not encoding.isascii():
import warnings
warnings.warn(
"Support for non-ascii encoding names will be removed in 3.17",
DeprecationWarning, stacklevel=2)
chars = []
punct = False
for c in encoding:
+18 -17
View File
@@ -13,6 +13,7 @@ import warnings
from test import support
from test.support import os_helper
from test.support import warnings_helper
try:
import _testlimitedcapi
@@ -3875,32 +3876,27 @@ class Rot13UtilTest(unittest.TestCase):
class CodecNameNormalizationTest(unittest.TestCase):
"""Test codec name normalization"""
def test_codecs_lookup(self):
FOUND = (1, 2, 3, 4)
NOT_FOUND = (None, None, None, None)
def search_function(encoding):
if encoding == "aaa_8":
return FOUND
if encoding.startswith("test."):
return (encoding, 2, 3, 4)
else:
return NOT_FOUND
return None
codecs.register(search_function)
self.addCleanup(codecs.unregister, search_function)
self.assertEqual(FOUND, codecs.lookup('aaa_8'))
self.assertEqual(FOUND, codecs.lookup('AAA-8'))
self.assertEqual(FOUND, codecs.lookup('AAA---8'))
self.assertEqual(FOUND, codecs.lookup('AAA 8'))
self.assertEqual(FOUND, codecs.lookup('aaa\xe9\u20ac-8'))
self.assertEqual(NOT_FOUND, codecs.lookup('AAA.8'))
self.assertEqual(NOT_FOUND, codecs.lookup('AAA...8'))
self.assertEqual(NOT_FOUND, codecs.lookup('BBB-8'))
self.assertEqual(NOT_FOUND, codecs.lookup('BBB.8'))
self.assertEqual(NOT_FOUND, codecs.lookup('a\xe9\u20ac-8'))
self.assertEqual(codecs.lookup('test.aaa_8'), ('test.aaa_8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA-8'), ('test.aaa-8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA 8'), ('test.aaa-8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA---8'), ('test.aaa---8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA 8'), ('test.aaa---8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA.8'), ('test.aaa.8', 2, 3, 4))
self.assertEqual(codecs.lookup('TEST.AAA...8'), ('test.aaa...8', 2, 3, 4))
with self.assertWarns(DeprecationWarning):
self.assertEqual(codecs.lookup('TEST.AAA\xe9\u20ac-8'), ('test.aaa\xe9\u20ac-8', 2, 3, 4))
def test_encodings_normalize_encoding(self):
# encodings.normalize_encoding() ignores non-ASCII characters.
normalize = encodings.normalize_encoding
self.assertEqual(normalize('utf_8'), 'utf_8')
self.assertEqual(normalize('utf\xE9\u20AC\U0010ffff-8'), 'utf_8')
self.assertEqual(normalize('utf 8'), 'utf_8')
# encodings.normalize_encoding() doesn't convert
# characters to lower case.
@@ -3908,6 +3904,11 @@ class CodecNameNormalizationTest(unittest.TestCase):
self.assertEqual(normalize('utf.8'), 'utf.8')
self.assertEqual(normalize('utf...8'), 'utf...8')
# Non-ASCII *encoding* is deprecated.
msg = "Support for non-ascii encoding names will be removed in 3.17"
with warnings_helper.check_warnings((msg, DeprecationWarning)):
self.assertEqual(normalize('utf\xE9\u20AC\U0010ffff-8'), 'utf_8')
class CodecCacheTest(unittest.TestCase):
def test_cache_bounded(self):
+1 -1
View File
@@ -170,7 +170,7 @@ class TimeTestCase(unittest.TestCase):
# Improved exception #81267
with self.assertRaises(TypeError) as errmsg:
time.sleep([])
self.assertIn("integer or float", str(errmsg.exception))
self.assertIn("real number", str(errmsg.exception))
def test_sleep(self):
for value in [-0.0, 0, 0.0, 1e-100, 1e-9, 1e-6, 1, 1.2]:
+4 -2
View File
@@ -193,9 +193,11 @@ impl Py<PyMappingProxy> {
op.method_name(&vm.ctx).as_str()
))
})?;
if !matches!(op, PyComparisonOp::Eq | PyComparisonOp::Ne) {
return Ok(Either::B(PyComparisonValue::NotImplemented));
}
let obj = zelf.to_object(vm)?;
// CPython parity (Objects/descrobject.c::mappingproxy_richcompare):
// delegate to PyObject_RichCompare on the underlying mapping.
// Equality comparisons preserve the underlying mapping's result.
obj.rich_compare(other.to_owned(), op, vm).map(Either::A)
}
+23 -3
View File
@@ -253,7 +253,7 @@ impl CodecsRegistry {
}
pub(crate) fn register_manual(&self, name: &str, codec: PyCodec) {
let name = normalize_encoding_name(name);
let name = normalize_registry_name(name);
self.inner
.write()
.search_cache
@@ -262,7 +262,7 @@ impl CodecsRegistry {
pub fn lookup(&self, encoding: &str, vm: &VirtualMachine) -> PyResult<PyCodec> {
let original_encoding = encoding;
let encoding = normalize_encoding_name(encoding);
let encoding = normalize_registry_name(encoding);
let search_path = {
let inner = self.inner.read();
if let Some(codec) = inner.search_cache.get(encoding.as_ref()) {
@@ -308,7 +308,7 @@ impl CodecsRegistry {
}
pub fn forget(&self, encoding: &str) -> Option<PyCodec> {
let encoding = normalize_encoding_name(encoding);
let encoding = normalize_registry_name(encoding);
self.inner.write().search_cache.remove(encoding.as_ref())
}
@@ -623,6 +623,26 @@ impl FastCodec {
}
}
fn normalize_registry_name(encoding: &str) -> Cow<'_, str> {
if !encoding
.bytes()
.any(|b| b.is_ascii_uppercase() || b == b' ')
{
return encoding.into();
}
encoding
.chars()
.map(|c| {
if c == ' ' {
'-'
} else {
c.to_ascii_lowercase()
}
})
.collect::<String>()
.into()
}
fn normalize_encoding_name(encoding: &str) -> Cow<'_, str> {
// _Py_normalize_encoding: collapse non-alphanumeric/non-dot chars into
// single underscore, strip non-ASCII, lowercase ASCII letters.
+17 -45
View File
@@ -18,9 +18,9 @@ mod decl {
use crate::convert::{ToPyException, ToPyObject};
use crate::{
AsObject, Py, PyObjectRef, PyResult, VirtualMachine,
builtins::{PyFloat, PyStr, PyStrRef, PyTypeRef},
builtins::{PyStr, PyStrRef, PyTypeRef},
class::PyClassDef,
common::wtf8::{Wtf8Buf, wtf8_concat},
common::wtf8::Wtf8Buf,
function::{Either, FuncArgs, OptionalArg, OptionalOption},
types::{PyStructSequence, PyStructSequenceData, struct_sequence_new},
};
@@ -128,8 +128,21 @@ mod decl {
vm.audit("time.sleep", || (object.clone(),))?;
let overflow = || vm.new_overflow_error("timestamp out of range for C PyTime_t");
let nanoseconds = if let Some(float) = object.downcast_ref::<PyFloat>() {
let seconds = float.to_f64();
let nanoseconds = if object.number().is_index() {
let seconds = object.try_index(vm).map_err(|e| {
if e.fast_isinstance(vm.ctx.exceptions.overflow_error) {
overflow()
} else {
e
}
})?;
seconds
.as_bigint()
.to_i64()
.and_then(|seconds| seconds.checked_mul(SEC_TO_NS))
.ok_or_else(overflow)?
} else {
let seconds = object.try_float(vm)?.to_f64();
if seconds.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
}
@@ -146,47 +159,6 @@ mod decl {
return Err(overflow());
}
nanoseconds as i64
} else {
let seconds = object.try_index(vm).or_else(|e| {
if e.fast_isinstance(vm.ctx.exceptions.overflow_error) {
Err(overflow())
} else if e.fast_isinstance(vm.ctx.exceptions.type_error) {
// CPython's %T reads stored type metadata, preserving surrogates.
let class = object.class();
let name = if let Some(heap_type) = &class.heaptype_ext {
let qualname = heap_type.qualname.read().clone();
let module = if let Some(dict) = class.attributes.as_dict() {
dict.get_item_opt(identifier!(vm, __module__), vm)?
.ok_or_else(|| vm.new_attribute_error("__module__"))?
} else {
class.__module__(vm)?
};
match module.downcast_ref::<PyStr>() {
Some(module)
if module.as_wtf8() != "builtins"
&& module.as_wtf8() != "__main__" =>
{
wtf8_concat!(module.as_wtf8(), ".", qualname.as_wtf8())
}
_ => qualname.as_wtf8().to_owned(),
}
} else {
Wtf8Buf::from(class.slot_name().to_owned())
};
Err(vm.new_type_error(wtf8_concat!(
"'",
name,
"' object cannot be interpreted as an integer or float"
)))
} else {
Err(e)
}
})?;
seconds
.as_bigint()
.to_i64()
.and_then(|seconds| seconds.checked_mul(SEC_TO_NS))
.ok_or_else(overflow)?
};
if nanoseconds < 0 {
return Err(vm.new_value_error("sleep length must be non-negative"));
+17 -5
View File
@@ -25,11 +25,13 @@ assert A.__dict__.get("a", "default") is A.a
assert A.__dict__.get("not here") is None
# A mappingproxy must preserve arbitrary rich-comparison results unchanged.
# A mappingproxy preserves arbitrary equality-comparison results unchanged.
def check_mappingproxy_comparison_results():
import operator
from types import MappingProxyType
from testutils import assert_raises
class Result:
def __bool__(self):
raise AssertionError("comparison result must not be coerced to bool")
@@ -58,10 +60,6 @@ def check_mappingproxy_comparison_results():
for operation, reflected in (
(operator.eq, "eq"),
(operator.ne, "ne"),
(operator.lt, "gt"),
(operator.le, "ge"),
(operator.gt, "lt"),
(operator.ge, "le"),
):
calls.clear()
assert operation(MappingProxyType(CustomMapping()), {}) is result
@@ -70,5 +68,19 @@ def check_mappingproxy_comparison_results():
assert operation(MappingProxyType({}), Compared()) is result
assert calls == [reflected], calls
for operation, reflected in (
(operator.lt, "gt"),
(operator.le, "ge"),
(operator.gt, "lt"),
(operator.ge, "le"),
):
calls.clear()
with assert_raises(TypeError):
operation(MappingProxyType(CustomMapping()), {})
assert calls == [], calls
calls.clear()
assert operation(MappingProxyType({}), Compared()) is result
assert calls == [reflected], calls
check_mappingproxy_comparison_results()
+3 -3
View File
@@ -31,7 +31,7 @@ assert (
# Lookup errors preserve the requested spelling, while search hooks receive
# the Python 3.14 normalized registry key.
# the Python 3.15 normalized registry key.
import codecs
seen_encoding_names = []
@@ -45,8 +45,8 @@ def record_unknown_encoding(name):
codecs.register(record_unknown_encoding)
try:
for requested, normalized in (
("NO Such Codec!", "no_such_codec"),
("x_ weird--Ω .Ab", "x_weird_.ab"),
("NO Such Codec!", "no-such--codec!"),
("x_ weird--Ω .Ab", "x_-weird--Ω-.ab"),
):
for lookup in (
codecs.lookup,
+30 -4
View File
@@ -234,13 +234,39 @@ class SleepTypeNamespace:
pass
for module, prefix in (("__main__", ""), ("sleep_test", "sleep_test.")):
for module in ("__main__", "sleep_test"):
SleepTypeNamespace.Invalid.__module__ = module
with assert_raises(TypeError) as error:
time.sleep(SleepTypeNamespace.Invalid())
assert str(error.exception) == (
f"'{prefix}SleepTypeNamespace.Invalid' object cannot be interpreted as an integer or float"
)
assert str(error.exception) == "must be real number, not Invalid"
class FloatSleepDuration:
def __float__(self):
return -0.5
class IndexedSleepDuration(FloatSleepDuration):
def __index__(self):
return -1
def __float__(self):
raise AssertionError("sleep must prefer __index__")
class InvalidIndexedSleepDuration(FloatSleepDuration):
def __index__(self):
raise TypeError("index conversion failed")
for duration in (FloatSleepDuration(), IndexedSleepDuration()):
with assert_raises(ValueError) as error:
time.sleep(duration)
assert str(error.exception) == "sleep length must be non-negative"
with assert_raises(TypeError) as error:
time.sleep(InvalidIndexedSleepDuration())
assert str(error.exception) == "index conversion failed"
# The optional second argument fills the fields that are not part of the
# sequence.