]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Synonyms riding on top of existing descriptors are now full proxies
authorJason Kirtland <jek@discorporate.us>
Thu, 6 Mar 2008 14:12:22 +0000 (14:12 +0000)
committerJason Kirtland <jek@discorporate.us>
Thu, 6 Mar 2008 14:12:22 +0000 (14:12 +0000)
  to those descriptors.

CHANGES
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/util.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index d76026a7bad0ad4ebf63c6c7fc6b0bfd894f9eba..edddfb9a71ceef4821f90c2484a470d45b94c1dc 100644 (file)
--- 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.
 
index 44f6a77b4757fea66c0f33482005f584d781c9f4..35936186cfad8c13fa52cdbd6cadf37555fc5984 100644 (file)
@@ -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)
index 4e8a837c9029aa0b2bc50ce72df92ac70df99db3..9adb3983db73bac0816c22c035358b9c879a609d 100644 (file)
@@ -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."""
 
index 6d36b7216eb74e33e762e76d86cb44d605a19e0f..637b890ead1d78c59f800c2bce41326896b441af 100644 (file)
@@ -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."""