diff --git a/extra_tests/snippets/class.py b/extra_tests/snippets/class.py index 9fc2c28cb..d0ae4ef26 100644 --- a/extra_tests/snippets/class.py +++ b/extra_tests/snippets/class.py @@ -1,3 +1,4 @@ +from testutils import assert_raises __name__ = "class" @@ -287,3 +288,12 @@ assert type(type.mro).__name__ == 'method_descriptor' B('x', (int,), {}).x() A() + + +# Check that any appropriate callable can be used as metaclass key: +meta = lambda *a, **k: type(*a, **k) +class _(metaclass=meta): pass + +# check that non-callables don't fail on an absent prepare: +with assert_raises(TypeError, _msg="'int' object not callable"): + class _(metaclass=20): pass \ No newline at end of file diff --git a/vm/src/stdlib/builtins.rs b/vm/src/stdlib/builtins.rs index 0932193d8..4baf0a274 100644 --- a/vm/src/stdlib/builtins.rs +++ b/vm/src/stdlib/builtins.rs @@ -872,13 +872,16 @@ mod builtins { let bases: PyObjectRef = bases.into(); // Prepare uses full __getattribute__ resolution chain. - // TODO RUSTPYTHON: Doesn't do what we want all the time: - // e.g class M(metaclass=2): pass should fail with 'int not callable'. - let prepare = vm.get_attribute(metaclass.clone(), "__prepare__")?; - let namespace = vm.invoke( - &prepare, - FuncArgs::new(vec![name_obj.clone().into(), bases.clone()], kwargs.clone()), - )?; + // If an AttributeError was raised (None), return an empty dict, + // else invoke prepare callable. + let namespace = vm + .get_attribute_opt(metaclass.clone(), "__prepare__")? + .map_or(Ok(vm.ctx.new_dict().into()), |prepare| { + vm.invoke( + &prepare, + FuncArgs::new(vec![name_obj.clone().into(), bases.clone()], kwargs.clone()), + ) + })?; // Accept any PyMapping as namespace. let namespace = PyMapping::try_from_object(vm, namespace.clone()).map_err(|_| {