more documentation.

This commit is contained in:
chris t 2020-07-20 19:07:53 -07:00
parent 04d4bbe556
commit b9b286b379
3 changed files with 47 additions and 9 deletions

View File

@ -1,7 +1,7 @@
Libkosokoso -- simple tagging with sqlalchemy.
==============================================
Libkosokoso provides compact and table-stingy tags to
Libkosokoso provides compact and table-stingy tags for
sqlalchemy-backed objects.
Usage
@ -10,17 +10,22 @@ Usage
::
import libkosokoso as kk
import sqlalchemy as sa
base = sa.ext.declarative.declarative_base()
class Foo(kk.Taggable):
kk.__dict__.update(kk.init_base(base))
class Foo(kk.Taggable, base):
__tablename__ = 'foos'
a = Foo()
a.tags.append('tag1')
a.tags.extend(['tag2', 'tag3'])
and so forth.
and so forth. (Note that certain key steps, like connecting to a
database, are omitted.)
This will generate tables like this::
The code above will generate tables like this::
foos:
db_id
@ -57,7 +62,9 @@ objects. The tag object has a useful member, "collection", which
provides direct access to the objects with this tag.
It's also possible to add kk.Tag objects directly to the tags
property on a taggable object, and they'll be handled correctly.
property on a taggable object, and they'll be handled correctly.
Similarly, it's possible to add objects to the collections member of a
Tag object.
Design considerations
@ -67,10 +74,31 @@ My basic reasoning was that at some point I'd have to go and muck
about in the database by hand, and I wanted a structure that was easy
to perceive and modify.
I also wanted to avoid modifying preexisting tables. This structure
can be dropped into a live system without interfering with any current
entities. (Likewise the reverse.)
At all times I aimed to have an interface that matches my intuitions.
Hence things like being able to both add tags to an object and add
objects to a tag, despite the massive increase in complexity for
arguably minimal gain.
I explicitly did not aim for efficiency. The underlying code is
object-happy and probably very inefficient.
Implementation notes
--------------------
The init_base() call required to set up the library is an absolute
mess. I'm pleased that I got it working, but kind of surprised.
The authors of SQLAlchemy would most likely be appalled. This model
breaks the "relational" element of the database quite badly. One
solution that might make everyone happy would be to add a tags table
for every current type, and use a view to consolidate them if desired.
Why is it called that?
----------------------
@ -78,6 +106,5 @@ Why is it called that?
secretly. I picked the name because tags always feel like a kind of
buzzing side channel to me.
(Originally, it was "guzuguzu", which is, like, slow. This took me
way longer to write than I was expecting.)
(I also sometimes call it "guzuguzu", which is, like, slow. This took
me way longer to write than I was expecting.)

View File

@ -154,7 +154,7 @@ class TaggableBase(object):
def cls_init(self, tag=None):
# traceback.print_stack()
# print 'cls_init called, type %s' % table
logging.debug("cls_init called, type %s".format(table))
super(TagAssociation, self).__init__()
self.target_table = table
# is this necessary? how does this get called?
@ -191,6 +191,7 @@ class TaggableBase(object):
def init_base(new_base):
"""Set up classes based on new_base."""
# based on https://stackoverflow.com/a/41927212
def init_cls(name, base):
current = globals()[name + 'Base']
new_ns = current.__dict__.copy()
@ -202,9 +203,13 @@ def init_base(new_base):
init_cls(c, new_base)
globals()['Base'] = new_base
# these are here because "Tag" needs to exist before adding the
# listener.
sa.event.listen(Tag, 'before_insert', delete_before_insert)
sa.event.listen(sa.orm.session.Session, 'before_flush',
enforce_unique_text)
# need to return new types so that the caller can incorporate them
# into its view of the module.
return {'Tag': globals()['Tag'],
'TagAssociation': globals()['TagAssociation'],
'Taggable': globals()['Taggable']}
@ -212,6 +217,7 @@ def init_base(new_base):
def delete_before_insert(mapper, conn, target):
# TODO figure out where exactly transactions happen.
# TODO can we just upsert? Or, for that matter, skip the insert?
r = conn.execute("SELECT db_id FROM kk_tags WHERE text='%s'" %
target.text)
if r:

View File

@ -84,6 +84,11 @@ class ks_basic(unittest.TestCase):
self.session.add(t2)
self.session.commit()
# del t1, t2
ts = self.session.query(kk.Tag).all()
self.assertEqual(1, len(ts))
def test_collection(self):
"""Test access to the collection of objects associated with a
tag."""