Commits

Mike Bayer committed 5a0a1be

- 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.

Comments (0)

Files changed (6)

     - 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

lib/sqlalchemy/orm/mapper.py

             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."""
 
 
             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
         # 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,
         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
         # 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):

lib/sqlalchemy/orm/strategies.py

         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, 

test/ext/declarative.py

         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'),

test/orm/inheritance/basic.py

         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
         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__":

test/orm/mapper.py

 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
           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'))
             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"""
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.