Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from functools import partial, partialmethod, cached_property
from graphlib import TopologicalSorter
from logging import LoggerAdapter, StreamHandler
from mailbox import Mailbox, _PartialFile
from mailbox import Mailbox
try:
import ctypes
except ImportError:
Expand Down Expand Up @@ -100,7 +100,9 @@

class BaseTest(unittest.TestCase):
"""Test basics."""
generic_types = [type, tuple, list, dict, set, frozenset, enumerate, memoryview,
generic_types = [type, tuple, list, dict, frozendict,
set, frozenset, enumerate, memoryview,
slice,
Comment on lines +103 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revert changes to generic-alias test data

For changes under Lib/test, adding frozendict and slice to generic_types changes the test data, while repository policy allows only expected-failure decorator edits. Revert this migrated type-list change rather than modifying the test inputs.

AGENTS.md reference: AGENTS.md:L278-L282

Useful? React with 👍 / 👎.

defaultdict, deque,
SequenceMatcher,
dircmp,
Expand All @@ -116,7 +118,7 @@ class BaseTest(unittest.TestCase):
Iterable, Iterator,
Reversible,
Container, Collection,
Mailbox, _PartialFile,
Mailbox,
ContextVar, Token,
Field,
Set, MutableSet,
Expand Down
34 changes: 17 additions & 17 deletions Lib/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def check_annotations(self, f):
print(f.__annotations__)

f.__annotate__ = lambda x: 42
with self.assertRaisesRegex(TypeError, r"__annotate__ returned non-dict of type 'int'"):
with self.assertRaisesRegex(TypeError, r"__annotate__\(\) must return a dict, not int"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the annotation test oracle

For changes under Lib/test, this assertRaisesRegex rewrite modifies a test assertion, but repository policy permits only adding or removing expected-failure decorators in test files. Restore the assertion and use an allowed marker if the implementation cannot satisfy it so the implementation change does not move its own oracle.

AGENTS.md reference: AGENTS.md:L278-L282

Useful? React with 👍 / 👎.

print(f.__annotations__)

f.__annotate__ = lambda x: {"x": x}
Expand Down Expand Up @@ -783,21 +783,6 @@ def test_non_name_annotations(self):


class RegressionTests(unittest.TestCase):
# gh-154902
def test_conditional_annotations_rebound(self):
# user code can rebind __conditional_annotations__ to any object
lefts = ("__conditional_annotations__",
'globals()["__conditional_annotations__"]')
values = ("0", "{}", "[]", "''", "object()", "frozenset()")
for left, value in itertools.product(lefts, values):
with self.subTest(left=left, value=value):
code = f"""
{left} = {value}
x: int
"""
with self.assertRaises(TypeError):
run_code(code)

# gh-132479
def test_complex_comprehension_inlining(self):
# Test that the various repro cases from the issue don't crash
Expand Down Expand Up @@ -871,9 +856,9 @@ def method(self, x: int):
"""
ns = run_code(code)
method = ns["Outer"].method
self.assertEqual(method.__annotate__.__qualname__, "Outer.method.__annotate__")
self.assertEqual(ns["f"].__annotate__.__qualname__, "f.__annotate__")
self.assertEqual(ns["f"]().__annotate__.__qualname__, "f.<locals>.nested.__annotate__")
self.assertEqual(method.__annotate__.__qualname__, "Outer.method.__annotate__")
self.assertEqual(ns["Outer"].__annotate__.__qualname__, "Outer.__annotate__")

# gh-138349
Expand Down Expand Up @@ -912,3 +897,18 @@ class Generic:
mod = build_module(code)
annos = mod.__annotations__
self.assertEqual(annos, {"annotated_name": 0})

# gh-154902
def test_conditional_annotations_rebound(self):
# user code can rebind __conditional_annotations__ to any object
lefts = ("__conditional_annotations__",
'globals()["__conditional_annotations__"]')
values = ("0", "{}", "[]", "''", "object()", "frozenset()")
for left, value in itertools.product(lefts, values):
with self.subTest(left=left, value=value):
code = f"""
{left} = {value}
x: int
"""
with self.assertRaises(TypeError):
run_code(code)
2 changes: 1 addition & 1 deletion benches/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use criterion::{
Bencher, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main,
measurement::WallTime,
};
use pyo3::types::{PyCode, PyCodeInput, PyCodeMethods, PyDict, PyDictMethods};
use pyo3::types::{PyCode, PyCodeMethods, PyDict, PyDictMethods, code::PyCodeInput};
use rustpython_compiler::Mode;
use rustpython_vm::{Interpreter, PyResult, Settings};
use std::{collections::HashMap, path::Path};
Expand Down
62 changes: 33 additions & 29 deletions crates/vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ impl PyFunction {
globals: PyRef<PyAnyDict>,
vm: &VirtualMachine,
) -> PyResult<Self> {
let name = PyMutex::new(code.obj_name.to_owned());
let name = PyMutex::new(code.co_name.clone());
let module = vm.unwrap_or_none(globals.inner_getitem_opt(identifier!(vm, __name__), vm)?);
let builtins = globals
.inner_getitem_opt(identifier!(vm, __builtins__), vm)?
Expand Down Expand Up @@ -1095,24 +1095,28 @@ impl Py<PyFunction> {
// Release locks before calling __annotate__ to avoid deadlock
if let Some(annotate_fn) = annotate_fn {
let one = vm.ctx.new_int(1);
let ann_dict = annotate_fn.call((one,), vm)?;
let ann_dict = ann_dict
.downcast::<crate::builtins::PyDict>()
.map_err(|obj| {
vm.new_type_error(format!(
"__annotate__ returned non-dict of type '{}'",
obj.class().name()
))
})?;
let result = annotate_fn.call((one,), vm);
drop(annotate_fn);
let ann_dict = match result?.downcast::<crate::builtins::PyDict>() {
Ok(dict) => dict,
Err(obj) => {
return Err(vm.new_type_error(format!(
"__annotate__() must return a dict, not {}",
obj.class().fully_qualified_name(vm)?
)));
}
};

// Cache the result
*self.annotations.lock() = Some(ann_dict.clone());
let old_annotations = self.annotations.lock().replace(ann_dict.clone());
drop(old_annotations);
return Ok(ann_dict);
}

// No __annotate__ or not callable, create empty dict
let new_dict = vm.ctx.new_dict();
*self.annotations.lock() = Some(new_dict.clone());
let old_annotations = self.annotations.lock().replace(new_dict.clone());
drop(old_annotations);
Ok(new_dict)
}

Expand All @@ -1122,23 +1126,19 @@ impl Py<PyFunction> {
value: PySetterValue<Option<PyObjectRef>>,
vm: &VirtualMachine,
) -> PyResult<()> {
match value {
let annotations = match value {
PySetterValue::Assign(Some(value)) => {
let annotations = value.downcast::<crate::builtins::PyDict>().map_err(|_| {
vm.new_type_error("__annotations__ must be set to a dict object")
})?;
*self.annotations.lock() = Some(annotations);
*self.annotate.lock() = None;
}
PySetterValue::Assign(None) => {
*self.annotations.lock() = None;
*self.annotate.lock() = None;
}
PySetterValue::Delete => {
// del only clears cached annotations; __annotate__ is preserved
*self.annotations.lock() = None;
Some(annotations)
}
}
PySetterValue::Assign(None) | PySetterValue::Delete => None,
};
let old_annotations = core::mem::replace(&mut *self.annotations.lock(), annotations);
drop(old_annotations);
let old_annotate = self.annotate.lock().take();
drop(old_annotate);
Ok(())
}

Expand Down Expand Up @@ -1171,16 +1171,20 @@ impl Py<PyFunction> {
if !value.is_callable() {
return Err(vm.new_type_error("__annotate__ must be callable or None"));
}
// Clear cached __annotations__ when __annotate__ is set
*self.annotations.lock() = None;
Some(value)
}
PySetterValue::Assign(None) => None,
PySetterValue::Delete => {
return Err(vm.new_type_error("__annotate__ cannot be deleted"));
}
};
*self.annotate.lock() = annotate;
let clear_annotations = annotate.is_some();
let old_annotate = core::mem::replace(&mut *self.annotate.lock(), annotate);
drop(old_annotate);
if clear_annotations {
let old_annotations = self.annotations.lock().take();
drop(old_annotations);
}
Ok(())
}

Expand Down Expand Up @@ -1288,7 +1292,7 @@ pub struct PyFunctionNewArgs {
#[pyarg(positional)]
globals: PyDictRef,
#[pyarg(any, optional, error_msg = "arg 3 (name) must be None or string")]
name: OptionalArg<PyStrRef>,
name: Option<PyStrRef>,
#[pyarg(any, optional, error_msg = "arg 4 (defaults) must be None or tuple")]
argdefs: Option<PyTupleRef>,
#[pyarg(any, optional, error_msg = "arg 5 (closure) must be None or tuple")]
Expand Down Expand Up @@ -1324,7 +1328,7 @@ impl Constructor for PyFunction {

let mut func = Self::new(args.code.clone(), args.globals.clone().into(), vm)?;
// Set function name if provided
if let Some(name) = args.name.into_option() {
if let Some(name) = args.name {
*func.name.lock() = name;
}
// Now set additional attributes directly
Expand Down
10 changes: 6 additions & 4 deletions crates/vm/src/builtins/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,10 +394,12 @@ impl PyModule {
// Call __annotate__(1) where 1 is FORMAT_VALUE
let result = annotate.call((1i32,), vm)?;
if !result.downcastable::<PyDict>() {
return Err(vm.new_type_error(format!(
"__annotate__ returned non-dict of type '{}'",
result.class().name()
)));
let error = result.class().fully_qualified_name(vm).map(|name| {
vm.new_type_error(format!("__annotate__() must return a dict, not {name}"))
});
drop(annotate);
drop(result);
return Err(error?);
}
result
} else {
Expand Down
10 changes: 4 additions & 6 deletions crates/vm/src/builtins/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,18 @@ impl PySlice {

Ok((start, stop, step))
}
}

// TODO: Uncomment when Python adds __class_getitem__ to slice
// #[pyclassmethod]
#[pyclass(with(Comparable, Representable, Hashable))]
impl Py<PySlice> {
#[pyclassmethod]
fn __class_getitem__(
cls: PyTypeRef,
args: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyGenericAlias> {
PyGenericAlias::from_args(cls, args, vm)
}
}

#[pyclass(with(Comparable, Representable, Hashable))]
impl Py<PySlice> {
#[pyslot]
fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
let slice: PySlice = match args.args.len() {
Expand Down
Loading
Loading