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:
youknowdotandCodex authored and GitHub committed 2026-10-29 10:01:24 +09:00
1 parent a0af5ff6b6
commit a1f3d54bec
7 files changed
+218 -83

No files matched your search

-1
View File
@@ -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)
-1
View File
@@ -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.
-1
View File
@@ -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
+125 -57
View File
@@ -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>,
}
+62 -11
View File
@@ -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))
}
}
+15
View File
@@ -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);
+16 -12
View File
@@ -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]