Introduction ------------ This paper describes the future. Devilstick's data access is realized through molecules and atoms. Molecules consist of atoms, which hold their attributes. Further they may contain molecules like zope containers do - Yes, the analogy is not perfect, but we are quite confident that it is sane that our molecules can contain molecules (see below for how you can interact with them) - feedback welcome! *Atoms* as a kind of powerful attributes and *molecules* are its containers. In general, and for all the examples that follow, you might not be allowed to do what you would like to do, as there is a model, that defines what is allowed and what not. For more on models, please see XXX. Data access objects ------------------- Devilstick's data access objects are persistent. There are atoms and molecules. Atoms ~~~~~ Atoms have a 'value' attribute enabling access to their value via __getattr__ and __setattr__, for newly created atoms it is None: >>> attr = SomeAttribute() >>> attr.value is None True >>> attr.value = 1 >>> attr.value 1 You cannot __delattr__ as it makes no sense (a value is alwaysw there): >>> del attr.value BANG! We could set the value to None for a __delattr__ call, but you would not expect an attribute to become None, if you delete it. Assign None, if that is what you want - no implicit unintuitive magic. Please let us know, if you have a proposal for __delattr__ *intuitive* magic. XXX: (flo) Is there sense to really *deleting* the value attribute? (no, jens) XXX (jens) Instead of None we should use an empty marker. The empty marker could then got assigned by delattr and used as its initial value. Further they may hold metadata. metada is any schema attribute of the atom not named value: >>> attr.mimetype = u'text/plain' >>> attr.mimetype u'text/plain' Usally we propose to avoid usage of metadata. Only in very special cases where its bound very strict to the object it makes sense, like the mimetype of a file. It does not make sense for an additional title of the file or the authors name. Use an own atom for such information and put the atoms in one molecule for such cases. Molecules ~~~~~~~~~ Molecules consist of atoms and may contain molecules; molecules are accessed and assigned by mapping syntax and atoms are assigned by attribute syntax; they may have the same name: >>> uid_mol = SomeMolecule() >>> uid_atom = SomeAtom() >>> mol['uid'] = uid_mol >>> mol.uid = uid_atom >>> mol['uid'] is uid_mol True >>> mol.uid is uid_atom.value True For molecules containing molecules, this is about all to it. Atoms are assigned to molecules via __setattr__: >>> mol = SomeMolecule() >>> attr1 = SomeAtom() >>> attr1.value = 1 >>> attr2 = SomeAtom() >>> attr2.value = 2 >>> attr3 = SomeAtom() >>> attr3.value = 3 >>> mol.attr1 = attr1 >>> mol.attr2 = attr2 >>> mol.attr3 = attr3 and removed again by __delattr__: >>> del mol.attr3 >>> getattr(mol, 'attr3', 'does not exist') 'does not exist' Note: It's good to accustom to not using hasattr() in a persistent world, though there is some save hasattr() around, which does not eat ZODB errors. Now things become a bit magic to allow for an intuitive API: Once an atom is connected to a molecule, you may retrieve it's value through the molecule's __getattr__: >>> mol.attr1 1 >>> mol.attr2 2 and assign a value through the molecule's __setattr__: >>> mol.attr1 = 10 >>> attr1.value 10 Once an attribute is deleted from a molecule, it is also not connected anymore: >>> mol.attr3 = attr3 >>> attr3.value 3 >>> del mol.attr3 >>> mol.attr3 = 17 >>> attr3.value 3 >>> attr3.value = 42 >>> attr3.value 42 >>> mol.attr3 17 The atom object itself, you get via the molecule's __atom__ call: >>> atom1 = mol.__atom__(self, 'attr1') >>> atom1 is attr1 True As this is tedious, there is an atom(mol, name) function in devilstick.dao.core, which does exactly this: >>> atom2 = atom(mol, 'attr2') >>> atom2 is attr2 True A tuple of all atoms is retrieved by the __atoms__ call, wrapped by the atoms function: >>> atoms1 = atoms(mol) >>> atoms2 = mol.__atoms__() For each call, a new tuple is returned, containing the same elements in the same order: >>> atoms1 is atoms2 False >>> atoms1[0] is atoms2[0] True >>> set(atoms1) - set(atoms2) set([]) This means you cannot delete an atom, by removing it from the returned list, because a) it is not a list, and b) it should not be a list. If you want to delete or add atoms, use __delattr__ and __getattr__ as shown above. XXX: Maybe it should be a list, but definitely not a magically connected list. Count on it, that it is an iterable. Now you might ask yourselve "Great so far, but I really would like to simply create new attributes (sorry, atoms), by assigning a plain integer, as I would do it with every other python object." This is possible! Whenever you are assigning something to a non-existent "attribute" of a molecule, an adapter to IAtomizer will be queried and used for the conversion. For all python builtin types adapters are provided. If there is no adapter or the adapter fails, you will get ValueError explaining what is going on. This will also happen, if your model forbidds what you are trying to do. Devilstick is no replacement for ZODB, but a generic, splendid frontend for model-defined storages, and more. >>> del mol >>> mol = SomeMolecule() >>> mol.int1 = 1 >>> mol.int1 1 >>> atom(mol, 'int1') IntAtom('1') >>> mol.str1 = 'some string' >>> atom(mol, 'str1') StringAtom('some string') >>> mol.str2 = u'some string' >>> atom(mol, 'str2') StringAtom(u'some string') Atoms for lists and mappings return special lists/mappings, that are connected to devilstick: >>> some_list = [1,2,3.4] >>> mol.list1 = some_list >>> mol.list1[0] 1 >>> con_list = mol.list1 >>> con_list[0] = 17 >>> mol.list1[0] 17 >>> con_list.__parent__ is mol True >>> mapping = dict(key1=1, key2=2) >>> mol.mapping1 = mapping >>> mol.mapping1['key1'] 1 >>> con_mapping = mol.mapping1 >>> con_mapping['key1'] = 17 >>> mol.mapping1['key1'] 17 This means, that for lists and mappings, what you assign is not what you receive: >>> mol.list1 is some_list False >>> mol.mapping1 is mapping False XXX: This is unintuitive and needs thinking or explanation. Transparent persistent wrappers would be nice. This is also relevent for further aggregated types. If you assign a tuple, you will implicitly generate a TupleAtom, that just returns plain tuples as values, as there is no need for a connection to the molecule - you cannot make changes anyway: >>> mol.tuple1 = (1,2,3) >>> mol.tuple1 (1, 2, 3) If you assign a tuple to an existing ListAtom, it will be overwritten by a TupleAtom (iff the model allows it, as always): >>> mol.list1 = (1,2) >>> mol.list1 (1, 2) Again, no unintuitive magic going on here, if you want the tuple to be converted to a list, then please do that explicitly: >>> mol.list1 = list((1,2)) >>> mol.list1 PowerList([1, 2], "I am not a real repr") XXX: This is probably not, how the repr should look like This does not mean, that a ListAtom cannot be initialized with an arbitrary iterable when generating it explicitly and passing it to its __init__ function. >>> list1 = ListAtom((1,2,3,4)) >>> mol.list1 = list1 >>> mol.list1 PowerList([1, 2, 3, 4], "I am not a real repr") Conclusion ~~~~~~~~~~ XXX: Atoms behave like normal attributes, at least most of the time (see below). Please let us know, if you see inconsistencies. In addition to a plain attribute, it is always possible to retrieve the atom object, which enables you for example to render views for the "attribute" in kss. Future ideas / dispute area --------------------------- >>> alsoProvides(mol, ISomeMarkerInterface) class IDevilstickSchema(IInterface): >>> devilstick_schemas = filter(IDevilstickSchema.providedBy(), list(providedBy(molecule))) >>> for schema in devilstick_schemas: ... for field in schema: (c) 2008 Florian Friesdorf