diff --git a/Lib/encodings/__init__.py b/Lib/encodings/__init__.py index 8548bbe04..18f5da70c 100644 --- a/Lib/encodings/__init__.py +++ b/Lib/encodings/__init__.py @@ -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: diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index fd21c7b05..bcaffb79c 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -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): diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index a14baa7a7..298c45faf 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -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]: diff --git a/crates/vm/src/builtins/mappingproxy.rs b/crates/vm/src/builtins/mappingproxy.rs index 7342fb3c0..3e53f1729 100644 --- a/crates/vm/src/builtins/mappingproxy.rs +++ b/crates/vm/src/builtins/mappingproxy.rs @@ -193,9 +193,11 @@ impl Py { 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) } diff --git a/crates/vm/src/codecs.rs b/crates/vm/src/codecs.rs index f3b6b1334..32724984e 100644 --- a/crates/vm/src/codecs.rs +++ b/crates/vm/src/codecs.rs @@ -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 { 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 { - 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::() + .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. diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 94c469b53..867891eff 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -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::() { - 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::() { - 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")); diff --git a/extra_tests/snippets/builtin_mappingproxy.py b/extra_tests/snippets/builtin_mappingproxy.py index c4d302136..8adf4d7e9 100644 --- a/extra_tests/snippets/builtin_mappingproxy.py +++ b/extra_tests/snippets/builtin_mappingproxy.py @@ -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() diff --git a/extra_tests/snippets/stdlib_codecs_wide.py b/extra_tests/snippets/stdlib_codecs_wide.py index 4b950ba70..0c45cc91c 100644 --- a/extra_tests/snippets/stdlib_codecs_wide.py +++ b/extra_tests/snippets/stdlib_codecs_wide.py @@ -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, diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index b5f3a944d..614d96105 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -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.