diff --git a/enaml/core/dynamicscope.py b/enaml/core/dynamicscope.py new file mode 100644 index 000000000..2e62244b3 --- /dev/null +++ b/enaml/core/dynamicscope.py @@ -0,0 +1,107 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) 2025, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# ------------------------------------------------------------------------------ +from functools import partial +from itertools import chain, repeat +from enaml.core.declarative import Declarative +from enaml.core._dynamicscope import _DynamicScope, UserKeyError + + +def d_iter(owner: Declarative): + """Iterate attribute names of the declarative and it's ancestors. + + Parameters + ---------- + owner: Declarative + The declarative to walk. + + Yields + ------ + name: string + The attribute name + + """ + while owner is not None: + for name in dir(owner): + yield name + owner = owner._parent + + +def include_key(key: str, used: set) -> bool: + """Filter function to determine whether the key should be included in the + dynamicscope's iter results. + + Parameters + ---------- + key: string + The scope key. + used: set[str] + The set of keys already seen. + + Returns + ------- + result: bool + Whether the key should be included. + """ + if key.startswith("__") or key in used: + return False + used.add(key) + return True + + +class DynamicScope(_DynamicScope): + """_DynamicScope is a C++ class which exposes the following attributes: + + _owner + _change + _f_writes + _f_locals + _f_globals + _f_builtins + + """ + + def __iter__(self): + """Iterate the keys available in the dynamicscope.""" + used = set() + fwrites_it = iter(self._f_writes or ()) + self_it = repeat("self", 1) + change_it = repeat("change", 1 if self._change else 0) + flocals_it = iter(self._f_locals) + fglobals_it = iter(self._f_globals) + fbuiltins_it = iter(self._f_builtins) + fields_it = d_iter(self._owner) + unique_scope_keys = partial(include_key, used=used) + return filter( + unique_scope_keys, + chain( + fwrites_it, + self_it, + change_it, + flocals_it, + fglobals_it, + fbuiltins_it, + fields_it, + ), + ) + + def keys(self): + """Iterate the keys available in the dynamicscope.""" + return iter(self) + + def values(self): + """Iterate the values available in the dynamicscope.""" + return (self[key] for key in self) + + def items(self): + """Iterate the (key, value) pairs available in the dynamicscope.""" + return ((key, self[key]) for key in self) + + def update(self, scope): + """Update the dynamicscope with a mapping of items.""" + for key, value in scope.items(): + self[key] = value diff --git a/enaml/src/dynamicscope.cpp b/enaml/src/dynamicscope.cpp index 7a1152cd3..089c1e46b 100644 --- a/enaml/src/dynamicscope.cpp +++ b/enaml/src/dynamicscope.cpp @@ -785,6 +785,53 @@ DynamicScope_contains( DynamicScope* self, PyObject* key ) } +PyObject* DynamicScope_get_owner( DynamicScope* self ) +{ + return cppy::incref( self->owner ); +} + + +PyObject* DynamicScope_get_change( DynamicScope* self ) +{ + return cppy::incref( self->change ? self->change : Py_None ); +} + + +PyObject* DynamicScope_get_f_locals( DynamicScope* self ) +{ + return cppy::incref( self->f_locals ); +} + + +PyObject* DynamicScope_get_f_globals( DynamicScope* self ) +{ + return cppy::incref( self->f_globals ); +} + + +PyObject* DynamicScope_get_f_builtins( DynamicScope* self ) +{ + return cppy::incref( self->f_builtins ); +} + + +PyObject* DynamicScope_get_f_writes( DynamicScope* self ) +{ + return cppy::incref( self->f_writes ? self->f_writes : Py_None ); +} + +static PyGetSetDef +DynamicScope_getset[] = { + { "_owner", ( getter )DynamicScope_get_owner, 0, "Get owner." }, + { "_change", ( getter )DynamicScope_get_change, 0, "Get change." }, + { "_f_locals", ( getter )DynamicScope_get_f_locals, 0, "Get f_locals." }, + { "_f_globals", ( getter )DynamicScope_get_f_globals, 0, "Get f_globals." }, + { "_f_builtins", ( getter )DynamicScope_get_f_builtins, 0, "Get f_builtins." }, + { "_f_writes", ( getter )DynamicScope_get_f_writes, 0, "Get f_writes." }, + { 0 } // sentinel +}; + + static PyMethodDef DynamicScope_methods[] = { {"get", reinterpret_cast(DynamicScope_get), METH_VARARGS, ""}, { 0 } // Sentinel @@ -797,6 +844,7 @@ static PyType_Slot DynamicScope_Type_slots[] = { { Py_tp_new, void_cast( DynamicScope_new ) }, /* tp_new */ { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ { Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */ + { Py_tp_getset, void_cast( DynamicScope_getset ) }, /* tp_getset */ { Py_tp_methods, void_cast( DynamicScope_methods ) }, /* tp_methods */ { Py_mp_subscript, void_cast( DynamicScope_getitem ) }, /* mp_subscript */ { Py_mp_ass_subscript, void_cast( DynamicScope_setitem ) }, /* mp_ass_subscript */ @@ -812,10 +860,11 @@ PyTypeObject* DynamicScope::TypeObject = NULL; PyType_Spec DynamicScope::TypeObject_Spec = { - "enaml.dynamicscope.DynamicScope", /* tp_name */ + "enaml._dynamicscope._DynamicScope", /* tp_name */ sizeof( DynamicScope ), /* tp_basicsize */ 0, /* tp_itemsize */ Py_TPFLAGS_DEFAULT + |Py_TPFLAGS_BASETYPE |Py_TPFLAGS_HAVE_GC |Py_TPFLAGS_DICT_SUBCLASS, /* tp_flags */ DynamicScope_Type_slots /* slots */ @@ -869,7 +918,7 @@ dynamicscope_modexec( PyObject *mod ) // DynamicScope cppy::ptr dynamicscope( pyobject_cast( DynamicScope::TypeObject ) ); - if( PyModule_AddObject( mod, "DynamicScope", dynamicscope.get() ) < 0 ) + if( PyModule_AddObject( mod, "_DynamicScope", dynamicscope.get() ) < 0 ) { return -1; } @@ -895,7 +944,7 @@ PyModuleDef_Slot dynamicscope_slots[] = { struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - "dynamicscope", + "_dynamicscope", "dynamicscope extension module", 0, dynamicscope_methods, @@ -912,7 +961,7 @@ struct PyModuleDef moduledef = { } // namespace enaml -PyMODINIT_FUNC PyInit_dynamicscope( void ) +PyMODINIT_FUNC PyInit__dynamicscope( void ) { return PyModuleDef_Init( &enaml::moduledef ); } diff --git a/setup.py b/setup.py index 2c8bd151d..e43b4ae83 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ language='c++', ), Extension( - 'enaml.core.dynamicscope', + 'enaml.core._dynamicscope', ['enaml/src/dynamicscope.cpp'], language='c++', ), diff --git a/tests/core/test_dynamicscope.py b/tests/core/test_dynamicscope.py index e2d282968..d1d13f508 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,8 @@ def __init__(self): self._parent = None self.attribute1 = 1 self._prop2 = 0 + self._top = 0 + self.should_raise = True owner = NonDataDescriptor() @@ -65,7 +69,8 @@ def prop2(self, value): @property def key_raise(self): - raise KeyError() + if self.should_raise: + raise KeyError() non_data_key_raise = NonDataDescriptor(True) @@ -242,6 +247,59 @@ def test_dynamicscope_del(dynamicscope): assert 'str' in excinfo.exconly() +def test_dynamicscope_mapping(dynamicscope): + """Test the contains items, keys, value, update, and iter.""" + dynamicscope, extra = dynamicscope + owner = extra[0] + change = extra[4] + + assert "attribute1" in list(dynamicscope) + + keys = { + "_parent", + "_prop2", + "a", + "b", + "c", + "e", + "self", + "change", + "attribute1", + "attribute2", + "key_raise", + "non_data_key_raise", + "owner", + "prop1", + "prop2", + "write_only", + "should_raise", + "top" + } + # There is a bunch of __...__ we don't care about' + assert not keys.difference(set(dynamicscope.keys())) + all_keys = list(dynamicscope) + print(all_keys) + assert not keys.difference(set(all_keys)) + + # These cause errors... + owner.should_raise = False + owner.__class__.non_data_key_raise.should_raise = False + + parent = owner._parent + values = list(dynamicscope.values()) + for v in (0, 1, 2, 3, 5, owner, change, parent): + assert v in values + + dynamicscope.update({"x": "y"}) + + with pytest.raises(AttributeError): + dynamicscope.update(1) # not mapping + with pytest.raises(TypeError): + dynamicscope.update({1: 2}) # invalid key type + + keys.add("x") + assert dict(dynamicscope.items())["x"] == "y" + @pytest.fixture def nonlocals(dynamicscope): """Access the nonlocals of a dynamic scope.