Intermediate `__abstract__` inheriting from model doesn't transfer model's attrs

Issue #3219 resolved
David Lord created an issue

I have the following models:

  • Device
  • DeviceSource has a foreign key and relationship to Device
  • LDAPDeviceSource is a joined-table inheritance of DeviceSource

In order to make defining new DeviceSource subclasses easier, I created DeviceSourceMixin, which inherits from DeviceSource and provides declared attrs for __tablename__ and the foreign primary id. DeviceSourceMixin is __abstract__ so that it doesn't create a table of its own.

The issue is that this intermediate __abstract__ seems to break the declarative model. The foreign key and relationship in DeviceSource do not get inherited by LDAPDeviceSource when subclassing DeviceSourceMixin.

This code demonstrates the issue:

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr, as_declarative
from sqlalchemy.orm import Session, relationship

engine = sa.create_engine('sqlite:///:memory:', echo=True)
session = Session(bind=engine)


@as_declarative(bind=engine)
class Base(object):
    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

    id = sa.Column(sa.Integer, primary_key=True)


class Device(Base):
    pass


class DeviceSource(Base):
    type = sa.Column(sa.String, nullable=False)
    device_id = sa.Column(sa.Integer, sa.ForeignKey(Device.id), nullable=False)

    device = relationship(Device, backref='sources')

    __mapper_args__ = {
        'polymorphic_on': type
    }


class DeviceSourceMixin(DeviceSource):
    __abstract__ = True

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

    @declared_attr
    def id(cls):
        return sa.Column(sa.Integer, sa.ForeignKey(DeviceSource.id), primary_key=True)


class LDAPDeviceSource(DeviceSourceMixin):
    name = sa.Column(sa.String, nullable=False)

    __mapper_args__ = {
        'polymorphic_identity': 'ldap'
    }


Base.metadata.create_all()
d1 = Device()
s1 = LDAPDeviceSource(device=d1, name='s1')
session.add(s1)
session.commit()

It produces the following error:

Traceback (most recent call last):
  File "/home/david/Projects/cedar/example2.py", line 62, in <module>
    s1 = LDAPDeviceSource(device=d1, name='s1')
  File "<string>", line 4, in __init__
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/state.py", line 260, in _initialize_instance
    return manager.original_init(*mixed[1:], **kwargs)
  File "<string>", line 6, in __init__
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/ext/declarative/base.py", line 526, in _declarative_constructor
    setattr(self, k, kwargs[k])
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 226, in __set__
    instance_dict(instance), value, None)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 812, in set
    value = self.fire_replace_event(state, dict_, value, old, initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 832, in fire_replace_event
    state, value, previous, initiator or self._replace_token)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 1148, in emit_backref_from_scalar_set_event
    passive=PASSIVE_NO_FETCH)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 980, in append
    collection.append_with_event(value, initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/collections.py", line 653, in append_with_event
    self._data()._sa_appender(item, _sa_initiator=initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/collections.py", line 1047, in append
    item = __set(self, item, _sa_initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/collections.py", line 1019, in __set
    item = executor.fire_append_event(item, _sa_initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/collections.py", line 716, in fire_append_event
    item, initiator)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 929, in fire_append_event
    value = fn(state, value, initiator or self._append_token)
  File "/home/david/.virtualenvs/cedar/lib/python3.4/site-packages/sqlalchemy/orm/attributes.py", line 1157, in emit_backref_from_collection_append_event
    child_impl = child_state.manager[key].impl
KeyError: 'device'

Comments (8)

  1. David Lord reporter

    I can "solve" it by making DeviceSourceMixin inherit from object, and just subclass both DeviceSourceMixin and DeviceSource, but it seems like this is something __abstract__ should be able to handle.

  2. Mike Bayer repo owner

    that would be the workaround for now, yes. intricate declarative fixes like this are targeted at 1.0 for now.

  3. Mike Bayer repo owner

    here's a patch

    diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
    index 291608b..451457a 100644
    --- a/lib/sqlalchemy/ext/declarative/base.py
    +++ b/lib/sqlalchemy/ext/declarative/base.py
    @@ -35,6 +35,21 @@ def _declared_mapping_info(cls):
             return None
    
    
    +def _resolve_for_abstract(cls):
    +    if cls is object:
    +        return None
    +
    +    if _get_immediate_cls_attr(cls, '__abstract__'):
    +        for sup in cls.__bases__:
    +            sup = _resolve_for_abstract(sup)
    +            if sup is not None:
    +                return sup
    +        else:
    +            return None
    +    else:
    +        return cls
    +
    +
     def _get_immediate_cls_attr(cls, attrname):
         """return an attribute of the class that is either present directly
         on the class, e.g. not on a superclass, or is from a superclass but
    @@ -46,6 +61,9 @@ def _get_immediate_cls_attr(cls, attrname):
         inherit from.
    
         """
    +    if not issubclass(cls, object):
    +        return None
    +
         for base in cls.__mro__:
             _is_declarative_inherits = hasattr(base, '_decl_class_registry')
             if attrname in base.__dict__:
    @@ -388,6 +406,9 @@ class _MapperConfig(object):
             table_args = self.table_args
             declared_columns = self.declared_columns
             for c in cls.__bases__:
    +            c = _resolve_for_abstract(c)
    +            if c is None:
    +                continue
                 if _declared_mapping_info(c) is not None and \
                         not _get_immediate_cls_attr(
                             c, '_sa_decl_prepare_nocascade'):
    
  4. Mike Bayer repo owner
    • Fixed bug where using an __abstract__ mixin in the middle of a declarative inheritance hierarchy would prevent attributes and configuration being correctly propagated from the base class to the inheriting class. fixes #3219 fixes #3240

    → <<cset 95e53d0b6072>>

  5. Log in to comment