diff --git a/development.rst b/development.rst index 33c847167..b8725806a 100644 --- a/development.rst +++ b/development.rst @@ -5,3 +5,6 @@ To regenerate the parser: python -m enaml.core.parser.generate_enaml_parser black enaml/core/parser/enaml_parser.py + +To compile with debug symbols: + CFLAGS="-O0 -g" CPPFLAGS="-O0 -g" pip install -e ./ diff --git a/enaml/src/dynamicscope.cpp b/enaml/src/dynamicscope.cpp index f26478386..2ff7363c0 100644 --- a/enaml/src/dynamicscope.cpp +++ b/enaml/src/dynamicscope.cpp @@ -55,6 +55,40 @@ struct DynamicScope static bool Ready(); + static int TypeCheck( PyObject* object ) + { + return PyObject_TypeCheck( object, TypeObject ); + } + +}; + +namespace ScopeIterator +{ + + enum State: uint8_t { + FWrites, + Self, + Change, + FLocals, + FGlobals, + FBuiltins, + ParentTypeDict, + ParentInstanceDict, +}; + +} + +struct DynamicScopeIterator +{ + PyObject_HEAD + PyObject* scope; + PyObject* parent; + PyObject* iter; + ScopeIterator::State state; + + static PyType_Spec TypeObject_Spec; + static PyTypeObject* TypeObject; + static bool Ready(); }; @@ -63,6 +97,8 @@ namespace static PyObject* parent_str; +static PyObject* self_str; +static PyObject* change_str; static PyObject* dynamic_load_str; static PyObject* UserKeyError; @@ -506,6 +542,233 @@ bool Nonlocals::Ready() return true; } +namespace +{ +/*----------------------------------------------------------------------------- + | DynamicScopeIterator * + |----------------------------------------------------------------------------*/ + +PyObject* +DynamicScopeIterator_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) +{ + PyObject* scope; + static char* kwlist[] = {"scope", 0}; + if( !PyArg_ParseTupleAndKeywords( args, kwargs, "O", kwlist, &scope ) ) + return 0; + if( !DynamicScope::TypeCheck( scope ) ) + return cppy::type_error( scope, "dynamicscope" ); + PyObject* self = PyType_GenericNew( type, 0, 0 ); + if( !self ) + return 0; + DynamicScopeIterator* iter = reinterpret_cast( self ); + iter->scope = cppy::incref( scope ); + iter->state = ScopeIterator::FWrites; + iter->iter = 0; + return self; +} + +void +DynamicScopeIterator_clear( DynamicScopeIterator* self ) +{ + Py_CLEAR( self->scope ); + Py_CLEAR( self->parent ); + Py_CLEAR( self->iter ); +} + + +int +DynamicScopeIterator_traverse( DynamicScopeIterator* self, visitproc visit, void* arg ) +{ + Py_VISIT( self->scope ); + Py_VISIT( self->parent ); + Py_VISIT( self->iter ); + return 0; +} + + +void +DynamicScopeIterator_dealloc( DynamicScopeIterator* self ) +{ + PyObject_GC_UnTrack( self ); + DynamicScopeIterator_clear( self ); + Py_TYPE(self)->tp_free( pyobject_cast( self ) ); +} + +PyObject* +DynamicScopeIterator_iter( DynamicScopeIterator* self ) +{ + // TODO: Should this return a clone? + return cppy::incref( pyobject_cast(self) ); +} + +PyObject* +DynamicScopeIterator_next( DynamicScopeIterator* self ) +{ + DynamicScope* scope = reinterpret_cast( self->scope ); + switch( self->state ) + { + case ScopeIterator::FWrites: + { + if ( scope->f_writes ) + { + if ( !self->iter ) + { + self->iter = PyObject_GetIter( scope->f_writes ); + if ( !self->iter ) + return 0; + } + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + } + self->state = ScopeIterator::Self; + // Fall through + } + case ScopeIterator::Self: + { + self->state = ScopeIterator::Change; + return cppy::incref( self_str ); + } + case ScopeIterator::Change: + { + self->state = ScopeIterator::FLocals; + if ( scope->change && scope->change != Py_None ) + return cppy::incref( change_str ); + // else fall through + } + // TODO: Nonlocals ? + case ScopeIterator::FLocals: + { + if ( !self->iter ) + { + self->iter = PyObject_GetIter( scope->f_locals ); + if ( !self->iter ) + return 0; + } + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + self->state = ScopeIterator::FGlobals; + // Fall through + } + case ScopeIterator::FGlobals: + { + if ( !self->iter ) + { + self->iter = PyObject_GetIter( scope->f_globals ); + if ( !self->iter ) + return 0; + } + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + self->state = ScopeIterator::FBuiltins; + // Fall through + } + case ScopeIterator::FBuiltins: + { + if ( !self->iter ) + { + self->iter = PyObject_GetIter( scope->f_builtins ); + if ( !self->iter ) + return 0; + } + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + self->state = ScopeIterator::ParentTypeDict; + self->parent = cppy::incref( scope->owner ); + // Fall through + } + case ScopeIterator::ParentTypeDict: + { + if ( !self->iter ) + { + if ( !self->parent ) + break; + cppy::ptr dict( PyType_GetDict( Py_TYPE( self->parent ) ) ); + if ( !dict ) + return 0; + self->iter = PyObject_GetIter( dict.get() ); + if ( !self->iter ) + return 0; + } + + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + self->state = ScopeIterator::ParentInstanceDict; + // Fall through + } + case ScopeIterator::ParentInstanceDict: + { + if ( !self->parent ) + break; + if ( !self->iter ) + { + PyObject**dictptr = _PyObject_GetDictPtr( self->parent ); + if ( dictptr && *dictptr ) { + self->iter = PyObject_GetIter( *dictptr ); + if ( !self->iter ) + return 0; + } // else no instance dict + } + + if ( self->iter ) + { + cppy::ptr item( PyIter_Next( self->iter ) ); + if ( item ) + return item.release(); + Py_CLEAR( self->iter ); + } + + // Walk up to parent + Py_SETREF( self->parent, PyObject_GetAttr(self->parent, parent_str) ); + self->state = ScopeIterator::ParentInstanceDict; + return DynamicScopeIterator_next( self ); + } + } + PyErr_SetNone(PyExc_StopIteration); + return 0; +} + +} // namespace + + +static PyType_Slot DynamicScopeIterator_Type_slots[] = { + { Py_tp_new, void_cast( DynamicScopeIterator_new ) }, /* tp_new */ + { Py_tp_dealloc, void_cast( DynamicScopeIterator_dealloc ) }, /* tp_dealloc */ + { Py_tp_traverse, void_cast( DynamicScopeIterator_traverse ) }, /* tp_traverse */ + { Py_tp_clear, void_cast( DynamicScopeIterator_clear ) }, /* tp_clear */ + { Py_tp_iter, void_cast( DynamicScopeIterator_iter ) }, /* tp_iter */ + { Py_tp_iternext, void_cast( DynamicScopeIterator_next ) }, /* tp_clear */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */ + { 0, 0 }, +}; + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* DynamicScopeIterator::TypeObject = NULL; + +PyType_Spec DynamicScopeIterator::TypeObject_Spec = { + "enaml.dynamicscope.DynamicScopeIterator", /* tp_name */ + sizeof( DynamicScopeIterator ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + DynamicScopeIterator_Type_slots /* slots */ +}; + +bool DynamicScopeIterator::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + return TypeObject != 0; +} namespace { @@ -785,71 +1048,71 @@ DynamicScope_contains( DynamicScope* self, PyObject* key ) } PyObject* -_DynamicScope_dict( DynamicScope* self) +DynamicScope_iter( DynamicScope* self) { - cppy::ptr items( PyDict_New() ); - if ( !items ) - return 0; - - if ( PyDict_Update( items.get(), self->f_builtins ) ) - return 0; - - if ( PyDict_Update( items.get(), self->f_globals ) ) - return 0; - - if ( PyDict_Update( items.get(), self->f_locals ) ) - return 0; - - if ( self->f_writes && PyDict_Update( items.get(), self->f_writes ) ) - return 0; - - if ( self->change && PyDict_SetItemString( items.get(), "change", cppy::incref(self->change) ) ) - return 0; - - if ( PyDict_SetItemString( items.get(), "self", cppy::incref(self->owner) ) ) + cppy::ptr iter( PyObject_CallOneArg( pyobject_cast( DynamicScopeIterator::TypeObject ), pyobject_cast( self ) ) ); + if ( !iter ) return 0; - - return items.release(); + return iter.release(); } PyObject* -DynamicScope_items( DynamicScope* self) +DynamicScope_keys( DynamicScope* self) { - cppy::ptr dict( _DynamicScope_dict( self ) ); - if ( !dict ) - return 0; - return PyDict_Items( dict.get() ); + return DynamicScope_iter( self ); } PyObject* -DynamicScope_keys( DynamicScope* self) +DynamicScope_items( DynamicScope* self) { - cppy::ptr dict( _DynamicScope_dict( self ) ); - if ( !dict ) + cppy::ptr items( PyList_New( 0 ) ); + if ( !items ) + return 0; + cppy::ptr iter( PyObject_GetIter( pyobject_cast(self) ) ); + if ( !iter ) return 0; - return PyDict_Keys( dict.get() ); + + cppy::ptr key; + while ( (key = iter.next()) ) + { + cppy::ptr value( DynamicScope_getitem( self, key.get() ) ); + if ( !value ) + return 0; + cppy::ptr item( PyTuple_New(2) ); + if ( !item ) + return 0; + PyTuple_SET_ITEM(item.get(), 0, key.release() ); + PyTuple_SET_ITEM(item.get(), 1, value.release() ); + if ( PyList_Append( items.get(), item.get() ) < 0 ) + return 0; + } + return items.release(); } PyObject* DynamicScope_values( DynamicScope* self) { - cppy::ptr dict( _DynamicScope_dict( self ) ); - if ( !dict ) + // TODO estimate size somehow + cppy::ptr values( PyList_New( 0 ) ); + if ( !values ) return 0; - return PyDict_Values( dict.get() ); -} - - -PyObject* -DynamicScope_iter( DynamicScope* self) -{ - cppy::ptr dict( _DynamicScope_dict( self ) ); - if ( !dict ) + cppy::ptr iter( PyObject_GetIter( pyobject_cast(self) ) ); + if ( !iter ) return 0; - return dict.iter(); + + cppy::ptr key; + while ( (key = iter.next()) ) + { + cppy::ptr value( DynamicScope_getitem( self, key.get() ) ); + if ( !value ) + return 0; + if ( PyList_Append( values.get(), value.get() ) < 0 ) + return 0; + } + return values.release(); } @@ -949,6 +1212,13 @@ dynamicscope_modexec( PyObject *mod ) { return -1; } + self_str = PyUnicode_FromString( "self" ); + if( !self_str ) + return -1; + change_str = PyUnicode_FromString( "change" ); + if( !change_str ) + return -1; + UserKeyError = PyErr_NewException( "dynamicscope.UserKeyError", 0, 0 ); if( !UserKeyError ) { @@ -964,6 +1234,9 @@ dynamicscope_modexec( PyObject *mod ) return -1; } + if ( !DynamicScopeIterator::Ready() ) + return -1; + // DynamicScope cppy::ptr dynamicscope( pyobject_cast( DynamicScope::TypeObject ) ); if( PyModule_AddObject( mod, "DynamicScope", dynamicscope.get() ) < 0 ) diff --git a/tests/core/test_dynamicscope.py b/tests/core/test_dynamicscope.py index 2249e0646..58b1b3578 100644 --- a/tests/core/test_dynamicscope.py +++ b/tests/core/test_dynamicscope.py @@ -27,6 +27,8 @@ def __init__(self, should_raise=False): self.should_raise = should_raise def __get__(self, instance, objtype=None): + if instance is None: + return self if not self.should_raise: return instance else: @@ -50,6 +52,7 @@ def __init__(self): self._parent = None self.attribute1 = 1 self._prop2 = 0 + self.should_raise = True owner = NonDataDescriptor() @@ -65,7 +68,8 @@ def prop2(self, value): @property def key_raise(self): - raise KeyError() + if self.should_raise: + raise KeyError() non_data_key_raise = NonDataDescriptor(True) @@ -243,30 +247,62 @@ def test_dynamicscope_del(dynamicscope): def test_dynamicscope_mapping(dynamicscope): - """Test the contains items, keys, value, update, and iter. - - """ + """Test the contains items, keys, value, update, and iter.""" dynamicscope, extra = dynamicscope owner = extra[0] change = extra[4] - assert set(dynamicscope.keys()) == {'a', 'b', 'c', 'e', 'self', 'change'} - - for v in dynamicscope.values(): - assert v in [1, 2, 3, 4, 5, owner, change] + assert "attribute1" in list(dynamicscope) + + keys = { + "__dict__", + "__doc__", + "__firstlineno__", + "__init__", + "__module__", + "__static_attributes__", + "__weakref__", + "_parent", + "_prop2", + "a", + "b", + "c", + "e", + "self", + "change", + "attribute1", + "attribute2", + "key_raise", + "non_data_key_raise", + "owner", + "prop1", + "prop2", + "write_only", + "should_raise", + } + assert set(dynamicscope.keys()) == keys + + # These cause errors... + owner.should_raise = False + owner.__class__.non_data_key_raise.should_raise = False + + parent = owner._parent + values = dynamicscope.values() + for v in (0, 1, 2, 3, 5, owner, change, parent): + assert v in values dynamicscope.update({"x": "y"}) with pytest.raises(TypeError): - dynamicscope.update(1) # not mapping + dynamicscope.update(1) # not mapping with pytest.raises(TypeError): - dynamicscope.update({1: 2}) # invalid key type + dynamicscope.update({1: 2}) # invalid key type - assert {k for k in dynamicscope} == {'a', 'b', 'c', 'e', 'self', 'change', 'x'} + keys.add("x") + assert {k for k in dynamicscope} == keys assert dict(dynamicscope.items())["x"] == "y" - @pytest.fixture def nonlocals(dynamicscope): """Access the nonlocals of a dynamic scope.