Search code examples
pythonsqlalchemyboilerplate

Proper autogenerate of __str__() implementation also for sqlalchemy classes?


I would like to display / print my sqlalchemy classes nice and clean.

In Is there a way to auto generate a __str__() implementation in python? the answer You can iterate instance attributes using vars, dir, ...:... helps in the case of simple classes.

When I try to apply it to a Sqlalchemy class (like the one from Introductory Tutorial of Python’s SQLAlchemy - see below), I get - apart from the member variables also the following entry as a member variable:

_sa_instance_state=<sqlalchemy.orm.state.InstanceState object at 0x000000004CEBCC0>

How can I avoid that this entry appears in the __str__ representation?

For the sake of completeness, I put the solution of the linked stackoverflow question below, too.

import os
import sys
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy import create_engine

Base = declarative_base()

class Person(Base):
    __tablename__ = 'person'
    # Here we define columns for the table person
    # Notice that each column is also a normal Python instance attribute.
    id = Column(Integer, primary_key=True)
    name = Column(String(250), nullable=False)

As mentioned, this is the solution from Is there a way to auto generate a __str__() implementation in python?:

def auto_str(cls):
    def __str__(self):
        return '%s(%s)' % (
            type(self).__name__,
            ', '.join('%s=%s' % item for item in vars(self).items())
        )
    cls.__str__ = __str__
    return cls

@auto_str
class Foo(object):
    def __init__(self, value_1, value_2):
        self.attribute_1 = value_1
         self.attribute_2 = value_2

Applied:

>>> str(Foo('bar', 'ping'))
'Foo(attribute_2=ping, attribute_1=bar)'

Solution

  • This is what I use:

    def todict(obj):
        """ Return the object's dict excluding private attributes, 
        sqlalchemy state and relationship attributes.
        """
        excl = ('_sa_adapter', '_sa_instance_state')
        return {k: v for k, v in vars(obj).items() if not k.startswith('_') and
                not any(hasattr(v, a) for a in excl)}
    
    class Base:
    
        def __repr__(self):
            params = ', '.join(f'{k}={v}' for k, v in todict(self).items())
            return f"{self.__class__.__name__}({params})"
    
    Base = declarative_base(cls=Base)
    

    Any models that inherit from Base will have the default __repr__() method defined and if I need to do something different I can just override the method on that particular class.

    It excludes the value of any private attributes denoted with a leading underscore, the SQLAlchemy instance state object, and any relationship attributes from the string. I exclude the relationship attributes as I most often don't want the repr to cause a relationship to lazy load, and where the relationship is bi-directional, including relationship attribs can cause infinite recursion.

    The result looks like: ClassName(attr=val, ...).

    --EDIT--

    The todict() func that I mention above is a helper that I often call upon to construct a dict out of a SQLA object, mostly for serialisation. I was lazily using it in this context but it isn't very efficient as it's constructing a dict (in todict()) to construct a dict (in __repr__()). I've since modified the pattern to call upon a generator:

    def keyvalgen(obj):
        """ Generate attr name/val pairs, filtering out SQLA attrs."""
        excl = ('_sa_adapter', '_sa_instance_state')
        for k, v in vars(obj).items():
            if not k.startswith('_') and not any(hasattr(v, a) for a in excl):
                yield k, v
    

    Then the base Base looks like this:

    class Base:
    
        def __repr__(self):
            params = ', '.join(f'{k}={v}' for k, v in keyvalgen(self))
            return f"{self.__class__.__name__}({params})"
    

    The todict() func leverages off of the keyvalgen() generator as well but isn't needed to construct the repr anymore.