From ccfda8fb054c2f64b5e419df2b333f9e7ffebd1a Mon Sep 17 00:00:00 2001 From: "legolas.zhan" Date: Wed, 18 May 2022 15:50:43 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20support/fixed=20signal=20when=20?= =?UTF-8?q?using=20@transaction=20because=20of=20session.begin=5Fnested?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 5 +++++ hobbit_core/db.py | 8 +++++++- setup.py | 2 +- tests/test_app/models.py | 17 +++++++++++++++++ tests/test_app/run.py | 2 +- tests/test_app/views.py | 27 +++++++++++++++++++++++++++ tests/test_db.py | 19 +++++++++++++++++++ 7 files changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 07756cc..9f6c2b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,11 @@ Change history ============== +2.2.3 (2022-05-18) +****************** + +* Support use nested=None(`@transaction(db.session, nested=None)`) to avoid bug from `flask_sqlalchemy.models_committed` signal. + 2.2.2 (2022-02-17) ****************** diff --git a/hobbit_core/db.py b/hobbit_core/db.py index cffa3c6..74bd97a 100644 --- a/hobbit_core/db.py +++ b/hobbit_core/db.py @@ -312,6 +312,9 @@ def transaction(session: Session, nested: bool = False): or ``session.autocommit=True``. See more: http://flask-sqlalchemy.pocoo.org/2.3/api/#sessions + 2022-05-18 Updated: Use `nested=None` to prevent signal bug, See more: + https://github.com/pallets-eco/flask-sqlalchemy/issues/645 + Tips: * **Can't** do ``session.commit()`` **in func**, **otherwise raise** ``sqlalchemy.exc.ResourceClosedError``: `This transaction is closed`. @@ -361,8 +364,11 @@ def inner(*args, **kwargs): if session.autocommit is True and nested is False: session.begin() # start a transaction try: - with session.begin_nested(): + if nested is None: resp = func(*args, **kwargs) + else: + with session.begin_nested(): + resp = func(*args, **kwargs) if not nested: # commit - begin(), transaction finished session.commit() diff --git a/setup.py b/setup.py index b8a86be..d4d57a0 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def gen_data(data_root='static'): setup( name='hobbit-core', - version='2.2.2', + version='2.2.3', python_requires='>=3.6, <4', description='Hobbit - A flask project generator.', long_description=long_description, diff --git a/tests/test_app/models.py b/tests/test_app/models.py index 37b754f..d44ae8c 100644 --- a/tests/test_app/models.py +++ b/tests/test_app/models.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- from sqlalchemy import UniqueConstraint, func, DateTime, BigInteger +from flask_sqlalchemy import models_committed from hobbit_core.db import Column, BaseModel, EnumExt @@ -18,6 +19,22 @@ class User(BaseModel): role = Column(db.Enum(RoleEnum), doc='角色', default=RoleEnum.admin) +@models_committed.connect +def signalling(app, changes, **kwargs): + for instance, operation in changes: + if instance.__tablename__ in [i.__tablename__ for i in [User]]: + models_committed.disconnect(signalling) + session = db.create_scoped_session() + user = session.query(User).first() + if user and user.username == 'signalling_test': + user.username = 'signalling_ok' + session.merge(user) + session.commit() + session.remove() + models_committed.connect(signalling) + break + + class Role(BaseModel): # just for assert multi model worked name = Column(db.String(50), nullable=False, unique=True) diff --git a/tests/test_app/run.py b/tests/test_app/run.py index c04e83f..cdffdcc 100644 --- a/tests/test_app/run.py +++ b/tests/test_app/run.py @@ -15,7 +15,7 @@ class ConfigClass: 'oracle': 'oracle://scott:tiger@localhost/test', 'mysql': 'mysql+pymysql://root:root@localhost/hobbit_core', } - SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_TRACK_MODIFICATIONS = True # SQLALCHEMY_ECHO = True TESTING = True diff --git a/tests/test_app/views.py b/tests/test_app/views.py index f560dd3..8a39c98 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -4,8 +4,11 @@ from webargs.flaskparser import use_kwargs as base_use_kwargs from hobbit_core.utils import use_kwargs +from hobbit_core.db import transaction from .schemas import UserSchema +from .exts import db +from .models import User bp = Blueprint('test', __name__) @@ -48,3 +51,27 @@ def use_kwargs_dictargmap_partial(**kwargs): }) def base_use_kwargs_dictargmap_partial(**kwargs): return jsonify(wrapper_kwargs(kwargs)) + + +@bp.route('/create_user/success/', methods=['POST']) +@base_use_kwargs({'email': fields.Str()}) +def create_user_success(email): + @transaction(db.session, nested=None) + def create_user(): + user1 = User(username='signalling_test', email=email, password='1') + db.session.add(user1) + + create_user() + return jsonify({}) + + +@bp.route('/create_user/failed/', methods=['POST']) +@base_use_kwargs({'email': fields.Str()}) +def create_user_failed(email): + @transaction(db.session) + def create_user(): + user1 = User(username='signalling_test', email=email, password='1') + db.session.add(user1) + + create_user() + return jsonify({}) diff --git a/tests/test_db.py b/tests/test_db.py index 8de2604..bc58333 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -301,3 +301,22 @@ def test_autocommittrue_not_excepted(self, auto_session, assert_session): # assert not rollback. Be very careful when using commit. 😒😒😒😒 assert len(assert_session.query(User).all()) == 1 + + +class TestNestedSessionSignal(BaseTest): + + def test_transaction_signal_success(self, client, assert_session): + email = "test@test.com" + resp = client.post('/create_user/success/', json={"email": email}) + assert resp.status_code == 200 + + user = assert_session.query(User).filter(User.email == email).first() + assert user and user.username == "signalling_ok" + + def test_transaction_signal_dailed(self, client, assert_session): + email = "test@test.com" + resp = client.post('/create_user/failed/', json={"email": email}) + assert resp.status_code == 200 + + user = assert_session.query(User).filter(User.email == email).first() + assert user and user.username != "signalling_ok"