Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
orderinglist.py440 linesDownload Raw Back to ext
1# ext/orderinglist.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""A custom list that manages index/position information for contained
9elements.
10
11:author: Jason Kirtland
12
13``orderinglist`` is a helper for mutable ordered relationships.  It will
14intercept list operations performed on a :func:`_orm.relationship`-managed
15collection and
16automatically synchronize changes in list position onto a target scalar
17attribute.
18
19Example: A ``slide`` table, where each row refers to zero or more entries
20in a related ``bullet`` table.   The bullets within a slide are
21displayed in order based on the value of the ``position`` column in the
22``bullet`` table.   As entries are reordered in memory, the value of the
23``position`` attribute should be updated to reflect the new sort order::
24
25
26    Base = declarative_base()
27
28
29    class Slide(Base):
30        __tablename__ = "slide"
31
32        id = Column(Integer, primary_key=True)
33        name = Column(String)
34
35        bullets = relationship("Bullet", order_by="Bullet.position")
36
37
38    class Bullet(Base):
39        __tablename__ = "bullet"
40        id = Column(Integer, primary_key=True)
41        slide_id = Column(Integer, ForeignKey("slide.id"))
42        position = Column(Integer)
43        text = Column(String)
44
45The standard relationship mapping will produce a list-like attribute on each
46``Slide`` containing all related ``Bullet`` objects,
47but coping with changes in ordering is not handled automatically.
48When appending a ``Bullet`` into ``Slide.bullets``, the ``Bullet.position``
49attribute will remain unset until manually assigned.   When the ``Bullet``
50is inserted into the middle of the list, the following ``Bullet`` objects
51will also need to be renumbered.
52
53The :class:`.OrderingList` object automates this task, managing the
54``position`` attribute on all ``Bullet`` objects in the collection.  It is
55constructed using the :func:`.ordering_list` factory::
56
57    from sqlalchemy.ext.orderinglist import ordering_list
58
59    Base = declarative_base()
60
61
62    class Slide(Base):
63        __tablename__ = "slide"
64
65        id = Column(Integer, primary_key=True)
66        name = Column(String)
67
68        bullets = relationship(
69            "Bullet",
70            order_by="Bullet.position",
71            collection_class=ordering_list("position"),
72        )
73
74
75    class Bullet(Base):
76        __tablename__ = "bullet"
77        id = Column(Integer, primary_key=True)
78        slide_id = Column(Integer, ForeignKey("slide.id"))
79        position = Column(Integer)
80        text = Column(String)
81
82With the above mapping the ``Bullet.position`` attribute is managed::
83
84    s = Slide()
85    s.bullets.append(Bullet())
86    s.bullets.append(Bullet())
87    s.bullets[1].position
88    >>> 1
89    s.bullets.insert(1, Bullet())
90    s.bullets[2].position
91    >>> 2
92
93The :class:`.OrderingList` construct only works with **changes** to a
94collection, and not the initial load from the database, and requires that the
95list be sorted when loaded.  Therefore, be sure to specify ``order_by`` on the
96:func:`_orm.relationship` against the target ordering attribute, so that the
97ordering is correct when first loaded.
98
99.. warning::
100
101  :class:`.OrderingList` only provides limited functionality when a primary
102  key column or unique column is the target of the sort.  Operations
103  that are unsupported or are problematic include:
104
105    * two entries must trade values.  This is not supported directly in the
106      case of a primary key or unique constraint because it means at least
107      one row would need to be temporarily removed first, or changed to
108      a third, neutral value while the switch occurs.
109
110    * an entry must be deleted in order to make room for a new entry.
111      SQLAlchemy's unit of work performs all INSERTs before DELETEs within a
112      single flush.  In the case of a primary key, it will trade
113      an INSERT/DELETE of the same primary key for an UPDATE statement in order
114      to lessen the impact of this limitation, however this does not take place
115      for a UNIQUE column.
116      A future feature will allow the "DELETE before INSERT" behavior to be
117      possible, alleviating this limitation, though this feature will require
118      explicit configuration at the mapper level for sets of columns that
119      are to be handled in this way.
120
121:func:`.ordering_list` takes the name of the related object's ordering
122attribute as an argument.  By default, the zero-based integer index of the
123object's position in the :func:`.ordering_list` is synchronized with the
124ordering attribute: index 0 will get position 0, index 1 position 1, etc.  To
125start numbering at 1 or some other integer, provide ``count_from=1``.
126
127
128"""
129from __future__ import annotations
130
131from typing import Any
132from typing import Callable
133from typing import Dict
134from typing import Iterable
135from typing import List
136from typing import Optional
137from typing import overload
138from typing import Sequence
139from typing import Type
140from typing import TypeVar
141from typing import Union
142
143from ..orm.collections import collection
144from ..orm.collections import collection_adapter
145from ..util.typing import SupportsIndex
146
147_T = TypeVar("_T")
148OrderingFunc = Callable[[int, Sequence[_T]], object]
149
150
151__all__ = ["ordering_list"]
152
153
154def ordering_list(
155    attr: str,
156    count_from: Optional[int] = None,
157    ordering_func: Optional[OrderingFunc[_T]] = None,
158    reorder_on_append: bool = False,
159) -> Callable[[], OrderingList[_T]]:
160    """Prepares an :class:`OrderingList` factory for use in mapper definitions.
161
162    Returns an object suitable for use as an argument to a Mapper
163    relationship's ``collection_class`` option.  e.g.::
164
165        from sqlalchemy.ext.orderinglist import ordering_list
166
167
168        class Slide(Base):
169            __tablename__ = "slide"
170
171            id = Column(Integer, primary_key=True)
172            name = Column(String)
173
174            bullets = relationship(
175                "Bullet",
176                order_by="Bullet.position",
177                collection_class=ordering_list("position"),
178            )
179
180    :param attr:
181      Name of the mapped attribute to use for storage and retrieval of
182      ordering information
183
184    :param count_from:
185      Set up an integer-based ordering, starting at ``count_from``.  For
186      example, ``ordering_list('pos', count_from=1)`` would create a 1-based
187      list in SQL, storing the value in the 'pos' column.  Ignored if
188      ``ordering_func`` is supplied.
189
190    Additional arguments are passed to the :class:`.OrderingList` constructor.
191
192    """
193
194    kw = _unsugar_count_from(
195        count_from=count_from,
196        ordering_func=ordering_func,
197        reorder_on_append=reorder_on_append,
198    )
199    return lambda: OrderingList(attr, **kw)
200
201
202# Ordering utility functions
203
204
205def count_from_0(index: int, collection: object) -> int:
206    """Numbering function: consecutive integers starting at 0."""
207
208    return index
209
210
211def count_from_1(index: int, collection: object) -> int:
212    """Numbering function: consecutive integers starting at 1."""
213
214    return index + 1
215
216
217def count_from_n_factory(start: int) -> OrderingFunc[Any]:
218    """Numbering function: consecutive integers starting at arbitrary start."""
219
220    def f(index: int, collection: object) -> int:
221        return index + start
222
223    try:
224        f.__name__ = "count_from_%i" % start
225    except TypeError:
226        pass
227    return f
228
229
230def _unsugar_count_from(**kw: Any) -> Dict[str, Any]:
231    """Builds counting functions from keyword arguments.
232
233    Keyword argument filter, prepares a simple ``ordering_func`` from a
234    ``count_from`` argument, otherwise passes ``ordering_func`` on unchanged.
235    """
236
237    count_from = kw.pop("count_from", None)
238    if kw.get("ordering_func", None) is None and count_from is not None:
239        if count_from == 0:
240            kw["ordering_func"] = count_from_0
241        elif count_from == 1:
242            kw["ordering_func"] = count_from_1
243        else:
244            kw["ordering_func"] = count_from_n_factory(count_from)
245    return kw
246
247
248class OrderingList(List[_T]):
249    """A custom list that manages position information for its children.
250
251    The :class:`.OrderingList` object is normally set up using the
252    :func:`.ordering_list` factory function, used in conjunction with
253    the :func:`_orm.relationship` function.
254
255    """
256
257    ordering_attr: str
258    ordering_func: OrderingFunc[_T]
259    reorder_on_append: bool
260
261    def __init__(
262        self,
263        ordering_attr: str,
264        ordering_func: Optional[OrderingFunc[_T]] = None,
265        reorder_on_append: bool = False,
266    ):
267        """A custom list that manages position information for its children.
268
269        ``OrderingList`` is a ``collection_class`` list implementation that
270        syncs position in a Python list with a position attribute on the
271        mapped objects.
272
273        This implementation relies on the list starting in the proper order,
274        so be **sure** to put an ``order_by`` on your relationship.
275
276        :param ordering_attr:
277          Name of the attribute that stores the object's order in the
278          relationship.
279
280        :param ordering_func: Optional.  A function that maps the position in
281          the Python list to a value to store in the
282          ``ordering_attr``.  Values returned are usually (but need not be!)
283          integers.
284
285          An ``ordering_func`` is called with two positional parameters: the
286          index of the element in the list, and the list itself.
287
288          If omitted, Python list indexes are used for the attribute values.
289          Two basic pre-built numbering functions are provided in this module:
290          ``count_from_0`` and ``count_from_1``.  For more exotic examples
291          like stepped numbering, alphabetical and Fibonacci numbering, see
292          the unit tests.
293
294        :param reorder_on_append:
295          Default False.  When appending an object with an existing (non-None)
296          ordering value, that value will be left untouched unless
297          ``reorder_on_append`` is true.  This is an optimization to avoid a
298          variety of dangerous unexpected database writes.
299
300          SQLAlchemy will add instances to the list via append() when your
301          object loads.  If for some reason the result set from the database
302          skips a step in the ordering (say, row '1' is missing but you get
303          '2', '3', and '4'), reorder_on_append=True would immediately
304          renumber the items to '1', '2', '3'.  If you have multiple sessions
305          making changes, any of whom happen to load this collection even in
306          passing, all of the sessions would try to "clean up" the numbering
307          in their commits, possibly causing all but one to fail with a
308          concurrent modification error.
309
310          Recommend leaving this with the default of False, and just call
311          ``reorder()`` if you're doing ``append()`` operations with
312          previously ordered instances or when doing some housekeeping after
313          manual sql operations.
314
315        """
316        self.ordering_attr = ordering_attr
317        if ordering_func is None:
318            ordering_func = count_from_0
319        self.ordering_func = ordering_func
320        self.reorder_on_append = reorder_on_append
321
322    # More complex serialization schemes (multi column, e.g.) are possible by
323    # subclassing and reimplementing these two methods.
324    def _get_order_value(self, entity: _T) -> Any:
325        return getattr(entity, self.ordering_attr)
326
327    def _set_order_value(self, entity: _T, value: Any) -> None:
328        setattr(entity, self.ordering_attr, value)
329
330    def reorder(self) -> None:
331        """Synchronize ordering for the entire collection.
332
333        Sweeps through the list and ensures that each object has accurate
334        ordering information set.
335
336        """
337        for index, entity in enumerate(self):
338            self._order_entity(index, entity, True)
339
340    # As of 0.5, _reorder is no longer semi-private
341    _reorder = reorder
342
343    def _order_entity(
344        self, index: int, entity: _T, reorder: bool = True
345    ) -> None:
346        have = self._get_order_value(entity)
347
348        # Don't disturb existing ordering if reorder is False
349        if have is not None and not reorder:
350            return
351
352        should_be = self.ordering_func(index, self)
353        if have != should_be:
354            self._set_order_value(entity, should_be)
355
356    def append(self, entity: _T) -> None:
357        super().append(entity)
358        self._order_entity(len(self) - 1, entity, self.reorder_on_append)
359
360    def _raw_append(self, entity: _T) -> None:
361        """Append without any ordering behavior."""
362
363        super().append(entity)
364
365    _raw_append = collection.adds(1)(_raw_append)
366
367    def insert(self, index: SupportsIndex, entity: _T) -> None:
368        super().insert(index, entity)
369        self._reorder()
370
371    def remove(self, entity: _T) -> None:
372        super().remove(entity)
373
374        adapter = collection_adapter(self)
375        if adapter and adapter._referenced_by_owner:
376            self._reorder()
377
378    def pop(self, index: SupportsIndex = -1) -> _T:
379        entity = super().pop(index)
380        self._reorder()
381        return entity
382
383    @overload
384    def __setitem__(self, index: SupportsIndex, entity: _T) -> None: ...
385
386    @overload
387    def __setitem__(self, index: slice, entity: Iterable[_T]) -> None: ...
388
389    def __setitem__(
390        self,
391        index: Union[SupportsIndex, slice],
392        entity: Union[_T, Iterable[_T]],
393    ) -> None:
394        if isinstance(index, slice):
395            step = index.step or 1
396            start = index.start or 0
397            if start < 0:
398                start += len(self)
399            stop = index.stop or len(self)
400            if stop < 0:
401                stop += len(self)
402            entities = list(entity)  # type: ignore[arg-type]
403            for i in range(start, stop, step):
404                self.__setitem__(i, entities[i])
405        else:
406            self._order_entity(int(index), entity, True)  # type: ignore[arg-type] # noqa: E501
407            super().__setitem__(index, entity)  # type: ignore[assignment]
408
409    def __delitem__(self, index: Union[SupportsIndex, slice]) -> None:
410        super().__delitem__(index)
411        self._reorder()
412
413    def __reduce__(self) -> Any:
414        return _reconstitute, (self.__class__, self.__dict__, list(self))
415
416    for func_name, func in list(locals().items()):
417        if (
418            callable(func)
419            and func.__name__ == func_name
420            and not func.__doc__
421            and hasattr(list, func_name)
422        ):
423            func.__doc__ = getattr(list, func_name).__doc__
424    del func_name, func
425
426
427def _reconstitute(
428    cls: Type[OrderingList[_T]], dict_: Dict[str, Any], items: List[_T]
429) -> OrderingList[_T]:
430    """Reconstitute an :class:`.OrderingList`.
431
432    This is the adjoint to :meth:`.OrderingList.__reduce__`.  It is used for
433    unpickling :class:`.OrderingList` objects.
434
435    """
436    obj = cls.__new__(cls)
437    obj.__dict__.update(dict_)
438    list.extend(obj, items)
439    return obj
440 
codekingpro/portable-devtools · Team Ai