Skip to content

Commit

Permalink
Merge pull request #571 from frmdstryr/dynamicscope-iter
Browse files Browse the repository at this point in the history
Add a proper dynamicscope iterator
  • Loading branch information
sccolbert authored Jan 31, 2025
2 parents e00ca76 + 07fedc2 commit b7d662c
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 6 deletions.
107 changes: 107 additions & 0 deletions enaml/core/dynamicscope.py
Original file line number Diff line number Diff line change
@@ -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
57 changes: 53 additions & 4 deletions enaml/src/dynamicscope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<PyCFunction>(DynamicScope_get), METH_VARARGS, ""},
{ 0 } // Sentinel
Expand All @@ -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 */
Expand All @@ -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 */
Expand Down Expand Up @@ -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;
}
Expand All @@ -895,7 +944,7 @@ PyModuleDef_Slot dynamicscope_slots[] = {

struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"dynamicscope",
"_dynamicscope",
"dynamicscope extension module",
0,
dynamicscope_methods,
Expand All @@ -912,7 +961,7 @@ struct PyModuleDef moduledef = {
} // namespace enaml


PyMODINIT_FUNC PyInit_dynamicscope( void )
PyMODINIT_FUNC PyInit__dynamicscope( void )
{
return PyModuleDef_Init( &enaml::moduledef );
}
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
language='c++',
),
Extension(
'enaml.core.dynamicscope',
'enaml.core._dynamicscope',
['enaml/src/dynamicscope.cpp'],
language='c++',
),
Expand Down
58 changes: 57 additions & 1 deletion tests/core/test_dynamicscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()

Expand All @@ -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)

Expand Down Expand Up @@ -242,6 +247,57 @@ 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 = {
"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):
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.
Expand Down

0 comments on commit b7d662c

Please sign in to comment.