mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
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:
1 parent
73ea4179d1
commit
d7b4be2978
9 files changed
+119
-80
No files matched your search
Vendored
+6
@@ -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:
|
||||
|
||||
Vendored
+18
-17
@@ -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):
|
||||
|
||||
Vendored
+1
-1
@@ -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]:
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user