From: Mike Bayer Date: Sun, 20 Jul 2008 18:23:44 +0000 (+0000) Subject: - An inheriting class can now override an attribute X-Git-Tag: rel_0_5beta3~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=419753f59b6fa68ae282b5a75ad3b3a3b5f91c50;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - An inheriting class can now override an attribute inherited from the base class with a plain descriptor, or exclude an inherited attribute via the include_properties/exclude_properties collections. --- diff --git a/CHANGES b/CHANGES index 03082e3bd9..f6bd11f66c 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,11 @@ CHANGES - A critical fix to dynamic relations allows the "modified" history to be properly cleared after a flush(). + + - An inheriting class can now override an attribute + inherited from the base class with a plain descriptor, + or exclude an inherited attribute via the + include_properties/exclude_properties collections. - Added a new SessionExtension hook called after_attach(). This is called at the point of attachment for objects diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index e8bae46a4f..f313a09ee7 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -220,6 +220,9 @@ class Mapper(object): o = o or bool(mapper.delete_orphans) return o + def has_property(self, key): + return key in self.__props + def get_property(self, key, resolve_synonyms=False, raiseerr=True): """return a MapperProperty associated with the given key.""" @@ -636,6 +639,31 @@ class Mapper(object): return getattr(getattr(cls, clskey), key) + def _should_exclude(self, name): + """determine whether a particular property should be implicitly present on the class. + + This occurs when properties are propagated from an inherited class, or are + applied from the columns present in the mapped table. + + """ + # check for an existing descriptor + if isinstance( + getattr(self.class_, name, None), + property): + return True + + if (self.include_properties is not None and + name not in self.include_properties): + self.__log("not including property %s" % (name)) + return True + + if (self.exclude_properties is not None and + name in self.exclude_properties): + self.__log("excluding property %s" % (name)) + return True + + return False + def __compile_properties(self): # object attribute names mapped to MapperProperty objects @@ -654,7 +682,7 @@ class Mapper(object): # pull properties from the inherited mapper if any. if self.inherits: for key, prop in self.inherits.__props.iteritems(): - if key not in self.__props: + if key not in self.__props and not self._should_exclude(key): self._adapt_inherited_property(key, prop) # create properties for each column in the mapped table, @@ -662,15 +690,8 @@ class Mapper(object): for column in self.mapped_table.columns: if column in self._columntoproperty: continue - - if (self.include_properties is not None and - column.key not in self.include_properties): - self.__log("not including property %s" % (column.key)) - continue - - if (self.exclude_properties is not None and - column.key in self.exclude_properties): - self.__log("excluding property %s" % (column.key)) + + if self._should_exclude(column.key): continue column_key = (self.column_prefix or '') + column.key @@ -687,6 +708,8 @@ class Mapper(object): # in the 'with_polymorphic' selectable but we need it for the base mapper if self.polymorphic_on and self.polymorphic_on not in self._columntoproperty: col = self.mapped_table.corresponding_column(self.polymorphic_on) or self.polymorphic_on + if self._should_exclude(col.key): + raise sa_exc.InvalidRequestError("Cannot exclude or override the discriminator column %r" % col.key) self._compile_property(col.key, ColumnProperty(col), init=False, setparent=True) def _adapt_inherited_property(self, key, prop): diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 7eecc33202..64162fecbb 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -24,7 +24,7 @@ class DefaultColumnLoader(LoaderStrategy): self.logger.info("%s register managed attribute" % self) for mapper in self.parent.polymorphic_iterator(): - if mapper is self.parent or not mapper.concrete: + if (mapper is self.parent or not mapper.concrete) and mapper.has_property(self.key): sessionlib.register_attribute( mapper.class_, self.key, diff --git a/test/ext/declarative.py b/test/ext/declarative.py index c7bde68020..6cf553a375 100644 --- a/test/ext/declarative.py +++ b/test/ext/declarative.py @@ -37,7 +37,7 @@ class DeclarativeTest(testing.TestBase, testing.AssertsExecutionResults): eq_(Address.__table__.c['id'].name, 'id') eq_(Address.__table__.c['_email'].name, 'email') eq_(Address.__table__.c['_user_id'].name, 'user_id') - + u1 = User(name='u1', addresses=[ Address(email='one'), Address(email='two'), diff --git a/test/orm/inheritance/basic.py b/test/orm/inheritance/basic.py index 831d346e63..4600ee063d 100644 --- a/test/orm/inheritance/basic.py +++ b/test/orm/inheritance/basic.py @@ -695,7 +695,7 @@ class SyncCompileTest(ORMTest): assert len(session.query(C).all()) == 1 class OverrideColKeyTest(ORMTest): - """test overriding of column names with a common name from parent to child.""" + """test overriding of column attributes.""" def define_tables(self, metadata): global base, subtable @@ -829,6 +829,25 @@ class OverrideColKeyTest(ORMTest): sess.add(s1) sess.flush() assert sess.query(Sub).get(10) is s1 + + def test_plain_descriptor(self): + """test that descriptors prevent inheritance from propigating properties to subclasses.""" + + class Base(object): + pass + class Sub(Base): + @property + def data(self): + return "im the data" + + mapper(Base, base) + mapper(Sub, subtable, inherits=Base) + + s1 = Sub() + sess = create_session() + sess.add(s1) + sess.flush() + assert sess.query(Sub).one().data == "im the data" if __name__ == "__main__": diff --git a/test/orm/mapper.py b/test/orm/mapper.py index cc994acefe..410b904602 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -3,7 +3,7 @@ import testenv; testenv.configure_for_tests() from testlib import sa, testing from testlib.sa import MetaData, Table, Column, Integer, String, ForeignKey -from testlib.sa.orm import mapper, relation, backref, create_session +from testlib.sa.orm import mapper, relation, backref, create_session, class_mapper from testlib.sa.orm import defer, deferred, synonym, attributes from testlib.testing import eq_ import pickleable @@ -396,7 +396,7 @@ class MapperTest(_fixtures.FixtureTest): exclude_properties=('vendor_id',)) m_m = mapper(Manager, inherits=e_m, polymorphic_identity='manager', - include_properties=()) + include_properties=('id', 'type')) v_m = mapper(Vendor, inherits=p_m, polymorphic_identity='vendor', exclude_properties=('boss_id', 'employee_number')) @@ -412,15 +412,32 @@ class MapperTest(_fixtures.FixtureTest): want = set(want) eq_(have, want) + def assert_instrumented(cls, want): + have = set([p.key for p in class_mapper(cls).iterate_properties]) + want = set(want) + eq_(have, want) + assert_props(Person, ['id', 'name', 'type']) + assert_instrumented(Person, ['id', 'name', 'type']) assert_props(Employee, ['boss', 'boss_id', 'employee_number', 'id', 'name', 'type']) + assert_instrumented(Employee,['boss', 'boss_id', 'employee_number', + 'id', 'name', 'type']) assert_props(Manager, ['boss', 'boss_id', 'employee_number', 'peon', 'id', 'name', 'type']) + + # 'peon' and 'type' are both explicitly stated properties + assert_instrumented(Manager, ['peon', 'type', 'id']) + assert_props(Vendor, ['vendor_id', 'id', 'name', 'type']) assert_props(Hoho, ['id', 'name', 'type']) assert_props(Lala, ['p_employee_number', 'p_id', 'p_name', 'p_type']) + # excluding the discriminator column is currently not allowed + class Foo(Person): + pass + self.assertRaises(sa.exc.InvalidRequestError, mapper, Foo, inherits=Person, polymorphic_identity='foo', exclude_properties=('type',) ) + @testing.resolve_artifact_names def test_mapping_to_join(self): """Mapping to a join"""