From c37ed5cbb39f1928b4f4432c445a877bf5b3e3f1 Mon Sep 17 00:00:00 2001 From: Jason Kirtland Date: Mon, 17 Mar 2008 22:06:49 +0000 Subject: [PATCH] - Added comparable_property(), adds query Comparator behavior to regular, unmanaged Python properties - 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 | 42 +++++------ examples/vertical/dictlike-polymorphic.py | 23 +----- lib/sqlalchemy/orm/__init__.py | 41 ++++++++++- lib/sqlalchemy/orm/mapper.py | 13 +++- lib/sqlalchemy/orm/properties.py | 28 +++++++- test/orm/mapper.py | 87 +++++++++++++++++++++++ 6 files changed, 187 insertions(+), 47 deletions(-) diff --git a/CHANGES b/CHANGES index 00c5485e2c..41d18462fc 100644 --- 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 diff --git a/examples/vertical/dictlike-polymorphic.py b/examples/vertical/dictlike-polymorphic.py index 66dcd4856a..4065337c2e 100644 --- a/examples/vertical/dictlike-polymorphic.py +++ b/examples/vertical/dictlike-polymorphic.py @@ -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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index d7b5580647..1384bdccf6 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -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. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 36a6632554..15fab08166 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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: diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 1866693b4d..0fd8ac2ef1 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -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 diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 713cbced7d..1228c6c779 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -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): -- 2.47.3