From 1c64e403c4c7c1ef7a1aa13268c3b83dc5d4e822 Mon Sep 17 00:00:00 2001 From: Bastien Montagne Date: Mon, 29 Sep 2025 15:01:08 +0200 Subject: [PATCH] Fix #146802: Py-defined property min/max handling is broken. Regression from 469f54f484, somehow the clamping for default set of Int properties was removed in this big refactor, merely add it back, and add some unittests covering min/max handling. Pull Request: https://projects.blender.org/blender/blender/pulls/146975 --- source/blender/makesrna/intern/rna_access.cc | 5 +- tests/python/bl_pyapi_prop.py | 101 +++++++++++++++++-- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/source/blender/makesrna/intern/rna_access.cc b/source/blender/makesrna/intern/rna_access.cc index 0f62174153c..8f1c751f32a 100644 --- a/source/blender/makesrna/intern/rna_access.cc +++ b/source/blender/makesrna/intern/rna_access.cc @@ -3108,6 +3108,7 @@ void RNA_property_int_set(PointerRNA *ptr, PropertyRNA *prop, int value) } if (idprop) { + RNA_property_int_clamp(ptr, &iprop->property, &value); IDP_int_set(idprop, value); rna_idproperty_touch(idprop); } @@ -3119,6 +3120,7 @@ void RNA_property_int_set(PointerRNA *ptr, PropertyRNA *prop, int value) } else if (iprop->property.flag & PROP_EDITABLE) { if (IDProperty *group = RNA_struct_system_idprops(ptr, true)) { + RNA_property_int_clamp(ptr, &iprop->property, &value); IDP_AddToGroup( group, blender::bke::idprop::create(prop_rna_or_id.identifier, value, IDP_FLAG_STATIC_TYPE) @@ -3555,6 +3557,7 @@ void RNA_property_float_set(PointerRNA *ptr, PropertyRNA *prop, float value) } if (idprop) { + RNA_property_float_clamp(ptr, &fprop->property, &value); if (idprop->type == IDP_FLOAT) { IDP_float_set(idprop, value); } @@ -3570,8 +3573,6 @@ void RNA_property_float_set(PointerRNA *ptr, PropertyRNA *prop, float value) fprop->set_ex(ptr, &fprop->property, value); } else if (fprop->property.flag & PROP_EDITABLE) { - /* FIXME: This is only called here? What about already existing IDProps (see above)? And - * similar code for Int properties? */ RNA_property_float_clamp(ptr, &fprop->property, &value); if (IDProperty *group = RNA_struct_system_idprops(ptr, true)) { IDP_AddToGroup( diff --git a/tests/python/bl_pyapi_prop.py b/tests/python/bl_pyapi_prop.py index 3e8973cd00a..2baa106692f 100644 --- a/tests/python/bl_pyapi_prop.py +++ b/tests/python/bl_pyapi_prop.py @@ -38,11 +38,21 @@ class TestPropertyGroup(bpy.types.PropertyGroup): class TestPropNumerical(unittest.TestCase): default_value = 0 custom_value = 1 + min_value = -1 + max_value = 5 def setUp(self): id_type.test_bool = BoolProperty(default=bool(self.default_value)) - id_type.test_int = IntProperty(default=int(self.default_value)) - id_type.test_float = FloatProperty(default=float(self.default_value)) + id_type.test_int = IntProperty( + default=int(self.default_value), + min=int(self.min_value), + max=int(self.max_value), + ) + id_type.test_float = FloatProperty( + default=float(self.default_value), + min=float(self.min_value), + max=float(self.max_value), + ) self.test_bool_storage = bool(self.custom_value) self.test_int_storage = int(self.custom_value) @@ -64,11 +74,15 @@ class TestPropNumerical(unittest.TestCase): ) id_type.test_int_getset = IntProperty( default=int(self.default_value), + min=int(self.min_value), + max=int(self.max_value), get=lambda s: self.test_int_storage, set=int_set_, ) id_type.test_float_getset = FloatProperty( default=float(self.default_value), + min=float(self.min_value), + max=float(self.max_value), get=lambda s: self.test_float_storage, set=float_set_, ) @@ -80,11 +94,15 @@ class TestPropNumerical(unittest.TestCase): ) id_type.test_int_transform = IntProperty( default=int(self.default_value), + min=int(self.min_value), + max=int(self.max_value), get_transform=lambda s, c_v, isset: c_v, set_transform=lambda s, n_v, c_v, isset: n_v, ) id_type.test_float_transform = FloatProperty( default=float(self.default_value), + min=float(self.min_value), + max=float(self.max_value), get_transform=lambda s, c_v, isset: c_v, set_transform=lambda s, n_v, c_v, isset: n_v, ) @@ -98,6 +116,8 @@ class TestPropNumerical(unittest.TestCase): ) id_type.test_int_getset_transform = IntProperty( default=int(self.default_value), + min=int(self.min_value), + max=int(self.max_value), get=lambda s: self.test_int_storage, set=int_set_, get_transform=lambda s, c_v, isset: c_v, @@ -105,6 +125,8 @@ class TestPropNumerical(unittest.TestCase): ) id_type.test_float_getset_transform = FloatProperty( default=float(self.default_value), + min=float(self.min_value), + max=float(self.max_value), get=lambda s: self.test_float_storage, set=float_set_, get_transform=lambda s, c_v, isset: c_v, @@ -128,7 +150,22 @@ class TestPropNumerical(unittest.TestCase): del id_type.test_int_getset_transform del id_type.test_bool_getset_transform - def do_test_access(self, prop_name, py_type, expected_value): + def do_min_max_expect_success(self, prop_name, py_type): + # This property is expected to properly clamp set values within required range. + setattr(id_inst, prop_name, py_type(self.min_value - 1)) + self.assertEqual(getattr(id_inst, prop_name), py_type(self.min_value)) + setattr(id_inst, prop_name, py_type(self.max_value + 1)) + self.assertEqual(getattr(id_inst, prop_name), py_type(self.max_value)) + + def do_min_max_expect_failure(self, prop_name, py_type): + # This property is not expected to properly clamp set values within required range. + # This happens when using custom setters. + setattr(id_inst, prop_name, py_type(self.min_value - 1)) + self.assertNotEqual(getattr(id_inst, prop_name), py_type(self.min_value)) + setattr(id_inst, prop_name, py_type(self.max_value + 1)) + self.assertNotEqual(getattr(id_inst, prop_name), py_type(self.max_value)) + + def do_test_access(self, prop_name, py_type, expected_value, do_min_max=None): v = getattr(id_inst, prop_name) self.assertIsInstance(v, py_type) self.assertEqual(v, expected_value) @@ -136,42 +173,84 @@ class TestPropNumerical(unittest.TestCase): self.assertEqual(getattr(id_inst, prop_name), expected_value) setattr(id_inst, prop_name, py_type(self.custom_value)) self.assertEqual(getattr(id_inst, prop_name), py_type(self.custom_value)) + if do_min_max: + do_min_max(prop_name, py_type) def test_access_bool(self): self.do_test_access("test_bool", bool, bool(self.default_value)) def test_access_int(self): - self.do_test_access("test_int", int, int(self.default_value)) + self.do_test_access( + "test_int", + int, + int(self.default_value), + do_min_max=self.do_min_max_expect_success, + ) def test_access_float(self): - self.do_test_access("test_float", float, float(self.default_value)) + self.do_test_access( + "test_float", + float, + float(self.default_value), + do_min_max=self.do_min_max_expect_success, + ) def test_access_bool_getset(self): self.do_test_access("test_bool_getset", bool, bool(self.custom_value)) def test_access_int_getset(self): - self.do_test_access("test_int_getset", int, int(self.custom_value)) + self.do_test_access( + "test_int_getset", + int, + int(self.custom_value), + do_min_max=self.do_min_max_expect_failure, + ) def test_access_float_getset(self): - self.do_test_access("test_float_getset", float, float(self.custom_value)) + self.do_test_access( + "test_float_getset", + float, + float(self.custom_value), + do_min_max=self.do_min_max_expect_failure, + ) def test_access_bool_transform(self): self.do_test_access("test_bool_transform", bool, bool(self.default_value)) def test_access_int_transform(self): - self.do_test_access("test_int_transform", int, int(self.default_value)) + self.do_test_access( + "test_int_transform", + int, + int(self.default_value), + do_min_max=self.do_min_max_expect_success, + ) def test_access_float_transform(self): - self.do_test_access("test_float_transform", float, float(self.default_value)) + self.do_test_access( + "test_float_transform", + float, + float(self.default_value), + do_min_max=self.do_min_max_expect_success, + ) def test_access_bool_getset_transform(self): self.do_test_access("test_bool_getset_transform", bool, bool(self.custom_value)) def test_access_int_getset_transform(self): - self.do_test_access("test_int_getset_transform", int, int(self.custom_value)) + self.do_test_access( + "test_int_getset_transform", + int, + int(self.custom_value), + do_min_max=self.do_min_max_expect_failure, + ) def test_access_float_getset_transform(self): - self.do_test_access("test_float_getset_transform", float, float(self.custom_value)) + self.do_test_access( + "test_float_getset_transform", + float, + float(self.custom_value), + do_min_max=self.do_min_max_expect_failure, + ) # TODO: Add expected failure cases (e.g. handling of out-of range values).