Skip to content

Commit a1f3d54

Browse files
youknowdotcodex
andauthored
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>
1 parent a0af5ff commit a1f3d54

7 files changed

Lines changed: 218 additions & 83 deletions

File tree

‎Lib/test/test_complex.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,6 @@ def test_boolcontext(self):
458458
def test_conjugate(self):
459459
self.assertEqual(complex(5.3, 9.8).conjugate(), 5.3-9.8j)
460460

461-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: DeprecationWarning not triggered
462461
def test_constructor(self):
463462
def check(z, x, y):
464463
self.assertIs(type(z), complex)

‎Lib/test/test_float.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ def test_float_mod(self):
431431
self.assertEqualAndEqualSign(mod(1e-100, -1.0), -1.0)
432432
self.assertEqualAndEqualSign(mod(1.0, -1.0), -0.0)
433433

434-
@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: must be real number, not complex
435434
@support.requires_IEEE_754
436435
def test_float_pow(self):
437436
# test builtin pow and ** operator for IEEE 754 special cases.

‎Lib/test/test_funcattrs.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ def test(): pass
7272
test.__code__ = self.b.__code__
7373
self.assertEqual(test(), 3) # self.b always returns 3, arbitrarily
7474

75-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: DeprecationWarning not triggered
7675
def test_invalid___code___assignment(self):
7776
def A(): pass
7877
def B(): yield

‎crates/vm/src/builtins/complex.rs‎

Lines changed: 125 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,26 @@ impl PyObjectRef {
9797
if let Some(complex) = self.downcast_ref_if_exact::<PyComplex>(vm) {
9898
return Ok(Some((complex.as_complex(), true)));
9999
}
100+
if let Some(value) = self.try_complex_special_method(vm)? {
101+
return Ok(Some((value.as_complex(), true)));
102+
}
103+
// `complex` does not have a `__complex__` by default, so subclasses might not either,
104+
// use the actual stored value in this case
105+
if let Some(complex) = self.downcast_ref::<PyComplex>() {
106+
return Ok(Some((complex.as_complex(), true)));
107+
}
108+
109+
if let Some(float) = self.try_float_opt(vm) {
110+
return Ok(Some((Complex64::new(float?.to_f64(), 0.0), false)));
111+
}
112+
113+
Ok(None)
114+
}
115+
116+
fn try_complex_special_method(
117+
&self,
118+
vm: &VirtualMachine,
119+
) -> PyResult<Option<PyRef<PyComplex>>> {
100120
if let Some(method) = vm.get_method(self.clone(), identifier!(vm, __complex__)) {
101121
let result = method?.call((), vm)?;
102122

@@ -115,27 +135,17 @@ impl PyObjectRef {
115135
vm,
116136
)?;
117137

118-
return Ok(Some((ret.as_complex(), true)));
138+
return Ok(Some(ret.to_owned()));
119139
}
120140

121141
return match result.downcast_ref::<PyComplex>() {
122-
Some(complex_obj) => Ok(Some((complex_obj.as_complex(), true))),
142+
Some(complex_obj) => Ok(Some(complex_obj.to_owned())),
123143
None => Err(vm.new_type_error(format!(
124144
"__complex__ returned non-complex (type '{}')",
125145
result.class().name()
126146
))),
127147
};
128148
}
129-
// `complex` does not have a `__complex__` by default, so subclasses might not either,
130-
// use the actual stored value in this case
131-
if let Some(complex) = self.downcast_ref::<PyComplex>() {
132-
return Ok(Some((complex.as_complex(), true)));
133-
}
134-
135-
if let Some(float) = self.try_float_opt(vm) {
136-
return Ok(Some((Complex64::new(float?.to_f64(), 0.0), false)));
137-
}
138-
139149
Ok(None)
140150
}
141151
}
@@ -395,13 +405,17 @@ impl Constructor for PyComplex {
395405
type Args = ComplexArgs;
396406

397407
fn slot_new(cls: PyTypeRef, func_args: FuncArgs, vm: &VirtualMachine) -> PyResult {
398-
// Optimization: return exact complex as-is (only when imag is not provided)
399-
if cls.is(vm.ctx.types.complex_type)
400-
&& func_args.args.len() == 1
401-
&& func_args.kwargs.is_empty()
402-
&& func_args.args[0].class().is(vm.ctx.types.complex_type)
403-
{
404-
return Ok(func_args.args[0].clone());
408+
if func_args.kwargs.is_empty() && func_args.args.len() <= 1 {
409+
let payload = if let Some(value) = func_args.args.first() {
410+
if cls.is(vm.ctx.types.complex_type) && value.class().is(vm.ctx.types.complex_type)
411+
{
412+
return Ok(value.clone());
413+
}
414+
Self::from_single_argument(value, vm)?
415+
} else {
416+
Self::from(Complex64::new(0.0, 0.0))
417+
};
418+
return payload.into_ref_with_type(vm, cls).map(Into::into);
405419
}
406420

407421
let args: Self::Args = func_args.bind_for(vm, Self::NAME)?;
@@ -411,51 +425,75 @@ impl Constructor for PyComplex {
411425

412426
fn py_new(_cls: &Py<PyType>, args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
413427
let imag_missing = args.imag.is_missing();
414-
let (real, real_was_complex) = match args.real {
415-
OptionalArg::Missing => (Complex64::new(0.0, 0.0), false),
416-
OptionalArg::Present(val) => {
417-
if let Some(c) = val.try_complex(vm)? {
418-
c
419-
} else if let Some(s) = val.downcast_ref::<PyStr>() {
420-
if args.imag.is_present() {
421-
return Err(vm.new_type_error(
422-
"complex() can't take second arg if first is a string",
423-
));
424-
}
425-
if has_misplaced_underscore(s.as_wtf8().as_bytes()) {
426-
let repr = val.repr(vm)?;
427-
return Err(vm.new_value_error(format!(
428-
"could not convert string to complex: {repr}"
429-
)));
430-
}
431-
let (re, im) = rustpython_literal::complex::parse_str(
432-
&crate::protocol::numeric_literal_from_str(s),
433-
)
434-
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
435-
return Ok(Self::from(Complex64 { re, im }));
436-
} else {
437-
return Err(vm.new_type_error(format!(
438-
"complex() argument must be a string or a number, not {}",
439-
val.class().slot_name()
440-
)));
441-
}
428+
let original_real = match args.real {
429+
OptionalArg::Missing => vm.ctx.new_int(0).into(),
430+
OptionalArg::Present(value) => value,
431+
};
432+
let converted_real = original_real.try_complex_special_method(vm)?;
433+
if converted_real.is_none()
434+
&& original_real.downcast_ref::<Self>().is_none()
435+
&& !has_real_number_conversion(&original_real)
436+
{
437+
return Err(vm.new_type_error(format!(
438+
"complex() argument 'real' must be a real number, not {}",
439+
original_real.class().fully_qualified_name(vm)?
440+
)));
441+
}
442+
if let OptionalArg::Present(imag) = &args.imag
443+
&& imag.downcast_ref::<Self>().is_none()
444+
&& !has_real_number_conversion(imag)
445+
{
446+
return Err(vm.new_type_error(format!(
447+
"complex() argument 'imag' must be a real number, not {}",
448+
imag.class().fully_qualified_name(vm)?
449+
)));
450+
}
451+
452+
let complex_real = converted_real
453+
.as_ref()
454+
.map(|value| value.as_complex())
455+
.or_else(|| {
456+
original_real
457+
.downcast_ref::<Self>()
458+
.map(|value| value.as_complex())
459+
});
460+
let real_was_complex = complex_real.is_some();
461+
let real = if let Some(value) = complex_real {
462+
drop(converted_real);
463+
if !has_real_number_conversion(&original_real) {
464+
_warnings::warn(
465+
vm.ctx.exceptions.deprecation_warning,
466+
format!(
467+
"complex() argument 'real' must be a real number, not {}",
468+
original_real.class().fully_qualified_name(vm)?
469+
),
470+
1,
471+
vm,
472+
)?;
442473
}
474+
value
475+
} else {
476+
Complex64::new(original_real.try_float(vm)?.to_f64(), 0.0)
443477
};
444478

445479
let (imag, imag_was_complex) = match args.imag {
446480
// Copy the imaginary from the real to the real of the imaginary
447481
// if an imaginary argument is not passed in
448482
OptionalArg::Missing => (Complex64::new(real.im, 0.0), false),
449483
OptionalArg::Present(obj) => {
450-
if let Some(c) = obj.try_complex(vm)? {
451-
c
452-
} else if obj.class().fast_issubclass(vm.ctx.types.str_type) {
453-
return Err(vm.new_type_error("complex() second arg can't be a string"));
484+
if let Some(value) = obj.downcast_ref::<Self>() {
485+
_warnings::warn(
486+
vm.ctx.exceptions.deprecation_warning,
487+
format!(
488+
"complex() argument 'imag' must be a real number, not {}",
489+
obj.class().fully_qualified_name(vm)?
490+
),
491+
1,
492+
vm,
493+
)?;
494+
(value.as_complex(), true)
454495
} else {
455-
return Err(vm.new_type_error(format!(
456-
"complex() second argument must be a number, not '{}'",
457-
obj.class().name()
458-
)));
496+
(Complex64::new(obj.try_float(vm)?.to_f64(), 0.0), false)
459497
}
460498
}
461499
};
@@ -476,7 +514,37 @@ impl Constructor for PyComplex {
476514
}
477515
}
478516

517+
fn has_real_number_conversion(value: &PyObject) -> bool {
518+
let number = &value.class().slots().as_number;
519+
number.float.load().is_some() || number.index.load().is_some()
520+
}
521+
479522
impl PyComplex {
523+
fn from_single_argument(value: &PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
524+
if let Some(string) = value.downcast_ref::<PyStr>() {
525+
if has_misplaced_underscore(string.as_wtf8().as_bytes()) {
526+
let repr = value.repr(vm)?;
527+
return Err(
528+
vm.new_value_error(format!("could not convert string to complex: {repr}"))
529+
);
530+
}
531+
let (re, im) = rustpython_literal::complex::parse_str(
532+
&crate::protocol::numeric_literal_from_str(string),
533+
)
534+
.ok_or_else(|| vm.new_value_error("complex() arg is a malformed string"))?;
535+
return Ok(Self::from(Complex64 { re, im }));
536+
}
537+
value
538+
.try_complex(vm)?
539+
.map(|(value, _)| Self::from(value))
540+
.ok_or_else(|| {
541+
vm.new_type_error(format!(
542+
"complex() argument must be a string or a number, not {}",
543+
value.class().slot_name()
544+
))
545+
})
546+
}
547+
480548
#[deprecated(note = "use PyComplex::from(...).into_ref() instead")]
481549
pub fn new_ref(value: Complex64, ctx: &Context) -> PyRef<Self> {
482550
Self::from(value).into_ref(ctx)
@@ -763,10 +831,10 @@ impl Representable for PyComplex {
763831

764832
#[derive(FromArgs)]
765833
pub struct ComplexArgs {
766-
// Missing real is 0. A string real uses the imag-missing path.
834+
// Component construction defaults the missing real part to 0.
767835
#[pyarg(any, default, py_default = "0")]
768836
real: OptionalArg<PyObjectRef>,
769-
// Missing imag is 0 and is distinct from imag=0 when real is a string.
837+
// Missing imag retains the imaginary part of a converted real argument.
770838
#[pyarg(any, default, py_default = "0")]
771839
imag: OptionalArg<PyObjectRef>,
772840
}

‎crates/vm/src/builtins/float.rs‎

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,70 @@ fn inner_divmod(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult<(f64, f64)> {
164164
}
165165

166166
pub(crate) fn float_pow(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult {
167-
if v1.is_zero() && v2.is_sign_negative() {
168-
Err(vm.new_zero_division_error("zero to a negative power"))
169-
} else if v1.is_sign_negative() && (v2.floor() - v2).abs() > f64::EPSILON {
170-
let v1 = Complex64::new(v1, 0.);
171-
let v2 = Complex64::new(v2, 0.);
172-
Ok(super::complex::complex_pow(v1, v2, vm)?.to_pyobject(vm))
173-
} else {
174-
let ans = v1.powf(v2);
175-
if ans.is_infinite() && !(v1.is_infinite() || v2.is_infinite()) {
176-
Err(vm.new_overflow_error("math range error"))
167+
// Resolve special values before finite negative bases or platform pow.
168+
if v2 == 0.0 {
169+
return Ok(1.0.to_pyobject(vm));
170+
}
171+
if v1.is_nan() {
172+
return Ok(v1.to_pyobject(vm));
173+
}
174+
if v2.is_nan() {
175+
return Ok(if v1 == 1.0 { 1.0 } else { v2 }.to_pyobject(vm));
176+
}
177+
if v2.is_infinite() {
178+
let magnitude = v1.abs();
179+
let result = if magnitude == 1.0 {
180+
1.0
181+
} else if (v2 > 0.0) == (magnitude > 1.0) {
182+
f64::INFINITY
183+
} else {
184+
0.0
185+
};
186+
return Ok(result.to_pyobject(vm));
187+
}
188+
189+
let exponent_is_odd = v2.abs() % 2.0 == 1.0;
190+
if v1.is_infinite() {
191+
let result = if v2 > 0.0 {
192+
if exponent_is_odd { v1 } else { v1.abs() }
193+
} else if exponent_is_odd {
194+
0.0_f64.copysign(v1)
177195
} else {
178-
Ok(ans.to_pyobject(vm))
196+
0.0
197+
};
198+
return Ok(result.to_pyobject(vm));
199+
}
200+
if v1 == 0.0 {
201+
if v2 < 0.0 {
202+
return Err(vm.new_zero_division_error("zero to a negative power"));
203+
}
204+
return Ok(if exponent_is_odd { v1 } else { 0.0 }.to_pyobject(vm));
205+
}
206+
207+
let mut magnitude = v1;
208+
let negate_result = if v1 < 0.0 {
209+
if v2 != v2.floor() {
210+
let base = Complex64::new(v1, 0.0);
211+
let exponent = Complex64::new(v2, 0.0);
212+
return Ok(super::complex::complex_pow(base, exponent, vm)?.to_pyobject(vm));
179213
}
214+
magnitude = -v1;
215+
exponent_is_odd
216+
} else {
217+
false
218+
};
219+
if magnitude == 1.0 {
220+
return Ok(if negate_result { -1.0 } else { 1.0 }.to_pyobject(vm));
221+
}
222+
223+
let mut result = magnitude.powf(v2);
224+
if negate_result {
225+
result = -result;
226+
}
227+
if result.is_infinite() {
228+
Err(vm.new_overflow_error("math range error"))
229+
} else {
230+
Ok(result.to_pyobject(vm))
180231
}
181232
}
182233

‎crates/vm/src/builtins/function.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,21 @@ impl Py<PyFunction> {
10201020
n_free,
10211021
)));
10221022
}
1023+
let mask = bytecode::CodeFlags::GENERATOR
1024+
| bytecode::CodeFlags::COROUTINE
1025+
| bytecode::CodeFlags::ASYNC_GENERATOR;
1026+
let old_flags = self.code.flags;
1027+
let new_flags = code.flags;
1028+
if (old_flags & mask) != (new_flags & mask) {
1029+
crate::stdlib::_warnings::warn(
1030+
vm.ctx.exceptions.deprecation_warning,
1031+
"Assigning a code object of non-matching type is deprecated \
1032+
(e.g., from a generator to a plain function)"
1033+
.to_owned(),
1034+
1,
1035+
vm,
1036+
)?;
1037+
}
10231038
#[cfg(feature = "jit")]
10241039
let mut jit_guard = self.jitted_code.lock();
10251040
self.code.swap_to_temporary_refs(code, vm);

0 commit comments

Comments
 (0)