diff options
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/ext/orderinglist.py | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/lib/sqlalchemy/ext/orderinglist.py b/lib/sqlalchemy/ext/orderinglist.py new file mode 100644 index 000000000..27ff408dc --- /dev/null +++ b/lib/sqlalchemy/ext/orderinglist.py @@ -0,0 +1,181 @@ +""" +A custom list implementation for mapped relations that syncs position in a +Python list with a position attribute on the mapped objects. +""" + +__all__ = [ 'ordering_list' ] + + +def ordering_list(attr, count_from=None, **kw): + """ + Prepares an OrderingList factory for use as an argument to a + Mapper relation's 'collection_class' option. Arguments are: + + attr + Name of the mapped attribute to use for storage and retrieval of + ordering information + + count_from (optional) + Set up an integer-based ordering, starting at 'count_from'. For example, + ordering_list('pos', count_from=1) would create a 1-based list in SQL, + storing the value in the 'pos' column. Ignored if ordering_func is + supplied. + + Passes along any keyword arguments to OrderingList constructor. + """ + + kw = _unsugar_count_from(count_from=count_from, **kw) + return lambda: OrderingList(attr, **kw) + +# Ordering utility functions +def count_from_0(index, collection): + """Numbering function: consecutive integers starting at 0.""" + + return index + +def count_from_1(index, collection): + """Numbering function: consecutive integers starting at 1.""" + + return index + 1 + +def count_from_n_factory(start): + """Numbering function: consecutive integers starting at arbitrary start.""" + + def f(index, collection): + return index + start + try: + f.__name__ = 'count_from_%i' % start + except TypeError: + pass + return f + +def _unsugar_count_from(**kw): + """Keyword argument filter, prepares a simple ordering_func from + a 'count_from' argument, otherwise passes ordering_func on unchanged.""" + + count_from = kw.pop('count_from', None) + if kw.get('ordering_func', None) is None and count_from is not None: + if count_from == 0: + kw['ordering_func'] = count_from_0 + elif count_from == 1: + kw['ordering_func'] = count_from_1 + else: + kw['ordering_func'] = count_from_n_factory(count_from) + return kw + +class OrderingList(list): + def __init__(self, ordering_attr=None, ordering_func=None, + reorder_on_append=False): + """ + A 'collection_class' list implementation that syncs position in a + Python list with a position attribute on the mapped objects. + + This implementation counts on the list starting in the proper + order, so be SURE to put an order_by on your relation. + Arguments are: + + ordering_attr + Name of the attribute that stores the object's order in the relation. + + ordering_func + Optional. A function that maps the position in the Python list to a + value to store in the ordering_attr. Values returned are usually + (but need not be!) integers. + + ordering_funcs are called with two positional parameters: index of + the element in the list, and the list itself. + + If omitted, list indexes are used for the attribute values. Two + basic pre-built numbering functions are provided: 'count_from_0' and + 'count_from_1'. For more exotic examples like stepped numbering, + alphabetical and Fibonacci numbering, see the unit tests. + + reorder_on_append + Default false. When appending an object with an existing (non-None) + ordering value, that value will be left untouched unless + reorder_on_append is true. This is an optimization to avoid a + variety of dangerous unexpected database writes. + + SQLAlchemy will add instances to the list via append() when your + object loads. If for some reason the result set from the database + skips a step in the ordering (say, row '1' is missing but you get + '2', '3', and '4'), reorder_on_append=True would immediately + renumber the items to '1', '2', '3'. If you have multiple sessions + making changes, any of whom happen to load this collection even in + passing, all of the sessions would try to 'clean up' the numbering + in their commits, possibly causing all but one to fail with a + concurrent modification error. Spooky action at a distance. + + Recommend leaving this with the default of False, and just call + ._reorder() if you're doing append() operations with previously + ordered instances or doing housekeeping after manual sql operations. + """ + + self.ordering_attr = ordering_attr + if ordering_func is None: + ordering_func = count_from_0 + self.ordering_func = ordering_func + self.reorder_on_append = reorder_on_append + + # More complex serialization schemes (multi column, e.g.) are possible by + # subclassing and reimplementing these two methods. + def _get_order_value(self, entity): + return getattr(entity, self.ordering_attr) + + def _set_order_value(self, entity, value): + setattr(entity, self.ordering_attr, value) + + def _reorder(self): + """Sweep through the list and ensure that each object has accurate + ordering information set.""" + + for index, entity in enumerate(self): + self._order_entity(index, entity, True) + + def _order_entity(self, index, entity, reorder=True): + have = self._get_order_value(entity) + + # Don't disturb existing ordering if reorder is False + if have is not None and not reorder: + return + + should_be = self.ordering_func(index, self) + if have <> should_be: + self._set_order_value(entity, should_be) + + def append(self, entity): + super(OrderingList, self).append(entity) + self._order_entity(len(self) - 1, entity, self.reorder_on_append) + + def _raw_append(self, entity): + """Append without any ordering behavior.""" + + super(OrderingList, self).append(entity) + + def insert(self, index, entity): + self[index:index] = [entity] + + def remove(self, entity): + super(OrderingList, self).remove(entity) + self._reorder() + + def pop(self, index=-1): + entity = super(OrderingList, self).pop(index) + self._reorder() + return entity + + def __setitem__(self, index, entity): + super(OrderingList, self).__setitem__(index, entity) + self._order_entity(index, entity, True) + + def __delitem__(self, index): + super(OrderingList, self).__delitem__(index) + self._reorder() + + def __setslice__(self, start, end, values): + super(OrderingList, self).__setslice__(start, end, values) + self._reorder() + + def __delslice__(self, start, end): + super(OrderingList, self).__delslice__(start, end) + self._reorder() |