├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py ├── sqlalchemy_repr.py └── tests ├── __init__.py ├── test_readme.py └── test_sqlalchemy_repr.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' 6 | workflow_dispatch: 7 | jobs: 8 | build-sdist: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-python@v4 13 | with: 14 | python-version: '3.10' 15 | - run: python setup.py sdist --verbose 16 | - uses: actions/upload-artifact@v3 17 | with: 18 | path: dist/sqlalchemy-repr-*.tar.gz 19 | deploy: 20 | needs: 21 | - build-sdist 22 | runs-on: ubuntu-20.04 23 | steps: 24 | - uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.10' 27 | - run: pip install twine 28 | - uses: actions/download-artifact@v3 29 | - run: twine upload --verbose artifact/* 30 | env: 31 | TWINE_NON_INTERACTIVE: 1 32 | TWINE_REPOSITORY_URL: https://upload.pypi.org/legacy/ 33 | TWINE_USERNAME: __token__ 34 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-20.04 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: 10 | - '2.7' 11 | - '3.5' 12 | - '3.6' 13 | - '3.7' 14 | - '3.8' 15 | - '3.9' 16 | - '3.10' 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-python@v4 20 | with: 21 | python-version: "${{ matrix.python-version }}" 22 | - run: pip install . 23 | - run: python -m unittest discover --verbose 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/7751c25c6662ce6f9dc50f014e37156298ccf065/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | 93 | 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ryosuke Ito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst 2 | recursive-include tests *.py 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | sqlalchemy-repr 2 | =============== 3 | 4 | .. image:: https://travis-ci.org/manicmaniac/sqlalchemy-repr.svg?branch=master 5 | :target: https://travis-ci.org/manicmaniac/sqlalchemy-repr 6 | 7 | Automatically generates pretty ``repr`` of a SQLAlchemy model. 8 | 9 | Install 10 | ------- 11 | 12 | .. code:: sh 13 | 14 | pip install sqlalchemy-repr 15 | 16 | 17 | Usage 18 | ----- 19 | 20 | .. code:: python 21 | 22 | from sqlalchemy.ext.declarative import declarative_base 23 | from sqlalchemy_repr import RepresentableBase 24 | 25 | Base = declarative_base(cls=RepresentableBase) 26 | 27 | 28 | Example 29 | ------- 30 | 31 | ``sqlalchemy_repr.RepresentableBase`` is mixin to add simple representation of columns. 32 | 33 | .. code:: python 34 | 35 | >>> from datetime import datetime 36 | 37 | >>> from sqlalchemy import Column, DateTime, Integer, Unicode, create_engine 38 | >>> from sqlalchemy.ext.declarative import declarative_base 39 | >>> from sqlalchemy.orm import sessionmaker 40 | >>> from sqlalchemy_repr import RepresentableBase 41 | 42 | >>> Base = declarative_base(cls=RepresentableBase) 43 | 44 | >>> class User(Base): 45 | ... __tablename__ = 'users' 46 | ... id = Column(Integer, primary_key=True) 47 | ... name = Column(Unicode(255), nullable=False, unique=True) 48 | ... created = Column(DateTime, nullable=False) 49 | 50 | >>> engine = create_engine('sqlite://') 51 | >>> Base.metadata.create_all(engine) 52 | 53 | >>> Session = sessionmaker(bind=engine) 54 | >>> session = Session() 55 | 56 | >>> user = User(name='spam', created=datetime(2016, 6, 1)) 57 | >>> session.add(user) 58 | >>> session.commit() 59 | 60 | >>> print(user) 61 | 62 | 63 | ``sqlalchemy_repr.PrettyRepresentableBase`` brings pretty, indented multi-line representation. 64 | 65 | .. code:: python 66 | 67 | >>> from sqlalchemy_repr import PrettyRepresentableBase 68 | >>> Base = declarative_base(cls=PrettyRepresentableBase) 69 | 70 | >>> class User(Base): 71 | ... __tablename__ = 'users' 72 | ... id = Column(Integer, primary_key=True) 73 | ... first_name = Column(Unicode(255), nullable=False, unique=True) 74 | ... last_name = Column(Unicode(255), nullable=False, unique=True) 75 | ... email = Column(Unicode(255), nullable=False) 76 | ... created = Column(DateTime, nullable=False) 77 | ... modified = Column(DateTime, nullable=False) 78 | 79 | >>> engine = create_engine('sqlite://') 80 | >>> Base.metadata.create_all(engine) 81 | 82 | >>> Session = sessionmaker(bind=engine) 83 | >>> session = Session() 84 | 85 | >>> user = User(first_name='spam', last_name='ham', email='spam@example.com', created=datetime(2016, 6, 1), modified=datetime(2016, 6, 1)) 86 | >>> session.add(user) 87 | >>> session.commit() 88 | 89 | >>> print(user) 90 | 97 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='sqlalchemy-repr', 5 | version='0.1.0', 6 | description='Automatically generates pretty repr of a SQLAlchemy model.', 7 | long_description=open('README.rst').read(), 8 | keywords='pprint pretty print repr sqlalchemy extension', 9 | author='Ryosuke Ito', 10 | author_email='rito.0305@gmail.com', 11 | license="MIT", 12 | url='https://github.com/manicmaniac/sqlalchemy-repr', 13 | classifiers=[ 14 | 'Development Status :: 4 - Beta', 15 | 'Operating System :: OS Independent', 16 | 'Programming Language :: Python', 17 | 'Programming Language :: Python :: 2', 18 | 'Programming Language :: Python :: 2.7', 19 | 'Programming Language :: Python :: 3', 20 | 'Programming Language :: Python :: 3.5', 21 | 'Programming Language :: Python :: 3.6', 22 | 'Programming Language :: Python :: 3.7', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'Programming Language :: Python :: 3.10', 26 | 'Topic :: Database :: Front-Ends', 27 | ], 28 | py_modules=['sqlalchemy_repr'], 29 | install_requires=['SQLAlchemy'], 30 | test_suite='tests', 31 | ) 32 | -------------------------------------------------------------------------------- /sqlalchemy_repr.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import io 4 | import sys 5 | 6 | from sqlalchemy import inspect 7 | from sqlalchemy.ext.declarative import DeclarativeMeta 8 | 9 | try: 10 | from reprlib import Repr as _Repr 11 | except ImportError: 12 | from repr import Repr as _Repr 13 | 14 | __all__ = ['Repr', 'PrettyRepr', 'RepresentableBase', 15 | 'PrettyRepresentableBase'] 16 | 17 | 18 | class Repr(_Repr): 19 | def repr(self, obj): 20 | if isinstance(obj.__class__, DeclarativeMeta): 21 | return self.repr_Base(obj, self.maxlevel) 22 | if sys.version_info < (3,): 23 | return _Repr.repr(self, obj) 24 | else: 25 | return super(Repr, self).repr(obj) 26 | 27 | def repr_Base(self, obj, level): 28 | return '<%s %s>' % (self._repr_class(obj, level), 29 | self._repr_attrs(obj, level)) 30 | 31 | def _repr_class(self, obj, level): 32 | return obj.__class__.__name__ 33 | 34 | def _repr_attrs(self, obj, level): 35 | represented_attrs = [] 36 | for attr in self._iter_attrs(obj): 37 | represented_attr = self._repr_attr(attr, level) 38 | represented_attrs.append(represented_attr) 39 | return ', '.join(represented_attrs) 40 | 41 | def _repr_attr(self, obj, level): 42 | attr_name, attr_value = obj 43 | if hasattr(attr_value, 'isoformat'): 44 | return '%s=%r' % (attr_name, attr_value.isoformat()) 45 | return '%s=%r' % (attr_name, attr_value) 46 | 47 | def _iter_attrs(self, obj): 48 | blacklist = set(getattr(obj, '__repr_blacklist__', set())) 49 | whitelist = set(getattr(obj, '__repr_whitelist__', set())) 50 | 51 | attr_names = inspect(obj.__class__).columns.keys() 52 | for attr_name in attr_names: 53 | if attr_name in blacklist: 54 | continue 55 | 56 | if whitelist and attr_name not in whitelist: 57 | continue 58 | 59 | yield (attr_name, getattr(obj, attr_name)) 60 | 61 | 62 | class PrettyRepr(Repr): 63 | def __init__(self, *args, **kwargs): 64 | indent = kwargs.pop('indent', None) 65 | if indent is None: 66 | self.indent = ' ' * 4 67 | else: 68 | self.indent = indent 69 | if sys.version_info < (3,): 70 | Repr.__init__(self, *args, **kwargs) 71 | else: 72 | super(PrettyRepr, self).__init__(*args, **kwargs) 73 | 74 | def repr_Base(self, obj, level): 75 | output = io.StringIO() 76 | output.write('<%s' % self._repr_class(obj, level)) 77 | is_first_attr = True 78 | for attr in self._iter_attrs(obj): 79 | if not is_first_attr: 80 | output.write(',') 81 | is_first_attr = False 82 | represented_attr = self._repr_attr(attr, level) 83 | output.write('\n' + self.indent + represented_attr) 84 | output.write('>') 85 | return output.getvalue() 86 | 87 | 88 | _shared_repr = Repr() 89 | _shared_pretty_repr = PrettyRepr() 90 | 91 | 92 | class RepresentableBase(object): 93 | def __repr__(self): 94 | return _shared_repr.repr(self) 95 | 96 | 97 | class PrettyRepresentableBase(object): 98 | def __repr__(self): 99 | return _shared_pretty_repr.repr(self) 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manicmaniac/sqlalchemy-repr/994c616107e466562448ae94c89313574835bb33/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_readme.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import sys 3 | 4 | 5 | if sys.version_info >= (3,): 6 | def load_tests(loader, tests, ignore): 7 | return doctest.DocFileSuite('../README.rst') 8 | -------------------------------------------------------------------------------- /tests/test_sqlalchemy_repr.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import re 3 | from textwrap import dedent 4 | import unittest 5 | 6 | from sqlalchemy import (Column, DateTime, ForeignKey, Integer, Unicode, 7 | UnicodeText, create_engine) 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import sessionmaker 10 | 11 | from sqlalchemy_repr import Repr, PrettyRepr 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class User(Base): 17 | __tablename__ = 'users' 18 | id = Column(Integer, primary_key=True) 19 | name = Column(Unicode(255), nullable=False, unique=True) 20 | created = Column(DateTime, nullable=False) 21 | 22 | 23 | class Entry(Base): 24 | __tablename__ = 'entries' 25 | id = Column(Integer, primary_key=True) 26 | title = Column(Unicode(255), nullable=False) 27 | text = Column(UnicodeText, nullable=False, default='') 28 | user_id = Column(Integer, ForeignKey('users.id'), nullable=False) 29 | 30 | 31 | class EntryWithBlacklistAndWhitelist(Base): 32 | __tablename__ = 'entries_with_blacklist' 33 | id = Column(Integer, primary_key=True) 34 | title = Column(Unicode(255), nullable=False) 35 | text = Column(UnicodeText, nullable=False, default='') 36 | user_id = Column(Integer, ForeignKey('users.id'), nullable=False) 37 | __repr_blacklist__ = ('text',) 38 | __repr_whitelist__ = ('text', 'title') 39 | 40 | 41 | class TestRepr(unittest.TestCase): 42 | def setUp(self): 43 | engine = create_engine('sqlite://') 44 | Base.metadata.create_all(engine) 45 | Session = sessionmaker(bind=engine) 46 | 47 | self._date = datetime.now() 48 | self._date_str = self._date.isoformat() 49 | 50 | self.session = Session() 51 | self.entry = Entry(title='ham', text=self.dummy_text, user_id=1) 52 | self.blacklist_entry = EntryWithBlacklistAndWhitelist(title='ham', text=self.dummy_text, user_id=1) 53 | self.user = User(name='spam', created=self._date) 54 | self.session.add(self.user) 55 | self.session.add(self.entry) 56 | self.session.add(self.blacklist_entry) 57 | self.session.commit() 58 | 59 | def test_repr_with_user(self): 60 | result = Repr().repr(self.user) 61 | pattern = r"".format( 78 | self._date_str 79 | ) 80 | self.assertMatch(result, pattern) 81 | 82 | def test_pretty_repr_with_entry(self): 83 | result = PrettyRepr().repr(self.entry) 84 | pattern = r"" 85 | self.assertMatch(result, pattern) 86 | 87 | def test_pretty_repr_with_blacklist_and_whitelist(self): 88 | result = PrettyRepr().repr(self.blacklist_entry) 89 | pattern = r"" 90 | self.assertMatch(result, pattern) 91 | 92 | def assertMatch(self, string, pattern): 93 | if not re.match(pattern, string): 94 | message = "%r doesn't match %r" % (string, pattern) 95 | raise AssertionError(message) 96 | 97 | dummy_text = dedent("""\ 98 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, 99 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 100 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 101 | ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit 102 | in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 103 | Excepteur sint occaecat cupidatat non proident, 104 | sunt in culpa qui officia deserunt mollit anim id est laborum. 105 | """) 106 | 107 | 108 | if __name__ == '__main__': 109 | unittest.main() 110 | --------------------------------------------------------------------------------