#263 Support Flask-SQLAlchemy 3.0
Closed 2 years ago by kparal. Opened 2 years ago by kparal.

file modified
+2 -1
@@ -361,7 +361,8 @@ 

          setup_logging()

  

      # run the requested command

-     args.func(args)

+     with app.app_context():

+         args.func(args)

  

  

  if __name__ == '__main__':

file modified
+1 -2
@@ -7,8 +7,7 @@ 

  bodhi-client >= 5.7.5

  email_validator

  Flask-Admin

- # FIXME: https://pagure.io/fedora-qa/blockerbugs/issue/260

- Flask-SQLAlchemy ~= 2.5

+ Flask-SQLAlchemy

  Flask-WTF

  iso8601

  Jinja2

file modified
+6 -4
@@ -22,6 +22,7 @@ 

  import os

  import sys

  import blockerbugs

+ from blockerbugs import app

  from blockerbugs import cli

  

  if __name__ == '__main__':
@@ -29,12 +30,13 @@ 

      # verify we have a database ready, bail out if not or if it's not in

      # consistent state

  

-     if not cli.initialize_db(destructive=False):

-         print("Database is in unknown state, manual intervention needed")

-         sys.exit(1)

+     with app.app_context():

+         if not cli.initialize_db(destructive=False):

+             print("Database is in unknown state, manual intervention needed")

+             sys.exit(1)

  

      # now that we have FAS integration, we don't want to default to production

-     # when we're not running though mod_wsgi

+     # when we're not running through mod_wsgi

  

      if not (os.getenv('TEST') == 'true' or os.getenv('PROD') == 'true'):

          os.environ['DEV'] = 'true'

file modified
+23
@@ -19,6 +19,7 @@ 

  """Configuration and utils for pytest"""

  

  import os

+ import pytest

  

  

  def pytest_configure(config):
@@ -28,3 +29,25 @@ 

      # and that another config is not specified by mistake

      os.environ.pop('DEV', None)

      os.environ.pop('PROD', None)

+ 

+ 

+ @pytest.fixture

+ def app_ctx():

+     """Run the decorated function/method under the Flask app context (necessary whenever touching

+     the DB).

+     Read more at:

+         https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/contexts/

+         https://flask.palletsprojects.com/en/2.2.x/appcontext/

+         https://docs.pytest.org/en/7.2.x/how-to/fixtures.html

+ 

+     Note: The application context must be the same during the whole test lifecycle (including setup

+     and teardown methods, when used). You can't use multiple `with app.app_context():` blocks,

+     because each block creates a new `db.session()`. You'll then often encounter one of two errors,

+     either "Instance I is not bound to a Session" or "Object O is already attached to session N".

+     Flask itself uses the *same* session (and an app context) when handling a single request, so we

+     need to do it manually the same way.

+     """

+     # this import must happen here, because pytest_configure() must run first

+     from blockerbugs import app

+     with app.app_context():

+         yield

file modified
+33 -30
@@ -4,6 +4,7 @@ 

  import http.client as httplib

  

  import mock

+ import pytest

  

  from blockerbugs import db

  from blockerbugs import app
@@ -18,50 +19,50 @@ 

  from blockerbugs.util import pagure_bot

  

  

- class TestRestAPI(object):

-     @classmethod

-     def setup_class(cls):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestRestAPI:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()

-         s = db.session()

-         s.expire_on_commit = False  # avoid LazyLoading error, https://docs.sqlalchemy.org/en/14/errors.html#error-bhk3

-         cls.client = app.test_client()

-         cls.release = add_release(99)

-         cls.milestone = add_milestone(cls.release, 'final', 100, 101,

+         self.client = app.test_client()

+         self.release = add_release(99)

+         self.milestone = add_milestone(self.release, 'final', 100, 101,

                                        '99-final', True)

-         cls.milestone.current = True

+         self.milestone.current = True

  

-         cls.milestone2 = add_milestone(cls.release, 'beta', 200, 201,

+         self.milestone2 = add_milestone(self.release, 'beta', 200, 201,

                                         '99-beta')

-         bug1 = add_bug(9000, 'testbug1', cls.milestone)

+         bug1 = add_bug(9000, 'testbug1', self.milestone)

          bug1.accepted_fe = True

          bug1.status = 'CLOSED'

          bug1.discussion_link = 'example.com'

-         bug1copy = add_bug(9000, 'testbug1', cls.milestone2)  # different milestone than bug1

+         bug1copy = add_bug(9000, 'testbug1', self.milestone2)  # different milestone than bug1

          bug1copy.accepted_fe = True

          bug1copy.status = 'CLOSED'

-         bug2 = add_bug(9002, 'testbug2', cls.milestone)

+         bug2 = add_bug(9002, 'testbug2', self.milestone)

          bug2.accepted_blocker = False

          bug2.proposed_fe = True

-         bug2copy = add_bug(9002, 'testbug2', cls.milestone2)  # different milestone than bug2

+         bug2copy = add_bug(9002, 'testbug2', self.milestone2)  # different milestone than bug2

          bug2copy.accepted_blocker = False

          bug2copy.proposed_fe = True

-         bug3 = add_bug(9003, 'testbug3', cls.milestone)

+         bug3 = add_bug(9003, 'testbug3', self.milestone)

          bug3.accepted_blocker = False

          bug3.proposed_fe = True

-         bug4 = add_bug(9003, 'testbug3', cls.milestone2)  # different milestone and proposals than bug3

+         bug4 = add_bug(9003, 'testbug3', self.milestone2)  # different milestone and proposals than bug3

          bug4.accepted_blocker = True

          bug4.proposed_fe = False

          bug4.status = 'CLOSED'

-         cls.update_pending_stable = add_update('test-pending-stable.fc99', cls.release, 'testing',

-                                                [bug1, bug1copy])

-         cls.update_pending_stable.date_submitted = datetime(1990, 1, 1)

-         cls.update_pending_stable.request = 'stable'

-         cls.update_pending_stable.title = 'mega fixer'

-         cls.update_testing2 = add_update('test-testing2.fc99', cls.release, 'testing', [bug2])

- 

-         cls.webhook_data = {

+         self.update_pending_stable = add_update('test-pending-stable.fc99', self.release, 'testing',

+                                                 [bug1, bug1copy])

+         self.update_pending_stable.date_submitted = datetime(1990, 1, 1)

+         self.update_pending_stable.request = 'stable'

+         self.update_pending_stable.title = 'mega fixer'

+         self.update_testing2 = add_update('test-testing2.fc99', self.release, 'testing', [bug2])

+ 

+         self.webhook_data = {

              'msg': {

                  'issue': {

                      'id': 6666,
@@ -71,14 +72,16 @@ 

              'topic': 'issue.comment.added',

          }

  

-         cls.api_user = UserInfo('api_user')

-         passwd = cls.api_user.generate_api_key()

-         cls.headers = {'X-Auth-User': 'api_user', 'X-Auth-Key': passwd}

-         db.session.add(cls.api_user)

+         self.api_user = UserInfo('api_user')

+         passwd = self.api_user.generate_api_key()

+         self.headers = {'X-Auth-User': 'api_user', 'X-Auth-Key': passwd}

+         db.session.add(self.api_user)

          db.session.commit()

  

-     @classmethod

-     def teardown_class(cls):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.drop_all()

  

file modified
+10 -1
@@ -12,8 +12,11 @@ 

                          'last_change_time': datetime.utcnow()}]}

  

  

+ @pytest.mark.usefixtures('setup_teardown')

  class TestProposeBlocker:

-     def setup_method(self, method):

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          self.ref_trackers = {'blocker': 123456, 'fe': 234567}

          self.ref_proposed = 345678

          self.ref_justification = 'because I said so'
@@ -22,6 +25,12 @@ 

          self.ref_user = 'joe@allthebugs.com'

          self.ref_cc = 'cc@allthebugs.com'

  

+         # === run method ===

+         yield

+ 

+         # === teardown ===

+         # not needed

+ 

      def test_propose_blocker(self):

          stubbz = mock.Mock()

          stubbz.update_bugs.return_value = True

@@ -1,3 +1,4 @@ 

+ import pytest

  from munch import Munch

  from copy import deepcopy

  from blockerbugs.util.bug_sync import BugSync
@@ -17,11 +18,20 @@ 

  })

  

  

- class TestSyncExtractInformation(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('setup_teardown')

+ class TestSyncExtractInformation:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          self.testbug = deepcopy(basicbug)

          self.testsync = BugSync(None, MagicMock())

  

+         # === run method ===

+         yield

+ 

+         # === teardown ===

+         # not needed

+ 

      def test_nth_isaccepted(self):

          self.testbug.whiteboard = 'AcceptedFreezeException'

          ref_isaccepted = True

file modified
+39 -23
@@ -107,16 +107,20 @@ 

      return all_sections

  

  

- class TestEmptyDB(object):

-     @classmethod

-     def setup_class(cls):

-         cls.client = app.test_client()

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestEmptyDB:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

+         self.client = app.test_client()

          db.session.rollback()

          db.drop_all()

          db.create_all()

  

-     @classmethod

-     def teardown_class(cls):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.drop_all()

  
@@ -127,10 +131,12 @@ 

          assert 'current milestone' in str(excinfo.value)

  

  

- class TestNotSyncedMilestone(object):

-     @classmethod

-     def setup_class(cls):

-         cls.client = app.test_client()

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestNotSyncedMilestone:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

+         self.client = app.test_client()

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -138,8 +144,10 @@ 

          add_milestone(release, 'final', 100, 101, '99-final', True)

          add_milestone(release, 'beta', 200, 201, '99-beta')

  

-     @classmethod

-     def teardown_class(cls):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.drop_all()

  
@@ -166,10 +174,12 @@ 

          assert rv.status_code == 200

  

  

- class TestSyncedMilestone(object):

-     @classmethod

-     def setup_class(cls):

-         cls.client = app.test_client()

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestSyncedMilestone:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

+         self.client = app.test_client()

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -182,8 +192,10 @@ 

          final_milestone.last_bug_sync = datetime.datetime.utcnow()

          db.session.commit()

  

-     @classmethod

-     def teardown_class(cls):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.drop_all()

  
@@ -220,9 +232,11 @@ 

          assert b'101' in rv.data

  

  

- class TestGetFunctions(object):

-     @classmethod

-     def setup_method(self):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestGetFunctions:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          self.client = app.test_client()

          db.session.rollback()

          db.drop_all()
@@ -273,8 +287,10 @@ 

                                             [self.propbetafebug, self.finalfebug])

          db.session.commit()

  

-     @classmethod

-     def teardown_method(self):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

@@ -1,4 +1,6 @@ 

  import mock

+ import pytest

+ 

  from blockerbugs import db

  from testing.test_controllers import add_release, add_milestone, add_bug

  from blockerbugs.util import pagure_interface
@@ -14,8 +16,11 @@ 

  }

  

  

- class TestInactiveDiscussions(object):

-     def setup_method(self):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestInactiveDiscussions:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -37,7 +42,10 @@ 

  

          db.session.commit()

  

-     def teardown_method(self):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

file modified
+9 -3
@@ -10,8 +10,11 @@ 

  from blockerbugs.models.bug import Bug

  

  

- class TestInit(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestInit:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -48,7 +51,10 @@ 

          db.session.add(self.update1)

          db.session.commit()

  

-     def teardown_method(self, method):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

file modified
+8 -8
@@ -64,7 +64,7 @@ 

      monkeypatch.setattr(pagure_interface, 'get_group', stub_pagure_group_call)

  

  

- class TestPagureBotBasics(object):

+ class TestPagureBotBasics:

      @pytest.mark.parametrize("inversed", [False, True])

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("vote", ['+1', '-1', '0'])
@@ -247,7 +247,7 @@ 

          assert line_contains(summary, ['the last processed comment was', '#comment-1'])

  

  

- class TestPagureBotTypos(object):

+ class TestPagureBotTypos:

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("sign", ['+', '-'])

      def test_typo(self, sign, keyword):
@@ -298,7 +298,7 @@ 

              assert tracker.votes == {}

  

  

- class TestPagureBotGotchas(object):

+ class TestPagureBotGotchas:

      @pytest.mark.parametrize("inversed", [False, True])

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("sign", ['+', '–'])  # these chars are not + or -
@@ -419,7 +419,7 @@ 

          assert trackers[expand_fe(keyword)].votes == {}

  

  

- class TestPagureBotMultiline(object):

+ class TestPagureBotMultiline:

      @pytest.mark.parametrize("inversed", [False, True])

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("sign", ['+', '-'])
@@ -510,7 +510,7 @@ 

          }

  

  

- class TestPagureBotSinglelineMultivote(object):

+ class TestPagureBotSinglelineMultivote:

      @pytest.mark.parametrize("inversed", itertools.product([False, True], [False, True]))

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("vote", [('+1', '-1'), ('-1', '+1'), ('+1', '0'),
@@ -637,7 +637,7 @@ 

          assert trackers["betablocker"].votes == {}

  

  

- class TestPagureBotAgreedRevote(object):

+ class TestPagureBotAgreedRevote:

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("outcome", ['Accepted', 'Rejected'])

      def test_agreed_single_acceptedrejected_needs_summary(self, outcome, keyword):
@@ -911,7 +911,7 @@ 

          }

  

  

- class TestPagureBotWhitespaces(object):

+ class TestPagureBotWhitespaces:

      @pytest.mark.parametrize("inversed", [False, True])

      @pytest.mark.parametrize("keyword", EXTENDED_TRACKERS)

      @pytest.mark.parametrize("vote", ['+1', '-1', '0'])
@@ -1127,7 +1127,7 @@ 

              assert trackers[expand_fe(keyword)].need_summary_post

  

  

- class TestPagureBotHaywire(object):

+ class TestPagureBotHaywire:

      def test_bot_behaves(self):

          user = app.config['PAGURE_BOT_USERNAME']

          comments = [pagure_bot.Comment(c) for c in [

@@ -4,7 +4,7 @@ 

  from blockerbugs.util import pagure_interface

  

  

- class TestPagureInterface(object):

+ class TestPagureInterface:

      @mock.patch('blockerbugs.util.pagure_interface.requests.post')

      def test_create_issue_rises_non_200(selfm, mock_post):

          mock_post.return_value.status_code = 404

file modified
+11 -3
@@ -2,6 +2,8 @@ 

  

  from datetime import datetime

  

+ import pytest

+ 

  from blockerbugs import db

  from blockerbugs.models.update import Update

  from blockerbugs.models.release import Release
@@ -10,13 +12,19 @@ 

  from blockerbugs.util import testdata

  

  

- class TestTestData(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestTestData:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()

  

-     def teardown_method(self, method):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

file modified
+11 -3
@@ -3,14 +3,19 @@ 

  import datetime

  import copy

  

+ import pytest

+ 

  from blockerbugs import db

  from blockerbugs.models.update import Update

  from blockerbugs.models.release import Release

  from blockerbugs.models.bug import Bug

  

  

- class TestUpdateModel(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestUpdateModel:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -51,7 +56,10 @@ 

              'bugs': [1, 2],

          }

  

-     def teardown_method(self, method):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

@@ -1,3 +1,4 @@ 

+ import pytest

  from munch import Munch

  from copy import deepcopy

  from blockerbugs.util.update_sync import UpdateSync
@@ -79,11 +80,20 @@ 

  )

  

  

- class TestUpdateSyncExtractInformation(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('setup_teardown')

+ class TestUpdateSyncExtractInformation:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          self.testupdate = deepcopy(basicupdate)

          self.testsync = UpdateSync(None, MagicMock())

  

+         # === run method ===

+         yield

+ 

+         # === teardown ===

+         # not needed

+ 

      def test_extract_simple(self):

          updateinfo = self.testsync.extract_information(self.testupdate)

  

file modified
+1 -1
@@ -5,7 +5,7 @@ 

  from blockerbugs.controllers.api.errors import ValidationError

  

  

- class TestValidators(object):

+ class TestValidators:

      def test_string(self):

          string_validator = validators.StringValidator()

          valid_string_value = 'abc'

file modified
+11 -3
@@ -1,5 +1,7 @@ 

  import datetime

  

+ import pytest

+ 

  from blockerbugs.models.milestone import Milestone

  from blockerbugs.models.release import Release

  from blockerbugs.models.bug import Bug
@@ -31,15 +33,21 @@ 

                    bugs=bugs)

  

  

- class TestfuncBugModel(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestfuncBugModel:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()

          self.ref_release = generate_release()

          self.ref_milestone = generate_milestone('alpha', '99-alpha', self.ref_release)

  

-     def teardown_method(self, method):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

file modified
+15 -20
@@ -1,4 +1,6 @@ 

  import copy

+ 

+ import pytest

  from munch import Munch

  import datetime

  
@@ -42,7 +44,7 @@ 

  }

  

  

- class StubBzInterface(object):

+ class StubBzInterface:

      def __init__(self, deps=[], bugs=[]):

          self.deps = deps

          self.bugs = bugs
@@ -54,9 +56,11 @@ 

          return self.bugs

  

  

- class TestfuncBugsync(object):

-     @classmethod

-     def setup_class(cls):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestfuncBugsync:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -71,26 +75,17 @@ 

          db.session.add(test_milestone17beta)

          db.session.commit()

  

- 

-     @classmethod

-     def teardown_class(cls):

-         db.session.rollback()

-         db.session.close()

-         db.drop_all()

- 

-     def setup_method(self, method):

          self.basicbug = copy.deepcopy(templatebug)

          self.basicbuginfo = copy.deepcopy(templatebuginfo)

          self.milestone = Milestone.query.filter_by(active=True).first()

  

-     def teardown_method(self, method):

-         oldbugs = Bug.query.all()

-         if oldbugs:

-             for oldbug in oldbugs:

-                 db.session.delete(oldbug)

-             db.session.commit()

-         else:

-             print("no old bug found to delete!")

+         # === run method ===

+         yield

+ 

+         # === teardown ===

+         db.session.rollback()

+         db.session.close()

+         db.drop_all()

  

  

      # test that a bug which is no longer blocking a tracker but not marked as

@@ -2,6 +2,7 @@ 

  from datetime import datetime

  from copy import copy

  

+ import pytest

  from munch import Munch

  from mock import patch, Mock

  from fedora.client import ServerError
@@ -64,7 +65,7 @@ 

  })

  

  

- class FakeBodhiInterface(object):

+ class FakeBodhiInterface:

      releases = [

          {'name': 'F99'},

          {'name': 'F99F'},
@@ -108,8 +109,11 @@ 

                            'Internal Server Error')

  

  

- class TestfuncUpdateSync(object):

-     def setup_method(self, method):

+ @pytest.mark.usefixtures('app_ctx', 'setup_teardown')

+ class TestfuncUpdateSync:

+     @pytest.fixture

+     def setup_teardown(self):

+         # === setup ===

          db.session.rollback()

          db.drop_all()

          db.create_all()
@@ -127,7 +131,10 @@ 

          self.fbi = FakeBodhiInterface()

          self.update_sync = UpdateSync(db, self.fbi)

  

-     def teardown_method(self, method):

+         # === run method ===

+         yield

+ 

+         # === teardown ===

          db.session.rollback()

          db.session.close()

          db.drop_all()

Since Flask-SQLAlchemy 3.0, it is necessary to manually push Flask
application context when needed (in tests, in CLI commands not
registered through Flask). This patch adjusts our code to set the app
context when needed.

I chose a bit different approach to Frantisek's PR #262, using pytest
fixtures instead. While doing that, I converted all old xunit-style
classes to pytest fixtures. One of the reasons is that xunit-style
special methods can't receive fixture arguments, but it's not
communicated and confuses the developer, so I converted them everywhere.

Fixes: https://pagure.io/fedora-qa/blockerbugs/issue/260
Related: https://pagure.io/fedora-qa/blockerbugs/pull-request/262

Build succeeded.

Looks good, thanks, now do the big green button thingy!

Autoclosing seems broken, merged as commit af523ed

Pull-Request has been closed by kparal

2 years ago