Skip to content

support special class of error messages generated for inspect() of decl _DeferredMappedConfig #4470

Closed
@halfdan

Description

@halfdan

I'm trying to implement concrete table inheritance with AbstractConcreteBase as described in the docs. I'm unable to get it to work without first instantiating the subclass of the class that implements AbstractConcreteBase otherwise I'm ending up with an unmapped entity.

Is this the expected behaviour? How do I get around calling CompanyListItem() to get it registered as a mapped entity?

Code:

from sqlalchemy import (
    Column,
    Integer,
    Text,
    func,
    String,
    Boolean,
    DateTime,
    Date,
    TIMESTAMP,
    ForeignKey,
    or_,
    and_,
    UniqueConstraint,
)
from sqlalchemy.orm import relationship, backref
from sqlalchemy.sql import expression
from sqlalchemy.ext.declarative import AbstractConcreteBase

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import sessionmaker

engine = create_engine('postgresql://localhost/flsqla')
Session = sessionmaker(bind=engine)
session = Session()

Base = declarative_base()

class List(Base):
    __tablename__ = "lists"

    id = Column(Integer, primary_key=True)
    label = Column(String(50), nullable=False)
    items = relationship("ListItem", back_populates="llist")


class Company(Base):
    __tablename__ = "companies"

    id = Column(Integer, primary_key=True)
    label = Column(String(50), nullable=False)
    tracked = Column(Boolean, server_default=expression.true(), nullable=False)


class ListItem(AbstractConcreteBase, Base):
    id = Column(Integer, primary_key=True)

    @declared_attr
    def list_id(cls):
        return Column(ForeignKey('lists.id'))

    @declared_attr
    def llist(cls):
        return relationship("List")


class CompanyListItem(ListItem):
    __tablename__ = "company_list_item"

    company_id = Column(ForeignKey("companies.id", deferrable=True, initially="DEFERRED"), nullable=False)
    company = relationship("Company", primaryjoin="CompanyListItem.company_id == Company.id")

    __mapper_args__ = {"polymorphic_identity": "company_item", "concrete": True}


Base.metadata.create_all(engine)

Error:

>>> from app import ListItem, session, CompanyListItem, Company, List
>>> session.query(ListItem)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/halfdan/flsqla-schema/.venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1538, in query
    return self._query_cls(entities, self, **kwargs)
  File "/Users/halfdan/flsqla-schema/.venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 167, in __init__
    self._set_entities(entities)
  File "/Users/halfdan/flsqla-schema/.venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 197, in _set_entities
    entity_wrapper(self, ent)
  File "/Users/halfdan/flsqla-schema/.venv/lib/python3.6/site-packages/sqlalchemy/orm/query.py", line 4209, in __init__
    "expected - got '%r'" % (column,)
sqlalchemy.exc.InvalidRequestError: SQL expression, column, or mapped entity expected - got '<class 'app.ListItem'>'
>>> CompanyListItem()
<app.CompanyListItem object at 0x105c9ac88>
>>> session.query(ListItem)
<sqlalchemy.orm.query.Query object at 0x106e38fd0>

Activity

zzzeek

zzzeek commented on Jan 27, 2019

@zzzeek
Member

can you call configure_mappers() ? that should set things up

added
questionissue where a "fix" on the SQLAlchemy side is unlikely, hence more of a usage question
on Jan 27, 2019
zzzeek

zzzeek commented on Jan 27, 2019

@zzzeek
Member

works:

from sqlalchemy.orm import configure_mappers

configure_mappers()

session.query(ListItem)

seems like docs should mention this

halfdan

halfdan commented on Jan 27, 2019

@halfdan
Author

That does seem to work. I'm wondering if sqlalchemy should call this automatically however when a class that extends AbstractConcreteBase is used in a session.query.

https://docs.sqlalchemy.org/en/latest/orm/mapping_api.html#sqlalchemy.orm.configure_mappers

This function can be called any number of times, but in most cases is invoked automatically, the first time mappings are used, as well as whenever mappings are used and additional not-yet-configured mappers have been constructed.
Points at which this occur include when a mapped class is instantiated into an instance, as well as when the Session.query() method is used.

It seems like that is not entirely true currently. At least from this I would expect session.query(ListItem) to work without having to call configure_mappers manually.

zzzeek

zzzeek commented on Jan 27, 2019

@zzzeek
Member

technically, it is "entirely true" that it is "in most cases invoked automatically" :)

unfortunately the current implementation of AbstractConcreteBase cannot be automatically configured, the configure_mappers() step has to occur from somewhere else.

zzzeek

zzzeek commented on Jan 27, 2019

@zzzeek
Member

note all the classes that inherit from your ABC do invoke the configure step automatically. it's just the actual base that doesn't. it is not mapped until all the subclasses are set up.

halfdan

halfdan commented on Jan 27, 2019

@halfdan
Author

Haha fair point.

Would calling session.query with the ABC not suggest that I expect all its subclasses to have loaded?

If that's not possible I think session.query should at least emit a warning when it encounters a class that implements ABC

zzzeek

zzzeek commented on Jan 30, 2019

@zzzeek
Member

Haha fair point.

Would calling session.query with the ABC not suggest that I expect all its subclasses to have loaded?

it should, but at the moment, it does not, because the class ListItem here has no mapper at all until configure_mappers() is called. this is again an issue w/ the implementation of abstractconcretebase.

If that's not possible I think session.query should at least emit a warning when it encounters a class that implements ABC

that would be the same as just having it call configure_mappers() for us, e.g., to fix the issue. there would need to be some new flag or something that gives it an extra hint.

the better solution would be that AbstractConcreteBase can be mapped up front and that it can alter the internals of this mapping as new subclasses are created (and thus change the base "polymorphic selectable" to which it is mapped). but the "extra hint" solution might have to suffice for the near term.

changed the title [-]AbstractConcreteBase requires subclasses to be instantiated before first use[/-] [+]support special class of error messages generated for inspect() of decl _DeferredMappedConfig[/+] on Jan 30, 2019
zzzeek

zzzeek commented on Jan 30, 2019

@zzzeek
Member

e.g. AbstractConcreteBase, DeferredReflection, automap.

this is a patch which would do it but adds a chunk of latency to inspect(), so something would have to be worked out here to improve this:

diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py
index 139481ba59..31705cfdd3 100644
--- a/lib/sqlalchemy/ext/declarative/api.py
+++ b/lib/sqlalchemy/ext/declarative/api.py
@@ -755,3 +755,38 @@ class DeferredReflection(object):
             autoload_with=engine,
             schema=table.schema,
         )
+
+
+from ... import inspection
+from ...orm.base import _inspect_mapped_class
+from ...orm import exc as orm_exc
+
+
+@inspection._inspects(DeferredReflection)
+def _inspect_deferred_reflection(cls):
+    mp = _inspect_mapped_class(cls)
+    if mp is None:
+        raise orm_exc.UnmappedClassError(
+            cls,
+            msg="Class %s is a subclass of DeferredReflection.  "
+            "Mappings are not produced until the .prepare() "
+            "method is called on the class hierarchy."
+            % orm_exc._safe_cls_name(cls),
+        )
+    return mp
+
+
+@inspection._inspects(AbstractConcreteBase)
+def _inspect_abc(cls):
+    mp = _inspect_mapped_class(cls)
+    if mp is None:
+        if _DeferredMapperConfig.has_cls(cls):
+            raise orm_exc.UnmappedClassError(
+                cls,
+                msg="Class %s is a subclass of AbstractConcreteBase and "
+                "has a mapping pending until all subclasses are defined. "
+                "Call the sqlalchemy.orm.configure_mappers() method to "
+                "complete the mapping of this class."
+                % orm_exc._safe_cls_name(cls),
+            )
+    return mp
diff --git a/lib/sqlalchemy/inspection.py b/lib/sqlalchemy/inspection.py
index 7f71a011c1..3fb84333aa 100644
--- a/lib/sqlalchemy/inspection.py
+++ b/lib/sqlalchemy/inspection.py
@@ -55,7 +55,11 @@ def inspect(subject, raiseerr=True):
 
      """
     type_ = type(subject)
-    for cls in type_.__mro__:
+
+    look = type_.__mro__
+    if issubclass(type_, type):
+        look = subject.__mro__ + look
+    for cls in look:
         if cls in _registrars:
             reg = _registrars[cls]
             if reg is True:
diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py
index b64b35e72e..e192c01dfb 100644
--- a/lib/sqlalchemy/orm/base.py
+++ b/lib/sqlalchemy/orm/base.py
@@ -347,7 +347,6 @@ def _is_mapped_class(entity):
     """Return True if the given object is a mapped class,
     :class:`.Mapper`, or :class:`.AliasedClass`.
     """
-
     insp = inspection.inspect(entity, False)
     return (
         insp is not None
sqla-tester

sqla-tester commented on Jan 30, 2019

@sqla-tester
Collaborator

Mike Bayer has proposed a fix for this issue in the master branch:

Add informative failure modes to _DeferredMapperConfig https://gerrit.sqlalchemy.org/1120

added
bugSomething isn't working
declarativehas to do with the declarative API, scanning classes and mixins for attributes to be mapped
and removed
questionissue where a "fix" on the SQLAlchemy side is unlikely, hence more of a usage question
on Jan 30, 2019

4 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdeclarativehas to do with the declarative API, scanning classes and mixins for attributes to be mappedorm

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @zzzeek@halfdan@sqla-tester

        Issue actions

          support special class of error messages generated for inspect() of decl _DeferredMappedConfig · Issue #4470 · sqlalchemy/sqlalchemy