"cascading" declared_attr ?

Issue #2952 duplicate
Mike Bayer repo owner created an issue

use case:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base, declared_attr, cascading_declared_attr

Base = declarative_base()

class HasId(object):
    @cascading_declared_attr
    def id(cls):
        if cls.__name__=='Content':
            return Column('id', Integer, primary_key=True)
        else:
            return Column('id', Integer, ForeignKey('content.id'), primary_key=True)

class Content(HasId, Base):

    content_type = Column(String(20))

    @cascading_declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

class Project(Content):
    currency  = Column(String(3))
    target = Column(Integer)
    current = Column(Integer)



#!diff
diff --git a/lib/sqlalchemy/ext/declarative/__init__.py b/lib/sqlalchemy/ext/declarative/__init__.py
index 0ee4e33..4566324 100644
--- a/lib/sqlalchemy/ext/declarative/__init__.py
+++ b/lib/sqlalchemy/ext/declarative/__init__.py
@@ -1300,7 +1300,7 @@ Mapped instances then make usage of
 from .api import declarative_base, synonym_for, comparable_using, \
     instrument_declarative, ConcreteBase, AbstractConcreteBase, \
     DeclarativeMeta, DeferredReflection, has_inherited_table,\
-    declared_attr, as_declarative
+    declared_attr, cascading_declared_attr, as_declarative


 __all__ = ['synonym_for', 'has_inherited_table',
diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py
index 2418c6e..dc6c42b 100644
--- a/lib/sqlalchemy/ext/declarative/api.py
+++ b/lib/sqlalchemy/ext/declarative/api.py
@@ -162,6 +162,26 @@ class declared_attr(interfaces._MappedAttribute, property):
     def __get__(desc, self, cls):
         return desc.fget(cls)

+class cascading_declared_attr(declared_attr):
+    """A :class:`.declared_attr` that will be invoked for all subclasses.
+
+    Use :class:`.cascading_declared_attr` when a particular ``@declared_attr``
+    needs to be invoked individually for each subclass in a hierarchy::
+
+        class HasId(object):
+            @cascading_declared_attr
+            def id(cls):
+                if has_inherited_table(cls):
+                    return Column(Integer, ForeignKey("content.id"), primary_key=True)
+                else:
+                    return Column(Integer, primary_key=True)
+
+        class Content(HasId, Base):
+            pass
+
+        class SubContent(Content):
+            pass
+    """

 def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
                      name='Base', constructor=_declarative_constructor,
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 4fda9c7..a8702cc 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -31,7 +31,7 @@ def _declared_mapping_info(cls):


 def _as_declarative(cls, classname, dict_):
-    from .api import declared_attr
+    from .api import declared_attr, cascading_declared_attr

     # dict_ will be a dictproxy, which we can't write to, and we need to!
     dict_ = dict(dict_)
@@ -96,6 +96,12 @@ def _as_declarative(cls, classname, dict_):
                             "not applying to subclass %s."
                             % (base.__name__, name, base, cls))
                 continue
+            elif isinstance(obj, cascading_declared_attr):
+                ret = obj.__get__(obj, cls)
+                dict_[name] = column_copies[obj] = ret
+                if isinstance(ret, (Column, MapperProperty)) and \
+                    ret.doc is None:
+                    ret.doc = obj.__doc__
             elif base is not cls:
                 # we're a mixin.
                 if isinstance(obj, Column):
@@ -125,8 +131,8 @@ def _as_declarative(cls, classname, dict_):
                         "be declared as @declared_attr callables "
                         "on declarative mixin classes.")
                 elif isinstance(obj, declarative_props):
-                    dict_[name](name) = ret = \
-                            column_copies[obj] = getattr(cls, name)
+                    ret = getattr(cls, name)
+                    dict_[name] = column_copies[obj] = ret
                     if isinstance(ret, (Column, MapperProperty)) and \
                         ret.doc is None:
                         ret.doc = obj.__doc__

Comments (8)

  1. Mike Bayer reporter

    note that I don't like this name too much. It would be nicer if declared_attr could be the namespace, like:

    @declared_attr(all_subclasses=True)
    def id(cls):
       ...
    
    @declared_attr.all_subclasses  #  ?
    def id(cls):
       ...
    
  2. Silvio Haedrich

    Interesting. Not long ago I was trying to solve smth. like this in an inheritance context where I have a augmented base class, a (some) "parent" class(es) inheriting from base and a couple of "child" classes inheriting from "parent".

    I'm setting the id/pk in the base class so all the "parents" have their id, but then had to set the foreign key on all "children" and thought it's un-DRY (wet? :) ). I tried to use has_parent_table (the way you do above) until I realised I can't because it doesn't work for columns.

    In one case I needed to use a mixin for all the children (because of a unique constraint I had no idea how to create otherwise) so I could easily add the id with the foreign key to it. In the other case I still have the id's defined for every child.

    It would be great if the solution you proposed would work for inheritance contexts too. (Maybe this is in your code already?)

  3. Silvio Haedrich

    With your patch it works as I had desired.

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base, declared_attr, cascading_declared_attr, has_inherited_table
    
    class Base(object):
        @cascading_declared_attr
        def id(cls):
            if has_inherited_table(cls):
                parent_table = cls.__bases__[0].__tablename__
                foreign_key = '{0}.id'.format(parent_table)
                return Column(Integer, ForeignKey(foreign_key), primary_key=True)
            return Column(Integer, primary_key=True)
    
        @declared_attr
        def __tablename__(cls):
            return cls.__name__.lower()
    
    Base = declarative_base(cls=Base)
    
    class Content(Base):
        content_type = Column(String(20))
    
    class Project(Content):
        currency  = Column(String(3))
        target = Column(Integer)
        current = Column(Integer)
    

    PS: In my first comment I had mistakenly written that "has_parent_table()" had not worked but I actually confused it with @declared_attr. (What you said in this google group, might be worth adding to the docs? )

    FWIW Given that tablename was already mapped on subclasses anyway it wouldn't be necessary to use @cascading_declared_attr for it or do I miss something?

    Out of curiosity: What exactly blocks you from implementing it as a namespace attribute for @declared_attr?

    If there is anything else (I think I haven't really done something yet) I can do for SQLAlchemy, I would be glad to try to.

  4. Log in to comment