From: Jason Kirtland Date: Thu, 6 Mar 2008 14:12:22 +0000 (+0000) Subject: - Synonyms riding on top of existing descriptors are now full proxies X-Git-Tag: rel_0_4_4~28 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4f6d0ff71e72fc4f5171a062eda6b577475bb3b1;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Synonyms riding on top of existing descriptors are now full proxies to those descriptors. --- diff --git a/CHANGES b/CHANGES index d76026a7ba..edddfb9a71 100644 --- a/CHANGES +++ b/CHANGES @@ -97,7 +97,10 @@ CHANGES many-to-many "secondary" table will now work with eager loading, previously the "order by" wasn't aliased against the secondary table's alias. - + + - Synonyms riding on top of existing descriptors are now + full proxies to those descriptors. + - dialects - Invalid SQLite connection URLs now raise an error. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 44f6a77b47..35936186cf 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -4,7 +4,7 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -import weakref, threading, operator +import weakref, threading, operator, inspect from itertools import chain import UserDict from sqlalchemy import util @@ -67,8 +67,9 @@ class InstrumentedAttribute(interfaces.PropComparator): property = property(_property, doc="the MapperProperty object associated with this attribute") class ProxiedAttribute(InstrumentedAttribute): - """a 'proxy' attribute which adds InstrumentedAttribute - class-level behavior to any user-defined class property. + """Adds InstrumentedAttribute class-level behavior to a regular descriptor. + + Obsoleted by proxied_attribute_factory. """ class ProxyImpl(object): @@ -101,6 +102,59 @@ class ProxiedAttribute(InstrumentedAttribute): def __delete__(self, instance): return self.user_prop.__delete__(instance) +def proxied_attribute_factory(descriptor): + """Create an InstrumentedAttribute / user descriptor hybrid. + + Returns a new InstrumentedAttribute type that delegates + descriptor behavior and getattr() to the given descriptor. + """ + + class ProxyImpl(object): + accepts_scalar_loader = False + def __init__(self, key): + self.key = key + + class Proxy(InstrumentedAttribute): + """A combination of InsturmentedAttribute and a regular descriptor.""" + + def __init__(self, key, descriptor, comparator): + self.key = key + # maintain ProxiedAttribute.user_prop compatability. + self.descriptor = self.user_prop = descriptor + self._comparator = comparator + self.impl = ProxyImpl(key) + + def comparator(self): + if callable(self._comparator): + self._comparator = self._comparator() + return self._comparator + comparator = property(comparator) + + def __get__(self, instance, owner): + """Delegate __get__ to the original descriptor.""" + if instance is None: + descriptor.__get__(instance, owner) + return self + return descriptor.__get__(instance, owner) + + def __set__(self, instance, value): + """Delegate __set__ to the original descriptor.""" + return descriptor.__set__(instance, value) + + def __delete__(self, instance): + """Delegate __delete__ to the original descriptor.""" + return descriptor.__delete__(instance) + + def __getattr__(self, attribute): + """Delegate __getattr__ to the original descriptor.""" + return getattr(descriptor, attribute) + Proxy.__name__ = type(descriptor).__name__ + 'Proxy' + + util.monkeypatch_proxied_specials(Proxy, type(descriptor), + name='descriptor', + from_instance=descriptor) + return Proxy + class AttributeImpl(object): """internal implementation for instrumented attributes.""" @@ -1233,7 +1287,8 @@ def register_attribute(class_, key, uselist, useobject, callable_=None, proxy_pr return if proxy_property: - inst = ProxiedAttribute(key, proxy_property, comparator=comparator) + proxy_type = proxied_attribute_factory(proxy_property) + inst = proxy_type(key, proxy_property, comparator) else: inst = InstrumentedAttribute(_create_prop(class_, key, uselist, callable_, useobject=useobject, typecallable=typecallable, mutable_scalars=mutable_scalars, **kwargs), comparator=comparator) diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 4e8a837c90..9adb3983db 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -344,6 +344,35 @@ def warn_exception(func, *args, **kwargs): except: warn("%s('%s') ignored" % sys.exc_info()[0:2]) +def monkeypatch_proxied_specials(into_cls, from_cls, skip=None, only=None, + name='self.proxy', from_instance=None): + """Automates delegation of __specials__ for a proxying type.""" + + if only: + dunders = only + else: + if skip is None: + skip = ('__slots__', '__del__', '__getattribute__', + '__metaclass__', '__getstate__', '__setstate__') + dunders = [m for m in dir(from_cls) + if (m.startswith('__') and m.endswith('__') and + not hasattr(into_cls, m) and m not in skip)] + for method in dunders: + try: + spec = inspect.getargspec(getattr(from_cls, method)) + fn_args = inspect.formatargspec(spec[0]) + d_args = inspect.formatargspec(spec[0][1:]) + except TypeError: + fn_args = '(self, *args, **kw)' + d_args = '(*args, **kw)' + + py = ("def %(method)s%(fn_args)s: " + "return %(name)s.%(method)s%(d_args)s" % locals()) + + env = from_instance is not None and {name: from_instance} or {} + exec py in env + setattr(into_cls, method, env[method]) + class SimpleProperty(object): """A *default* property accessor.""" diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 6d36b7216e..637b890ead 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -545,6 +545,11 @@ class MapperTest(MapperSuperTest): sess = create_session() assert_col = [] + class extendedproperty(property): + attribute = 123 + def __getitem__(self, key): + return 'value' + class User(object): def _get_user_name(self): assert_col.append(('get', self.user_name)) @@ -552,10 +557,10 @@ class MapperTest(MapperSuperTest): def _set_user_name(self, name): assert_col.append(('set', name)) self.user_name = name - uname = property(_get_user_name, _set_user_name) + uname = extendedproperty(_get_user_name, _set_user_name) mapper(User, users, properties = dict( - addresses = relation(mapper(Address, addresses), lazy = True), + addresses = relation(mapper(Address, addresses), lazy=True), uname = synonym('user_name'), adlist = synonym('addresses', proxy=True), adname = synonym('addresses') @@ -585,6 +590,8 @@ class MapperTest(MapperSuperTest): assert u.user_name == "some user name" assert u in sess.dirty + assert User.uname.attribute == 123 + assert User.uname['key'] == 'value' def test_column_synonyms(self): """test new-style synonyms which automatically instrument properties, set up aliased column, etc."""