Merge pull request #250 from AndrewPashkin/introduce_generic_repr_model_mixin
Introduce generic_repr() model decorator, that adds generic __repr__() to an ORM model.
This commit is contained in:
@@ -8,3 +8,11 @@ Timestamp
|
||||
.. module:: sqlalchemy_utils.models
|
||||
|
||||
.. autoclass:: Timestamp
|
||||
|
||||
|
||||
generic_repr
|
||||
------------
|
||||
|
||||
.. module:: sqlalchemy_utils.models
|
||||
|
||||
.. autofunction:: generic_repr
|
||||
|
@@ -53,7 +53,7 @@ from .listeners import ( # noqa
|
||||
force_auto_coercion,
|
||||
force_instant_defaults
|
||||
)
|
||||
from .models import Timestamp # noqa
|
||||
from .models import generic_repr, Timestamp # noqa
|
||||
from .observer import observes # noqa
|
||||
from .primitives import Country, Currency, Ltree, WeekDay, WeekDays # noqa
|
||||
from .proxy_dict import proxy_dict, ProxyDict # noqa
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.util.langhelpers import symbol
|
||||
|
||||
|
||||
class Timestamp(object):
|
||||
@@ -31,3 +32,67 @@ def timestamp_before_update(mapper, connection, target):
|
||||
# When a model with a timestamp is updated; force update the updated
|
||||
# timestamp.
|
||||
target.updated = datetime.utcnow()
|
||||
|
||||
|
||||
NO_VALUE = symbol('NO_VALUE')
|
||||
NOT_LOADED_REPR = '<not loaded>'
|
||||
|
||||
|
||||
def _generic_repr_method(self, fields):
|
||||
state = sa.inspect(self)
|
||||
field_reprs = []
|
||||
if not fields:
|
||||
fields = state.mapper.columns.keys()
|
||||
for key in fields:
|
||||
value = state.attrs[key].loaded_value
|
||||
if value == NO_VALUE:
|
||||
value = NOT_LOADED_REPR
|
||||
else:
|
||||
value = repr(value)
|
||||
field_reprs.append('='.join((key, value)))
|
||||
|
||||
return '%s(%s)' % (self.__class__.__name__, ', '.join(field_reprs))
|
||||
|
||||
|
||||
def generic_repr(*fields):
|
||||
"""Adds generic ``__repr__()`` method to a decalrative SQLAlchemy model.
|
||||
|
||||
In case if some fields are not loaded from a database, it doesn't
|
||||
force their loading and instead repesents them as ``<not loaded>``.
|
||||
|
||||
In addition, user can provide field names as arguments to the decorator
|
||||
to specify what fields should present in the string representation
|
||||
and in what order.
|
||||
|
||||
Example::
|
||||
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy_utils import generic_repr
|
||||
|
||||
|
||||
@generic_repr
|
||||
class MyModel(Base):
|
||||
__tablename__ = 'mymodel'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.String)
|
||||
category = sa.Column(sa.String)
|
||||
|
||||
session.add(MyModel(name='Foo', category='Bar'))
|
||||
session.commit()
|
||||
foo = session.query(MyModel).options(sa.orm.defer('category')).one(s)
|
||||
|
||||
assert repr(foo) == 'MyModel(id=1, name='Foo', category=<not loaded>)'
|
||||
"""
|
||||
if len(fields) == 1 and callable(fields[0]):
|
||||
target = fields[0]
|
||||
target.__repr__ = lambda self: _generic_repr_method(self, fields=None)
|
||||
return target
|
||||
else:
|
||||
def decorator(cls):
|
||||
cls.__repr__ = lambda self: _generic_repr_method(
|
||||
self,
|
||||
fields=fields
|
||||
)
|
||||
return cls
|
||||
return decorator
|
||||
|
@@ -1,21 +1,20 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
|
||||
from sqlalchemy_utils import Timestamp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def Article(Base):
|
||||
class Article(Base, Timestamp):
|
||||
__tablename__ = 'article'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.Unicode(255), default=u'Some article')
|
||||
return Article
|
||||
from sqlalchemy_utils import generic_repr, Timestamp
|
||||
|
||||
|
||||
class TestTimestamp(object):
|
||||
@pytest.fixture
|
||||
def Article(self, Base):
|
||||
class Article(Base, Timestamp):
|
||||
__tablename__ = 'article'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.Unicode(255), default=u'Some article')
|
||||
return Article
|
||||
|
||||
def test_created(self, session, Article):
|
||||
then = datetime.utcnow()
|
||||
@@ -38,3 +37,52 @@ class TestTimestamp(object):
|
||||
session.commit()
|
||||
|
||||
assert article.updated >= then and article.updated <= datetime.utcnow()
|
||||
|
||||
|
||||
class TestGenericRepr:
|
||||
@pytest.fixture
|
||||
def Article(self, Base):
|
||||
class Article(Base):
|
||||
__tablename__ = 'article'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
name = sa.Column(sa.Unicode(255), default=u'Some article')
|
||||
return Article
|
||||
|
||||
def test_repr(self, Article):
|
||||
"""Representation of a basic model."""
|
||||
Article = generic_repr(Article)
|
||||
article = Article(id=1, name=u'Foo')
|
||||
if sys.version_info[0] == 2:
|
||||
expected_repr = u'Article(id=1, name=u\'Foo\')'
|
||||
elif sys.version_info[0] == 3:
|
||||
expected_repr = u'Article(id=1, name=\'Foo\')'
|
||||
else:
|
||||
raise AssertionError
|
||||
actual_repr = repr(article)
|
||||
|
||||
assert actual_repr == expected_repr
|
||||
|
||||
def test_repr_partial(self, Article):
|
||||
"""Representation of a basic model with selected fields."""
|
||||
Article = generic_repr('id')(Article)
|
||||
article = Article(id=1, name=u'Foo')
|
||||
expected_repr = u'Article(id=1)'
|
||||
actual_repr = repr(article)
|
||||
|
||||
assert actual_repr == expected_repr
|
||||
|
||||
def test_not_loaded(self, session, Article):
|
||||
""":py:func:`~sqlalchemy_utils.models.generic_repr` doesn't force
|
||||
execution of additional queries if some fields are not loaded and
|
||||
instead represents them as "<not loaded>".
|
||||
"""
|
||||
Article = generic_repr(Article)
|
||||
article = Article(name=u'Foo')
|
||||
session.add(article)
|
||||
session.commit()
|
||||
|
||||
article = session.query(Article).options(sa.orm.defer('name')).one()
|
||||
actual_repr = repr(article)
|
||||
|
||||
expected_repr = u'Article(id={}, name=<not loaded>)'.format(article.id)
|
||||
assert actual_repr == expected_repr
|
||||
|
Reference in New Issue
Block a user