diff --git a/Readme.rst b/Readme.rst index b812693..37fe186 100644 --- a/Readme.rst +++ b/Readme.rst @@ -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.) diff --git a/kosokoso.py b/kosokoso.py index a934676..9c62da6 100644 --- a/kosokoso.py +++ b/kosokoso.py @@ -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: diff --git a/tests/test_basic.py b/tests/test_basic.py index 918b160..3541a33 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -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."""