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

An Extendible class #37

Closed
wants to merge 2 commits into from
Closed
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
78 changes: 78 additions & 0 deletions dol/appendable.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,84 @@ def gen_keys():
NotAVal = type('NotAVal', (), {})() # singleton instance to distinguish from None


from collections.abc import MutableMapping
from functools import partial
from operator import add


def read_add_write(store, key, iterable, add_iterables=add):
"""Retrieves """
if key in store:
store[key] = add_iterables(store[key], iterable)
else:
store[key] = iterable


class Extender:
"""Extends a value in a store.

The value in the store (if it exists) must be an iterable.
The value to extend must also be an iterable.

Unless a different ``extend_store_value`` function is given,
the sum of the two iterables must be an iterable.

The default ``extend_store_value`` is such that if the key is not in the store,
the value is simply written in the store.

The default ``append_method`` is ``None``, which means that the ``append`` method
is not defined. If you want to define it, you can pass a function that takes
the ``Extender`` instance as first argument, and the object to append as second
argument. The ``append`` method will then be defined as a partial of this function
with the ``Extender`` instance as first argument.

>>> store = {'a': 'pple'}
>>> # test normal extend
>>> a_extender = Extender(store, 'a')
>>> a_extender.extend('sauce')
>>> store
{'a': 'pplesauce'}
>>> # test creation (when key is not in store)
>>> b_extender = Extender(store, 'b')
>>> b_extender.extend('anana')
>>> store
{'a': 'pplesauce', 'b': 'anana'}
>>> # you can use the += operator too
>>> b_extender += ' split'
>>> store
{'a': 'pplesauce', 'b': 'anana split'}

"""
def __init__(
self,
store: MutableMapping,
key,
*,
extend_store_value=read_add_write,
append_method=None,
):
self.store = store
self.key = key
self.extend_store_value = extend_store_value

# Note: Not sure this is a good idea.
# Note: I'm not documenting it or testing it until I let class mature.
# Note: Yes, I tried making this a method of the class, but it became ugly.
if append_method is not None:
self.append = partial(append_method, self)

def extend(self, iterable):
"""Extend the iterable stored in """
return self.extend_store_value(self.store, self.key, iterable)

__iadd__ = extend # Note: Better to forward dunders to non-dunder-methods

# TODO: Should we even have this? Is it violating the purity of the class?
@property
def value(self):
return self.store[self.key]


#
# class FixedSizeStack(Sequence):
# """A finite Sequence that can have no more than one element.
Expand Down
73 changes: 73 additions & 0 deletions dol/tests/test_appendable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Tests for appendable.py
"""


def test_extender():
store = {'a': 'pple'}
# test normal extend
a_extender = Extender(store, 'a')
a_extender.extend('sauce')
assert store == {'a': 'pplesauce'}
# test creation (when key is not in store)
b_extender = Extender(store, 'b')
b_extender.extend('anana')
assert store == {'a': 'pplesauce', 'b': 'anana'}
# you can use the += operator too
b_extender += ' split'
assert store == {'a': 'pplesauce', 'b': 'anana split'}

# test append
# Need to define an append method that makes sense.
# Here, with strings, we can just call extend.
b_bis_extender = Extender(store, 'b', append_method=lambda self, obj: self.extend(obj))
b_bis_extender.append('s')
assert store == {'a': 'pplesauce', 'b': 'anana splits'}
# But if our "extend" values were lists, we'd need to have a different append method,
# one that puts the single object into a list, so that its sum with the existing list
# is a list.
store = {'c': [1,2,3]}
c_extender = Extender(store, 'c', append_method=lambda self, obj: self.extend([obj]))
c_extender.append(4)
assert store == {'c': [1,2,3,4]}
# And if the values were tuples, we'd have to put the single object into a tuple.
store = {'d': (1,2,3)}
d_extender = Extender(store, 'd', append_method=lambda self, obj: self.extend((obj,)))
d_extender.append(4)
assert store == {'d': (1,2,3,4)}

# Now, the default extend method is `read_add_write`, which retrieves the existing
# value, sums it to the new value, and writes it back to the store.
# If the values of your store have a sum defined (i.e. an `__add__` method),
# **and** that sum method does what you want, then you can use the default
# `extend_store_value` function.
# O ye numpy users, beware! The sum of numpy arrays is an elementwise sum,
# not a concatenation (you'd have to use `np.concatenate` for that).
import numpy as np
store = {'e': np.array([1,2,3])}
e_extender = Extender(store, 'e')
e_extender.extend(np.array([4,5,6]))
assert all(store['e'] == np.array([5,7,9]))
# This is what the `extend_store_value` function is for: you can pass it a function
# that does what you want.
store = {'f': np.array([1,2,3])}
def extend_store_value_for_numpy(store, key, iterable):
store[key] = np.concatenate([store[key], iterable])
f_extender = Extender(store, 'f', extend_store_value=extend_store_value_for_numpy)
f_extender.extend(np.array([4,5,6]))
assert all(store['f'] == np.array([1,2,3,4,5,6]))
# WARNING: See that the `extend_store_value`` defined here doesn't accomodate for
# the case where the key is not in the store. It is the user's responsibility to
# handle that aspect in the `extend_store_value` they provide.
# For your convenience, the `read_add_write` that is used as a default has
# (and which **does** handle the non-existing key case by simply writing the value in
# the store) has an `add_iterables` argument that can be set to whatever
# makes sense for your use case.
from functools import partial
store = {'g': np.array([1,2,3])}
extend_store_value_for_numpy = partial(
read_add_write, add_iterables=lambda x, y: np.concatenate([x, y])
)
g_extender = Extender(store, 'g', extend_store_value=extend_store_value_for_numpy)
g_extender.extend(np.array([4,5,6]))
assert all(store['g'] == np.array([1,2,3,4,5,6]))