Skip to content

Instantly share code, notes, and snippets.

@nonducor
Last active August 28, 2022 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nonducor/755d1ba64ede1fe879a41c76a7eed878 to your computer and use it in GitHub Desktop.
Save nonducor/755d1ba64ede1fe879a41c76a7eed878 to your computer and use it in GitHub Desktop.
A simple set of classes that wraps a dictionary or list and allows acessing the elements of the dicitonary as if they were attributes of the class. Answer to this question in stackoverflow: https://stackoverflow.com/questions/72630875/python-json-finding-elements-on-a-dynamic-path/72665554#72665554
# The MIT License (MIT)
#
# Copyright (c) 2022 Rodrigo Rizzi Starr
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from collections.abc import Mapping, Sequence, MutableSequence
import time
class BaseWrapper:
__slots__ = ('_data')
def __init__(self, data):
super().__setattr__('_data', data)
def __repr__(self):
return f'{self.__class__.__name__}({repr(self._data)})'
class MappingWrapper(BaseWrapper):
"""Wraps a dictionary and provides the keys of the dictionary as class members.
Create new keys when they do not exist."""
def __getattr__(self, name):
# Note: these two lines allow automatic creation of attributes, e.g. in an object 'obj'
# that doesn't have an attribute 'car', the following is possible:
# >> o.car.colour = 'blue'
# And all the missing levels will be automatically created
if name not in self._data and not name.startswith('_'):
self._data[name] = {}
return wrap(self._data[name])
def __setattr__(self, name, value):
self._data[name] = unwrap(value)
# Implements standard dictionary access
def __getitem__(self, name):
return wrap(self._data[name])
def __setitem__(self, name, value):
self._data[name] = unwrap(value)
def __delitem__(self, name):
del self._data[name]
def __len__(self):
return len(self._data)
def __dir__(self):
return super().__dir__() + list(self._data.keys())
class ListWrapper(BaseWrapper, MutableSequence):
"""Wraps a list. Essentially, provides wrapping of elements of the list."""
def __getitem__(self, idx):
return wrap(self._data[idx])
def __setitem__(self, idx, value):
self._data[idx] = unwrap(value)
def __delitem__(self, idx):
del self._data[idx]
def __len__(self):
return len(self._data)
def insert(self, index, obj):
self._data.insert(index, unwrap(obj))
def wrap(obj):
if isinstance(obj, dict):
return MappingWrapper(obj)
if isinstance(obj, list):
return ListWrapper(obj)
return obj
def unwrap(obj):
if isinstance(obj, BaseWrapper):
return obj._data
return obj
if __name__ == '__main__':
test_data = {'basket': {'items': [{'name': 'apple', 'colour': 'green'},
{'name': 'pineapple', 'taste': 'sweet',},
],
'cost': 12.3,
},
'name': 'Other'}
o = wrap(test_data)
assert len(o) == 2
assert len(o.basket.items) == 2
print(o.basket.items)
print(o.basket.cost)
o.basket.cost = 10.0
assert o.basket.cost == 10.0
o.basket.items.append({'name': 'orange'})
o.basket.items[2].colour = 'yellow'
assert o.basket.items[2].name == 'orange'
assert o.basket.items[2].colour == 'yellow'
# You can get a part of it and it holds a reference to the original
b = o.basket
b.type = 'groceries'
assert o.basket.type == 'groceries'
# It is also possible to create a separate wrapped part and then join:
employees = wrap({})
employees.Clara.id = 101
employees.Clara.age = 23
employees.Lucia.id = 102
employees.Lucia.age = 29
o.employees = employees
assert isinstance(unwrap(o)['employees'], dict)
o['employed'] = employees
assert isinstance(unwrap(o)['employed'], dict)
# Compare performance:
tstart = time.perf_counter()
for _ in range(1_000_000):
a = test_data['basket']['items'][1]['name']
tend = time.perf_counter()
tbasic = tend - tstart
print(f'Basic access took: {tbasic} s')
tstart = time.perf_counter()
for _ in range(1_000_000):
a = o.basket.items[1].name
tend = time.perf_counter()
tfancy = tend - tstart
print(f'Basic access took: {tfancy} s')
print(f'Slow down: {tfancy / tbasic:.2f}')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment