Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add property support for Box #239

Merged
merged 1 commit into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Code contributions:
- Dominic (Yobmod)
- Ivan Pepelnjak (ipspace)
- Michał Górny (mgorny)
- Serge Lu (Serge45)

Suggestions and bug reporting:

Expand Down
28 changes: 27 additions & 1 deletion box/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ def _get_box_config():
"__safe_keys": {},
}

def _get_property_func(obj, key):
"""
Try to get property helper functions of given object and property name.

:param obj: object to be checked for property
:param key: property name
:return: a tuple for helper functions(fget, fset, fdel). If no such property, a (None, None, None) returns
"""
obj_type = type(obj)

if not hasattr(obj_type, key):
return None, None, None
attr = getattr(obj_type, key)
return attr.fget, attr.fset, attr.fdel

class Box(dict):
"""
Expand Down Expand Up @@ -580,7 +594,12 @@ def __setattr__(self, key, value):
safe_key = self._safe_attr(key)
if safe_key in self._box_config["__safe_keys"]:
key = self._box_config["__safe_keys"][safe_key]
self.__setitem__(key, value)

# if user has customized property setter, fall back to default implementation
if _get_property_func(self, key)[1] is not None:
super().__setattr__(key, value)
else:
self.__setitem__(key, value)

def __delitem__(self, key):
if self._box_config["frozen_box"]:
Expand Down Expand Up @@ -615,6 +634,13 @@ def __delattr__(self, item):
raise BoxError('"_box_config" is protected')
if item in self._protected_keys:
raise BoxKeyError(f'Key name "{item}" is protected')

property_fdel = _get_property_func(self, item)[2]

# if user has customized property deleter, route to it
if property_fdel is not None:
property_fdel(self)
return
try:
self.__delitem__(item)
except KeyError as err:
Expand Down
30 changes: 30 additions & 0 deletions test/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -1355,3 +1355,33 @@ def test_box_default_not_create_on_get(self):
assert "c" not in box2.a.b

assert box2 == Box()

def test_box_property_support(self):
class BoxWithProperty(Box):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

@property
def field(self):
return self._field

@field.setter
def field(self, value):
self._field = value

@field.deleter
def field(self):
"""
This is required to make `del box.field` work properly otherwise a `BoxKeyError` would be thrown.
"""
del self._field

box = BoxWithProperty()
box.field = 5

assert 'field' not in box
assert '_field' in box
assert box.field == 5
assert box._field == 5
del box.field
assert not '_field' in box