Intermediate `__abstract__` inheriting from model doesn't transfer model's attrs
I have the following models:
Device
DeviceSource
has a foreign key and relationship toDevice
LDAPDeviceSource
is a joined-table inheritance ofDeviceSource
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)
-
reporter -
reporter I can "solve" it by making
DeviceSourceMixin
inherit fromobject
, and just subclass bothDeviceSourceMixin
andDeviceSource
, but it seems like this is something__abstract__
should be able to handle. -
repo owner that would be the workaround for now, yes. intricate declarative fixes like this are targeted at 1.0 for now.
-
repo owner - changed milestone to 1.0
-
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'):
-
repo owner Issue
#3240was marked as a duplicate of this issue. -
repo owner make sure to add tests for the
#3240condition. -
repo owner - changed status to resolved
- 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#3219fixes#3240
→ <<cset 95e53d0b6072>>
- Log in to comment