cant redefine `__hash__()` or `__nonzero__()` on mapped classes

Issue #676 resolved
Mike Bayer repo owner created an issue

Because the Session/UOW stores instances in Sets all over the place, it relies upon the hash behavior of mapped instances to determine instance identity. All of the set-based storage of instances should be modified to use some special set which guarantees to use the id(obj) of the instance regardless of __hash__().

example stack trace:

File '<string>', line 1 in <lambda>
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/Pylons-0.9.6rc1-py2.5.egg/pylons/decorators/__init__.py', line 160 in wrapper
  return func(self, *args, **kwargs)
File '/Users/dialtone/dev/mipworld/mip/mip/controllers/links/main.py', line 36 in add_POST
  new.tags.append(newt)
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/SQLAlchemy-0.3.9-py2.5.egg/sqlalchemy/orm/attributes.py', line 528 in append
  self.__setrecord(item)
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/SQLAlchemy-0.3.9-py2.5.egg/sqlalchemy/orm/attributes.py', line 500 in __setrecord
  self.attr.append_event(event, self.obj, item)
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/SQLAlchemy-0.3.9-py2.5.egg/sqlalchemy/orm/attributes.py', line 343 in append_event
  ext.append(event or self, obj, value)
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/SQLAlchemy-0.3.9-py2.5.egg/sqlalchemy/orm/unitofwork.py', line 44 in append
  if self.cascade is not None and self.cascade.save_update and item not in sess:
File '/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/site-packages/SQLAlchemy-0.3.9-py2.5.egg/sqlalchemy/orm/session.py', line 642 in __contains__
  return self._is_attached(obj) and (obj in self.uow.new or self.identity_map.has_key(obj._instance_key))
AttributeError: 'Tag' object has no attribute '_instance_key'

with a class such as:

class Tag(Base):
def __init__(self, name):

       30         self.name = name

       31

       32     def __repr__(self):

       33         return "%s(%r)" % (self.__class__.__name__, self.name)

       34

       35     def __eq__(self, other):

       36         if isinstance(other, (str, unicode)):

       37             return self.name == other

       38         return self.name == other.name

       39

       40     def __hash__(self):

       41         return hash(self.name)

Comments (4)

  1. jek

    2d3f907ac0a23d410ecc3c74afc6d63bd2abc186 has an id()-based set implementation that could do the trick. Also this little ditty is useful for rooting out current usage:

    Index: test/testlib/__init__.py
    ===================================================================
    --- test/testlib/__init__.py    (revision 3684)
    +++ test/testlib/__init__.py    (working copy)
    @@ -11,7 +11,20 @@
     import testlib.engines as engines
    
    
    +import sqlalchemy.orm
    +def mapper(type_, *args, **kw):
    +    if '__hash__' not in type_.__dict__:
    +        def __hash__(*a):
    +            raise AssertionError("%s.__hash__ called." % type_.__name__)
    +        type_.__hash__ = __hash__
    +    if '__nonzero__' not in type_.__dict__:
    +        def __nonzero__(*a):
    +            raise AssertionError("%s.__nonzero__ called." % type_.__name__)
    +        type_.__nonzero__ = __nonzero__
    +    return sqlalchemy.orm.mapper(type_, *args, **kw)
    +
     __all__ = ('testing',
    +           'mapper',
                'Table', 'Column',
                'PersistTest', 'AssertMixin', 'ORMTest', 'SQLCompileTest',
                'profiling', 'engines')
    
  2. jek

    This (and __eq__ support) is in 429e69db67baa8fc93ff2b55361ba2831cc26144. Also added --noncomparable, --unhashable and --truthless options to the test suite- these set up instrumented __methods__ on every class that goes through mapper(). There's also a compact mini-test for everyday test runs to catch obvious regressions.

  3. Log in to comment