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

风格不统一 提交于 2019-12-02 06:32:18

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.

I define this __repr__ method on my base model:

def __repr__(self):
    fmt = '{}.{}({})'
    package = self.__class__.__module__
    class_ = self.__class__.__name__
    attrs = sorted((col.name, getattr(self, col.name)) for col in self.__table__.columns)
    sattrs = ', '.join('{}={!r}'.format(*x) for x in attrs)
    return fmt.format(package, class_, sattrs)

The method displays the names of all of a table's columns (but not relationships), and the repr of their values, in alphabetical order. I don't usually define a __str__ unless I need a particular form - perhaps str(User(name='Alice')) would just be Alice - so str(model_instance) will call the __repr__ method.

Sample code

import datetime

import sqlalchemy as sa
from sqlalchemy.ext import declarative


class BaseModel(object):

    __abstract__ = True

    def __repr__(self):
        fmt = u'{}.{}({})'
        package = self.__class__.__module__
        class_ = self.__class__.__name__
        attrs = sorted((c.name, getattr(self, c.name)) for c in self.__table__.columns)
        sattrs = u', '.join('{}={!r}'.format(*x) for x in attrs)
        return fmt.format(package, class_, sattrs)


Base = declarative.declarative_base(cls=BaseModel)


class MyModel(Base):

    __tablename__ = 'mytable'

    foo = sa.Column(sa.Unicode(32))
    bar = sa.Column(sa.Integer, primary_key=True)
    baz = sa.Column(sa.DateTime)

>>> mm = models.MyModel(foo='Foo', bar=42, baz=datetime.datetime.now())
>>> mm
models.MyModel(bar=42, baz=datetime.datetime(2019, 1, 4, 7, 37, 59, 350432), foo='Foo')
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!