from sqlalchemy.orm.collections import collection
class MyClass(object):
# ...
-
+
@collection.adds(1)
def store(self, item):
self.data.append(item)
-
+
@collection.removes_return()
def pop(self):
return self.data.pop()
__all__ = ['collection', 'collection_adapter',
'mapped_collection', 'column_mapped_collection',
'attribute_mapped_collection']
-
+
def column_mapped_collection(mapping_spec):
"""A dictionary-based collection type with column-based keying.
# Bundled as a class solely for ease of use: packaging, doc strings,
# importability.
-
+
def appender(cls, fn):
"""Tag the method as the collection appender.
database contains rows that violate your collection semantics, you
will need to get creative to fix the problem, as access via the
collection will not work.
-
+
If the appender method is internally instrumented, you must also
receive the keyword argument '_sa_initiator' and ensure its
promulgation to collection events.
receive the keyword argument '_sa_initiator' and ensure its
promulgation to collection events.
"""
-
+
setattr(fn, '_sa_instrument_role', 'remover')
return fn
remover = classmethod(remover)
@collection.internally_instrumented
def extend(self, items): ...
"""
-
+
setattr(fn, '_sa_instrumented', True)
return fn
internally_instrumented = classmethod(internally_instrumented)
the instance. A single argument is passed: the collection adapter
that has been linked, or None if unlinking.
"""
-
+
setattr(fn, '_sa_instrument_role', 'on_link')
return fn
on_link = classmethod(on_link)
+ def converter(cls, fn):
+ """Tag the method as the collection converter.
+
+ This optional method will be called when a collection is being
+ replaced entirely, as in::
+
+ myobj.acollection = [newvalue1, newvalue2]
+
+ The converter method will receive the object being assigned and should
+ return an iterable of values suitable for use by the ``appender``
+ method. A converter must not assign values or mutate the collection,
+ it's sole job is to adapt the value the user provides into an iterable
+ of values for the ORM's use.
+
+ The default converter implementation will use duck-typing to do the
+ conversion. A dict-like collection will be convert into an iterable
+ of dictionary values, and other types will simply be iterated.
+
+ @collection.converter
+ def convert(self, other): ...
+
+ If the duck-typing of the object does not match the type of this
+ collection, a TypeError is raised.
+
+ Supply an implementation of this method if you want to expand the
+ range of possible types that can be assigned in bulk or perform
+ validation on the values about to be assigned.
+ """
+
+ setattr(fn, '_sa_instrument_role', 'converter')
+ return fn
+ converter = classmethod(converter)
+
def adds(cls, arg):
"""Mark the method as adding an entity to the collection.
the method. The decorator argument indicates which method argument
holds the SQLAlchemy-relevant value to be added, and return value, if
any will be considered the value to remove.
-
+
Arguments can be specified positionally (i.e. integer) or by name::
@collection.replaces(2)
def __setitem__(self, index, item): ...
"""
-
+
def decorator(fn):
setattr(fn, '_sa_instrument_before', ('fire_append_event', arg))
setattr(fn, '_sa_instrument_after', 'fire_remove_event')
return fn
return decorator
removes = classmethod(removes)
-
+
def removes_return(cls):
"""Mark the method as removing an entity in the collection.
raise TypeError("'%s' object is not iterable" %
type(collection).__name__)
-
+
class CollectionAdapter(object):
"""Bridges between the ORM and arbitrary Python collections.
if hasattr(data, '_sa_on_link'):
getattr(data, '_sa_on_link')(None)
+ def adapt_like_to_iterable(self, obj):
+ """Converts collection-compatible objects to an iterable of values.
+
+ Can be passed any type of object, and if the underlying collection
+ determines that it can be adapted into a stream of values it can
+ use, returns an iterable of values suitable for append()ing.
+
+ This method may raise TypeError or any other suitable exception
+ if adaptation fails.
+
+ If a converter implementation is not supplied on the collection,
+ a default duck-typing-based implementation is used.
+ """
+
+ converter = getattr(self._data(), '_sa_converter', None)
+ if converter is not None:
+ return converter(obj)
+
+ setting_type = sautil.duck_type_collection(obj)
+
+ if obj is None or setting_type != self.attr.collection_interface:
+ raise TypeError(
+ "Incompatible collection type: %s is not %s-like" %
+ (type(obj).__name__, self.attr.collection_interface.__name__))
+
+ # If the object is an adapted collection, return the (iterable) adapter.
+ if getattr(obj, '_sa_adapter', None) is not None:
+ return getattr(obj, '_sa_adapter')
+ elif setting_type == dict:
+ return getattr(obj, 'itervalues', getattr(obj, 'values'))()
+ else:
+ return iter(obj)
+
def append_with_event(self, item, initiator=None):
"""Add an entity to the collection, firing mutation events."""
mutation, and should be left as None unless you are passing along
an initiator value from a chained operation.
"""
-
+
if initiator is not False and item is not None:
self.attr.fire_append_event(self.owner_state, item, initiator)
if initiator is not False and item is not None:
self.attr.fire_remove_event(self.owner_state, item, initiator)
-
+
def __getstate__(self):
return { 'key': self.attr.key,
'owner_state': self.owner_state,
# FIXME: more formally document this as a decoratorless/Python 2.3
# option for specifying instrumentation. (likely doc'd here in code only,
# not in online docs.)
- #
+ #
# __instrumentation__ = {
# 'rolename': 'methodname', # ...
# 'methods': {
raise exceptions.ArgumentError(
"Can not instrument a built-in type. Use a "
"subclass, even a trivial one.")
-
+
collection_type = sautil.duck_type_collection(cls)
if collection_type in __interfaces:
roles = __interfaces[collection_type].copy()
# note role declarations
if hasattr(method, '_sa_instrument_role'):
role = method._sa_instrument_role
- assert role in ('appender', 'remover', 'iterator', 'on_link')
+ assert role in ('appender', 'remover', 'iterator',
+ 'on_link', 'converter')
roles[role] = name
# transfer instrumentation requests from decorated function
for method, (before, argument, after) in methods.items():
setattr(cls, method,
_instrument_membership_mutator(getattr(cls, method),
- before, argument, after))
+ before, argument, after))
# intern the role map
for role, method in roles.items():
setattr(cls, '_sa_%s' % role, getattr(cls, method))
executor = None
else:
executor = getattr(args[0], '_sa_adapter', None)
-
+
if before and executor:
getattr(executor, before)(value, initiator)
executor = getattr(collection, '_sa_adapter', None)
if executor:
getattr(executor, 'fire_append_event')(item, _sa_initiator)
-
+
def __del(collection, item, _sa_initiator=None):
"""Run del events, may eventually be inlined into decorators."""
executor = getattr(collection, '_sa_adapter', None)
if executor:
getattr(executor, 'fire_remove_event')(item, _sa_initiator)
-
+
def _list_decorators():
"""Hand-turned instrumentation wrappers that can decorate any list-like
class."""
-
+
def _tidy(fn):
setattr(fn, '_sa_instrumented', True)
fn.__doc__ = getattr(getattr(list, fn.__name__), '__doc__')
fn(self, start, end, values)
_tidy(__setslice__)
return __setslice__
-
+
def __delslice__(fn):
def __delslice__(self, start, end):
for value in self[start:end]:
self.append(value)
_tidy(extend)
return extend
-
+
def pop(fn):
def pop(self, index=-1):
item = fn(self, index)
'remover': 'remove',
'iterator': '__iter__', }
-class InstrumentedSet(sautil.Set):
+class InstrumentedSet(sautil.Set):
"""An instrumented version of the built-in set (or Set)."""
__instrumentation__ = {
'remover': 'remove',
'iterator': '__iter__', }
-class InstrumentedDict(dict):
+class InstrumentedDict(dict):
"""An instrumented version of the built-in dict."""
__instrumentation__ = {
callable that takes an object and returns an object for use as a dictionary
key.
"""
-
+
def __init__(self, keyfunc):
"""Create a new collection with keying provided by keyfunc.
self.__setitem__(key, value, _sa_initiator)
set = collection.internally_instrumented(set)
set = collection.appender(set)
-
+
def remove(self, value, _sa_initiator=None):
"""Remove an item from the collection by value, consulting this instance's keyfunc for the key."""
-
+
key = self.keyfunc(value)
# Let self[key] raise if key is not in this collection
if self[key] != value:
self.__delitem__(key, _sa_initiator)
remove = collection.internally_instrumented(remove)
remove = collection.remover(remove)
+
+ def _convert(self, dictlike):
+ """Validate and convert a dict-like object into values for set()ing.
+
+ This is called behind the scenes when a MappedCollection is replaced
+ entirely by another collection, as in::
+
+ myobj.mappedcollection = {'a':obj1, 'b': obj2} # ...
+
+ Raises a TypeError if the key in any (key, value) pair in the dictlike
+ object does not match the key that this collection's keyfunc would
+ have assigned for that value.
+ """
+
+ for incoming_key, value in sautil.dictlike_iteritems(dictlike):
+ new_key = self.keyfunc(value)
+ if incoming_key != new_key:
+ raise TypeError(
+ "Found incompatible key %r for value %r; this collection's "
+ "keying function requires a key of %r for this value." % (
+ incoming_key, value, new_key))
+ yield value
+ _convert = collection.converter(_convert)
self.assert_(o.keys() == ['a', 'b', 'snack', 'c'])
self.assert_(o.values() == [1, 2, 'attack', 3])
-
+
o.pop('snack')
self.assert_(o.keys() == ['a', 'b', 'c'])
assert False
except exceptions.ArgumentError, e:
assert str(e) == "__contains__ requires a string argument"
-
+
def test_compare(self):
cc1 = sql.ColumnCollection()
cc2 = sql.ColumnCollection()
class ArgSingletonTest(unittest.TestCase):
def test_cleanout(self):
util.ArgSingleton.instances.clear()
-
+
class MyClass(object):
__metaclass__ = util.ArgSingleton
def __init__(self, x, y):
self.x = x
self.y = y
-
+
m1 = MyClass(3, 4)
m2 = MyClass(1, 5)
m3 = MyClass(3, 4)
assert m1 is m3
assert m2 is not m3
assert len(util.ArgSingleton.instances) == 2
-
+
m1 = m2 = m3 = None
MyClass.dispose(MyClass)
assert len(util.ArgSingleton.instances) == 0
+
class ImmutableSubclass(str):
pass
self.assertRaises(TypeError, hash, ids)
+class DictlikeIteritemsTest(unittest.TestCase):
+ baseline = set([('a', 1), ('b', 2), ('c', 3)])
+
+ def _ok(self, instance):
+ iterator = util.dictlike_iteritems(instance)
+ self.assertEquals(set(iterator), self.baseline)
+
+ def _notok(self, instance):
+ self.assertRaises(TypeError,
+ util.dictlike_iteritems,
+ instance)
+
+ def test_dict(self):
+ d = dict(a=1,b=2,c=3)
+ self._ok(d)
+
+ def test_subdict(self):
+ class subdict(dict):
+ pass
+ d = subdict(a=1,b=2,c=3)
+ self._ok(d)
+
+ def test_UserDict(self):
+ import UserDict
+ d = UserDict.UserDict(a=1,b=2,c=3)
+ self._ok(d)
+
+ def test_object(self):
+ self._notok(object())
+
+ def test_duck_1(self):
+ class duck1(object):
+ def iteritems(duck):
+ return iter(self.baseline)
+ self._ok(duck1())
+
+ def test_duck_2(self):
+ class duck2(object):
+ def items(duck):
+ return list(self.baseline)
+ self._ok(duck2())
+
+ def test_duck_3(self):
+ class duck3(object):
+ def iterkeys(duck):
+ return iter(['a', 'b', 'c'])
+ def __getitem__(duck, key):
+ return dict(a=1,b=2,c=3).get(key)
+ self._ok(duck3())
+
+ def test_duck_4(self):
+ class duck4(object):
+ def iterkeys(duck):
+ return iter(['a', 'b', 'c'])
+ self._notok(duck4())
+
+ def test_duck_5(self):
+ class duck5(object):
+ def keys(duck):
+ return ['a', 'b', 'c']
+ def get(duck, key):
+ return dict(a=1,b=2,c=3).get(key)
+ self._ok(duck5())
+
+ def test_duck_6(self):
+ class duck6(object):
+ def keys(duck):
+ return ['a', 'b', 'c']
+ self._notok(duck6())
+
+
if __name__ == "__main__":
testbase.main()