]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added comparable_property(), adds query Comparator behavior to regular, unmanaged...
authorJason Kirtland <jek@discorporate.us>
Mon, 17 Mar 2008 22:06:49 +0000 (22:06 +0000)
committerJason Kirtland <jek@discorporate.us>
Mon, 17 Mar 2008 22:06:49 +0000 (22:06 +0000)
- Some aspects of MapperProperty initialization are streteched pretty thin now
  and need a refactor; will proceed with these on the user_defined_state branch

CHANGES
examples/vertical/dictlike-polymorphic.py
lib/sqlalchemy/orm/__init__.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/properties.py
test/orm/mapper.py

diff --git a/CHANGES b/CHANGES
index 00c5485e2cffb154364558f1190ba0b504f293f9..41d18462fc025c9d6e24a026e5d102fdb72f2c5a 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -5,29 +5,31 @@ CHANGES
 0.4.5
 =====
 - orm
-    - when attributes are expired on a pending instance, an 
-      error will not be raised when the "refresh" action 
-      is triggered and returns no result
-      
-    - fixed/covered case when using a False value as a 
-      polymorphic discriminator
-    
-    - fixed bug which was preventing synonym() attributes
-      from being used with inheritance
+    - Added comparable_property(), adds query Comparator behavior
+      to regular, unmanaged Python properties
+
+    - When attributes are expired on a pending instance, an error
+      will not be raised when the "refresh" action is triggered
+      and returns no result
+
+    - Fixed/covered case when using a False value as a polymorphic
+      discriminator
+
+    - Fixed bug which was preventing synonym() attributes from
+      being used with inheritance
 
     - Session.execute can now find binds from metadata
-    
-    - fixed "cascade delete" operation of dynamic relations,
-      which had only been implemented for foreign-key nulling
-      behavior in 0.4.2 and not actual cascading deletes
-      [ticket:895]
-      
+
+    - Fixed "cascade delete" operation of dynamic relations, which
+      had only been implemented for foreign-key nulling behavior
+      in 0.4.2 and not actual cascading deletes [ticket:895]
+
 - extensions
-    - the "synonym" function is now directly usable with 
-      "declarative".  Pass in the decorated property using 
-      the "instrument" keyword argument, e.g.:
-      somekey = synonym('_somekey', instrument=property(g, s))
-      
+    - The "synonym" function is now directly usable with
+      "declarative".  Pass in the decorated property using the
+      "instrument" keyword argument, e.g.: somekey =
+      synonym('_somekey', instrument=property(g, s))
+
 0.4.4
 ------
 - sql
index 66dcd4856a8a22188e1f5dab0bbe22332f9054ac..4065337c2ea04859e7f79e299d47f3a982d5f8dd 100644 (file)
@@ -30,7 +30,7 @@ Note: Something much like 'comparable_property' is slated for inclusion in a
 """
 
 from sqlalchemy.orm.interfaces import PropComparator, MapperProperty
-from sqlalchemy.orm import session as sessionlib
+from sqlalchemy.orm import session as sessionlib, comparable_property
 
 # Using the VerticalPropertyDictMixin from the base example
 from dictlike import VerticalPropertyDictMixin
@@ -130,27 +130,6 @@ class PolymorphicVerticalProperty(object):
         return '<%s %r=%r>' % (self.__class__.__name__, self.key, self.value)
 
 
-class comparable_property(MapperProperty):
-    """Instruments a Python property for use in query expressions."""
-
-    def __init__(self, comparator, property):
-        self.property = property
-        self.comparator = comparator(self)
-
-    def do_init(self):
-        class_ = self.parent.class_
-        sessionlib.register_attribute(class_, self.key, uselist=False,
-                                      proxy_property=self.property,
-                                      useobject=False,
-                                      comparator=self.comparator)
-
-    def setup(self, querycontext, **kwargs):
-        pass
-
-    def create_row_processor(self, selectcontext, mapper, row):
-        return (None, None, None)
-
-
 if __name__ == '__main__':
     from sqlalchemy import *
     from sqlalchemy.orm import mapper, relation, create_session
index d7b5580647a2b9dd97b76ed5667f7366ad5bf41d..1384bdccf672d1392a478635bd982467bdc86c73 100644 (file)
@@ -14,7 +14,7 @@ documentation for an overview of how this module is used.
 from sqlalchemy import util as sautil
 from sqlalchemy.orm.mapper import Mapper, object_mapper, class_mapper, _mapper_registry
 from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE, EXT_STOP, EXT_PASS, ExtensionOption, PropComparator
-from sqlalchemy.orm.properties import SynonymProperty, PropertyLoader, ColumnProperty, CompositeProperty, BackRef
+from sqlalchemy.orm.properties import SynonymProperty, ComparableProperty, PropertyLoader, ColumnProperty, CompositeProperty, BackRef
 from sqlalchemy.orm import mapper as mapperlib
 from sqlalchemy.orm import strategies
 from sqlalchemy.orm.query import Query
@@ -29,7 +29,7 @@ __all__ = [ 'relation', 'column_property', 'composite', 'backref', 'eagerload',
             'undefer', 'undefer_group', 'extension', 'mapper', 'clear_mappers',
             'compile_mappers', 'class_mapper', 'object_mapper', 'sessionmaker',
             'scoped_session', 'dynamic_loader', 'MapperExtension',
-            'polymorphic_union',
+            'polymorphic_union', 'comparable_property',
             'create_session', 'synonym', 'contains_alias', 'Query',
             'contains_eager', 'EXT_CONTINUE', 'EXT_STOP', 'EXT_PASS',
             'object_session', 'PropComparator' ]
@@ -591,6 +591,43 @@ def synonym(name, map_column=False, instrument=None, proxy=False):
 
     return SynonymProperty(name, map_column=map_column, instrument=instrument)
 
+def comparable_property(comparator_factory, descriptor=None):
+    """Provide query semantics for an unmanaged attribute.
+
+    Allows a regular Python @property (descriptor) to be used in Queries and
+    SQL constructs like a managed attribute.  comparable_property wraps a
+    descriptor with a proxy that directs operator overrides such as ==
+    (__eq__) to the supplied comparator but proxies everything else through
+    to the original descriptor.
+
+      class MyClass(object):
+          @property
+          def myprop(self):
+              return 'foo'
+
+      class MyComparator(sqlalchemy.orm.interfaces.PropComparator):
+          def __eq__(self, other):
+              ....
+
+      mapper(MyClass, mytable, properties=dict(
+               'myprop': comparable_property(MyComparator)))
+
+    Used with the ``properties`` dictionary sent to  [sqlalchemy.orm#mapper()].
+
+    comparator_factory
+      A PropComparator subclass or factory that defines operator behavior
+      for this property.
+
+    descriptor
+
+      Optional when used in a ``properties={}`` declaration.  The Python
+      descriptor or property to layer comparison behavior on top of.
+
+      The like-named descriptor will be automatically retreived from the
+      mapped class if left blank in a ``properties`` declaration.
+    """
+    return ComparableProperty(comparator_factory, descriptor)
+
 def compile_mappers():
     """Compile all mappers that have been defined.
 
index 36a6632554761408446cdd66e4d2c729890ac640..15fab081660f2412ddeeae147d9007f65a1511fb 100644 (file)
@@ -35,9 +35,11 @@ NO_ATTRIBUTE = util.symbol('NO_ATTRIBUTE')
 # lock used to synchronize the "mapper compile" step
 _COMPILE_MUTEX = util.threading.Lock()
 
-# initialize these two lazily
+# initialize these lazily
 ColumnProperty = None
 SynonymProperty = None
+ComparableProperty = None
+
 
 class Mapper(object):
     """Define the correlation of class attributes to database table
@@ -531,7 +533,7 @@ class Mapper(object):
     _equivalent_columns = property(_equivalent_columns)
 
     class _CompileOnAttr(PropComparator):
-        """placeholder class attribute which fires mapper compilation on access"""
+        """A placeholder descriptor which triggers compilation on access."""
 
         def __init__(self, class_, key):
             self.class_ = class_
@@ -661,6 +663,13 @@ class Mapper(object):
                 if not key in self.mapped_table.c:
                     raise exceptions.ArgumentError("Can't compile synonym '%s': no column on table '%s' named '%s'"  % (prop.name, self.mapped_table.description, key))
                 self._compile_property(prop.name, ColumnProperty(self.mapped_table.c[key]), init=init, setparent=setparent)
+        elif isinstance(prop, ComparableProperty) and setparent:
+            # refactor me
+            if prop.descriptor is None:
+                prop.descriptor = getattr(self.class_, key, None)
+                if isinstance(prop.descriptor, Mapper._CompileOnAttr):
+                    prop.descriptor = object.__getattribute__(prop.descriptor,
+                                                              'existing_prop')
         self.__props[key] = prop
 
         if setparent:
index 1866693b4da4fe2b622e074dcdbd62444682248e..0fd8ac2ef1ba52e98654ef34ff4f276b9a7c1485 100644 (file)
@@ -22,7 +22,7 @@ from sqlalchemy.exceptions import ArgumentError
 import weakref
 
 __all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty',
-           'PropertyLoader', 'BackRef')
+           'ComparableProperty', 'PropertyLoader', 'BackRef')
 
 
 class ColumnProperty(StrategizedProperty):
@@ -183,6 +183,31 @@ class SynonymProperty(MapperProperty):
         pass
 SynonymProperty.logger = logging.class_logger(SynonymProperty)
 
+
+class ComparableProperty(MapperProperty):
+    """Instruments a Python property for use in query expressions."""
+
+    def __init__(self, comparator_factory, descriptor=None):
+        self.descriptor = descriptor
+        self.comparator = comparator_factory(self)
+
+    def do_init(self):
+        """Set up a proxy to the unmanaged descriptor."""
+
+        class_ = self.parent.class_
+        # refactor me
+        sessionlib.register_attribute(class_, self.key, uselist=False,
+                                      proxy_property=self.descriptor,
+                                      useobject=False,
+                                      comparator=self.comparator)
+
+    def setup(self, querycontext, **kwargs):
+        pass
+
+    def create_row_processor(self, selectcontext, mapper, row):
+        return (None, None, None)
+
+
 class PropertyLoader(StrategizedProperty):
     """Describes an object property that holds a single item or list
     of items that correspond to a related database table.
@@ -857,3 +882,4 @@ class BackRef(object):
 
 mapper.ColumnProperty = ColumnProperty
 mapper.SynonymProperty = SynonymProperty
+mapper.ComparableProperty = ComparableProperty
index 713cbced7d4e69bc92c9abc6a03e9bcc2d5eadde..1228c6c77917333e88cabf88ec9913434e212056 100644 (file)
@@ -201,6 +201,7 @@ class MapperTest(MapperSuperTest):
 
     def test_add_property(self):
         assert_col = []
+
         class User(object):
             def _get_user_name(self):
                 assert_col.append(('get', self._user_name))
@@ -210,11 +211,31 @@ class MapperTest(MapperSuperTest):
                 self._user_name = name
             user_name = property(_get_user_name, _set_user_name)
 
+            def _uc_user_name(self):
+                if self._user_name is None:
+                    return None
+                return self._user_name.upper()
+            uc_user_name = property(_uc_user_name)
+            uc_user_name2 = property(_uc_user_name)
+
         m = mapper(User, users)
         mapper(Address, addresses)
+
+        class UCComparator(PropComparator):
+            def __eq__(self, other):
+                cls = self.prop.parent.class_
+                col = getattr(cls, 'user_name')
+                if other is None:
+                    return col == None
+                else:
+                    return func.upper(col) == func.upper(other)
+
         m.add_property('_user_name', deferred(users.c.user_name))
         m.add_property('user_name', synonym('_user_name'))
         m.add_property('addresses', relation(Address))
+        m.add_property('uc_user_name', comparable_property(UCComparator))
+        m.add_property('uc_user_name2', comparable_property(
+                UCComparator, User.uc_user_name2))
 
         sess = create_session(transactional=True)
         assert sess.query(User).get(7)
@@ -224,6 +245,8 @@ class MapperTest(MapperSuperTest):
         def go():
             self.assert_result([u], User, user_address_result[0])
             assert u.user_name == 'jack'
+            assert u.uc_user_name == 'JACK'
+            assert u.uc_user_name2 == 'JACK'
             assert assert_col == [('get', 'jack')], str(assert_col)
         self.assert_sql_count(testing.db, go, 2)
 
@@ -646,6 +669,70 @@ class MapperTest(MapperSuperTest):
         assert u.user_name == 'foo'
         assert assert_col == [('get', 'jack'), ('set', 'foo'), ('get', 'foo')]
 
+    def test_comparable(self):
+        class extendedproperty(property):
+            attribute = 123
+            def __getitem__(self, key):
+                return 'value'
+
+        class UCComparator(PropComparator):
+            def __eq__(self, other):
+                cls = self.prop.parent.class_
+                col = getattr(cls, 'user_name')
+                if other is None:
+                    return col == None
+                else:
+                    return func.upper(col) == func.upper(other)
+
+        def map_(with_explicit_property):
+            class User(object):
+                @extendedproperty
+                def uc_user_name(self):
+                    if self.user_name is None:
+                        return None
+                    return self.user_name.upper()
+            if with_explicit_property:
+                args = (UCComparator, User.uc_user_name)
+            else:
+                args = (UCComparator,)
+
+            mapper(User, users, properties=dict(
+                    uc_user_name = comparable_property(*args)))
+            return User
+
+        for User in (map_(True), map_(False)):
+            sess = create_session()
+            sess.begin()
+            q = sess.query(User)
+
+            assert hasattr(User, 'user_name')
+            assert hasattr(User, 'uc_user_name')
+
+            # test compile
+            assert not isinstance(User.uc_user_name == 'jack', bool)
+            u = q.filter(User.uc_user_name=='JACK').one()
+
+            assert u.uc_user_name == "JACK"
+            assert u not in sess.dirty
+
+            u.user_name = "some user name"
+            assert u.user_name == "some user name"
+            assert u in sess.dirty
+            assert u.uc_user_name == "SOME USER NAME"
+
+            sess.flush()
+            sess.clear()
+
+            q = sess.query(User)
+            u2 = q.filter(User.user_name=='some user name').one()
+            u3 = q.filter(User.uc_user_name=='SOME USER NAME').one()
+
+            assert u2 is u3
+
+            assert User.uc_user_name.attribute == 123
+            assert User.uc_user_name['key'] == 'value'
+            sess.rollback()
+
 class OptionsTest(MapperSuperTest):
     @testing.fails_on('maxdb')
     def test_synonymoptions(self):