Michael Bayer wrote:
On Apr 27, 2006, at 11:05 PM, Daniel Miller wrote:
the __session__() method is only called to get a session, *when the
instance is not already associated with a session*. this method is
merely there as the standard way to plug into systems such as the
SessionContext __metaclass__ youve built, which is associating
classes with a particular session context.
Actually, it's not doing that at all. It's associating instances with
a particular Session (not a SessionContext). OK, the base/meta classes
do (internally) associate the object with a context. But there should
be no need to obtain the context from an instance. When the current
session is needed it should be obtained directly from the context and
not through some arbitrary mapped-object instance. If a user needs to
pass a context around, then he should do that with his own plumbing.
It seems that you are saying you never want an instance added to a
Session automatically, in any case, ever - SA in all cases requires an
explicit save()/save-or-update(), and there is no way to change that
behavior, since there is no internal mechanism for a mapper to get a
session for an instance just created (recall that Mapper decorates a
classes' __init__ method to make this possible). correct ?
No, that's not what I want. But that made me see bug in the SessionContext
implementation, which may explain why you might think that. I have attached a
patch that fixes this bug...oh wait, I see that I've changed my mind about the
magic method you've proposed. I'll use that instead.
if that is the case, then we disagree; I believe there must be an
"implicit session" pattern available, never by default but very easy to
turn on without any extra programming, and should also be
customizable. This pattern seems to be very well accepted by the
current SA userbase and i dont think it should be removed:
mapper(User, user_table)
u = User() # 'u' is now associated with a session
I'm assume that implies a statement like install_mod("threadlocal") right? So
your example is really more like this:
install_mod("threadlocal")
class User(object):
...
m = mapper(User, user_table)
u = User() # 'u' is now associated with a session
Here's the SessionContext equivalent:
context = SessionContext()
class User(context.baseclass):
...
m = mapper(User, user_table)
u = User() # 'u' is now associated with session
The above could be using just the thread-local session, but i thought it
was a nice idea that you could also associate the User class with a
particular session context. otherwise i dont really understand the
point of having the baseclass and metaclass you proposed, it seems kind
of cruel to supply an extension that associates sessions with objects,
but then SA has no way to use it automatically.
You can associate the User class with whatever context instance you want by
using the baseclass or get_classmethod() (took the place of metaclass) of the
respective SessionContext instance.
[__session__() is] also not the kind of function people would
implement on a casual basis, its there to make a more advanced
concept possible in a clean, non-monkeypatching way.
perhaps naming __session__() as __sessioncontext__(), and having it
return a callable which then returns the contextual Session, will
make it clearer...though that seems more confusing.
From an OO perspective, allowing the contextual-session to be obtained
from an instance essentially gives that instance two sessions--the one
it is bound to and the context-current one. I think that's a
"Bozo-No-No" as my old CompSci prof used to say. Just because you can
doesn't mean you should... Really, I see no need for this. If the (SA)
framework really needs to know about the contextual session, then
provide a way to explicitly get a SessionContext instance rather than
using magic methods.
we can play rock-em-sock-em straw men if you want: I doubt your comp
sci class was teaching Python...what do you think he would say if you
told him "the effective type of an object should be determinable based
solely on the methods that happen to be available off of it" ? im sure
he would have flunked Guido.
I doubt it since C++ was the language used in the course...Guido is probably
more of an expert at that language than my prof was. But I digress :)
back to the issue at hand, the distinguishment to be made with the
__sessioncontext__ method, and i should make this clearer, is that we
are associating a session context with a class, not an instance. so
there is no "two sessions with an instance" going on, the instance has
one session, the class is optionally associated with a particular
context. the instance may contain a state that is specialized against
the default supplied by the class, and there is nothing wrong with
that. just like you associate a Mapper with the class, but an instance
might have been loaded by a different Mapper. it could be done the
opposite way, you can stick your SessionContext callable into the
mapper() call; same deal. Maybe that is a little clearer. although
then you cant easily associate a full class hierarchy with a particular
context...
OK, that makes a lot more sense. If it's a class method and not an instance
method, then it's very similar to the __init__ methods in SessionContext (I
think I like it even better). I implemented the SessionContext baseclass to use
that instead of a custom __init__.
If the method is returning a Session instance, then it should be called
__contextsession__. If it's called __sessioncontext__ then I would expect it to
return a SessionContext, not a Session. We should call it __contextsession__ so
we don't have to make the mapper (or any other part of SA that uses it) know
about the interface of SessionContext.
I've removed the metaclass in favor of a get_classmethod() method, which is
simpler and not quite as scary for those who aren't comfortable with
metaclasses. It's used like this:
class User(object):
__contextsession__ = context.get_classmethod()
The other option would be to formalize the interface of SessionContext (i.e.
move it into the orm.session module), and do it like this:
class User(object):
__sessioncontext__ = context
Then current_session() would just call obj.__sessioncontext__.current. Let me
know what you think.
~ Daniel
Index: lib/sqlalchemy/ext/sessioncontext.py
===================================================================
--- lib/sqlalchemy/ext/sessioncontext.py (revision 1366)
+++ lib/sqlalchemy/ext/sessioncontext.py (working copy)
@@ -1,118 +1,75 @@
-from sqlalchemy.util import ScopedRegistry
-
-class SessionContext(object):
- """A simple wrapper for ScopedRegistry that provides a "current" property
- which can be used to get, set, or remove the session in the current scope.
-
- By default this object provides thread-local scoping, which is the default
- scope provided by sqlalchemy.util.ScopedRegistry.
-
- Usage:
- engine = create_engine(...)
- def session_factory():
- return Session(bind_to=engine)
- context = SessionContext(session_factory)
-
- s = context.current # get thread-local session
- context.current = Session(bind_to=other_engine) # set current session
- del context.current # discard the thread-local session (a new one will
- # be created on the next call to context.current)
- """
- def __init__(self, session_factory, scopefunc=None):
- self.registry = ScopedRegistry(session_factory, scopefunc)
- super(SessionContext, self).__init__()
-
- def get_current(self):
- return self.registry()
- def set_current(self, session):
- self.registry.set(session)
- def del_current(self):
- self.registry.clear()
- current = property(get_current, set_current, del_current,
- """Property used to get/set/del the session in the current scope""")
-
- def create_metaclass(session_context):
- """return a metaclass to be used by objects that wish to be bound to a
- thread-local session upon instantiatoin.
-
- Note non-standard use of session_context rather than self as the name
- of the first arguement of this method.
-
- Usage:
- context = SessionContext(...)
- class MyClass(object):
- __metaclass__ = context.metaclass
- ...
- """
- try:
- return session_context._metaclass
- except AttributeError:
- class metaclass(type):
- def __init__(cls, name, bases, dct):
- old_init = getattr(cls, "__init__")
- def __init__(self, *args, **kwargs):
- session_context.current.save(self)
- old_init(self, *args, **kwargs)
- setattr(cls, "__init__", __init__)
- super(metaclass, cls).__init__(name, bases, dct)
- session_context._metaclass = metaclass
- return metaclass
- metaclass = property(create_metaclass)
-
- def create_baseclass(session_context):
- """return a baseclass to be used by objects that wish to be bound to a
- thread-local session upon instantiatoin.
-
- Note non-standard use of session_context rather than self as the name
- of the first arguement of this method.
-
- Usage:
- context = SessionContext(...)
- class MyClass(context.baseclass):
- ...
- """
- try:
- return session_context._baseclass
- except AttributeError:
- class baseclass(object):
- def __init__(self, *args, **kwargs):
- session_context.current.save(self)
- super(baseclass, self).__init__(*args, **kwargs)
- session_context._baseclass = baseclass
- return baseclass
- baseclass = property(create_baseclass)
-
-
-def test():
-
- def run_test(class_, context):
- obj = class_()
- assert context.current == get_session(obj)
-
- # keep a reference so the old session doesn't get gc'd
- old_session = context.current
-
- context.current = create_session()
- assert context.current != get_session(obj)
- assert old_session == get_session(obj)
-
- del context.current
- assert context.current != get_session(obj)
- assert old_session == get_session(obj)
-
- obj2 = class_()
- assert context.current == get_session(obj2)
-
- # test metaclass
- context = SessionContext(create_session)
- class MyClass(object): __metaclass__ = context.metaclass
- run_test(MyClass, context)
-
- # test baseclass
- context = SessionContext(create_session)
- class MyClass(context.baseclass): pass
- run_test(MyClass, context)
-
-if __name__ == "__main__":
- test()
- print "All tests passed!"
+from sqlalchemy.util import ScopedRegistry
+from sqlalchemy.orm.session import Session, object_session
+
+
+class SessionContext(object):
+ """A simple wrapper for ScopedRegistry that provides a "current" property
+ which can be used to get, set, or delete the session in the current scope.
+
+ By default this object provides thread-local scoping, which is the default
+ scope provided by sqlalchemy.util.ScopedRegistry.
+
+ Usage:
+ engine = create_engine(...)
+ def session_factory():
+ return Session(bind_to=engine)
+ context = SessionContext(session_factory)
+
+ s = context.current # get thread-local session
+ context.current = Session(bind_to=other_engine) # set current session
+ del context.current # discard the thread-local session (a new one will
+ # be created on the next call to context.current)
+ """
+ def __init__(self, session_factory=Session, scopefunc=None):
+ self.registry = ScopedRegistry(session_factory, scopefunc)
+ super(SessionContext, self).__init__()
+
+ def get_current(self):
+ return self.registry()
+ def set_current(self, session):
+ self.registry.set(session)
+ def del_current(self):
+ self.registry.clear()
+ current = property(get_current, set_current, del_current,
+ """Property used to get/set/del the session in the current scope""")
+
+ def get_classmethod(self):
+ """return a __contextsession__ classmethod to be assigned to a mapped
+ class.
+
+ Usage:
+ context = SessionContext(...)
+ class MyClass(object):
+ __contextsession__ = context.get_classmethod()
+ """
+ try:
+ return self._classmethod
+ except AttributeError:
+ def __contextsession__(cls):
+ return self.current
+ self._classmethod = classmethod(__contextsession__)
+ return self._classmethod
+
+ def create_baseclass(context):
+ """return a baseclass to be used by objects that wish to be bound to a
+ thread-local session upon instantiatoin.
+
+ The baseclass produced by this method is for convenience only. Its use
+ is not required.
+
+ Note non-standard use of 'context' rather than 'self' as the name of
+ the first arguement of this method.
+
+ Usage:
+ context = SessionContext(...)
+ class MyClass(context.baseclass):
+ ...
+ """
+ try:
+ return context._baseclass
+ except AttributeError:
+ class baseclass(object):
+ __contextsession__ = context.get_classmethod()
+ context._baseclass = baseclass
+ return baseclass
+ baseclass = property(create_baseclass)
Index: lib/sqlalchemy/orm/session.py
===================================================================
--- lib/sqlalchemy/orm/session.py (revision 1366)
+++ lib/sqlalchemy/orm/session.py (working copy)
@@ -424,8 +424,8 @@
_sessions = weakref.WeakValueDictionary()
def current_session(obj=None):
- if hasattr(obj.__class__, '__sessioncontext__'):
- return obj.__class__.__sessioncontext__()
+ if hasattr(obj.__class__, '__contextsession__'):
+ return obj.__class__.__contextsession__()
else:
return _default_session(obj=obj)
Index: test/sessioncontext.py
===================================================================
--- test/sessioncontext.py (revision 0)
+++ test/sessioncontext.py (revision 0)
@@ -0,0 +1,99 @@
+'''
+def test():
+ def run_test(class_, context):
+ obj = class_()
+ assert context.current == object_session(obj)
+
+ # keep a reference so the old session doesn't get gc'd
+ old_session = context.current
+
+ context.current = Session()
+ assert context.current != object_session(obj)
+ assert old_session == object_session(obj)
+
+ del context.current
+ assert context.current != object_session(obj)
+ assert old_session == object_session(obj)
+
+ obj2 = class_()
+ assert context.current == object_session(obj2)
+
+ # test metaclass
+ context = SessionContext(Session)
+ class MyClass(object): __contextsession__ = context.get_classmethod()
+ run_test(MyClass, context)
+
+ # test baseclass
+ context = SessionContext(Session)
+ class MyClass(context.baseclass): pass
+ run_test(MyClass, context)
+
+if __name__ == "__main__":
+ test()
+ print "All tests passed!"
+'''
+
+from testbase import PersistTest, AssertMixin
+import unittest, sys, os
+from sqlalchemy.ext.sessioncontext import SessionContext
+from sqlalchemy.orm.session import object_session
+from sqlalchemy import *
+import testbase
+
+db = testbase.db
+
+users = Table('users', db,
+ Column('user_id', Integer, Sequence('user_id_seq', optional=True),
primary_key = True),
+ Column('user_name', String(40)),
+ mysql_engine='innodb'
+)
+
+User = None
+
+class HistoryTest(AssertMixin):
+ def setUpAll(self):
+ db.echo = False
+ users.create()
+ db.echo = testbase.echo
+ def tearDownAll(self):
+ db.echo = False
+ users.drop()
+ db.echo = testbase.echo
+ def setUp(self):
+ clear_mappers()
+
+ def do_test(self, class_, context):
+ """test session assignment on object creation"""
+ obj = class_()
+ assert context.current == object_session(obj)
+
+ # keep a reference so the old session doesn't get gc'd
+ old_session = context.current
+
+ context.current = Session()
+ assert context.current != object_session(obj)
+ assert old_session == object_session(obj)
+
+ new_session = context.current
+ del context.current
+ assert context.current != new_session
+ assert old_session == object_session(obj)
+
+ obj2 = class_()
+ assert context.current == object_session(obj2)
+
+ def test_classmethod(self):
+ context = SessionContext()
+ class User(object): __contextsession__ = context.get_classmethod()
+ User.mapper = mapper(User, users)
+ self.do_test(User, context)
+
+ def test_baseclass(self):
+ context = SessionContext()
+ class User(context.baseclass): pass
+ User.mapper = mapper(User, users)
+ self.do_test(User, context)
+
+
+if __name__ == "__main__":
+ testbase.main()