graphql-python / graphene-sqlalchemy

Graphene SQLAlchemy integration
http://docs.graphene-python.org/projects/sqlalchemy/en/latest/
MIT License
980 stars 226 forks source link

AttributeError: entity #245

Closed david-freistrom closed 5 years ago

david-freistrom commented 5 years ago

Python 3.7.4 SQLAlchemy 1.3.7 graphene-sqlalchemy 2.2.2 Flask 1.1.1 Flask-SQLAlchemy 2.4.0 psycopg2 2.8.3

Linux xxx 5.1.21-200.fc29.x86_64 #1 SMP Mon Jul 29 15:30:04 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

The following code snippets show just a simple One-to-Many Relationship. I tried it with and back_populates and with backref, Uni- and Bidirectional. I also tried Many-to-Many relationships with a association Table. But nothing helped.

I always get the error shown in the last Bash-snippet.

Whats wrong here? I found out, that sqlalchemy.orm.relationships.RelationshipProperty give that exception when I try to call .entity on it.

I already opened an issue on the sqlalchemy github https://github.com/sqlalchemy/sqlalchemy/issues/4819 and got the answer above. Hopefully it helps you to help me to fix this issue ;)

that stack trace is not very easy to create as it involves an unusual attribute error being generated when mappings are being resolved, and it is masquerading as an attribute error for the "entity" attribute, which is in fact a function. in python 3, any ofher kind of exception will be displayed as is, so it's very strange for it to be an attribute error.

in case that doesn't make sense, it means there is another exception happening that we're not able to see.

I unfortunately cannot reproduce your error with your mappings. The condition may be due to whatever graphene-sqlalchemy is doing, The method call here:

File "/home/david/projects/Qubic/lib64/python3.7/site-packages/graphene/utils/subclass_with_meta.py", line 52, in init_subclass super_class.init_subclass_with_meta(**options)

which is then calling down to class_mapper(), looks suspicious to me; I would first off not be using a metaclass for anything in conjunction wtih SQLAlchemy declarative because metaclasses are very unwieldy and declarative is already using one (they should use mixin classes or class decorators for whatever it is they need to add to SQLAlchemy models) and if I were, I'd not be trying to run class_mapper() inside of it as this could in theory lead to some difficult re-entrant conditions which is likely what's happening here.

in short I think you need to report this to graphene-sqlalchemy.

It is happen in sqlalchemy/orm/mapper.py(1947)_post_configure_properties() on line 1947

1932        def _post_configure_properties(self):                                                                                                                                   │
1933            """Call the ``init()`` method on all ``MapperProperties``                                                                                                           │
1934            attached to this mapper.                                                                                                                                            │
1935                                                                                                                                                                                │
1936            This is a deferred configuration step which is intended                                                                                                             │
1937            to execute once all mappers have been constructed.                                                                                                                  │
1938                                                                                                                                                                                │
1939            """                                                                                                                                                                 │
1940                                                                                                                                                                                │
1941            self._log("_post_configure_properties() started")                                                                                                                   │
1942            l = [(key, prop) for key, prop in self._props.items()]                                                                                                              │
1943            for key, prop in l:                                                                                                                                                 │
1944                self._log("initialize prop %s", key)                                                                                                                            │
1945                                                                                                                                                                                │
1946                if prop.parent is self and not prop._configure_started:                                                                                                         │
1947 ->                 prop.init()                                                                                                                                                 │
1948                                                                                                                                                                                │
1949                if prop._configure_finished:                                                                                                                                    │
1950                    prop.post_instrument_class(self)                                                                                                                            │
1951                                                                                      
(Pdb) dir(prop)
['Comparator', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__','__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_add_reverse_property', '_all_strategies', '_cascade', '_check_cascade_settings', '_check_conflicts', '_columns_are_mapped', '_configure_finished', '_configure_started', '_create_joins', '_creation_order', '_default_path_loader_key', '_dependency_processor', '_fallback_getattr', '_generate_backref', '_get_attr_w_warn_on_none', '_get_cascade', '_get_context_loader', '_get_strategy', '_is_internal_proxy', '_is_self_referential', '_lazy_none_clause', '_memoized_attr__default_path_loader_key', '_memoized_attr_wildcard_token', '_memoized_attr_info', '_optimized_compare', '_persists_for', '_post_init', '_process_dependent_arguments', '_reverse_property', '_set_cascade', '_setup_join_conditions', '_should_log_debug', '_should_log_info', '_strategies', '_strategy_lookup', '_use_get', '_user_defined_foreign_keys', '_value_as_iterable', '_wildcard_token', '_with_parent', 'active_history', 'argument', 'back_populates', 'backref', 'bake_queries', 'cascade', 'cascade_backrefs', 'cascade_iterator', 'class_attribute', 'collection_class', 'comparator', 'comparator_factory', 'create_row_processor', 'direction', 'distinct_target_key', 'do_init', 'doc', 'enable_typechecks', 'entity', 'extension', 'extension_type', 'info', 'init', 'innerjoin', 'instrument_class', 'is_aliased_class', 'is_attribute', 'is_clause_element', 'is_instance', 'is_mapper', 'is_property', 'is_selectable', 'join_depth', 'key', 'lazy', 'load_on_pending', 'local_remote_pairs', 'logger', 'mapper', 'merge', 'omit_join', 'order_by', 'parent', 'passive_deletes', 'passive_updates', 'post_instrument_class', 'post_update', 'primaryjoin', 'query_class', 'remote_side', 'secondary', 'secondaryjoin', 'set_parent', 'setup', 'single_parent', 'strategy', 'strategy_for', 'strategy_key', 'strategy_wildcard_key', 'uselist', 'viewonly']
(Pdb) prop.entity
*** AttributeError: entity                                   
(Pdb) prop.__class__
<class 'sqlalchemy.orm.relationships.RelationshipProperty'>

app/models/roles.py

from ..models import db, bcrypt
from sqlalchemy import Column, Integer, String, Boolean, Binary

class Role(db.Model):
  __tablename__ = "roles" 

  id = Column(Integer, primary_key=True, autoincrement=True)
  name = Column(String(80), unique=True)
  description = Column(String(255))
  users = db.relationship("models.User", backref=db.backref('role', lazy='joined'), lazy=True)

  def __repr__(self):
    return '<Role %r>' % (self.name)

__all__ = [ Role ]

app/models/users.py


from ..models import db, bcrypt
from sqlalchemy import Column, Integer, String, Boolean, Binary, DateTime, Text, ForeignKey
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method

class User(db.Model): tablename = "users"

id = Column(Integer, primary_key=True, autoincrement=True) first_name = Column(String(255), nullable=False) last_name = Column(String(255), nullable=False) email = Column(String(255), unique=True, nullable=False) public_key = Column(Text, unique=True) _secret_access_key = Column(Binary(60), unique=True) access_key_id = Column(String(255), unique=True) active = Column(Boolean()) confirmed_at = Column(DateTime()) confirmation_token = Column(String(255), unique=True) confirmation_sent_at = Column(DateTime()) role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)

@hybrid_property def secret_access_key(self): return self._secret_access_key

@secret_access_key.setter def secret_access_key(self, plaintext_key): self._secret_access_key = bcrypt.generate_password_hash(plaintext_key, 15)

@hybrid_method def is_correct_secret_access_key(self, plaintext_key): return bcrypt.check_password_hash(self.secret_access_key, plaintext_key)

def repr(self): return '<User %r %r>' % (self.first_name, self.last_name)

all = [ User ]


> app/models/__init__.py
```python
from flask.cli import with_appcontext
import click
from flask.cli import AppGroup
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
import pdb

db = SQLAlchemy()
bcrypt = Bcrypt()

role_cli = AppGroup('role')
user_cli = AppGroup('user')

def init_app(app):
  print("app.models.__init__.init_app()")

  db.init_app(app)
  bcrypt.init_app(app)
  app.cli.add_command(role_cli)
  app.cli.add_command(user_cli)
  app.cli.add_command(init_db)
  app.cli.add_command(seed)

@click.command('init-db')
@with_appcontext
def init_db():
  pdb.set_trace()
  db.create_all()
  click.echo('Initialized the database.')

@role_cli.command('create')
@click.argument("name")
@click.argument("description")
@with_appcontext
def create_role(name, description):
  from .roles import Role
  role = Role(name=name, description=description)
  db.session.add(role)
  db.session.commit()
  click.echo('Role created.')

@user_cli.command('create')
@click.argument("first_name")
@click.argument("last_name")
@click.argument("email")
@with_appcontext
def create_role(first_name, last_name, email):
  from .users import User
  from ..tasks.mailer import send_confirmation_mail
  user = User(first_name=first_name, last_name=last_name, email=email)
  db.session.add(user)
  db.session.commit()
  send_confirmation_mail.delay({'subject': 'Please complete your registration confirmation', 'to': user.email, 'from': 'xxx@yyy.zz'})
  click.echo('User created.')

@click.command('seed')
@with_appcontext
def seed():
  from .roles import Role
  from .users import User
  from flask import current_app
  from datetime import datetime
  import secrets

  entities = []
  entities.append(Role(name='Admin', description='Administrator'))
  entities.append(Role(name='ClusterAdmin', description='Administrator of one Redis Cluster'))

  secret_access_key=secrets.token_hex()
  entities.append(User(
    first_name=current_app.config["ADMIN"]["FIRST_NAME"], 
    last_name=current_app.config["ADMIN"]["LAST_NAME"],
    email=current_app.config["ADMIN"]["EMAIL"],
    confirmed_at=datetime.now(),
    public_key=current_app.config["ADMIN"]["PUBLIC_KEY"],
    access_key_id=secrets.token_hex(),
    secret_access_key=secret_access_key
  ))

  for entity in entities:
    try:
      db.session.add(entity)
      db.session.commit()
      click.echo("Add Entity " + str(entity) +" to Database.")
      if isinstance(entity, User):
        click.echo("SECRET_ACCESS_KEY: " + secret_access_key)
        click.echo("ACCESS_KEY_ID: " + entity.access_key_id)
    except Exception as err:
      click.echo("Entity " + str(entity) + " already exist!")

  click.echo('Database seeding Done.')

from .users import User
from .roles import Role
from .clusters import Cluster
from .groups import Group
from .workers import Worker
__all__ = [Role, User, Worker, Cluster, Group]
(Qubic) [david@doha Qubic]$ export FLASK_APP=app
(Qubic) [david@doha Qubic]$ export FLASK_ENV=development
(Qubic) [david@doha Qubic]$ flask init-db
app.schemas.user.UserObject
Traceback (most recent call last):
  File "/home/david/projects/Qubic/bin/flask", line 11, in <module>
    sys.exit(main())
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/flask/cli.py", line 966, in main
    cli.main(prog_name="python -m flask" if as_module else None)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/flask/cli.py", line 586, in main
    return super(FlaskGroup, self).main(*args, **kwargs)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/click/core.py", line 1132, in invoke
    cmd_name, cmd, args = self.resolve_command(ctx, args)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/click/core.py", line 1171, in resolve_command
    cmd = self.get_command(ctx, cmd_name)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/flask/cli.py", line 542, in get_command
    rv = info.load_app().cli.get_command(ctx, name)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/flask/cli.py", line 388, in load_app
    app = locate_app(self, import_name, name)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/flask/cli.py", line 240, in locate_app
    __import__(module_name)
  File "/home/david/projects/Qubic/app/__init__.py", line 6, in <module>
    from . import schemas
  File "/home/david/projects/Qubic/app/schemas/__init__.py", line 14, in <module>
    from .user import UserObject, UserObjectConnection, CreateUser, UpdateUser, DeleteUser, DeleteAllUser, ConfirmUser
  File "/home/david/projects/Qubic/app/schemas/user.py", line 18, in <module>
    class UserObject(SQLAlchemyObjectType):
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/graphene/utils/subclass_with_meta.py", line 52, in __init_subclass__
    super_class.__init_subclass_with_meta__(**options)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/graphene_sqlalchemy/types.py", line 224, in __init_subclass_with_meta__
    assert is_mapped_class(model), (
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/graphene_sqlalchemy/utils.py", line 28, in is_mapped_class
    class_mapper(cls)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/base.py", line 441, in class_mapper
    mapper = _inspect_mapped_class(class_, configure=configure)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/base.py", line 420, in _inspect_mapped_class
    mapper._configure_all()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/mapper.py", line 1337, in _configure_all
    configure_mappers()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/mapper.py", line 3229, in configure_mappers
    mapper._post_configure_properties()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/mapper.py", line 1947, in _post_configure_properties
    prop.init()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/interfaces.py", line 196, in init
    self.do_init()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/relationships.py", line 1860, in do_init
    self._process_dependent_arguments()
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/orm/relationships.py", line 1922, in _process_dependent_arguments
    self.target = self.entity.persist_selectable
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 949, in __getattr__
    return self._fallback_getattr(key)
  File "/home/david/projects/Qubic/lib64/python3.7/site-packages/sqlalchemy/util/langhelpers.py", line 923, in _fallback_getattr
    raise AttributeError(key)
AttributeError: entity
jnak commented 5 years ago

As far as I can tell, class_mapper() is raising an error because sqlalchemy is not able to map your model classes. I would recommend that you write a test for your models and iteratively add / remove columns / relationship definitions until you find the root cause.

I'm closing this issue because it does not seem to have anything to do with graphene-sqlalchemy.

zzzeek commented 5 years ago

note that the model as given works fine in an isolated test case without all the flask/graphene-sqlalchemy imports added, so the model as given is fine, I ran it and it has no issues. someone needs to pdb into the exception to see what the ultimate AttributeError (I assume it's an AttributeError) is being raised.

shaozi commented 2 years ago

Ran into exactly the same error multiple times and it is driving me crazy! SQLAlchemy itself passed all tests. It only shows this error when combined with graphene.

For one time, removing back_populations and remove the relationship from one side made the error disappear. For another time, a simple import statement of the model caused this error.

zzzeek commented 2 years ago

did anyone try calling configure_mappers() at the right time so that backrefs are all set up?

shaozi commented 2 years ago

I eventually found the answer. The reason for the error is that the model resolution needs the model to be imported somewhere before. So try to import the model in the beginning of the app fixed my issue.

The accepted answer in this stack overflow hit the nail on the head: https://stackoverflow.com/questions/45534903/python-sqlalchemy-attributeerror-mapper/45540141#45540141

github-actions[bot] commented 1 year ago

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related topics referencing this issue.