Cannot initialize or set relations with tuples

Issue #3456 wontfix
‮rekcäH nitraM‮ created an issue

In our applications we like to use immutable types where not otherwise needed, just as a default to prevent bugs where you get a collection set on your object and then it changes without you noticing because it's actually a shared value and somebody else still thinks he owns it.

Since these bugs are very hard to spot, we like to use tuple() over list() to alleviate that concern. SQLA however insists on list() or list-like structures for initializing or set to many relations.

Since it already does quite a lot to adapt whatever type is incoming that might be iterable in any sensible way - can't it not also support tuples?

We're seeing this crop up (using declarative) in object initialization in code like User(groups=(group1, group2)) or user.groups = (group1, group2)

As far as I can see adapt_like_to_iterable seems to be the culprit here as it only accepts the mutable variants of the types.

Comments (7)

  1. Mike Bayer repo owner

    if you're just looking to be able to say A(coll = ()) or some_a.coll = (), that is easy, just use a @validates that states return list(value), or more generally use an attribute set event that runs across the board for all the collection relationships you care about. It's just that the actual collection at the EOD would be a list when you go to access it, as well as when the ORM loads it.

    Do you care if the tuples are turned into lists after the assignment?

  2. ‮rekcäH nitraM‮ reporter

    No, not at all, since SQLA owns the 'lists' and knows when they are tampered with, that is perfectly fine. Just in other parts of the application, I want to express the fact that other objects (usually) don't own the collections by expressing them as tuples.

    Regarding the API, thanks for the hint - we may do this as a stopgap, but I really think it would be better for SQLA to adapt iterables (like tuple) too when it tries to adapt what is set on the SQLA objects.

  3. Mike Bayer repo owner

    that would be a really huge change in the API behavior. It would mean I can assign any iterable to any collection-holding relationship regardless of collection_class. While that might be convenient, it also eliminates a whole class of error checking, such as if someone inadvertently assigns a string to a collection, or a dictionary to a collection and we only get the keys(), or a list to a dictionary and it has to guess what to do with the entries (are they tuples? or instances that we apply to the key-getter? do we have to check every entry?) or something like that.

    It's not hard to change in the code (well until it gets into all the dictionary-related edge cases) but I'm not really ready to go there, considering this is configurable if someone wants relaxed collection-adaption behavior.

  4. Mike Bayer repo owner

    actually here is the original way that collection adaption is configurable, this API is not used too often but it's there:

    from sqlalchemy import Integer, Column, ForeignKey
    from sqlalchemy.orm import relationship
    from sqlalchemy.orm import collections
    from sqlalchemy.ext.declarative import declarative_base
    
    Base = declarative_base()
    
    
    class TupleOkList(list):
        @collections.collection.converter
        def convert(self, thing):
            return list(thing)
    
    
    class A(Base):
        __tablename__ = 'a'
        id = Column(Integer, primary_key=True)
        bs = relationship("B", collection_class=TupleOkList)
    
    
    class B(Base):
        __tablename__ = 'b'
        id = Column(Integer, primary_key=True)
        a_id = Column(ForeignKey('a.id'))
    
    a1 = A(bs=(B(), B()))
    
  5. Robert Buchholz

    I am always amazed by the API that SQLAlchemy already has built in to make conversions like this possible. Your original point was that automatically converting strings or dicts would not be advisable, and I agree. What about just considering tuple to be list-like (in duck_type_collection)?

    We're trying to only use "()" in module scope as a best practice, and these default values sometimes end up in relations. A minimal example of that would look like this:

    def a_factory(bs=()):
        return A(bs=bs)
    
  6. Mike Bayer repo owner

    I'm looking into cleaning up this very old logic in #3457 for 1.1 and I'll see if there's some simple way to have lists/sets accept arbitrary iterables. The collection API does not make any assumptions about the collection type or what it stores, and this is probably wise, but perhaps for the base list/set an exception can be made.

  7. Mike Bayer repo owner

    #3457 will make this a little easier to customize at some point but for the moment I'm not comfortable automatically interpreting a tuple, as what cases this should or should not be done aren't clear and it's a lot safer to leave this decision up to the end user.

  8. Log in to comment