From d8204ea92abe4b74d99549b89260c616f82e9dc5 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 30 Jan 2007 01:01:22 +0000 Subject: [PATCH] further work on insuring clear_mappers() really works. assignmapper identified as a much trickier thing to clean out. added a unit test so that if any new collections get introduced we are still testing. --- lib/sqlalchemy/ext/assignmapper.py | 1 + lib/sqlalchemy/orm/__init__.py | 10 +++- lib/sqlalchemy/orm/mapper.py | 4 +- lib/sqlalchemy/util.py | 4 ++ test/orm/alltests.py | 1 + test/orm/memusage.py | 79 ++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 test/orm/memusage.py diff --git a/lib/sqlalchemy/ext/assignmapper.py b/lib/sqlalchemy/ext/assignmapper.py index 89cf962e72..b76f299433 100644 --- a/lib/sqlalchemy/ext/assignmapper.py +++ b/lib/sqlalchemy/ext/assignmapper.py @@ -39,3 +39,4 @@ def assign_mapper(ctx, class_, *args, **kwargs): for name in ['flush', 'delete', 'expire', 'refresh', 'expunge', 'merge', 'save', 'update', 'save_or_update']: monkeypatch_objectstore_method(ctx, class_, name) return m + diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 96890272d4..1e1a75b631 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -74,8 +74,10 @@ def clear_mappers(): when new mappers are created, they will be assigned to their classes as their primary mapper.""" for mapper in mapper_registry.values(): attribute_manager.reset_class_managed(mapper.class_) + mapper.class_key.dispose() + if hasattr(mapper.class_, 'c'): + del mapper.class_.c mapper_registry.clear() - mapperlib.ClassKey.instances.clear() def clear_mapper(m): """remove the given mapper from the storage of mappers. @@ -83,7 +85,11 @@ def clear_mapper(m): when a new mapper is created for the previous mapper's class, it will be used as that classes' new primary mapper.""" del mapper_registry[m.class_key] - + attribute_manager.reset_class_managed(m.class_) + if hasattr(m.class_, 'c'): + del m.class_.c + m.class_key.dispose() + def extension(ext): """return a MapperOption that will insert the given MapperExtension to the beginning of the list of extensions that will be called in the context of the Query. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index d6967934cd..1d4ce6bb9c 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1458,7 +1458,9 @@ class ClassKey(object): return self is other def __repr__(self): return "ClassKey(%s, %s)" % (repr(self.class_), repr(self.entity_name)) - + def dispose(self): + type(self).dispose_static(self.class_, self.entity_name) + def has_identity(object): return hasattr(object, '_instance_key') diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 7066309189..54b1afa9fb 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -49,6 +49,10 @@ def reversed(seq): class ArgSingleton(type): instances = {} + def dispose_static(self, *args): + hashkey = (self, args) + #if hashkey in ArgSingleton.instances: + del ArgSingleton.instances[hashkey] def __call__(self, *args): hashkey = (self, args) try: diff --git a/test/orm/alltests.py b/test/orm/alltests.py index ffcdd3d218..6d6aba2b26 100644 --- a/test/orm/alltests.py +++ b/test/orm/alltests.py @@ -17,6 +17,7 @@ def suite(): 'orm.relationships', 'orm.association', 'orm.merge', + 'orm.memusage', 'orm.cycles', 'orm.poly_linked_list', diff --git a/test/orm/memusage.py b/test/orm/memusage.py new file mode 100644 index 0000000000..84d3ebdb2e --- /dev/null +++ b/test/orm/memusage.py @@ -0,0 +1,79 @@ +from sqlalchemy import * +from sqlalchemy.orm import mapperlib, session, unitofwork, attributes +Mapper = mapperlib.Mapper +import gc +import testbase +import tables + +class A(object):pass +class B(object):pass + +class MapperCleanoutTest(testbase.AssertMixin): + """test that clear_mappers() removes everything related to the class. + + does not include classes that use the assignmapper extension.""" + def setUp(self): + global engine + engine = testbase.db + + def test_mapper_cleanup(self): + for x in range(0, 5): + self.do_test() + gc.collect() + for o in gc.get_objects(): + if isinstance(o, Mapper): + # the classes in the 'tables' package have assign_mapper called on them + # which is particularly sticky + # if getattr(tables, o.class_.__name__, None) is o.class_: + # continue + # well really we are just testing our own classes here + if (o.class_ not in [A,B]): + continue + assert False + assert True + + def do_test(self): + metadata = BoundMetaData(engine) + + table1 = Table("mytable", metadata, + Column('col1', Integer, primary_key=True), + Column('col2', String(30)) + ) + + table2 = Table("mytable2", metadata, + Column('col1', Integer, primary_key=True), + Column('col2', String(30)), + Column('col3', String(30), ForeignKey("mytable.col1")) + ) + + metadata.create_all() + + + m1 = mapper(A, table1, properties={ + "bs":relation(B) + }) + m2 = mapper(B, table2) + + m3 = mapper(A, table1, non_primary=True) + + sess = create_session() + a1 = A() + a2 = A() + a3 = A() + a1.bs.append(B()) + a1.bs.append(B()) + a3.bs.append(B()) + for x in [a1,a2,a3]: + sess.save(x) + sess.flush() + sess.clear() + + alist = sess.query(A).select() + for a in alist: + print "A", a, "BS", [b for b in a.bs] + + metadata.drop_all() + clear_mappers() + +if __name__ == '__main__': + testbase.main() \ No newline at end of file -- 2.47.2