diff -Nru flask-sqlalchemy-2.0/CHANGES flask-sqlalchemy-2.1/CHANGES --- flask-sqlalchemy-2.0/CHANGES 2014-08-29 04:58:24.000000000 +0000 +++ flask-sqlalchemy-2.1/CHANGES 2015-10-23 09:49:12.000000000 +0000 @@ -4,6 +4,21 @@ Here you can see the full list of changes between each Flask-SQLAlchemy release. +Version 3.0 +----------- + +In development, codename Dubnium + +Version 2.1 +----------- + +Released on October 23rd 2015, codename Caesium + +- Table names are automatically generated in more cases, including + subclassing mixins and abstract models. +- Allow using a custom MetaData object. +- Add support for binds parameter to session. + Version 2.0 ----------- diff -Nru flask-sqlalchemy-2.0/debian/changelog flask-sqlalchemy-2.1/debian/changelog --- flask-sqlalchemy-2.0/debian/changelog 2017-06-27 15:06:10.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/changelog 2017-06-27 15:57:20.000000000 +0000 @@ -1,11 +1,11 @@ -flask-sqlalchemy (2.0-1xenial1) xenial; urgency=medium +flask-sqlalchemy (2.1-1xenial1) xenial; urgency=medium * built for privacyidea - -- Cornelius Kölbel Tue, 27 Jun 2017 17:06:10 +0200 + -- Cornelius Kölbel Tue, 27 Jun 2017 17:57:20 +0200 -flask-sqlalchemy (2.0-1) unstable; urgency=low +flask-sqlalchemy (2.1-1) unstable; urgency=low * source package automatically created by stdeb 0.8.5 - -- Cornelius Kölbel Tue, 27 Jun 2017 17:06:08 +0200 + -- Cornelius Kölbel Tue, 27 Jun 2017 17:57:18 +0200 diff -Nru flask-sqlalchemy-2.0/debian/compat flask-sqlalchemy-2.1/debian/compat --- flask-sqlalchemy-2.0/debian/compat 2017-06-27 15:06:08.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/compat 2017-06-27 15:57:18.000000000 +0000 @@ -1 +1 @@ -9 +7 diff -Nru flask-sqlalchemy-2.0/debian/control flask-sqlalchemy-2.1/debian/control --- flask-sqlalchemy-2.0/debian/control 2017-06-27 15:06:08.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/control 2017-06-27 15:57:18.000000000 +0000 @@ -2,9 +2,10 @@ Maintainer: Cornelius Kölbel Section: python Priority: optional -Build-Depends: dh-python, python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 9) -Standards-Version: 3.9.6 -Homepage: http://github.com/mitsuhiko/flask-sqlalchemy +Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7) +Standards-Version: 3.9.1 + + Package: python-flask-sqlalchemy Architecture: all @@ -25,3 +26,5 @@ . . + + diff -Nru flask-sqlalchemy-2.0/debian/rules flask-sqlalchemy-2.1/debian/rules --- flask-sqlalchemy-2.0/debian/rules 2017-06-27 15:06:08.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/rules 2017-06-27 15:57:18.000000000 +0000 @@ -1,8 +1,31 @@ #!/usr/bin/make -f # This file was automatically generated by stdeb 0.8.5 at -# Tue, 27 Jun 2017 17:06:08 +0200 -export PYBUILD_NAME=flask-sqlalchemy +# Tue, 27 Jun 2017 17:57:18 +0200 + %: - dh $@ --with python2 --buildsystem=pybuild + dh $@ --with python2 --buildsystem=python_distutils + + +override_dh_auto_clean: + python setup.py clean -a + find . -name \*.pyc -exec rm {} \; + + + +override_dh_auto_build: + python setup.py build --force + + + +override_dh_auto_install: + python setup.py install --force --root=debian/python-flask-sqlalchemy --no-compile -O0 --install-layout=deb --prefix=/usr + + + +override_dh_python2: + dh_python2 --no-guessing-versions + + + diff -Nru flask-sqlalchemy-2.0/debian/source/options flask-sqlalchemy-2.1/debian/source/options --- flask-sqlalchemy-2.0/debian/source/options 1970-01-01 00:00:00.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/source/options 2017-06-27 15:57:18.000000000 +0000 @@ -0,0 +1 @@ +extend-diff-ignore="\.egg-info$" \ No newline at end of file diff -Nru flask-sqlalchemy-2.0/debian/watch flask-sqlalchemy-2.1/debian/watch --- flask-sqlalchemy-2.0/debian/watch 2017-06-27 15:06:08.000000000 +0000 +++ flask-sqlalchemy-2.1/debian/watch 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ -# please also check http://pypi.debian.net/Flask-SQLAlchemy/watch -version=3 -opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -http://pypi.debian.net/Flask-SQLAlchemy/Flask-SQLAlchemy-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) \ No newline at end of file diff -Nru flask-sqlalchemy-2.0/docs/api.rst flask-sqlalchemy-2.1/docs/api.rst --- flask-sqlalchemy-2.0/docs/api.rst 2014-08-23 15:40:48.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/api.rst 2015-07-28 19:15:32.000000000 +0000 @@ -27,6 +27,13 @@ Optionally declares the bind to use. `None` refers to the default bind. For more information see :ref:`binds`. + .. attribute:: __tablename__ + + The name of the table in the database. This is required by SQLAlchemy; + however, Flask-SQLAlchemy will set it automatically if a model has a + primary key defined. If the ``__table__`` or ``__tablename__`` is set + explicitly, that will be used instead. + .. autoclass:: BaseQuery :members: get, get_or_404, paginate, first_or_404 @@ -54,7 +61,7 @@ Return the first result of this query or `None` if the result doesn’t contain any rows. This results in an execution of the underlying query. - + Sessions ```````` diff -Nru flask-sqlalchemy-2.0/docs/config.rst flask-sqlalchemy-2.1/docs/config.rst --- flask-sqlalchemy-2.0/docs/config.rst 2014-08-27 17:13:28.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/config.rst 2015-07-28 19:15:32.000000000 +0000 @@ -35,7 +35,7 @@ unicode support. This is required for some database adapters (like PostgreSQL on some Ubuntu versions) when used with - inproper database defaults that specify + improper database defaults that specify encoding-less databases. ``SQLALCHEMY_POOL_SIZE`` The size of the database pool. Defaults to the engine's default (usually 5) @@ -53,11 +53,13 @@ its maximum size. When those additional connections are returned to the pool, they are disconnected and discarded. -``SQLALCHEMY_TRACK_MODIFICATIONS`` If set to `True` (the default) - Flask-SQLAlchemy will track - modifications of objects and emit - signals. This requires extra memory - and can be disabled if not needed. +``SQLALCHEMY_TRACK_MODIFICATIONS`` If set to ``True``, Flask-SQLAlchemy will + track modifications of objects and emit + signals. The default is ``None``, which + enables tracking but issues a warning + that it will be disabled by default in + the future. This requires extra memory + and should be disabled if not needed. ================================== ========================================= .. versionadded:: 0.8 @@ -73,6 +75,8 @@ .. versionadded:: 2.0 The ``SQLALCHEMY_TRACK_MODIFICATIONS`` configuration key was added. +.. versionchanged:: 2.1 + ``SQLALCHEMY_TRACK_MODIFICATIONS`` will warn if unset. Connection URI Format --------------------- @@ -107,3 +111,37 @@ SQLite (note the four leading slashes):: sqlite:////absolute/path/to/foo.db + +Using custom MetaData and naming conventions +-------------------------------------------- + +You can optionally construct the :class:`SQLAlchemy` object with a custom +:class:`~sqlalchemy.schema.MetaData` object. +This allows you to, among other things, +specify a `custom constraint naming convention +`_. +Doing so is important for dealing with database migrations (for instance using +`alembic `_ as stated +`here `_. Since SQL +defines no standard naming conventions, there is no guaranteed nor effective +compatibility by default among database implementations. You can define a +custom naming convention like this as suggested by the SQLAlchemy docs:: + + from sqlalchemy import MetaData + from flask import Flask + from flask.ext.sqlalchemy import SQLAlchemy + + convention = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + } + + metadata = MetaData(naming_convention=convention) + db = SQLAlchemy(app, metadata=metadata) + +For more info about :class:`~sqlalchemy.schema.MetaData`, +`check out the official docs on it +`_. diff -Nru flask-sqlalchemy-2.0/docs/conf.py flask-sqlalchemy-2.1/docs/conf.py --- flask-sqlalchemy-2.0/docs/conf.py 2014-08-23 15:41:35.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/conf.py 2015-07-28 19:15:32.000000000 +0000 @@ -51,10 +51,10 @@ try: release = pkg_resources.get_distribution('Flask-SQLAlchemy').version except pkg_resources.DistributionNotFound: - print 'To build the documentation, The distribution information of' - print 'Flask-SQLAlchemy has to be available. Either install the package' - print 'into your development environment or run "setup.py develop"' - print 'to setup the metadata. A virtualenv is recommended!' + print('To build the documentation, The distribution information of') + print('Flask-SQLAlchemy has to be available. Either install the package') + print('into your development environment or run "setup.py develop"') + print('to setup the metadata. A virtualenv is recommended!') sys.exit(1) del pkg_resources @@ -232,13 +232,13 @@ # fall back if theme is not there try: __import__('flask_theme_support') -except ImportError, e: - print '-' * 74 - print 'Warning: Flask themes unavailable. Building with default theme' - print 'If you want the Flask themes, run this command and build again:' - print - print ' git submodule update --init' - print '-' * 74 +except ImportError as e: + print('-' * 74) + print('Warning: Flask themes unavailable. Building with default theme') + print('If you want the Flask themes, run this command and build again:') + print() + print(' git submodule update --init') + print('-' * 74) pygments_style = 'tango' html_theme = 'default' diff -Nru flask-sqlalchemy-2.0/docs/contexts.rst flask-sqlalchemy-2.1/docs/contexts.rst --- flask-sqlalchemy-2.0/docs/contexts.rst 2014-08-23 15:40:48.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/contexts.rst 2015-07-28 19:15:32.000000000 +0000 @@ -16,7 +16,7 @@ is the :meth:`~SQLAlchemy.init_app` function:: from flask import Flask - from flask.ext.sqlalchemy import SQLAlchemy + from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() diff -Nru flask-sqlalchemy-2.0/docs/quickstart.rst flask-sqlalchemy-2.1/docs/quickstart.rst --- flask-sqlalchemy-2.0/docs/quickstart.rst 2014-08-27 14:56:03.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/quickstart.rst 2015-07-28 19:15:32.000000000 +0000 @@ -6,7 +6,7 @@ .. currentmodule:: flask.ext.sqlalchemy Flask-SQLAlchemy is fun to use, incredibly easy for basic applications, and -readily extends for larger applications. For the complete guide, checkout out +readily extends for larger applications. For the complete guide, checkout the API documentation on the :class:`SQLAlchemy` class. A Minimal Application @@ -22,7 +22,7 @@ used to declare models:: from flask import Flask - from flask.ext.sqlalchemy import SQLAlchemy + from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' @@ -41,7 +41,7 @@ def __repr__(self): return '' % self.username -To create the initial database, just import the `db` object from a +To create the initial database, just import the `db` object from an interactive Python shell and run the :meth:`SQLAlchemy.create_all` method to create the tables and database: diff -Nru flask-sqlalchemy-2.0/docs/signals.rst flask-sqlalchemy-2.1/docs/signals.rst --- flask-sqlalchemy-2.0/docs/signals.rst 2014-08-23 15:40:48.000000000 +0000 +++ flask-sqlalchemy-2.1/docs/signals.rst 2015-07-28 19:15:32.000000000 +0000 @@ -1,27 +1,24 @@ Signalling Support ================== +Connect to the following signals to get notified before and after changes are committed to the database. +These changes are only tracked if ``SQLALCHEMY_TRACK_MODIFICATIONS`` is enabled in the config. + .. versionadded:: 0.10 +.. versionchanged:: 2.1 + ``before_models_committed`` is triggered correctly. +.. deprecated:: 2.1 + This will be disabled by default in a future version. -Starting with Flask-SQLAlchemy 0.10 you can now connect to signals to get -notifications when certain things happen. +.. data:: models_committed -The following two signals exist: + This signal is sent when changed models were committed to the database. -.. data:: models_committed + The sender is the application that emitted the changes. + The receiver is passed the ``changes`` parameter with a list of tuples in the form ``(model instance, operation)``. - This signal is sent when changed models where committed to the - database. The sender is the application that emitted the changes - and the models and an operation identifier are passed as list of tuples - in the form ``(model, operation)`` to the receiver in the `changes` - parameter. - - The model is the instance of the model that was sent to the database - and the operation is ``'insert'`` when a model was inserted, - ``'delete'`` when the model was deleted and ``'update'`` if any - of the columns where updated. + The operation is one of ``'insert'``, ``'update'``, and ``'delete'``. .. data:: before_models_committed - Works exactly the same as :data:`models_committed` but is emitted - right before the committing takes place. + This signal works exactly like :data:`models_committed` but is emitted before the commit takes place. diff -Nru flask-sqlalchemy-2.0/flask_sqlalchemy/__init__.py flask-sqlalchemy-2.1/flask_sqlalchemy/__init__.py --- flask-sqlalchemy-2.0/flask_sqlalchemy/__init__.py 2014-08-29 05:15:38.000000000 +0000 +++ flask-sqlalchemy-2.1/flask_sqlalchemy/__init__.py 2015-10-23 09:49:49.000000000 +0000 @@ -14,20 +14,20 @@ import sys import time import functools +import warnings import sqlalchemy from math import ceil from functools import partial -from flask import _request_ctx_stack, abort +from flask import _request_ctx_stack, abort, has_request_context, request from flask.signals import Namespace from operator import itemgetter from threading import Lock -from sqlalchemy import orm, event +from sqlalchemy import orm, event, inspect from sqlalchemy.orm.exc import UnmappedClassError from sqlalchemy.orm.session import Session as SessionBase -from sqlalchemy.event import listen from sqlalchemy.engine.url import make_url from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta -from flask.ext.sqlalchemy._compat import iteritems, itervalues, xrange, \ +from flask_sqlalchemy._compat import iteritems, itervalues, xrange, \ string_types # the best timer function for the platform @@ -42,7 +42,7 @@ _app_ctx_stack = None -__version__ = '2.0' +__version__ = '2.1' # Which stack should we use? _app_ctx_stack is new in 0.9 @@ -142,24 +142,28 @@ :meth:`SQLAlchemy.create_session` function. .. versionadded:: 2.0 + + .. versionadded:: 2.1 + The `binds` option was added, which allows a session to be joined + to an external transaction. """ - def __init__(self, db, autocommit=False, autoflush=True, **options): + def __init__(self, db, autocommit=False, autoflush=True, app=None, **options): #: The application that this session belongs to. - self.app = db.get_app() - self._model_changes = {} - #: A flag that controls whether this session should keep track of - #: model modifications. The default value for this attribute - #: is set from the ``SQLALCHEMY_TRACK_MODIFICATIONS`` config - #: key. - self.emit_modification_signals = \ - self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] + self.app = app = db.get_app() + track_modifications = app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] bind = options.pop('bind', None) or db.engine - SessionBase.__init__(self, autocommit=autocommit, autoflush=autoflush, - bind=bind, - binds=db.get_binds(self.app), **options) + binds = options.pop('binds', None) or db.get_binds(app) + + if track_modifications is None or track_modifications: + _SessionSignalEvents.register(self) + + SessionBase.__init__( + self, autocommit=autocommit, autoflush=autoflush, + bind=bind, binds=binds, **options + ) - def get_bind(self, mapper, clause=None): + def get_bind(self, mapper=None, clause=None): # mapper is None if someone tries to just get a connection if mapper is not None: info = getattr(mapper.mapped_table, 'info', {}) @@ -171,62 +175,70 @@ class _SessionSignalEvents(object): + @classmethod + def register(cls, session): + if not hasattr(session, '_model_changes'): + session._model_changes = {} + + event.listen(session, 'before_flush', cls.record_ops) + event.listen(session, 'before_commit', cls.record_ops) + event.listen(session, 'before_commit', cls.before_commit) + event.listen(session, 'after_commit', cls.after_commit) + event.listen(session, 'after_rollback', cls.after_rollback) + + @classmethod + def unregister(cls, session): + if hasattr(session, '_model_changes'): + del session._model_changes + + event.remove(session, 'before_flush', cls.record_ops) + event.remove(session, 'before_commit', cls.record_ops) + event.remove(session, 'before_commit', cls.before_commit) + event.remove(session, 'after_commit', cls.after_commit) + event.remove(session, 'after_rollback', cls.after_rollback) - def register(self): - listen(SessionBase, 'before_commit', self.session_signal_before_commit) - listen(SessionBase, 'after_commit', self.session_signal_after_commit) - listen(SessionBase, 'after_rollback', self.session_signal_after_rollback) + @staticmethod + def record_ops(session, flush_context=None, instances=None): + try: + d = session._model_changes + except AttributeError: + return + + for targets, operation in ((session.new, 'insert'), (session.dirty, 'update'), (session.deleted, 'delete')): + for target in targets: + state = inspect(target) + key = state.identity_key if state.has_identity else id(target) + d[key] = (target, operation) @staticmethod - def session_signal_before_commit(session): - if not isinstance(session, SignallingSession): + def before_commit(session): + try: + d = session._model_changes + except AttributeError: return - d = session._model_changes + if d: - before_models_committed.send(session.app, changes=d.values()) + before_models_committed.send(session.app, changes=list(d.values())) @staticmethod - def session_signal_after_commit(session): - if not isinstance(session, SignallingSession): + def after_commit(session): + try: + d = session._model_changes + except AttributeError: return - d = session._model_changes + if d: models_committed.send(session.app, changes=list(d.values())) d.clear() @staticmethod - def session_signal_after_rollback(session): - if not isinstance(session, SignallingSession): + def after_rollback(session): + try: + d = session._model_changes + except AttributeError: return - session._model_changes.clear() - - -class _MapperSignalEvents(object): - - def __init__(self, mapper): - self.mapper = mapper - - def register(self): - listen(self.mapper, 'after_delete', self.mapper_signal_after_delete) - listen(self.mapper, 'after_insert', self.mapper_signal_after_insert) - listen(self.mapper, 'after_update', self.mapper_signal_after_update) - - def mapper_signal_after_delete(self, mapper, connection, target): - self._record(mapper, target, 'delete') - - def mapper_signal_after_insert(self, mapper, connection, target): - self._record(mapper, target, 'insert') - - def mapper_signal_after_update(self, mapper, connection, target): - self._record(mapper, target, 'update') - - @staticmethod - def _record(mapper, target, operation): - s = orm.object_session(target) - if isinstance(s, SignallingSession) and s.emit_modification_signals: - pk = tuple(mapper.primary_key_from_instance(target)) - s._model_changes[pk] = (target, operation) + d.clear() class _EngineDebuggingSignalEvents(object): @@ -237,8 +249,8 @@ self.app_package = import_name def register(self): - listen(self.engine, 'before_cursor_execute', self.before_cursor_execute) - listen(self.engine, 'after_cursor_execute', self.after_cursor_execute) + event.listen(self.engine, 'before_cursor_execute', self.before_cursor_execute) + event.listen(self.engine, 'after_cursor_execute', self.after_cursor_execute) def before_cursor_execute(self, conn, cursor, statement, parameters, context, executemany): @@ -419,16 +431,50 @@ abort(404) return rv - def paginate(self, page, per_page=20, error_out=True): + def paginate(self, page=None, per_page=None, error_out=True): """Returns `per_page` items from page `page`. By default it will abort with 404 if no items were found and the page was larger than 1. This behavor can be disabled by setting `error_out` to `False`. + If page or per_page are None, they will be retrieved from the + request query. If the values are not ints and ``error_out`` is + true, it will abort with 404. If there is no request or they + aren't in the query, they default to page 1 and 20 + respectively. + Returns an :class:`Pagination` object. """ + + if has_request_context(): + if page is None: + try: + page = int(request.args.get('page', 1)) + except (TypeError, ValueError): + if error_out: + abort(404) + + page = 1 + + if per_page is None: + try: + per_page = int(request.args.get('per_page', 20)) + except (TypeError, ValueError): + if error_out: + abort(404) + + per_page = 20 + else: + if page is None: + page = 1 + + if per_page is None: + per_page = 20 + if error_out and page < 1: abort(404) + items = self.limit(per_page).offset((page - 1) * per_page).all() + if not items and page != 1 and error_out: abort(404) @@ -504,24 +550,51 @@ return rv -def _defines_primary_key(d): - """Figures out if the given dictionary defines a primary key column.""" - return any(v.primary_key for k, v in iteritems(d) - if isinstance(v, sqlalchemy.Column)) +def _should_set_tablename(bases, d): + """Check what values are set by a class and its bases to determine if a + tablename should be automatically generated. + + The class and its bases are checked in order of precedence: the class + itself then each base in the order they were given at class definition. + + Abstract classes do not generate a tablename, although they may have set + or inherited a tablename elsewhere. + + If a class defines a tablename or table, a new one will not be generated. + Otherwise, if the class defines a primary key, a new name will be generated. + + This supports: + + * Joined table inheritance without explicitly naming sub-models. + * Single table inheritance. + * Inheriting from mixins or abstract models. + + :param bases: base classes of new class + :param d: new class dict + :return: True if tablename should be set + """ + + if '__tablename__' in d or '__table__' in d or '__abstract__' in d: + return False + + if any(v.primary_key for v in itervalues(d) if isinstance(v, sqlalchemy.Column)): + return True + + for base in bases: + if hasattr(base, '__tablename__') or hasattr(base, '__table__'): + return False + + for name in dir(base): + attr = getattr(base, name) + + if isinstance(attr, sqlalchemy.Column) and attr.primary_key: + return True class _BoundDeclarativeMeta(DeclarativeMeta): def __new__(cls, name, bases, d): - tablename = d.get('__tablename__') - - # generate a table name automatically if it's missing and the - # class dictionary declares a primary key. We cannot always - # attach a primary key to support model inheritance that does - # not use joins. We also don't want a table name if a whole - # table is defined - if not tablename and d.get('__table__') is None and \ - _defines_primary_key(d): + if _should_set_tablename(bases, d): def _join(match): word = match.group() if len(word) > 1: @@ -573,7 +646,7 @@ object it is usable right away or will attach as needed to a Flask application. - There are two usage modes which work very similar. One is binding + There are two usage modes which work very similarly. One is binding the instance to a very specific Flask application:: app = Flask(__name__) @@ -651,34 +724,28 @@ .. versionadded:: 0.16 `scopefunc` is now accepted on `session_options`. It allows specifying a custom function which will define the SQLAlchemy session's scoping. + + .. versionadded:: 2.1 + The `metadata` parameter was added. This allows for setting custom + naming conventions among other, non-trivial things. """ - def __init__(self, app=None, - use_native_unicode=True, - session_options=None): - self.use_native_unicode = use_native_unicode + def __init__(self, app=None, use_native_unicode=True, session_options=None, metadata=None): if session_options is None: session_options = {} - session_options.setdefault( - 'scopefunc', connection_stack.__ident_func__ - ) - + session_options.setdefault('scopefunc', connection_stack.__ident_func__) + self.use_native_unicode = use_native_unicode self.session = self.create_scoped_session(session_options) - self.Model = self.make_declarative_base() + self.Model = self.make_declarative_base(metadata) + self.Query = BaseQuery self._engine_lock = Lock() + self.app = app + _include_sqlalchemy(self) if app is not None: - self.app = app self.init_app(app) - else: - self.app = None - - _include_sqlalchemy(self) - _MapperSignalEvents(self.mapper).register() - _SessionSignalEvents().register() - self.Query = BaseQuery @property def metadata(self): @@ -703,9 +770,10 @@ """ return SignallingSession(self, **options) - def make_declarative_base(self): + def make_declarative_base(self, metadata=None): """Creates the declarative base.""" base = declarative_base(cls=Model, name='Model', + metadata=metadata, metaclass=_BoundDeclarativeMeta) base.query = _QueryProperty(self) return base @@ -726,7 +794,10 @@ app.config.setdefault('SQLALCHEMY_POOL_RECYCLE', None) app.config.setdefault('SQLALCHEMY_MAX_OVERFLOW', None) app.config.setdefault('SQLALCHEMY_COMMIT_ON_TEARDOWN', False) - app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', True) + track_modifications = app.config.setdefault('SQLALCHEMY_TRACK_MODIFICATIONS', None) + + if track_modifications is None: + warnings.warn('SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True to suppress this warning.') if not hasattr(app, 'extensions'): app.extensions = {} @@ -784,6 +855,12 @@ # which is fail. Let the user know that if info.database in (None, '', ':memory:'): detected_in_memory = True + from sqlalchemy.pool import StaticPool + options['poolclass'] = StaticPool + if 'connect_args' not in options: + options['connect_args'] = {} + options['connect_args']['check_same_thread'] = False + if pool_size == 0: raise RuntimeError('SQLite in memory database with an ' 'empty queue not possible due to data ' diff -Nru flask-sqlalchemy-2.0/Flask_SQLAlchemy.egg-info/PKG-INFO flask-sqlalchemy-2.1/Flask_SQLAlchemy.egg-info/PKG-INFO --- flask-sqlalchemy-2.0/Flask_SQLAlchemy.egg-info/PKG-INFO 2014-08-29 05:15:38.000000000 +0000 +++ flask-sqlalchemy-2.1/Flask_SQLAlchemy.egg-info/PKG-INFO 2015-10-23 09:49:49.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: Flask-SQLAlchemy -Version: 2.0 +Version: 2.1 Summary: Adds SQLAlchemy support to your Flask application Home-page: http://github.com/mitsuhiko/flask-sqlalchemy Author: Phil Howell diff -Nru flask-sqlalchemy-2.0/Flask_SQLAlchemy.egg-info/requires.txt flask-sqlalchemy-2.1/Flask_SQLAlchemy.egg-info/requires.txt --- flask-sqlalchemy-2.0/Flask_SQLAlchemy.egg-info/requires.txt 2014-08-29 05:15:38.000000000 +0000 +++ flask-sqlalchemy-2.1/Flask_SQLAlchemy.egg-info/requires.txt 2015-10-23 09:49:49.000000000 +0000 @@ -1,2 +1,2 @@ Flask>=0.10 -SQLAlchemy \ No newline at end of file +SQLAlchemy>=0.7 \ No newline at end of file diff -Nru flask-sqlalchemy-2.0/PKG-INFO flask-sqlalchemy-2.1/PKG-INFO --- flask-sqlalchemy-2.0/PKG-INFO 2014-08-29 05:15:38.000000000 +0000 +++ flask-sqlalchemy-2.1/PKG-INFO 2015-10-23 09:49:49.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: Flask-SQLAlchemy -Version: 2.0 +Version: 2.1 Summary: Adds SQLAlchemy support to your Flask application Home-page: http://github.com/mitsuhiko/flask-sqlalchemy Author: Phil Howell diff -Nru flask-sqlalchemy-2.0/README flask-sqlalchemy-2.1/README --- flask-sqlalchemy-2.0/README 2014-08-23 16:00:29.000000000 +0000 +++ flask-sqlalchemy-2.1/README 2015-07-28 19:15:32.000000000 +0000 @@ -7,11 +7,10 @@ Flask-SQLAlchemy is a Flask microframework extension which adds support for the SQLAlchemy SQL toolkit/ORM. - ~ Is it ready? - - Yes! Version 2.0 is close to release and version 1.0 is already - widely used. + ~ What's the latest version? + 2.0 is the most recent stable version. 2.1 is slated for release in early February. + ~ What do I need? SQLAlchemy, and Flask 0.10 or later. `pip` or `easy_install` will @@ -36,6 +35,12 @@ it on the command line: $ python test_sqlalchemy.py PaginationTestCase + + In case you have `tox` installed, you can also run that to have + virtualenvs created automatically and tests run inside of them + at your convenience. Running just `tox` is enough: + + $ tox ~ Where can I get help? diff -Nru flask-sqlalchemy-2.0/setup.py flask-sqlalchemy-2.1/setup.py --- flask-sqlalchemy-2.0/setup.py 2014-08-29 05:15:38.000000000 +0000 +++ flask-sqlalchemy-2.1/setup.py 2015-10-23 09:49:49.000000000 +0000 @@ -17,7 +17,7 @@ setup( name='Flask-SQLAlchemy', - version='2.0', + version='2.1', url='http://github.com/mitsuhiko/flask-sqlalchemy', license='BSD', author='Armin Ronacher', @@ -31,7 +31,7 @@ platforms='any', install_requires=[ 'Flask>=0.10', - 'SQLAlchemy' + 'SQLAlchemy>=0.7' ], test_suite='test_sqlalchemy.suite', classifiers=[ diff -Nru flask-sqlalchemy-2.0/test_sqlalchemy.py flask-sqlalchemy-2.1/test_sqlalchemy.py --- flask-sqlalchemy-2.0/test_sqlalchemy.py 2014-08-23 15:40:48.000000000 +0000 +++ flask-sqlalchemy-2.1/test_sqlalchemy.py 2015-07-28 19:15:32.000000000 +0000 @@ -1,11 +1,12 @@ from __future__ import with_statement -import os import atexit import unittest from datetime import datetime import flask -from flask.ext import sqlalchemy +import flask_sqlalchemy as sqlalchemy +from sqlalchemy import MetaData +from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import sessionmaker @@ -81,6 +82,63 @@ self.assertEqual(self.db.metadata, self.db.Model.metadata) +class CustomMetaDataTestCase(unittest.TestCase): + + def setUp(self): + self.app = flask.Flask(__name__) + self.app.config['SQLALCHEMY_ENGINE'] = 'sqlite://' + self.app.config['TESTING'] = True + + def test_custom_metadata_positive(self): + + convention = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + } + + metadata = MetaData(naming_convention=convention) + db = sqlalchemy.SQLAlchemy(self.app, metadata=metadata) + self.db = db + + class One(db.Model): + id = db.Column(db.Integer, primary_key=True) + myindex = db.Column(db.Integer, index=True) + + class Two(db.Model): + id = db.Column(db.Integer, primary_key=True) + one_id = db.Column(db.Integer, db.ForeignKey(One.id)) + myunique = db.Column(db.Integer, unique=True) + + self.assertEqual(list(One.__table__.constraints)[0].name, 'pk_one') + self.assertEqual(list(One.__table__.indexes)[0].name, 'ix_one_myindex') + + self.assertTrue('fk_two_one_id_one' in [c.name for c in Two.__table__.constraints]) + self.assertTrue('uq_two_myunique' in [c.name for c in Two.__table__.constraints]) + self.assertTrue('pk_two' in [c.name for c in Two.__table__.constraints]) + + def test_custom_metadata_negative(self): + db = sqlalchemy.SQLAlchemy(self.app, metadata=None) + self.db = db + + class One(db.Model): + id = db.Column(db.Integer, primary_key=True) + myindex = db.Column(db.Integer, index=True) + + class Two(db.Model): + id = db.Column(db.Integer, primary_key=True) + one_id = db.Column(db.Integer, db.ForeignKey(One.id)) + myunique = db.Column(db.Integer, unique=True) + + self.assertNotEqual(list(One.__table__.constraints)[0].name, 'pk_one') + + self.assertFalse('fk_two_one_id_one' in [c.name for c in Two.__table__.constraints]) + self.assertFalse('uq_two_myunique' in [c.name for c in Two.__table__.constraints]) + self.assertFalse('pk_two' in [c.name for c in Two.__table__.constraints]) + + class TestQueryProperty(unittest.TestCase): def setUp(self): @@ -129,6 +187,19 @@ def tearDown(self): self.db.drop_all() + def test_before_committed(self): + class Namespace(object): + is_received = False + + def before_committed(sender, changes): + Namespace.is_received = True + + with sqlalchemy.before_models_committed.connected_to(before_committed, sender=self.app): + todo = self.Todo('Awesome', 'the text') + self.db.session.add(todo) + self.db.session.commit() + self.assertTrue(Namespace.is_received) + def test_model_signals(self): recorded = [] def committed(sender, changes): @@ -157,20 +228,108 @@ self.assertEqual(recorded[0][1], 'delete') -class HelperTestCase(unittest.TestCase): - - def test_default_table_name(self): +class TablenameTestCase(unittest.TestCase): + def test_name(self): app = flask.Flask(__name__) - app.config['SQLALCHEMY_ENGINE'] = 'sqlite://' + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' db = sqlalchemy.SQLAlchemy(app) class FOOBar(db.Model): id = db.Column(db.Integer, primary_key=True) + class BazBar(db.Model): id = db.Column(db.Integer, primary_key=True) + class Ham(db.Model): + __tablename__ = 'spam' + id = db.Column(db.Integer, primary_key=True) + self.assertEqual(FOOBar.__tablename__, 'foo_bar') self.assertEqual(BazBar.__tablename__, 'baz_bar') + self.assertEqual(Ham.__tablename__, 'spam') + + def test_single_name(self): + """Single table inheritance should not set a new name.""" + + app = flask.Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + db = sqlalchemy.SQLAlchemy(app) + + class Duck(db.Model): + id = db.Column(db.Integer, primary_key=True) + + class Mallard(Duck): + pass + + self.assertEqual(Mallard.__tablename__, 'duck') + + def test_joined_name(self): + """Model has a separate primary key; it should set a new name.""" + + app = flask.Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + db = sqlalchemy.SQLAlchemy(app) + + class Duck(db.Model): + id = db.Column(db.Integer, primary_key=True) + + class Donald(Duck): + id = db.Column(db.Integer, db.ForeignKey(Duck.id), primary_key=True) + + self.assertEqual(Donald.__tablename__, 'donald') + + def test_mixin_name(self): + """Primary key provided by mixin should still allow model to set tablename.""" + + app = flask.Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + db = sqlalchemy.SQLAlchemy(app) + + class Base(object): + id = db.Column(db.Integer, primary_key=True) + + class Duck(Base, db.Model): + pass + + self.assertFalse(hasattr(Base, '__tablename__')) + self.assertEqual(Duck.__tablename__, 'duck') + + def test_abstract_name(self): + """Abstract model should not set a name. Subclass should set a name.""" + + app = flask.Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + db = sqlalchemy.SQLAlchemy(app) + + class Base(db.Model): + __abstract__ = True + id = db.Column(db.Integer, primary_key=True) + + class Duck(Base): + pass + + self.assertFalse(hasattr(Base, '__tablename__')) + self.assertEqual(Duck.__tablename__, 'duck') + + def test_complex_inheritance(self): + """Joined table inheritance, but the new primary key is provided by a mixin, not directly on the class.""" + + app = flask.Flask(__name__) + app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + db = sqlalchemy.SQLAlchemy(app) + + class Duck(db.Model): + id = db.Column(db.Integer, primary_key=True) + + class IdMixin(object): + @declared_attr + def id(cls): + return db.Column(db.Integer, db.ForeignKey(Duck.id), primary_key=True) + + class RubberDuck(IdMixin, Duck): + pass + + self.assertEqual(RubberDuck.__tablename__, 'rubber_duck') class PaginationTestCase(unittest.TestCase): @@ -193,6 +352,34 @@ p = sqlalchemy.Pagination(None, 1, 0, 500, []) self.assertEqual(p.pages, 0) + def test_query_paginate(self): + app = flask.Flask(__name__) + db = sqlalchemy.SQLAlchemy(app) + Todo = make_todo_model(db) + db.create_all() + + with app.app_context(): + db.session.add_all([Todo('', '') for _ in range(100)]) + db.session.commit() + + @app.route('/') + def index(): + p = Todo.query.paginate() + return '{0} items retrieved'.format(len(p.items)) + + c = app.test_client() + # request default + r = c.get('/') + self.assertEqual(r.status_code, 200) + # request args + r = c.get('/?per_page=10') + self.assertEqual(r.data.decode('utf8'), '10 items retrieved') + + with app.app_context(): + # query default + p = Todo.query.paginate() + self.assertEqual(p.total, 100) + class BindsTestCase(unittest.TestCase): @@ -304,7 +491,7 @@ self.assertTrue(db.Column == sqlalchemy_lib.Column) # The Query object we expose is actually our own subclass. - from flask.ext.sqlalchemy import BaseQuery + from flask_sqlalchemy import BaseQuery self.assertTrue(db.Query == BaseQuery) @@ -492,8 +679,9 @@ def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(BasicAppTestCase)) + suite.addTest(unittest.makeSuite(CustomMetaDataTestCase)) suite.addTest(unittest.makeSuite(TestQueryProperty)) - suite.addTest(unittest.makeSuite(HelperTestCase)) + suite.addTest(unittest.makeSuite(TablenameTestCase)) suite.addTest(unittest.makeSuite(PaginationTestCase)) suite.addTest(unittest.makeSuite(BindsTestCase)) suite.addTest(unittest.makeSuite(DefaultQueryClassTestCase))