Warm tip: This article is reproduced from stackoverflow.com, please click
proxy python session sqlalchemy

How is sqlalchemy.orm.scoping.scoped_session proxying ability implemented?

发布于 2020-03-30 21:14:31

I do understand, that a sqlalchemy.orm.scoping.scoped_session uses a session_factory to create a session and also possesses a registry to return an already present session through the __call__() call. But one can also directly call the .query method upon scoped_session and that completely confuses me, since scoped_session: 1. does not have this method 2. is not a dynamic wrapper of a sqlalchemy.orm.session.Session and 3. is not a subclass of sqlalchemy.orm.session.Session.

How is scoped_session able to dispatch a query? I just don't see any indirection or abstraction that would allow for this.. yet it works.

from sqlalchemy.orm import sessionmaker,scoped_session
from sqlalchemy import create_engine

user, password, server, dbname = "123","123","123", "123"
s = 'oracle://%s:%s@%s/%s' % (user, password, server, dbname)
some_engine = create_engine(s)
_sessionmaker = sessionmaker(bind=some_engine)
sc_sess = scoped_session(_sessionmaker) # here sc_sess is an isntance of "sqlalchemy.orm.scoping.scoped_session"
sc_sess.query(...) # works! but why?

# the following is what i expect to work and to be normal workflow
session = sc_sess() # returns an instance of sqlalchemy.orm.session.Session
session.query(...)

This behaviour is described in the SqlAlchemy Documentation:

Implicit Method Access

The job of the scoped_session is simple; hold onto a Session for all who ask for it. As a means of producing more transparent access to this Session, the scoped_session also includes proxy behavior, meaning that the registry itself can be treated just like a Session directly; when methods are called on this object, they are proxied to the underlying Session being maintained by the registry:

Session = scoped_session(some_factory)

# equivalent to:
#
# session = Session()
# print(session.query(MyClass).all())
#
print(Session.query(MyClass).all())

The above code accomplishes the same task as that of acquiring the current Session by calling upon the registry, then using that Session.

So this behaviour is normal, but how is it implemented? (not proxy in general, but precisely in this example)

Thanks.

Questioner
Roger Smith
Viewed
73
SuperShoot 2020-01-31 18:17

You've obviously had a good look at the sqlalchemy.orm.scoping.scoped_session class, and if you look just a little further down in the same module, you'll find the following snippet (link):

def instrument(name):
    def do(self, *args, **kwargs):
        return getattr(self.registry(), name)(*args, **kwargs)

    return do


for meth in Session.public_methods:
    setattr(scoped_session, meth, instrument(meth))

If we dissect that from the bottom up, we've first got the for meth in Session.public_methods: loop, where Session.public_methods is simply a tuple of the names of methods that a Session exposes, and the string "query" is one of those:

class Session(_SessionClassMethods):
    ...
    public_methods = (
        ...,
        "query",
        ...,
    )

Each of those names (meth) in Session.public_methods is passed to the setattr call inside the loop:

setattr(scoped_session, meth, instrument(meth))

The value that is assigned to the name of the method on the scoped_session is the return value of the call to instrument(meth), which is a closure called, do(). That function calls the scoped_session.registry to get the registered Session object, gets the named method (meth), and calls it with the *args & **kwargs that were passed to do().

As the for meth in Session.public_methods: loop is defined in the global namespace of the module, it is executed at compile time, before anything else has a chance to use the scoped_session. So by the time your code can get a hold of a scoped_session instance, those methods have already been monkey patched on there.