From 1e2d36b98e62456f27add59170edfaaffda4b0d0 Mon Sep 17 00:00:00 2001 From: frmdstryr Date: Sat, 23 May 2026 14:04:05 -0400 Subject: [PATCH 1/3] Add BoolPromote validation mode --- atom/scalars.py | 14 +++++++++++--- atom/src/behaviors.h | 1 + atom/src/enumtypes.cpp | 1 + atom/src/validatebehavior.cpp | 17 +++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/atom/scalars.py b/atom/scalars.py index 8b89b519..d8b6f9a6 100644 --- a/atom/scalars.py +++ b/atom/scalars.py @@ -82,13 +82,21 @@ def __init__(self, default=None, *, factory=None): class Bool(Value): - """A value of type `bool`.""" + """A value of type `bool`. + + By default, bools are strictly typed. Pass strict=False to the + constructor to use the result truthyness of the value (eg bool(value)). + + """ __slots__ = () - def __init__(self, default=False, *, factory=None): + def __init__(self, default=False, *, factory=None, strict=True): super(Bool, self).__init__(default, factory=factory) - self.set_validate_mode(Validate.Bool, None) + if strict: + self.set_validate_mode(Validate.Bool, None) + else: + self.set_validate_mode(Validate.BoolPromote, None) class Int(Value): diff --git a/atom/src/behaviors.h b/atom/src/behaviors.h index 4c466d79..5e55a2f1 100644 --- a/atom/src/behaviors.h +++ b/atom/src/behaviors.h @@ -123,6 +123,7 @@ enum Mode: uint8_t { NoOp, Bool, + BoolPromote, Int, IntPromote, Float, diff --git a/atom/src/enumtypes.cpp b/atom/src/enumtypes.cpp index adf658fb..2d2f46c0 100644 --- a/atom/src/enumtypes.cpp +++ b/atom/src/enumtypes.cpp @@ -258,6 +258,7 @@ bool init_enumtypes() } add_long( dict_ptr, expand_enum( NoOp ) ); add_long( dict_ptr, expand_enum( Bool ) ); + add_long( dict_ptr, expand_enum( BoolPromote ) ); add_long( dict_ptr, expand_enum( Int ) ); add_long( dict_ptr, expand_enum( IntPromote ) ); add_long( dict_ptr, expand_enum( Float ) ); diff --git a/atom/src/validatebehavior.cpp b/atom/src/validatebehavior.cpp index e6da684b..337d1461 100644 --- a/atom/src/validatebehavior.cpp +++ b/atom/src/validatebehavior.cpp @@ -336,6 +336,22 @@ bool_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalu } +PyObject* +bool_promote_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue ) +{ + switch ( PyObject_IsTrue( newvalue ) ) + { + case 0: + Py_RETURN_FALSE; + case 1: + Py_RETURN_TRUE; + default: + return 0; + } + +} + + PyObject* long_handler( Member* member, CAtom* atom, PyObject* oldvalue, PyObject* newvalue ) { @@ -982,6 +998,7 @@ static handler handlers[] = { no_op_handler, bool_handler, + bool_promote_handler, long_handler, long_promote_handler, float_handler, From 9a79aeed1e26618845ecf2c31ef3b81f5d2d41ef Mon Sep 17 00:00:00 2001 From: frmdstryr Date: Fri, 29 May 2026 07:26:07 -0400 Subject: [PATCH 2/3] Refactor member modes and index into single info packed struct --- atom/src/delattrbehavior.cpp | 6 ++--- atom/src/getattrbehavior.cpp | 10 ++++---- atom/src/getstatebehavior.cpp | 4 ++-- atom/src/member.cpp | 21 ++++++++-------- atom/src/member.h | 45 ++++++++++++++++++----------------- atom/src/propertyhelper.cpp | 6 ++--- atom/src/setattrbehavior.cpp | 10 ++++---- tests/test_member.py | 7 ++++++ tests/test_validators.py | 7 ++++++ 9 files changed, 66 insertions(+), 50 deletions(-) diff --git a/atom/src/delattrbehavior.cpp b/atom/src/delattrbehavior.cpp index 35859558..504b2246 100644 --- a/atom/src/delattrbehavior.cpp +++ b/atom/src/delattrbehavior.cpp @@ -67,7 +67,7 @@ deleted_args( CAtom* atom, Member* member, PyObject* value ) int slot_handler( Member* member, CAtom* atom ) { - if( member->index >= atom->get_slot_count() ) + if( member->info.index >= atom->get_slot_count() ) { cppy::attribute_error( pyobject_cast( atom ), (char const *)PyUnicode_AsUTF8( member->name ) ); return -1; @@ -77,10 +77,10 @@ slot_handler( Member* member, CAtom* atom ) PyErr_SetString( PyExc_AttributeError, "can't delete attribute of frozen Atom" ); return -1; } - cppy::ptr valueptr( atom->get_slot( member->index ) ); + cppy::ptr valueptr( atom->get_slot( member->info.index ) ); if( !valueptr ) return 0; - atom->set_slot( member->index, 0 ); + atom->set_slot( member->info.index, 0 ); if( atom->get_notifications_enabled() ) { cppy::ptr argsptr; diff --git a/atom/src/getattrbehavior.cpp b/atom/src/getattrbehavior.cpp index 809724bb..6c44212f 100644 --- a/atom/src/getattrbehavior.cpp +++ b/atom/src/getattrbehavior.cpp @@ -87,9 +87,9 @@ created_args( CAtom* atom, Member* member, PyObject* value ) PyObject* slot_handler( Member* member, CAtom* atom ) { - if( member->index >= atom->get_slot_count() ) + if( member->info.index >= atom->get_slot_count() ) return cppy::attribute_error( pyobject_cast( atom ), (char const *)PyUnicode_AsUTF8( member->name ) ); - cppy::ptr value( atom->get_slot( member->index ) ); + cppy::ptr value( atom->get_slot( member->info.index ) ); if( value ) { if( member->get_post_getattr_mode() ) @@ -102,7 +102,7 @@ slot_handler( Member* member, CAtom* atom ) value = member->full_validate( atom, Py_None, value.get() ); if( !value ) return 0; - atom->set_slot( member->index, value.get() ); + atom->set_slot( member->info.index, value.get() ); if( atom->get_notifications_enabled() ) { cppy::ptr argsptr; @@ -179,11 +179,11 @@ property_handler( Member* member, CAtom* atom ) PyObject* cached_property_handler( Member* member, CAtom* atom ) { - cppy::ptr value( atom->get_slot( member->index ) ); + cppy::ptr value( atom->get_slot( member->info.index ) ); if( value ) return value.release(); value = property_handler( member, atom ); - atom->set_slot( member->index, value.get() ); // exception-safe + atom->set_slot( member->info.index, value.get() ); // exception-safe return value.release(); } diff --git a/atom/src/getstatebehavior.cpp b/atom/src/getstatebehavior.cpp index ff5a8745..7bb2ac9c 100644 --- a/atom/src/getstatebehavior.cpp +++ b/atom/src/getstatebehavior.cpp @@ -53,10 +53,10 @@ include_handler( Member* member, CAtom* atom ) PyObject* include_non_default_handler( Member* member, CAtom* atom ) { - if( member->index >= atom->get_slot_count() ) { + if( member->info.index >= atom->get_slot_count() ) { return cppy::attribute_error( pyobject_cast( atom ), (char const *)PyUnicode_AsUTF8( member->name ) ); } - cppy::ptr value( atom->get_slot( member->index ) ); + cppy::ptr value( atom->get_slot( member->info.index ) ); if( value ) { return cppy::incref( Py_True ); } diff --git a/atom/src/member.cpp b/atom/src/member.cpp index 31e09504..c4db031b 100644 --- a/atom/src/member.cpp +++ b/atom/src/member.cpp @@ -219,9 +219,9 @@ Member_get_slot( Member* self, PyObject* object ) if( !CAtom::TypeCheck( object ) ) return cppy::type_error( object, "CAtom" ); CAtom* atom = catom_cast( object ); - if( self->index >= atom->get_slot_count() ) + if( self->info.index >= atom->get_slot_count() ) return cppy::attribute_error( object, (char *)PyUnicode_AsUTF8( self->name ) ); - cppy::ptr value( atom->get_slot( self->index ) ); + cppy::ptr value( atom->get_slot( self->info.index ) ); if( value ) return value.release(); Py_RETURN_NONE; @@ -238,9 +238,9 @@ Member_set_slot( Member* self, PyObject*const *args, Py_ssize_t n ) if( !CAtom::TypeCheck( object ) ) return cppy::type_error( object, "CAtom" ); CAtom* atom = catom_cast( object ); - if( self->index >= atom->get_slot_count() ) + if( self->info.index >= atom->get_slot_count() ) return cppy::attribute_error( object, (char *)PyUnicode_AsUTF8( self->name ) ); - atom->set_slot( self->index, value ); + atom->set_slot( self->info.index, value ); Py_RETURN_NONE; } @@ -251,9 +251,9 @@ Member_del_slot( Member* self, PyObject* object ) if( !CAtom::TypeCheck( object ) ) return cppy::type_error( object, "CAtom" ); CAtom* atom = catom_cast( object ); - if( self->index >= atom->get_slot_count() ) + if( self->info.index >= atom->get_slot_count() ) return cppy::attribute_error( object, (char *)PyUnicode_AsUTF8( self->name ) ); - atom->set_slot( self->index, 0 ); + atom->set_slot( self->info.index, 0 ); Py_RETURN_NONE; } @@ -390,8 +390,7 @@ Member_clone( Member* self ) if( !pyclone ) return 0; Member* clone = member_cast( pyclone ); - clone->modes = self->modes; - clone->index = self->index; + clone->info = self->info; clone->name = cppy::incref( self->name ); if( self->metadata ) clone->metadata = PyDict_Copy( self->metadata ); @@ -437,7 +436,7 @@ Member_set_name( Member* self, PyObject* value ) PyObject* Member_get_index( Member* self, void* context ) { - return PyLong_FromSsize_t( static_cast( self->index ) ); + return PyLong_FromSsize_t( static_cast( self->info.index ) ); } @@ -449,7 +448,9 @@ Member_set_index( Member* self, PyObject* value ) Py_ssize_t index = PyLong_AsSsize_t( value ); if( index < 0 && PyErr_Occurred() ) return 0; - self->index = static_cast( index < 0 ? 0 : index ); + if ( index < 0 || index >= 0xFFFF ) + return cppy::type_error("index must be in the range 0 to 0xFFFF "); + self->info.index = static_cast( index ); Py_RETURN_NONE; } diff --git a/atom/src/member.h b/atom/src/member.h index ac60fbd5..f921ea5b 100644 --- a/atom/src/member.h +++ b/atom/src/member.h @@ -25,17 +25,19 @@ namespace atom { -PACK(struct MemberModes +PACK(struct MemberInfo { GetAttr::Mode getattr: 4; PostGetAttr::Mode post_getattr: 3; SetAttr::Mode setattr: 4; PostSetAttr::Mode post_setattr: 3; DefaultValue::Mode default_value: 4; - Validate::Mode validate: 5; + Validate::Mode validate: 6; PostValidate::Mode post_validate: 3; DelAttr::Mode delattr: 3; GetState::Mode getstate: 3; + uint16_t index: 16; + uint16_t reserved: 15; }); struct Member @@ -54,8 +56,7 @@ struct Member PyObject* getstate_context; ModifyGuard* modify_guard; std::vector* static_observers; - MemberModes modes; - uint32_t index; + MemberInfo info; static PyType_Spec TypeObject_Spec; @@ -69,92 +70,92 @@ struct Member GetAttr::Mode get_getattr_mode() { - return modes.getattr; + return info.getattr; } void set_getattr_mode( GetAttr::Mode mode ) { - modes.getattr = mode; + info.getattr = mode; } SetAttr::Mode get_setattr_mode() { - return modes.setattr; + return info.setattr; } void set_setattr_mode( SetAttr::Mode mode ) { - modes.setattr = mode; + info.setattr = mode; } PostGetAttr::Mode get_post_getattr_mode() { - return modes.post_getattr; + return info.post_getattr; } void set_post_getattr_mode( PostGetAttr::Mode mode ) { - modes.post_getattr = mode; + info.post_getattr = mode; } PostSetAttr::Mode get_post_setattr_mode() { - return modes.post_setattr; + return info.post_setattr; } void set_post_setattr_mode( PostSetAttr::Mode mode ) { - modes.post_setattr = mode; + info.post_setattr = mode; } DefaultValue::Mode get_default_value_mode() { - return modes.default_value; + return info.default_value; } void set_default_value_mode( DefaultValue::Mode mode ) { - modes.default_value = mode; + info.default_value = mode; } Validate::Mode get_validate_mode() { - return modes.validate; + return info.validate; } void set_validate_mode( Validate::Mode mode ) { - modes.validate = mode; + info.validate = mode; } PostValidate::Mode get_post_validate_mode() { - return modes.post_validate; + return info.post_validate; } void set_post_validate_mode( PostValidate::Mode mode ) { - modes.post_validate = mode; + info.post_validate = mode; } DelAttr::Mode get_delattr_mode() { - return modes.delattr; + return info.delattr; } void set_delattr_mode( DelAttr::Mode mode ) { - modes.delattr = mode; + info.delattr = mode; } GetState::Mode get_getstate_mode() { - return modes.getstate; + return info.getstate; } void set_getstate_mode( GetState::Mode mode ) { - modes.getstate = mode; + info.getstate = mode; } PyObject* getattr( CAtom* atom ); diff --git a/atom/src/propertyhelper.cpp b/atom/src/propertyhelper.cpp index dd95b8ee..744fac6c 100644 --- a/atom/src/propertyhelper.cpp +++ b/atom/src/propertyhelper.cpp @@ -61,12 +61,12 @@ reset_property( PyObject* mod, PyObject* args ) } Member* member = member_cast( pymember ); CAtom* atom = catom_cast( pyatom ); - if( member->index >= atom->get_slot_count() ) + if( member->info.index >= atom->get_slot_count() ) { return cppy::system_error( "invalid member index" ); } - cppy::ptr oldptr( atom->get_slot( member->index ) ); - atom->set_slot( member->index, 0 ); + cppy::ptr oldptr( atom->get_slot( member->info.index ) ); + atom->set_slot( member->info.index, 0 ); bool has_static = member->has_observers( ChangeType::Property ); bool has_dynamic = atom->has_observers( member->name ); if( has_static || has_dynamic ) diff --git a/atom/src/setattrbehavior.cpp b/atom/src/setattrbehavior.cpp index 81fbfa21..e0064bcc 100644 --- a/atom/src/setattrbehavior.cpp +++ b/atom/src/setattrbehavior.cpp @@ -100,7 +100,7 @@ updated_args( CAtom* atom, Member* member, PyObject* oldvalue, PyObject* newvalu int slot_handler( Member* member, CAtom* atom, PyObject* value ) { - if( member->index >= atom->get_slot_count() ) + if( member->info.index >= atom->get_slot_count() ) { cppy::attribute_error( pyobject_cast( atom ), (char *)PyUnicode_AsUTF8( member->name ) ); return -1; @@ -110,7 +110,7 @@ slot_handler( Member* member, CAtom* atom, PyObject* value ) PyErr_SetString( PyExc_AttributeError, "can't set attribute of frozen Atom" ); return -1; } - cppy::ptr oldptr( atom->get_slot( member->index ) ); + cppy::ptr oldptr( atom->get_slot( member->info.index ) ); cppy::ptr newptr( cppy::incref( value ) ); if( oldptr == newptr ) return 0; @@ -120,7 +120,7 @@ slot_handler( Member* member, CAtom* atom, PyObject* value ) newptr = member->full_validate( atom, oldptr.get(), newptr.get() ); if( !newptr ) return -1; - atom->set_slot( member->index, newptr.get() ); + atom->set_slot( member->info.index, newptr.get() ); if( member->get_post_setattr_mode() ) { if( member->post_setattr( atom, oldptr.get(), newptr.get() ) < 0 ) @@ -183,12 +183,12 @@ constant_handler( Member* member, CAtom* atom, PyObject* value ) int read_only_handler( Member* member, CAtom* atom, PyObject* value ) { - if( member->index >= atom->get_slot_count() ) + if( member->info.index >= atom->get_slot_count() ) { cppy::attribute_error( pyobject_cast( atom ), (char *)PyUnicode_AsUTF8( member->name ) ); return -1; } - cppy::ptr slot( atom->get_slot( member->index ) ); + cppy::ptr slot( atom->get_slot( member->info.index ) ); if( slot ) { cppy::type_error( "cannot change the value of a read only member" ); diff --git a/tests/test_member.py b/tests/test_member.py index 2bb7668a..d1fe184b 100644 --- a/tests/test_member.py +++ b/tests/test_member.py @@ -150,6 +150,13 @@ class IndexTest(Atom): IndexTest.v1.set_index("") assert "int" in excinfo.exconly() + with pytest.raises(TypeError) as excinfo: + IndexTest.v1.set_index(-1) + assert "range 0 to 0xFFFF" in excinfo.exconly() + with pytest.raises(TypeError) as excinfo: + IndexTest.v1.set_index(2**16) + assert "range 0 to 0xFFFF" in excinfo.exconly() + def test_metadata_handling(): """Test writing and accessing the metadata of a Member.""" diff --git a/tests/test_validators.py b/tests/test_validators.py index 53d37ca2..554e8f63 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -9,6 +9,7 @@ no_op_handler bool_handler +bool_promote_handler int_handler int_promote_handler long_handler @@ -99,6 +100,12 @@ def c(x: object) -> int: (Value(), ["a", 1, None], ["a", 1, None], []), (ReadOnly(int), [1], [1], [1.0]), (Bool(), [True, False], [True, False], "r"), + ( + Bool(strict=False), + [True, False, 1, 0, None, "y", ""], + [True, False, True, False, False, True, False], + [], + ), (Int(strict=True), [1], [1], [1.0]), (Int(strict=False), [1, 1.0, (1)], 3 * [1], ["a"]), (Range(0, 2), [0, 2], [0, 2], [-1, 3, ""]), From ed6e09d34c02a463f2f98d0503b8d28031884f78 Mon Sep 17 00:00:00 2001 From: frmdstryr Date: Fri, 29 May 2026 14:54:55 -0400 Subject: [PATCH 3/3] Add release notes entry and update wording --- atom/scalars.py | 2 +- releasenotes.rst | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/atom/scalars.py b/atom/scalars.py index d8b6f9a6..d229b6d1 100644 --- a/atom/scalars.py +++ b/atom/scalars.py @@ -85,7 +85,7 @@ class Bool(Value): """A value of type `bool`. By default, bools are strictly typed. Pass strict=False to the - constructor to use the result truthyness of the value (eg bool(value)). + constructor to use the truthyness of the value (eg bool(value)). """ diff --git a/releasenotes.rst b/releasenotes.rst index 749ddaab..61cf1ccf 100644 --- a/releasenotes.rst +++ b/releasenotes.rst @@ -1,6 +1,11 @@ Atom Release Notes ================== +0.12.2 - Unreleased +------------------- + +- add strict argument to the Bool member to allow coercion + 0.12.1 - 02/10/2025 -------------------