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
|
.. module:: sqlalchemy_utils.models
|
||||||
|
|
||||||
.. autoclass:: Timestamp
|
.. autoclass:: Timestamp
|
||||||
|
|
||||||
|
|
||||||
|
generic_repr
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. module:: sqlalchemy_utils.models
|
||||||
|
|
||||||
|
.. autofunction:: generic_repr
|
||||||
|
@@ -53,7 +53,7 @@ from .listeners import ( # noqa
|
|||||||
force_auto_coercion,
|
force_auto_coercion,
|
||||||
force_instant_defaults
|
force_instant_defaults
|
||||||
)
|
)
|
||||||
from .models import Timestamp # noqa
|
from .models import generic_repr, Timestamp # noqa
|
||||||
from .observer import observes # noqa
|
from .observer import observes # noqa
|
||||||
from .primitives import Country, Currency, Ltree, WeekDay, WeekDays # noqa
|
from .primitives import Country, Currency, Ltree, WeekDay, WeekDays # noqa
|
||||||
from .proxy_dict import proxy_dict, ProxyDict # noqa
|
from .proxy_dict import proxy_dict, ProxyDict # noqa
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.util.langhelpers import symbol
|
||||||
|
|
||||||
|
|
||||||
class Timestamp(object):
|
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
|
# When a model with a timestamp is updated; force update the updated
|
||||||
# timestamp.
|
# timestamp.
|
||||||
target.updated = datetime.utcnow()
|
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
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from sqlalchemy_utils import Timestamp
|
from sqlalchemy_utils import generic_repr, 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
|
|
||||||
|
|
||||||
|
|
||||||
class TestTimestamp(object):
|
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):
|
def test_created(self, session, Article):
|
||||||
then = datetime.utcnow()
|
then = datetime.utcnow()
|
||||||
@@ -38,3 +37,52 @@ class TestTimestamp(object):
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
assert article.updated >= then and article.updated <= datetime.utcnow()
|
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