Thanks the for quick reply.
Trying to mush it to a single file I accidentally found the source (not the 
solution unfortunately) of the problem.
It only happens when I use FlaskSqlalchemy as the base class... so maybe 
it's something on their part?
I attached a file (python 3). You must use flask-sqlalchemy master to run 
it, as it uses a custom base class which I think isn't released yet.
I suppose their base class generates those events. I commented out the 
declarative_base usage that works (see attached file).

On Monday, August 15, 2016 at 4:57:19 PM UTC+3, Mike Bayer wrote:
>
> can you attach a quick single-file .py script that I can run, and will 
> show how you're getting this error?  you might be in an "impossible" 
> block there, in that you are trying to add an event listener within that 
> same event, but I need to see if what you're doing is really something 
> that should be made to work.   That is, perhaps declarative should use a 
> different system internally for the __declare_first__ in order to get 
> around this. 
>
>
>
> On 08/15/2016 09:38 AM, Tom Kedem wrote: 
> > I'm trying to create a dynamic many_to_many relationship, inspired by: 
> > http://techspot.zzzeek.org/2011/05/17/magic-a-new-orm/ 
> > 
> https://bitbucket.org/zzzeek/pycon2014_atmcraft/src/f50cbe745a19/atmcraft/model/meta/?at=master
>  
> > (orm.py and schema.py) 
> > 
> > I've got it working with just creating a secondary table in the 
> > __declare_first__ hook. 
> > 
> > Now I want it to be a "secondary" class and not just a table - and I'm 
> > having trouble. 
> > I've created a declared class in the __declare_first__ hook. Creating 
> > that class causes (Full stacktrace at the bottom): 
> > RuntimeError: deque mutated during iteration 
> > 
> > I suppose new events are being added when I do so. Searching google I 
> > came upon these: 
> > 
> https://bitbucket.org/zzzeek/sqlalchemy/issues/3163/use-deque-for-event-lists-so-that-remove
>  
> > 
> https://bitbucket.org/zzzeek/sqlalchemy/commits/4a4cccfee5a2#Llib/sqlalchemy/event/api.pyT61
>  
> > 
> > But since I'm not creating those events explicitly (I suspect the 
> > declared class creates them), I'm not sure about how to proceed. 
> > 
> > Full stacktrace if it's helpful: 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\orm\instrumentation.py",
>  
>
> > line 347, in _new_state_if_none 
> >     state = self._state_constructor(instance, self) 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\util\langhelpers.py",
>  
>
> > line 754, in __get__ 
> >     obj.__dict__[self.__name__] = result = self.fget(obj) 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\orm\instrumentation.py",
>  
>
> > line 177, in _state_constructor 
> >     self.dispatch.first_init(self, self.class_) 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\event\attr.py", 
> line 
> > 256, in __call__ 
> >     fn(*args, **kw) 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\orm\mapper.py", 
> line 
> > 2943, in _event_on_first_init 
> >     configure_mappers() 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\orm\mapper.py", 
> line 
> > 2822, in configure_mappers 
> >     Mapper.dispatch._for_class(Mapper).before_configured() 
> >   File 
> > 
> "C:\Users\vToMy\PycharmProjects\sqlalchemy\lib\sqlalchemy\event\attr.py", 
> line 
> > 217, in __call__ 
> >     for fn in self.parent_listeners: 
> > RuntimeError: deque mutated during iteration 
> > 
> > 
> > -- 
> > You received this message because you are subscribed to the Google 
> > Groups "sqlalchemy" group. 
> > To unsubscribe from this group and stop receiving emails from it, send 
> > an email to sqlalchemy+...@googlegroups.com <javascript:> 
> > <mailto:sqlalchemy+unsubscr...@googlegroups.com <javascript:>>. 
> > To post to this group, send email to sqlal...@googlegroups.com 
> <javascript:> 
> > <mailto:sqlal...@googlegroups.com <javascript:>>. 
> > Visit this group at https://groups.google.com/group/sqlalchemy. 
> > For more options, visit https://groups.google.com/d/optout. 
>

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.
from flask_sqlalchemy import SQLAlchemy, Model

from sqlalchemy import Column, ForeignKeyConstraint
from sqlalchemy import Integer
from sqlalchemy_utils import get_declarative_base
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declared_attr
import inspect


def _get_fk_column(ref_cls, refcol, nullable=None, primary_key=False):
    if nullable is None:
        nullable = not primary_key
    return Column(name="%s_%s" % (ref_cls.__tablename__, refcol.name),
                  nullable=nullable, primary_key=primary_key)


class References(object):
    """A mixin which creates foreign key references to related classes."""
    _to_ref = set()
    _references = _to_ref.add
    _to_biref = set()
    _bireferences = _to_biref.add
    _association_tables = {}
    _foreign_keys_columns = {}
    already_called = False  # TODO this is a patch

    @classmethod
    def __declare_first__(cls):
        """declarative hook called within the 'before_configure' mapper event."""

        if cls.already_called:
            return

        for lcl, rmt, delete_orphan in cls._to_biref:
            lcl._reference_m2m(rmt, delete_orphan)
        cls._to_biref.clear()

        for lcl, rmt, fk_nullable in cls._to_ref:
            cls._decl_class_registry[lcl]._reference_table(cls._decl_class_registry[rmt], fk_nullable)
        cls._to_ref.clear()

        cls.add_constraints()
        cls.already_called = True

    @classmethod
    def _reference_table(cls, ref_cls, fk_nullable):
        """Create a foreign key reference from the local class to the given remote table.

        Adds column references to the declarative class and adds a ForeignKeyConstraint.

        """
        ref_table = ref_cls.__table__
        # create pairs of (Foreign key column, primary key column)
        cols = [(_get_fk_column(ref_cls, refcol, fk_nullable), refcol) for refcol in ref_table.primary_key]

        # set "tablename_colname = Foreign key Column" on the local class
        for col, refcol in cols:
            setattr(cls, col.name, col)

        # add a ForeignKeyConstraint([local columns], [remote columns])
        cls.__table__.append_constraint(ForeignKeyConstraint(*zip(*cols)))
        cls._foreign_keys_columns[(cls, ref_cls)] = [i[0] for i in cols]

    @classmethod
    def _reference_m2m(cls, clsname, delete_orphan):
        tablename = '%s_%s' % (cls.__tablename__, clsname.__tablename__)
        local_pks = [(_get_fk_column(cls, refcol, primary_key=True), refcol) for refcol in cls.__table__.primary_key]
        remote_pks = [(_get_fk_column(clsname, refcol, primary_key=True), refcol) for refcol in clsname.__table__.primary_key]

        association_class_attributes = {
            '__tablename__': tablename,
        }

        for pk, refcol in local_pks + remote_pks:
            association_class_attributes[pk.name] = pk

        # TODO cascades?
        association_class_attributes[cls.__tablename__] = relationship(cls.__name__, foreign_keys=[i[0] for i in local_pks], backref=tablename)
        association_class_attributes[clsname.__tablename__] = relationship(clsname.__name__, foreign_keys=[i[0] for i in remote_pks], backref=tablename)

        association_class_name = cls.__name__ + clsname.__name__
        base_class = get_declarative_base(cls)
        association_class = type(association_class_name, (base_class,), association_class_attributes)
        association_class.__table__.append_constraint(ForeignKeyConstraint(*zip(*local_pks)))
        association_class.__table__.append_constraint(ForeignKeyConstraint(*zip(*remote_pks)))
        cls._association_tables[(cls, clsname)] = association_class

        # TODO figure this out
        #if delete_orphan:
        #    auto_delete_orphans(getattr(cls, rel_name))

    @classmethod
    def add_constraints(cls):
        if hasattr(cls, 'constraints'):
            constraints = getattr(cls, 'constraints')
            if isinstance(constraints, tuple):
                constraint_class, mixed = constraints

                columns = []
                for mix in mixed:
                    if isinstance(mix, Column):
                        columns.append(mix)
                    elif isinstance(mix, str) and hasattr(cls, mix):
                        attribute = getattr(cls, mix)
                        columns += attribute.property.columns
                    elif isinstance(mix, str) or inspect.isclass(mix):
                        mix_class = cls._decl_class_registry[mix] if isinstance(mix, str) else mix
                        foreign_key_columns = cls._foreign_keys_columns[(cls, mix_class)]
                        columns += foreign_key_columns

                constraint = constraint_class(*columns)
                cls.__table__.append_constraint(constraint)


def many_to_one(clsname, **kw):
    """Use an event to build a many-to-one relationship on a class.

    This makes use of the :meth:`.References._reference_table` method
    to generate a full foreign key relationship to the remote table.

    """
    name = clsname.__name__ if inspect.isclass(clsname) else clsname
    fk_nullable = kw.pop('nullable', True)
    @declared_attr
    def m2o(cls):
        rel = relationship(name, foreign_keys=lambda: cls._foreign_keys_columns[(cls, clsname)], **kw)
        cls._references((cls.__name__, name, fk_nullable))
        return rel
    return m2o


def one_to_many(clsname, **kw):
    """Use an event to build a one-to-many relationship on a class.

    This makes use of the :meth:`.References._reference_table` method
    to generate a full foreign key relationship from the remote table.

    """
    name = clsname.__name__ if inspect.isclass(clsname) else clsname
    fk_nullable = kw.pop('nullable', True)
    @declared_attr
    def o2m(cls):
        rel = relationship(name, foreign_keys=lambda: cls._foreign_keys_columns[(clsname, cls)], **kw)
        cls._references((name, cls.__name__, fk_nullable))
        return rel
    return o2m


def many_to_many(clsname, **kw):
    """Create an association table between parent/target as well as a relationship()."""
    name = clsname.__name__ if inspect.isclass(clsname) else clsname
    @declared_attr
    def m2m(cls):
        """Create an association table between parent/target as well as a relationship()."""
        delete_orphan = kw.pop('delete_orphan', False)

        rel_name = '%s_%s' % (cls.__tablename__, clsname.__tablename__)
        remote = clsname.__tablename__
        association = association_proxy(rel_name, remote)

        cls._bireferences((cls, clsname, delete_orphan))
        return association
    return m2m


def add_constraint(constraint_class, columns, **kwargs):
    @declared_attr
    def foo(cls):
        cls._constraints.append((constraint_class, columns, kwargs))


class Base(Model, References):
    pass


# This doesn't work!
session_options = dict(autoflush=False)
db = SQLAlchemy(model_class=Base, session_options=session_options)
Base = db.Model

# This works!
# Base = declarative_base()


class Tag(Base, References):
    __tablename__ = 'tag'
    id = Column(Integer, primary_key=True)


class User(Base, References):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    tags = many_to_many(Tag)

user = User()

Reply via email to