EliAndrewC / sideboard

BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Sideboard SQLAlchemy models should have a sensible default str/repr #14

Closed EliAndrewC closed 10 years ago

EliAndrewC commented 10 years ago

If we have a SQLAlchemy class like

class Foo(Base):
    bar = Column(Integer())
    baz = Column(Unicode())

then it would be good if the str and repr result gave you something like

Foo(bar=5, baz=u'abc')

The main feature that we probably want is to be able to override the default fields displayed, e.g. by setting a _repr_fields class attribute.

robdennis commented 10 years ago

I thought this was in, I'll talk with you offline

On Mon, Mar 24, 2014 at 6:42 PM, Eli Courtwright notifications@github.com wrote:

If we have a SQLAlchemy class like

class Foo(Base):
    bar = Column(Integer())
    baz = Column(Unicode())

then it would be good if the str and repr result gave you something like Foo(bar=5, baz=u'abc')

The main feature that we probably want is to be able to override the default fields displayed, e.g. by setting a _repr_fields class attribute.

Reply to this email directly or view it on GitHub: https://github.com/appliedsec/sideboard/issues/14

robdennis commented 10 years ago
class Base(object):
    """
    Functionality generally useful for each Model class
    """
    id = Column(Integer, primary_key=True)

    _repr_attr_names = ()
    _additional_repr_attr_names = ()

    @classmethod
    def _get_unique_constraint_column_names(cls):
        """
        Utility function for getting and then caching the column names
        associated with all the unique constraints for a given model object.
        This assists in fetching an existing object using the value of unique
        constraints in addition to the primary key of id.
        """
        if not hasattr(cls, '_unique_constraint_attributes'):
            cls._unique_constraint_attributes = [[column.name for column in constraint.columns]
                                                    for constraint in cls.__table__.constraints
                                                    if isinstance(constraint, UniqueConstraint)]

        return cls._unique_constraint_attributes

    @classmethod
    def _get_primary_key_names(cls):
        if not hasattr(cls, '_pk_names'):
            cls._pk_names = [column.name for column in
                             cls.__table__.primary_key.columns]
        return cls._pk_names

    def __repr__(self):
        """
        useful string representation for logging. Reprs do NOT return unicode,
        since python decodes it using the default encoding:
        http://bugs.python.org/issue5876
        """
        # if no repr attr names have been set, default to the set of all
        # unique constraints. This is unordered normally, so we'll order and
        # use it here
        if not self._repr_attr_names:
            # this flattens the unique constraint list
            _unique_attrs = itertools.chain.from_iterable(self._get_unique_constraint_column_names())
            _primary_keys = self._get_primary_key_names()

            attr_names = tuple(sorted(set(itertools.chain(_unique_attrs,
                                                          _primary_keys,
                                                          self._additional_repr_attr_names))))
        else:
            attr_names = self._repr_attr_names

        if not attr_names:
            # there should be SOMETHING, so id is a fallback
            attr_names = ('id',)

        # specifically using the string interpolation operator and the repr of
        # getattr so as to avoid any "hilarious" encode errors for non-ascii
        # characters
        _kwarg_list = ' '.join('%s=%s' % (name, repr(getattr(self, name, 'undefined')))
                               for name in attr_names)
        return ('<%s %s>' % (self.__class__.__name__, _kwarg_list)).encode('utf-8')

    def to_dict(self):
        """
        Convert the model object to a dictionary, based on the
        _repr_attr_names attribute. It will recurse if the attribute is itself
        a model object.
        WARNING: it does not detect cycles.
        """
        d = dict()
        for name in self._repr_attr_names:
            value = getattr(self, name, None)
            if value is not None:
                if hasattr(value, 'to_dict'):
                    d[name] = value.to_dict()
                else:
                    d[name] = value
        return d

the tests we had for this were generally subclasses of the Base classes being initialized and then asserting the repr

robdennis commented 10 years ago

still need a specific unit test

EliAndrewC commented 10 years ago

I'm marking this as closed because it's been implemented, and while more unit tests would be nice, I don't think we need to hold this ticket open until we have them.