Search code examples
pythonormsqlalchemydeclarative

Combining __table_args__ with constraints from mixin classes in SQLAlchemy


In SQLAlchemy, I have read on how to combine the __table_args__ from different mixins when declaring a declarative.

Combining Table/Mapper Arguments from Multiple Mixins

The question I had is, the example shows how this is done at the end of the chain (final class in the MRO), but how would one achieve this say, if I had these Mixins and wanted it to occur in the MyClientMixin or Base class to avoid duplicating this code for other types of mixins?

class LaneCarrierCommitmentSummaryMixin(object):
    """ Lane Carrier Commitment Summary.

    A base class for LCCS to mixin with a client specific class.
    """

    __tablename__ = 'lane_carrier_commitment_summary'
    __table_args__ = ((UniqueConstraint(['hashmap_key', 'bow'],
                                        name='uq_lane_carrier_commitment_summary_hashmap_key_bow')),)

class MyClientMixin(object):
    """ MyClient Mixin class for providing the ETL schema. """

    __table_args__ = {'schema': 'myclient_etl'}

class MyClientLaneCarrierCommitmentSummary(LaneCarrierCommitmentSummaryMixin, DateTrackedMixin, MyClientMixin, Base):
    pass

I am struggling a bit with this concept.


Solution

  • This base class will search all mixins for __table_args__ to add, then check the current class for __local_table_args__ to add. This way, __local_table_args__ doesn't clash with the declared attr. The base classes (cls.mro()) are checked in reverse order so that mixins lower down the chain are overridden by higher mixins.

    def _process_args(cls, attr, out_args, out_kwargs):
        try:
            args = getattr(cls, attr)
        except AttributeError:
            return
    
        if isinstance(args, Mapping):  # it's a dictionary
            out_kwargs.update(args)
        else:  # it's a list
            if isinstance(args[-1], Mapping):  # it has a dictionary at the end
                out_kwargs.update(args.pop())
    
            out_args.extend(args)
    
    class Base():
        @declared_attr
        def __mapper_args__(cls):
            args = []
            kwargs = {}
    
            for mixin in reversed(cls.mro()):
                _process_args(mixin, '__mapper_args__', args, kwargs)
    
            _process_args(mixin, '__local_mapper_args__', args, kwargs)
    
            return kwargs  # mapper only takes dict
    
        @declared_attr
        def __table_args__(cls):
            args = []
            kwargs = {}
    
            for mixin in reversed(cls.mro()):
                _process_args(mixin, '__table_args__', args, kwargs)
    
            _process_args(cls, '__local_table_args__', args, kwargs)
    
            args.append(kwargs)  # [item, item, ...,  kwargs]
            return tuple(args)
    

    All your mixins should define __table_args__ as normal, but the "real" class inheriting from Base should define __local_table_args__.