mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 19:24:57 +08:00
Align builtin behavior and atexit callbacks with CPython (#9442)
* Align complex construction and code-assignment warnings with CPython Preserve constructor conversion and warning priority, and validate code kinds before assignment. Retire two canonical test markers after focused native verification. Assisted-by: OpenAI Codex: model version unavailable * Match CPython float power special-value ordering Handle NaNs, infinities, signed zero, exact fractional exponents, and signed underflow before generic finite power. Retire the canonical float-power expected-failure marker through update_lib after native verification. Assisted-by: OpenAI Codex: model version unavailable * Preserve atexit callback snapshot and continue after SystemExit Assisted-by: OpenAI Codex: model version unavailable --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
a0af5ff6b6
commit
a1f3d54bec
7 files changed
+218
-83
No files matched your search
Vendored
-1
@@ -458,7 +458,6 @@ class ComplexTest(ComplexesAreIdenticalMixin, unittest.TestCase):
|
||||
def test_conjugate(self):
|
||||
self.assertEqual(complex(5.3, 9.8).conjugate(), 5.3-9.8j)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: DeprecationWarning not triggered
|
||||
def test_constructor(self):
|
||||
def check(z, x, y):
|
||||
self.assertIs(type(z), complex)
|
||||
|
||||
Vendored
-1
@@ -431,7 +431,6 @@ class GeneralFloatCases(unittest.TestCase):
|
||||
self.assertEqualAndEqualSign(mod(1e-100, -1.0), -1.0)
|
||||
self.assertEqualAndEqualSign(mod(1.0, -1.0), -0.0)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: must be real number, not complex
|
||||
@support.requires_IEEE_754
|
||||
def test_float_pow(self):
|
||||
# test builtin pow and ** operator for IEEE 754 special cases.
|
||||
|
||||
Vendored
-1
@@ -72,7 +72,6 @@ class FunctionPropertiesTest(FuncAttrsTest):
|
||||
test.__code__ = self.b.__code__
|
||||
self.assertEqual(test(), 3) # self.b always returns 3, arbitrarily
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: DeprecationWarning not triggered
|
||||
def test_invalid___code___assignment(self):
|
||||
def A(): pass
|
||||
def B(): yield
|
||||
|
||||
@@ -97,6 +97,26 @@ impl PyObjectRef {
|
||||
if let Some(complex) = self.downcast_ref_if_exact::<PyComplex>(vm) {
|
||||
return Ok(Some((complex.as_complex(), true)));
|
||||
}
|
||||
if let Some(value) = self.try_complex_special_method(vm)? {
|
||||
return Ok(Some((value.as_complex(), true)));
|
||||
}
|
||||
// `complex` does not have a `__complex__` by default, so subclasses might not either,
|
||||
// use the actual stored value in this case
|
||||
if let Some(complex) = self.downcast_ref::<PyComplex>() {
|
||||
return Ok(Some((complex.as_complex(), true)));
|
||||
}
|
||||
|
||||
if let Some(float) = self.try_float_opt(vm) {
|
||||
return Ok(Some((Complex64::new(float?.to_f64(), 0.0), false)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn try_complex_special_method(
|
||||
&self,
|
||||
vm: &VirtualMachine,
|
||||
) -> PyResult<Option<PyRef<PyComplex>>> {
|
||||
if let Some(method) = vm.get_method(self.clone(), identifier!(vm, __complex__)) {
|
||||
let result = method?.call((), vm)?;
|
||||
|
||||
@@ -115,27 +135,17 @@ impl PyObjectRef {
|
||||
vm,
|
||||
)?;
|
||||
|
||||
return Ok(Some((ret.as_complex(), true)));
|
||||
return Ok(Some(ret.to_owned()));
|
||||
}
|
||||
|
||||
return match result.downcast_ref::<PyComplex>() {
|
||||
Some(complex_obj) => Ok(Some((complex_obj.as_complex(), true))),
|
||||
Some(complex_obj) => Ok(Some(complex_obj.to_owned())),
|
||||
None => Err(vm.new_type_error(format!(
|
||||
"__complex__ returned non-complex (type '{}')",
|
||||
result.class().name()
|
||||
))),
|
||||
};
|
||||
}
|
||||
// `complex` does not have a `__complex__` by default, so subclasses might not either,
|
||||
// use the actual stored value in this case
|
||||
if let Some(complex) = self.downcast_ref::<PyComplex>() {
|
||||
return Ok(Some((complex.as_complex(), true)));
|
||||
}
|
||||
|
||||
if let Some(float) = self.try_float_opt(vm) {
|
||||
return Ok(Some((Complex64::new(float?.to_f64(), 0.0), false)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -395,13 +405,17 @@ impl Constructor for PyComplex {
|
||||
type Args = ComplexArgs;
|
||||
|
||||
fn slot_new(cls: PyTypeRef, func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
|
||||
// Optimization: return exact complex as-is (only when imag is not provided)
|
||||
if cls.is(vm.ctx.types.complex_type)
|
||||
&& func_args.args.len() == 1
|
||||
&& func_args.kwargs.is_empty()
|
||||
&& func_args.args[0].class().is(vm.ctx.types.complex_type)
|
||||
{
|
||||
return Ok(func_args.args[0].clone());
|
||||
if func_args.kwargs.is_empty() && func_args.args.len() <= 1 {
|
||||
let payload = if let Some(value) = func_args.args.first() {
|
||||
if cls.is(vm.ctx.types.complex_type) && value.class().is(vm.ctx.types.complex_type)
|
||||
{
|
||||
return Ok(value.clone());
|
||||
}
|
||||
Self::from_single_argument(value, vm)?
|
||||
} else {
|
||||
Self::from(Complex64::new(0.0, 0.0))
|
||||
};
|
||||
return payload.into_ref_with_type(vm, cls).map(Into::into);
|
||||
}
|
||||
|
||||
let args: Self::Args = func_args.bind_for(vm, Self::NAME)?;
|
||||
@@ -411,35 +425,55 @@ impl Constructor for PyComplex {
|
||||
|
||||
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
|
||||
let imag_missing = args.imag.is_missing();
|
||||
let (real, real_was_complex) = match args.real {
|
||||
OptionalArg::Missing => (Complex64::new(0.0, 0.0), false),
|
||||
OptionalArg::Present(val) => {
|
||||
if let Some(c) = val.try_complex(vm)? {
|
||||
c
|
||||
} else if let Some(s) = val.downcast_ref::<PyStr>() {
|
||||
if args.imag.is_present() {
|
||||
return Err(vm.new_type_error(
|
||||
"complex() can't take second arg if first is a string",
|
||||
));
|
||||
}
|
||||
if has_misplaced_underscore(s.as_wtf8().as_bytes()) {
|
||||
let repr = val.repr(vm)?;
|
||||
return Err(vm.new_value_error(format!(
|
||||
"could not convert string to complex: {repr}"
|
||||
)));
|
||||
}
|
||||
let (re, im) = rustpython_literal::complex::parse_str(
|
||||
&crate::protocol::numeric_literal_from_str(s),
|
||||
)
|
||||
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
|
||||
return Ok(Self::from(Complex64 { re, im }));
|
||||
} else {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"complex() argument must be a string or a number, not {}",
|
||||
val.class().slot_name()
|
||||
)));
|
||||
}
|
||||
let original_real = match args.real {
|
||||
OptionalArg::Missing => vm.ctx.new_int(0).into(),
|
||||
OptionalArg::Present(value) => value,
|
||||
};
|
||||
let converted_real = original_real.try_complex_special_method(vm)?;
|
||||
if converted_real.is_none()
|
||||
&& original_real.downcast_ref::<Self>().is_none()
|
||||
&& !has_real_number_conversion(&original_real)
|
||||
{
|
||||
return Err(vm.new_type_error(format!(
|
||||
"complex() argument 'real' must be a real number, not {}",
|
||||
original_real.class().fully_qualified_name(vm)?
|
||||
)));
|
||||
}
|
||||
if let OptionalArg::Present(imag) = &args.imag
|
||||
&& imag.downcast_ref::<Self>().is_none()
|
||||
&& !has_real_number_conversion(imag)
|
||||
{
|
||||
return Err(vm.new_type_error(format!(
|
||||
"complex() argument 'imag' must be a real number, not {}",
|
||||
imag.class().fully_qualified_name(vm)?
|
||||
)));
|
||||
}
|
||||
|
||||
let complex_real = converted_real
|
||||
.as_ref()
|
||||
.map(|value| value.as_complex())
|
||||
.or_else(|| {
|
||||
original_real
|
||||
.downcast_ref::<Self>()
|
||||
.map(|value| value.as_complex())
|
||||
});
|
||||
let real_was_complex = complex_real.is_some();
|
||||
let real = if let Some(value) = complex_real {
|
||||
drop(converted_real);
|
||||
if !has_real_number_conversion(&original_real) {
|
||||
_warnings::warn(
|
||||
vm.ctx.exceptions.deprecation_warning,
|
||||
format!(
|
||||
"complex() argument 'real' must be a real number, not {}",
|
||||
original_real.class().fully_qualified_name(vm)?
|
||||
),
|
||||
1,
|
||||
vm,
|
||||
)?;
|
||||
}
|
||||
value
|
||||
} else {
|
||||
Complex64::new(original_real.try_float(vm)?.to_f64(), 0.0)
|
||||
};
|
||||
|
||||
let (imag, imag_was_complex) = match args.imag {
|
||||
@@ -447,15 +481,19 @@ impl Constructor for PyComplex {
|
||||
// if an imaginary argument is not passed in
|
||||
OptionalArg::Missing => (Complex64::new(real.im, 0.0), false),
|
||||
OptionalArg::Present(obj) => {
|
||||
if let Some(c) = obj.try_complex(vm)? {
|
||||
c
|
||||
} else if obj.class().fast_issubclass(vm.ctx.types.str_type) {
|
||||
return Err(vm.new_type_error("complex() second arg can't be a string"));
|
||||
if let Some(value) = obj.downcast_ref::<Self>() {
|
||||
_warnings::warn(
|
||||
vm.ctx.exceptions.deprecation_warning,
|
||||
format!(
|
||||
"complex() argument 'imag' must be a real number, not {}",
|
||||
obj.class().fully_qualified_name(vm)?
|
||||
),
|
||||
1,
|
||||
vm,
|
||||
)?;
|
||||
(value.as_complex(), true)
|
||||
} else {
|
||||
return Err(vm.new_type_error(format!(
|
||||
"complex() second argument must be a number, not '{}'",
|
||||
obj.class().name()
|
||||
)));
|
||||
(Complex64::new(obj.try_float(vm)?.to_f64(), 0.0), false)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -476,7 +514,37 @@ impl Constructor for PyComplex {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_real_number_conversion(value: &PyObject) -> bool {
|
||||
let number = &value.class().slots().as_number;
|
||||
number.float.load().is_some() || number.index.load().is_some()
|
||||
}
|
||||
|
||||
impl PyComplex {
|
||||
fn from_single_argument(value: &PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
|
||||
if let Some(string) = value.downcast_ref::<PyStr>() {
|
||||
if has_misplaced_underscore(string.as_wtf8().as_bytes()) {
|
||||
let repr = value.repr(vm)?;
|
||||
return Err(
|
||||
vm.new_value_error(format!("could not convert string to complex: {repr}"))
|
||||
);
|
||||
}
|
||||
let (re, im) = rustpython_literal::complex::parse_str(
|
||||
&crate::protocol::numeric_literal_from_str(string),
|
||||
)
|
||||
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
|
||||
return Ok(Self::from(Complex64 { re, im }));
|
||||
}
|
||||
value
|
||||
.try_complex(vm)?
|
||||
.map(|(value, _)| Self::from(value))
|
||||
.ok_or_else(|| {
|
||||
vm.new_type_error(format!(
|
||||
"complex() argument must be a string or a number, not {}",
|
||||
value.class().slot_name()
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[deprecated(note = "use PyComplex::from(...).into_ref() instead")]
|
||||
pub fn new_ref(value: Complex64, ctx: &Context) -> PyRef<Self> {
|
||||
Self::from(value).into_ref(ctx)
|
||||
@@ -763,10 +831,10 @@ impl Representable for PyComplex {
|
||||
|
||||
#[derive(FromArgs)]
|
||||
pub struct ComplexArgs {
|
||||
// Missing real is 0. A string real uses the imag-missing path.
|
||||
// Component construction defaults the missing real part to 0.
|
||||
#[pyarg(any, default, py_default = "0")]
|
||||
real: OptionalArg<PyObjectRef>,
|
||||
// Missing imag is 0 and is distinct from imag=0 when real is a string.
|
||||
// Missing imag retains the imaginary part of a converted real argument.
|
||||
#[pyarg(any, default, py_default = "0")]
|
||||
imag: OptionalArg<PyObjectRef>,
|
||||
}
|
||||
@@ -164,19 +164,70 @@ fn inner_divmod(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult<(f64, f64)> {
|
||||
}
|
||||
|
||||
pub(crate) fn float_pow(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult {
|
||||
if v1.is_zero() && v2.is_sign_negative() {
|
||||
Err(vm.new_zero_division_error("zero to a negative power"))
|
||||
} else if v1.is_sign_negative() && (v2.floor() - v2).abs() > f64::EPSILON {
|
||||
let v1 = Complex64::new(v1, 0.);
|
||||
let v2 = Complex64::new(v2, 0.);
|
||||
Ok(super::complex::complex_pow(v1, v2, vm)?.to_pyobject(vm))
|
||||
} else {
|
||||
let ans = v1.powf(v2);
|
||||
if ans.is_infinite() && !(v1.is_infinite() || v2.is_infinite()) {
|
||||
Err(vm.new_overflow_error("math range error"))
|
||||
// Resolve special values before finite negative bases or platform pow.
|
||||
if v2 == 0.0 {
|
||||
return Ok(1.0.to_pyobject(vm));
|
||||
}
|
||||
if v1.is_nan() {
|
||||
return Ok(v1.to_pyobject(vm));
|
||||
}
|
||||
if v2.is_nan() {
|
||||
return Ok(if v1 == 1.0 { 1.0 } else { v2 }.to_pyobject(vm));
|
||||
}
|
||||
if v2.is_infinite() {
|
||||
let magnitude = v1.abs();
|
||||
let result = if magnitude == 1.0 {
|
||||
1.0
|
||||
} else if (v2 > 0.0) == (magnitude > 1.0) {
|
||||
f64::INFINITY
|
||||
} else {
|
||||
Ok(ans.to_pyobject(vm))
|
||||
0.0
|
||||
};
|
||||
return Ok(result.to_pyobject(vm));
|
||||
}
|
||||
|
||||
let exponent_is_odd = v2.abs() % 2.0 == 1.0;
|
||||
if v1.is_infinite() {
|
||||
let result = if v2 > 0.0 {
|
||||
if exponent_is_odd { v1 } else { v1.abs() }
|
||||
} else if exponent_is_odd {
|
||||
0.0_f64.copysign(v1)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
return Ok(result.to_pyobject(vm));
|
||||
}
|
||||
if v1 == 0.0 {
|
||||
if v2 < 0.0 {
|
||||
return Err(vm.new_zero_division_error("zero to a negative power"));
|
||||
}
|
||||
return Ok(if exponent_is_odd { v1 } else { 0.0 }.to_pyobject(vm));
|
||||
}
|
||||
|
||||
let mut magnitude = v1;
|
||||
let negate_result = if v1 < 0.0 {
|
||||
if v2 != v2.floor() {
|
||||
let base = Complex64::new(v1, 0.0);
|
||||
let exponent = Complex64::new(v2, 0.0);
|
||||
return Ok(super::complex::complex_pow(base, exponent, vm)?.to_pyobject(vm));
|
||||
}
|
||||
magnitude = -v1;
|
||||
exponent_is_odd
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if magnitude == 1.0 {
|
||||
return Ok(if negate_result { -1.0 } else { 1.0 }.to_pyobject(vm));
|
||||
}
|
||||
|
||||
let mut result = magnitude.powf(v2);
|
||||
if negate_result {
|
||||
result = -result;
|
||||
}
|
||||
if result.is_infinite() {
|
||||
Err(vm.new_overflow_error("math range error"))
|
||||
} else {
|
||||
Ok(result.to_pyobject(vm))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1020,6 +1020,21 @@ impl Py<PyFunction> {
|
||||
n_free,
|
||||
)));
|
||||
}
|
||||
let mask = bytecode::CodeFlags::GENERATOR
|
||||
| bytecode::CodeFlags::COROUTINE
|
||||
| bytecode::CodeFlags::ASYNC_GENERATOR;
|
||||
let old_flags = self.code.flags;
|
||||
let new_flags = code.flags;
|
||||
if (old_flags & mask) != (new_flags & mask) {
|
||||
crate::stdlib::_warnings::warn(
|
||||
vm.ctx.exceptions.deprecation_warning,
|
||||
"Assigning a code object of non-matching type is deprecated \
|
||||
(e.g., from a generator to a plain function)"
|
||||
.to_owned(),
|
||||
1,
|
||||
vm,
|
||||
)?;
|
||||
}
|
||||
#[cfg(feature = "jit")]
|
||||
let mut jit_guard = self.jitted_code.lock();
|
||||
self.code.swap_to_temporary_refs(code, vm);
|
||||
|
||||
@@ -3,9 +3,7 @@ pub(crate) use atexit::module_def;
|
||||
|
||||
#[pymodule]
|
||||
mod atexit {
|
||||
use crate::{
|
||||
AsObject, PyObjectRef, PyResult, VirtualMachine, common::rc::PyRc, function::FuncArgs,
|
||||
};
|
||||
use crate::{PyObjectRef, PyResult, VirtualMachine, common::rc::PyRc, function::FuncArgs};
|
||||
|
||||
#[pyfunction]
|
||||
fn register(func: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyObjectRef {
|
||||
@@ -19,7 +17,11 @@ mod atexit {
|
||||
|
||||
#[pyfunction]
|
||||
fn _clear(vm: &VirtualMachine) {
|
||||
vm.state.atexit_funcs.lock().clear();
|
||||
let funcs = core::mem::take(&mut *vm.state.atexit_funcs.lock());
|
||||
// Like clearing the callback list, release entries backwards and unlocked.
|
||||
for entry in funcs.into_iter().rev() {
|
||||
drop(entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
@@ -71,22 +73,24 @@ mod atexit {
|
||||
|
||||
#[pyfunction]
|
||||
pub fn _run_exitfuncs(vm: &VirtualMachine) {
|
||||
let funcs: Vec<_> = core::mem::take(&mut *vm.state.atexit_funcs.lock());
|
||||
let funcs = vm.state.atexit_funcs.lock().clone();
|
||||
// Callbacks stored in LIFO order, iterate forward
|
||||
for entry in funcs {
|
||||
let (func, args) = PyRc::try_unwrap(entry).unwrap_or_else(|e| (*e).clone());
|
||||
if let Err(e) = func.call(args, vm) {
|
||||
let exit = e.fast_isinstance(vm.ctx.exceptions.system_exit);
|
||||
for entry in &funcs {
|
||||
let (func, args) = &**entry;
|
||||
if let Err(e) = func.call(args.clone(), vm) {
|
||||
let msg = func
|
||||
.repr(vm)
|
||||
.ok()
|
||||
.map(|r| format!("Exception ignored in atexit callback {}", r.as_wtf8()));
|
||||
vm.run_unraisable(e, msg, vm.ctx.none());
|
||||
if exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep the whole snapshot alive through every callback, then release it
|
||||
// before clearing the live registry, as atexit_callfuncs does.
|
||||
for entry in funcs.into_iter().rev() {
|
||||
drop(entry);
|
||||
}
|
||||
_clear(vm);
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
|
||||
Reference in new issue
Block a user