diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-01-02 18:22:50 +0000 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2009-01-02 18:22:50 +0000 |
| commit | 50dfbc7e793f1bcfdd22f9cffcefde31f14b186b (patch) | |
| tree | 000b7150983c40ec134a31b0d7b3addc0c679b30 | |
| parent | a52a0c43c3d4880df53c46d1fd92fcf8d1a3a758 (diff) | |
| download | sqlalchemy-50dfbc7e793f1bcfdd22f9cffcefde31f14b186b.tar.gz | |
- Custom comparator classes used in conjunction with
column_property(), relation() etc. can define
new comparison methods on the Comparator, which will
become available via __getattr__() on the
InstrumentedAttribute. In the case of synonym()
or comparable_property(), attributes are resolved first
on the user-defined descriptor, then on the user-defined
comparator.
| -rw-r--r-- | CHANGES | 9 | ||||
| -rw-r--r-- | examples/postgis/postgis.py | 13 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 27 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 2 | ||||
| -rw-r--r-- | test/orm/mapper.py | 43 |
5 files changed, 81 insertions, 13 deletions
@@ -71,6 +71,15 @@ CHANGES next compile() call. This issue occurs frequently when using declarative. + - Custom comparator classes used in conjunction with + column_property(), relation() etc. can define + new comparison methods on the Comparator, which will + become available via __getattr__() on the + InstrumentedAttribute. In the case of synonym() + or comparable_property(), attributes are resolved first + on the user-defined descriptor, then on the user-defined + comparator. + - Added ScopedSession.is_active accessor. [ticket:976] - Can pass mapped attributes and column objects as keys diff --git a/examples/postgis/postgis.py b/examples/postgis/postgis.py index 841bce31c..c463cca26 100644 --- a/examples/postgis/postgis.py +++ b/examples/postgis/postgis.py @@ -123,21 +123,14 @@ class GisComparator(ColumnProperty.ColumnComparator): """Intercepts standard Column operators on mapped class attributes and overrides their behavior. - The PropComparator API currently does not allow "custom" - operators to be added, so only those operators which - already exist on Column can be overridden here. Additional - GIS-specific operators can be implemented as standalone - functions. """ def __eq__(self, other): return self.__clause_element__().op('~=')(_to_postgis(other)) -def intersects(x, y): - """An example standalone GIS-specific comparison operator.""" - - return _to_postgis(x).op('&&')(_to_postgis(y)) + def intersects(self, other): + return self.__clause_element__().op('&&')(_to_postgis(other)) class gis_element(object): """Represents a geometry value. @@ -219,7 +212,7 @@ if __name__ == '__main__': assert r1 is r2 is r3 # illustrate the "intersects" operator - print session.query(Road).filter(intersects(Road.road_geom, r1.road_geom)).all() + print session.query(Road).filter(Road.road_geom.intersects(r1.road_geom)).all() # illustrate usage of the "wkt" accessor. this requires a DB # execution to call the AsText() function so we keep this explicit. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index df607adf8..2b2760208 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -131,7 +131,17 @@ class QueryableAttribute(interfaces.PropComparator): def hasparent(self, state, optimistic=False): return self.impl.hasparent(state, optimistic=optimistic) - + + def __getattr__(self, key): + try: + return getattr(self.comparator, key) + except AttributeError: + raise AttributeError('Neither %r object nor %r object has an attribute %r' % ( + type(self).__name__, + type(self.comparator).__name__, + key) + ) + def __str__(self): return repr(self.parententity) + "." + self.property.key @@ -195,8 +205,19 @@ def proxied_attribute_factory(descriptor): return descriptor.__delete__(instance) def __getattr__(self, attribute): - """Delegate __getattr__ to the original descriptor.""" - return getattr(descriptor, attribute) + """Delegate __getattr__ to the original descriptor and/or comparator.""" + + try: + return getattr(descriptor, attribute) + except AttributeError: + try: + return getattr(self._comparator, attribute) + except AttributeError: + raise AttributeError('Neither %r object nor %r object has an attribute %r' % ( + type(descriptor).__name__, + type(self._comparator).__name__, + attribute) + ) def _property(self): return self._parententity.get_property(self.key, resolve_synonyms=True) diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index bf9bda366..675b505e7 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -42,6 +42,7 @@ class ColumnProperty(StrategizedProperty): self.group = kwargs.pop('group', None) self.deferred = kwargs.pop('deferred', False) self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator) + self.descriptor = kwargs.pop('descriptor', None) self.extension = kwargs.pop('extension', None) util.set_creation_order(self) if self.deferred: @@ -206,6 +207,7 @@ class SynonymProperty(MapperProperty): if obj is None: return s return getattr(obj, self.name) + self.descriptor = SynonymProp() def comparator_callable(prop, mapper): diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 4e8412bd9..5cab84175 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -717,11 +717,22 @@ class MapperTest(_fixtures.FixtureTest): def test_comparable(self): class extendedproperty(property): attribute = 123 + + def method1(self): + return "method1" + def __getitem__(self, key): return 'value' class UCComparator(sa.orm.PropComparator): __hash__ = None + + def method1(self): + return "uccmethod1" + + def method2(self, other): + return "method2" + def __eq__(self, other): cls = self.prop.parent.class_ col = getattr(cls, 'name') @@ -754,6 +765,14 @@ class MapperTest(_fixtures.FixtureTest): assert hasattr(User, 'name') assert hasattr(User, 'uc_name') + eq_(User.uc_name.method1(), "method1") + eq_(User.uc_name.method2('x'), "method2") + + self.assertRaisesMessage( + AttributeError, + "Neither 'extendedproperty' object nor 'UCComparator' object has an attribute 'nonexistent'", + getattr, User.uc_name, 'nonexistent') + # test compile assert not isinstance(User.uc_name == 'jack', bool) u = q.filter(User.uc_name=='JACK').one() @@ -780,6 +799,30 @@ class MapperTest(_fixtures.FixtureTest): sess.rollback() @testing.resolve_artifact_names + def test_comparable_column(self): + class MyComparator(sa.orm.properties.ColumnProperty.Comparator): + def __eq__(self, other): + # lower case comparison + return func.lower(self.__clause_element__()) == func.lower(other) + + def intersects(self, other): + # non-standard comparator + return self.__clause_element__().op('&=')(other) + + mapper(User, users, properties={ + 'name':sa.orm.column_property(users.c.name, comparator_factory=MyComparator) + }) + + self.assertRaisesMessage( + AttributeError, + "Neither 'InstrumentedAttribute' object nor 'MyComparator' object has an attribute 'nonexistent'", + getattr, User.name, "nonexistent") + + eq_(str((User.name == 'ed').compile(dialect=sa.engine.default.DefaultDialect())) , "lower(users.name) = lower(:lower_1)") + eq_(str((User.name.intersects('ed')).compile(dialect=sa.engine.default.DefaultDialect())), "users.name &= :name_1") + + + @testing.resolve_artifact_names def test_reconstructor(self): recon = [] |
