From 71b7d8025bb4c01d581cb14292f26361a4c639a6 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 20:53:24 +0200 Subject: [PATCH 01/24] Drop Python 3.7, add 3.10, 3.11, 3.12 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e2913dcc..776270170 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.7", "3.8", "3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] env: SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app @@ -193,7 +193,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.7] + python-version: [3.8] services: mongo: image: mongo:4.4.1-bionic From 554d09f9e44252ac7511325004b78b55fd3764c1 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 21:23:33 +0200 Subject: [PATCH 02/24] Update dependencies --- requirements.txt | 116 +++++++++++++++++++++++++++-------------------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/requirements.txt b/requirements.txt index ffe36d57d..3b5caedf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,32 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.8 +# by the following command: # # pip-compile # apispec[yaml]==6.3.0 # via Flask-AppBuilder (setup.py) -attrs==21.4.0 - # via jsonschema -babel==2.9.1 +attrs==23.1.0 + # via + # jsonschema + # referencing +babel==2.13.0 # via flask-babel -click==8.0.4 +blinker==1.6.3 + # via flask +click==8.1.7 # via # Flask-AppBuilder (setup.py) # flask -colorama==0.4.4 +colorama==0.4.6 # via Flask-AppBuilder (setup.py) -commonmark==0.9.1 - # via rich -deprecated==1.2.13 +deprecated==1.2.14 # via limits -dnspython==2.2.1 +dnspython==2.4.2 # via email-validator -email-validator==1.1.3 +email-validator==1.3.1 # via Flask-AppBuilder (setup.py) -flask==2.2.5 +flask==2.3.3 # via # Flask-AppBuilder (setup.py) # flask-babel @@ -35,104 +37,120 @@ flask==2.2.5 # flask-wtf flask-babel==2.0.0 # via Flask-AppBuilder (setup.py) -flask-jwt-extended==4.3.1 +flask-jwt-extended==4.5.3 # via Flask-AppBuilder (setup.py) -flask-limiter==3.2.0 +flask-limiter==3.5.0 # via Flask-AppBuilder (setup.py) -flask-login==0.6.0 +flask-login==0.6.2 # via Flask-AppBuilder (setup.py) flask-sqlalchemy==2.5.1 # via Flask-AppBuilder (setup.py) -flask-wtf==1.0.1 +flask-wtf==1.2.1 # via Flask-AppBuilder (setup.py) -idna==3.3 +greenlet==3.0.0 + # via sqlalchemy +idna==3.4 # via email-validator -importlib-metadata==6.6.0 +importlib-metadata==6.8.0 # via flask -itsdangerous==2.1.1 +importlib-resources==6.1.0 + # via + # jsonschema + # jsonschema-specifications + # limits +itsdangerous==2.1.2 # via # flask # flask-wtf -jinja2==3.0.3 +jinja2==3.1.2 # via # flask # flask-babel -jsonschema==3.2.0 +jsonschema==4.19.1 # via Flask-AppBuilder (setup.py) -limits==2.8.0 +jsonschema-specifications==2023.7.1 + # via jsonschema +limits==3.6.0 # via flask-limiter -markupsafe==2.1.1 +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.3 # via # jinja2 # werkzeug # wtforms -marshmallow==3.19.0 +marshmallow==3.20.1 # via # Flask-AppBuilder (setup.py) - # marshmallow-enum # marshmallow-sqlalchemy marshmallow-sqlalchemy==0.26.1 # via Flask-AppBuilder (setup.py) +mdurl==0.1.2 + # via markdown-it-py ordered-set==4.1.0 # via flask-limiter -packaging==21.3 +packaging==23.2 # via + # apispec # limits # marshmallow +pkgutil-resolve-name==1.3.10 + # via jsonschema prison==0.2.1 # via Flask-AppBuilder (setup.py) -pygments==2.15.0 +pygments==2.16.1 # via rich -pyjwt==2.6.0 +pyjwt==2.8.0 # via # Flask-AppBuilder (setup.py) # flask-jwt-extended -pyparsing==3.0.8 - # via packaging -pyrsistent==0.18.1 - # via jsonschema python-dateutil==2.8.2 # via Flask-AppBuilder (setup.py) -pytz==2021.1 +pytz==2023.3.post1 # via # babel # flask-babel -pyyaml==5.4.1 +pyyaml==6.0.1 # via apispec -rich==12.6.0 +referencing==0.30.2 + # via + # jsonschema + # jsonschema-specifications +rich==13.6.0 # via flask-limiter -six==1.16.0 +rpds-py==0.10.6 # via # jsonschema + # referencing +six==1.16.0 + # via # prison # python-dateutil - # sqlalchemy-utils -sqlalchemy==1.4.29 +sqlalchemy==1.4.49 # via # Flask-AppBuilder (setup.py) # flask-sqlalchemy # marshmallow-sqlalchemy # sqlalchemy-utils -sqlalchemy-utils==0.37.8 +sqlalchemy-utils==0.41.1 # via Flask-AppBuilder (setup.py) -typing-extensions==4.4.0 +typing-extensions==4.8.0 # via # flask-limiter # limits # rich -werkzeug==2.2.3 +werkzeug==3.0.0 # via # flask # flask-jwt-extended # flask-login -wrapt==1.14.1 +wrapt==1.15.0 # via deprecated -wtforms==3.0.1 +wtforms==3.1.0 # via # Flask-AppBuilder (setup.py) # flask-wtf -zipp==3.15.0 - # via importlib-metadata - -# The following packages are considered to be unsafe in a requirements file: -# setuptools +zipp==3.17.0 + # via + # importlib-metadata + # importlib-resources From 76d2227e832de28d3280b895aa68dc123c1d2bfc Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 21:38:42 +0200 Subject: [PATCH 03/24] Pin lower werkzeug version --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b5caedf4..0527ca351 100644 --- a/requirements.txt +++ b/requirements.txt @@ -139,8 +139,9 @@ typing-extensions==4.8.0 # flask-limiter # limits # rich -werkzeug==3.0.0 +werkzeug==2.3.7 # via + # Flask-AppBuilder (setup.py) # flask # flask-jwt-extended # flask-login From 6d8a0a537c432162f3f848649f9d80f938601a20 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 22:28:04 +0200 Subject: [PATCH 04/24] Migrate to nose2 --- .github/workflows/ci.yml | 8 +-- CONTRIBUTING.rst | 4 +- requirements-dev.txt | 4 +- setup.cfg | 4 ++ setup.py | 3 +- tests/test_fab_cli.py | 10 ++-- tests/test_mongoengine.py | 123 +++++++++++++++++++------------------- tests/test_sqlalchemy.py | 7 +-- tox.ini | 10 ++-- 9 files changed, 89 insertions(+), 84 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 776270170..4881f9e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -134,7 +134,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -184,7 +184,7 @@ jobs: sudo cp .github/workflows/odbcinst.ini /etc/odbcinst.ini - name: Run tests run: | - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -228,7 +228,7 @@ jobs: pip install -r requirements-mongodb.txt - name: Run tests run: | - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder tests/test_mongoengine.py + nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests/test_mongoengine.py - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 170d7b506..91b0b74db 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -32,7 +32,7 @@ can run a subset of tests targeting only Postgres. .. code-block:: bash - $ nosetests -v + $ nose2 -v You can also use tox @@ -69,7 +69,7 @@ Using Postgres .. code-block:: bash - $ nosetests -v tests.test_api:APITestCase.test_get_item_dotted_mo_notation + $ nose2 -v tests.test_api.APITestCase.test_get_item_dotted_mo_notation .. note:: diff --git a/requirements-dev.txt b/requirements-dev.txt index 091e877fa..2c1bb0146 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -black==19.3b0 +black==23.10.0 coverage==5.5 flake8-import-order==0.18.1 flake8==3.9.2 @@ -6,7 +6,7 @@ hiro==0.5.1 jmespath==0.9.5 mypy==0.910 mypy-extensions==0.4.3 -nose==1.3.7 +nose2[coverage_plugin]==0.14.0 parameterized==0.8.1 pip-tools==6.8.0 tox==3.24.3 diff --git a/setup.cfg b/setup.cfg index aa61b6dd6..f3f760d38 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,7 @@ +[unittest] +plugins = nose2.plugins.attrib + +# TODO: ! [nosetests] with-coverage = 1 cover-package = flask_appbuilder diff --git a/setup.py b/setup.py index 955f0c2a6..ed87e0641 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ def desc(): "SQLAlchemy<1.5", "sqlalchemy-utils>=0.32.21, <1", "WTForms<4", + "werkzeug<3" # Otherwise breaks Flask-Login ], extras_require={ "jmespath": ["jmespath>=0.9.5"], @@ -73,7 +74,7 @@ def desc(): "openid": ["Flask-OpenID>=1.2.5, <2"], "talisman": ["flask-talisman>=1.0.0, <2.0"], }, - tests_require=["nose>=1.0", "mockldap>=0.3.0", "hiro>=0.5.1"], + tests_require=["nose2==0.14.0", "mockldap>=0.3.0", "hiro>=0.5.1"], classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", diff --git a/tests/test_fab_cli.py b/tests/test_fab_cli.py index 519c39949..2691d936d 100644 --- a/tests/test_fab_cli.py +++ b/tests/test_fab_cli.py @@ -19,7 +19,6 @@ list_views, reset_password, ) -from nose.plugins.attrib import attr from .base import FABTestCase @@ -37,7 +36,6 @@ def setUp(self): def tearDown(self): log.debug("TEAR DOWN") - @attr("needs_inet") def test_create_app_invalid_secret_key(self): os.environ["FLASK_APP"] = "app:app" runner = CliRunner() @@ -52,7 +50,8 @@ def test_create_app_invalid_secret_key(self): ) self.assertIn("Invalid value for '--secret-key'", result.output) - @attr("needs_inet") + test_create_app_invalid_secret_key.needs_inet = True + def test_create_app(self): """ Test create app, create-user @@ -91,7 +90,8 @@ def test_create_app(self): runner.invoke(reset_password, ["--username=bob", "--password=bar"]) - @attr("needs_inet") + test_create_app.needs_inet = True + def test_list_views(self): """ CLI: Test list views @@ -103,6 +103,8 @@ def test_list_views(self): self.assertIn("List of registered views", result.output) self.assertIn(" Route:/api/v1/security", result.output) + test_list_views.needs_inet = True + def test_cast_int_like_to_int(self): scenarii = { -1: -1, diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 376e9e1e0..23d4f7b5e 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -21,7 +21,6 @@ from flask_appbuilder.views import CompactCRUDMixin, MasterDetailView from flask_mongoengine import MongoEngine import jinja2 -from nose.tools import eq_, ok_ from .base import FABTestCase from .mongoengine.models import Model1, Model2 @@ -224,7 +223,7 @@ def test_fab_views(self): """ Test views creation and registration """ - eq_(len(self.appbuilder.baseviews), 26) # current minimal views are 26 + self.assertEqual(len(self.appbuilder.baseviews), 26) # current minimal views are 26 def test_index(self): """ @@ -235,7 +234,7 @@ def test_index(self): # Check for Welcome Message rv = client.get("/") data = rv.data.decode("utf-8") - ok_(DEFAULT_INDEX_STRING in data) + self.assertTrue(DEFAULT_INDEX_STRING in data) def test_sec_login(self): """ @@ -245,28 +244,28 @@ def test_sec_login(self): # Try to List and Redirect to Login rv = client.get("/model1view/list/") - eq_(rv.status_code, 302) + self.assertEqual(rv.status_code, 302) rv = client.get("/model2view/list/") - eq_(rv.status_code, 302) + self.assertEqual(rv.status_code, 302) # Login and list with admin self.browser_login(client, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD) rv = client.get("/model1view/list/") - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) rv = client.get("/model2view/list/") - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) # Logout and and try to list self.browser_logout(client) rv = client.get("/model1view/list/") - eq_(rv.status_code, 302) + self.assertEqual(rv.status_code, 302) rv = client.get("/model2view/list/") - eq_(rv.status_code, 302) + self.assertEqual(rv.status_code, 302) # Invalid Login rv = self.browser_login(client, DEFAULT_ADMIN_USER, "password") data = rv.data.decode("utf-8") - ok_(INVALID_LOGIN_STRING in data) + self.assertTrue(INVALID_LOGIN_STRING in data) def test_sec_reset_password(self): """ @@ -284,7 +283,7 @@ def test_sec_reset_password(self): # Werkzeug update to 0.15.X sends this action to wrong redirect # Old test was: # data = rv.data.decode("utf-8") - # ok_(ACCESS_IS_DENIED in data) + # self.assertTrue(ACCESS_IS_DENIED in data) self.assertEqual(rv.status_code, 404) # Reset My password @@ -343,11 +342,11 @@ def test_model_crud(self): ), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) model = Model1.objects[0] - eq_(model.field_string, "test1") - eq_(model.field_integer, 1) + self.assertEqual(model.field_string, "test1") + self.assertEqual(model.field_integer, 1) model1 = Model1.objects(field_string="test1")[0] rv = client.post( @@ -355,18 +354,18 @@ def test_model_crud(self): data=dict(field_string="test2", field_integer="2"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) model = Model1.objects[0] - eq_(model.field_string, "test2") - eq_(model.field_integer, 2) + self.assertEqual(model.field_string, "test2") + self.assertEqual(model.field_integer, 2) rv = client.get( "/model1view/delete/{0}".format(model.id), follow_redirects=True ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) model = Model1.objects - eq_(len(model), 0) + self.assertEqual(len(model), 0) self.clean_data() def test_excluded_cols(self): @@ -376,31 +375,31 @@ def test_excluded_cols(self): client = self.app.test_client() rv = self.browser_login(client, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD) rv = client.get("/model22view/add") - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_("field_string" in data) - ok_("field_integer" in data) - ok_("field_float" in data) - ok_("field_date" in data) - ok_("excluded_string" not in data) + self.assertTrue("field_string" in data) + self.assertTrue("field_integer" in data) + self.assertTrue("field_float" in data) + self.assertTrue("field_date" in data) + self.assertTrue("excluded_string" not in data) self.insert_data2() model2 = Model2.objects[0] rv = client.get("/model22view/edit/{0}".format(model2.id)) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_("field_string" in data) - ok_("field_integer" in data) - ok_("field_float" in data) - ok_("field_date" in data) - ok_("excluded_string" not in data) + self.assertTrue("field_string" in data) + self.assertTrue("field_integer" in data) + self.assertTrue("field_float" in data) + self.assertTrue("field_date" in data) + self.assertTrue("excluded_string" not in data) rv = client.get("/model22view/show/{0}".format(model2.id)) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_("Field String" in data) - ok_("Field Integer" in data) - ok_("Field Float" in data) - ok_("Field Date" in data) - ok_("Excluded String" not in data) + self.assertTrue("Field String" in data) + self.assertTrue("Field Integer" in data) + self.assertTrue("Field Float" in data) + self.assertTrue("Field Date" in data) + self.assertTrue("Excluded String" not in data) self.clean_data() def test_query_rel_fields(self): @@ -414,15 +413,15 @@ def test_query_rel_fields(self): # Base filter string starts with rv = client.get("/model2view/add") data = rv.data.decode("utf-8") - ok_("G1" in data) - ok_("G2" not in data) + self.assertTrue("G1" in data) + self.assertTrue("G2" not in data) model2 = Model2.objects[0] # Base filter string starts with rv = client.get("/model2view/edit/{0}".format(model2.id)) data = rv.data.decode("utf-8") - ok_("G2" in data) - ok_("G1" not in data) + self.assertTrue("G2" in data) + self.assertTrue("G1" not in data) self.clean_data() def test_model_list_order(self): @@ -439,7 +438,7 @@ def test_model_list_order(self): follow_redirects=True, ) # TODO: fix this 405 Method not allowed error - # eq_(rv.status_code, 200) + # self.assertEqual(rv.status_code, 200) rv.data.decode("utf-8") # TODO # VALIDATE LIST IS ORDERED @@ -448,7 +447,7 @@ def test_model_list_order(self): follow_redirects=True, ) # TODO: fix this 405 Method not allowed error - # eq_(rv.status_code, 200) + # self.assertEqual(rv.status_code, 200) rv.data.decode("utf-8") # TODO # VALIDATE LIST IS ORDERED @@ -466,31 +465,31 @@ def test_model_add_validation(self): data=dict(field_string="test1", field_integer="1"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) rv = client.post( "/model1view/add", data=dict(field_string="test1", field_integer="2"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_(UNIQUE_VALIDATION_STRING in data) + self.assertTrue(UNIQUE_VALIDATION_STRING in data) model = Model1.objects() - eq_(len(model), 1) + self.assertEqual(len(model), 1) rv = client.post( "/model1view/add", data=dict(field_string="", field_integer="1"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_(NOTNULL_VALIDATION_STRING in data) + self.assertTrue(NOTNULL_VALIDATION_STRING in data) model = Model1.objects() - eq_(len(model), 1) + self.assertEqual(len(model), 1) self.clean_data() def test_model_edit_validation(self): @@ -516,18 +515,18 @@ def test_model_edit_validation(self): data=dict(field_string="test2", field_integer="2"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_(UNIQUE_VALIDATION_STRING in data) + self.assertTrue(UNIQUE_VALIDATION_STRING in data) rv = client.post( "/model1view/edit/{0}".format(model1.id), data=dict(field_string="", field_integer="2"), follow_redirects=True, ) - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_(NOTNULL_VALIDATION_STRING in data) + self.assertTrue(NOTNULL_VALIDATION_STRING in data) self.clean_data() def test_model_base_filter(self): @@ -538,19 +537,19 @@ def test_model_base_filter(self): self.browser_login(client, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD) self.insert_data() models = Model1.objects() - eq_(len(models), 23) + self.assertEqual(len(models), 23) # Base filter string starts with rv = client.get("/model1filtered1view/list/") data = rv.data.decode("utf-8") - ok_("atest" in data) - ok_("btest" not in data) + self.assertTrue("atest" in data) + self.assertTrue("btest" not in data) # Base filter integer equals rv = client.get("/model1filtered2view/list/") data = rv.data.decode("utf-8") - ok_("atest" in data) - ok_("btest" not in data) + self.assertTrue("atest" in data) + self.assertTrue("btest" not in data) self.clean_data() def test_model_list_method_field(self): @@ -561,9 +560,9 @@ def test_model_list_method_field(self): self.browser_login(client, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD) self.insert_data2() rv = client.get("/model2view/list/") - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) data = rv.data.decode("utf-8") - ok_("field_method_value" in data) + self.assertTrue("field_method_value" in data) self.clean_data() def test_compactCRUDMixin(self): @@ -574,7 +573,7 @@ def test_compactCRUDMixin(self): self.browser_login(client, DEFAULT_ADMIN_USER, DEFAULT_ADMIN_PASSWORD) self.insert_data2() rv = client.get("/model1compactview/list/") - eq_(rv.status_code, 200) + self.assertEqual(rv.status_code, 200) self.clean_data() diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index a0553a896..73e0a1da4 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -1,7 +1,6 @@ import unittest from flask_appbuilder.models.sqla.interface import _is_sqla_type -from nose.tools import eq_ import sqlalchemy as sa @@ -19,6 +18,6 @@ def test_is_sqla_type(self): t1 = sa.types.DateTime(timezone=True) t2 = CustomSqlaType() t3 = NotSqlaType() - eq_(True, _is_sqla_type(t1, sa.types.DateTime)) - eq_(True, _is_sqla_type(t2, sa.types.DateTime)) - eq_(False, _is_sqla_type(t3, sa.types.DateTime)) + self.assertTrue(_is_sqla_type(t1, sa.types.DateTime)) + self.assertTrue(_is_sqla_type(t2, sa.types.DateTime)) + self.assertFalse(_is_sqla_type(t3, sa.types.DateTime)) diff --git a/tox.ini b/tox.ini index 4a3fa5b09..e8309040d 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = setenv = SQLALCHEMY_DATABASE_URI = sqlite:/// commands = - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" [testenv:mysql] setenv = @@ -25,7 +25,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" [testenv:postgres] setenv = @@ -35,7 +35,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" [testenv:mssql] setenv = @@ -45,7 +45,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" [testenv:mongodb] deps = @@ -53,7 +53,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nosetests --stop -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests.test_mongoengine + nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests.test_mongoengine [testenv:black] commands = From 27d1c2c922e5be5947b913b66e98c1950a407611 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 22:50:37 +0200 Subject: [PATCH 05/24] Improve nose2 calls --- .github/workflows/ci.yml | 8 ++++---- setup.cfg | 10 +++------- tests/test_mongoengine.py | 10 ++++++++-- tox.ini | 10 +++++----- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4881f9e77..bca42e47e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -134,7 +134,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -184,7 +184,7 @@ jobs: sudo cp .github/workflows/odbcinst.ini /etc/odbcinst.ini - name: Run tests run: | - nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -228,7 +228,7 @@ jobs: pip install -r requirements-mongodb.txt - name: Run tests run: | - nose2 -F -v --with-coverage --cover-package=flask_appbuilder tests/test_mongoengine.py + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder tests/test_mongoengine.py - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python diff --git a/setup.cfg b/setup.cfg index f3f760d38..51ce984b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,9 @@ [unittest] plugins = nose2.plugins.attrib -# TODO: ! -[nosetests] -with-coverage = 1 -cover-package = flask_appbuilder -cover-html-dir=htmlcov -cover-html=1 -cover-erase=1 +[coverage] +always-on = True +coverage = flask_appbuilder [flake8] import-order-style = google diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 23d4f7b5e..e425a9ee9 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -44,6 +44,8 @@ class FlaskTestCase(FABTestCase): + mongo = True + def setUp(self): from flask import Flask from flask_appbuilder import AppBuilder @@ -223,7 +225,9 @@ def test_fab_views(self): """ Test views creation and registration """ - self.assertEqual(len(self.appbuilder.baseviews), 26) # current minimal views are 26 + self.assertEqual( + len(self.appbuilder.baseviews), 26 + ) # current minimal views are 26 def test_index(self): """ @@ -577,7 +581,9 @@ def test_compactCRUDMixin(self): self.clean_data() -class MongoImportExportTestCase(unittest.TestCase): +class MongoImportExportTestCase(unittest.TestCase): # + mongo = True + def setUp(self): basedir = os.path.abspath(os.path.dirname(__file__)) diff --git a/tox.ini b/tox.ini index e8309040d..0bf6c93a2 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = setenv = SQLALCHEMY_DATABASE_URI = sqlite:/// commands = - nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests [testenv:mysql] setenv = @@ -25,7 +25,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests [testenv:postgres] setenv = @@ -35,7 +35,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests [testenv:mssql] setenv = @@ -45,7 +45,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests --ignore-files="test_mongoengine\.py" + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests [testenv:mongodb] deps = @@ -53,7 +53,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -F -v --with-coverage --cover-package=flask_appbuilder flask_appbuilder.tests.test_mongoengine + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "mongo" tests [testenv:black] commands = From 1751f271992d6e3319d395a8b835190b5cfd83a3 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sat, 21 Oct 2023 23:09:03 +0200 Subject: [PATCH 06/24] Fix --- .github/workflows/ci.yml | 6 +++--- tox.ini | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bca42e47e..e82ee03ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -134,7 +134,7 @@ jobs: pip install -r requirements-extra.txt - name: Run tests run: | - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python @@ -184,7 +184,7 @@ jobs: sudo cp .github/workflows/odbcinst.ini /etc/odbcinst.ini - name: Run tests run: | - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python diff --git a/tox.ini b/tox.ini index 0bf6c93a2..a5dee59e6 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = setenv = SQLALCHEMY_DATABASE_URI = sqlite:/// commands = - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests [testenv:mysql] setenv = @@ -25,7 +25,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests [testenv:postgres] setenv = @@ -35,7 +35,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests [testenv:mssql] setenv = @@ -45,7 +45,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "!mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A '!mongo' tests [testenv:mongodb] deps = From 48bd23ffba1b926478e0d069fe418b63b078a901 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 00:12:26 +0200 Subject: [PATCH 07/24] Fix menu test --- tests/test_menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_menu.py b/tests/test_menu.py index 48939da96..a92e80e08 100644 --- a/tests/test_menu.py +++ b/tests/test_menu.py @@ -24,6 +24,7 @@ def setUp(self): self.db = SQLA(self.app) self.appbuilder = AppBuilder(self.app, self.db.session) + self.create_default_users(self.appbuilder) class Model1View(ModelView): datamodel = SQLAInterface(Model1) From 987123c99ae74223298e40261b76407897eb63df Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 00:20:38 +0200 Subject: [PATCH 08/24] Fix imports in mongo tests --- .github/workflows/ci.yml | 2 +- tests/test_mongoengine.py | 4 ++-- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e82ee03ec..c1df2eea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,7 +228,7 @@ jobs: pip install -r requirements-mongodb.txt - name: Run tests run: | - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder tests/test_mongoengine.py + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A 'mongo' tests - name: Upload code coverage run: | bash <(curl -s https://codecov.io/bash) -cF python diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index e425a9ee9..cec6bc44b 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -17,9 +17,7 @@ from flask_appbuilder.cli import export_roles, import_roles from flask_appbuilder.models.group import aggregate_avg, aggregate_count, aggregate_sum from flask_appbuilder.models.mongoengine.filters import FilterEqual, FilterStartsWith -from flask_appbuilder.security.mongoengine.manager import SecurityManager from flask_appbuilder.views import CompactCRUDMixin, MasterDetailView -from flask_mongoengine import MongoEngine import jinja2 from .base import FABTestCase @@ -50,7 +48,9 @@ def setUp(self): from flask import Flask from flask_appbuilder import AppBuilder from flask_appbuilder.models.mongoengine.interface import MongoEngineInterface + from flask_appbuilder.security.mongoengine.manager import SecurityManager from flask_appbuilder import ModelView + from flask_mongoengine import MongoEngine self.app = Flask(__name__) self.app.jinja_env.undefined = jinja2.StrictUndefined diff --git a/tox.ini b/tox.ini index a5dee59e6..6b66e69ca 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ deps = -rrequirements-dev.txt -rrequirements-extra.txt commands = - nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A "mongo" tests + nose2 -c setup.cfg -F -v --with-coverage --coverage flask_appbuilder -A 'mongo' tests [testenv:black] commands = From 55ea205d71bd63af803c2d012f4cdbd91fc217ed Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 00:24:30 +0200 Subject: [PATCH 09/24] Move additional imports --- tests/test_mongoengine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index cec6bc44b..f8bd8a227 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -21,7 +21,6 @@ import jinja2 from .base import FABTestCase -from .mongoengine.models import Model1, Model2 logging.basicConfig(format="%(asctime)s:%(levelname)s:%(name)s:%(message)s") logging.getLogger().setLevel(logging.DEBUG) @@ -52,6 +51,8 @@ def setUp(self): from flask_appbuilder import ModelView from flask_mongoengine import MongoEngine + from .mongoengine.models import Model1, Model2 + self.app = Flask(__name__) self.app.jinja_env.undefined = jinja2.StrictUndefined self.basedir = os.path.abspath(os.path.dirname(__file__)) From 1e1a962b3227d0587dc364a9e755504f64200ed8 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 00:32:41 +0200 Subject: [PATCH 10/24] Switch to a newer pymongo --- requirements-mongodb.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-mongodb.txt b/requirements-mongodb.txt index 07e58f778..39bcd05bb 100644 --- a/requirements-mongodb.txt +++ b/requirements-mongodb.txt @@ -1,3 +1,3 @@ flask-mongoengine==0.9.5 mongoengine==0.17.0 -pymongo==3.4.0 +pymongo==3.10.0 From 4db32c4e77713c0e5282ee0a34dd36043e95f6eb Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 10:42:39 +0200 Subject: [PATCH 11/24] Fix nose2 call --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 91b0b74db..1da8741d4 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -32,7 +32,7 @@ can run a subset of tests targeting only Postgres. .. code-block:: bash - $ nose2 -v + $ nose2 -c setup.cfg -A '!mongo' tests You can also use tox From b02d9031ca27aae414f5f0768a68f6305d34457a Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 10:54:33 +0200 Subject: [PATCH 12/24] Fix mvc security tests --- tests/security/test_mvc_security.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/security/test_mvc_security.py b/tests/security/test_mvc_security.py index 471269008..0ef3261ee 100644 --- a/tests/security/test_mvc_security.py +++ b/tests/security/test_mvc_security.py @@ -241,18 +241,19 @@ def test_auth_builtin_roles(self): """ client = self.app.test_client() self.browser_login(client, USERNAME_READONLY, PASSWORD_READONLY) - with model1_data(self.appbuilder.session, 1): + with model1_data(self.appbuilder.session, 1) as model_data: + model_id = model_data[0].id # Test authorized GET rv = client.get("/model1view/list/") self.assertEqual(rv.status_code, 200) # Test authorized SHOW - rv = client.get("/model1view/show/1") + rv = client.get(f"/model1view/show/{model_id}") self.assertEqual(rv.status_code, 200) # Test unauthorized EDIT - rv = client.get("/model1view/edit/1") + rv = client.get(f"/model1view/edit/{model_id}") self.assertEqual(rv.status_code, 302) # Test unauthorized DELETE - rv = client.get("/model1view/delete/1") + rv = client.get(f"/model1view/delete/{model_id}") self.assertEqual(rv.status_code, 302) def test_sec_reset_password(self): From d0b53b39f5d7c880b876d43195568424f63a6bc6 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 10:59:35 +0200 Subject: [PATCH 13/24] Bump ldap version --- requirements-extra.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-extra.txt b/requirements-extra.txt index 89ea25cb9..b3892f511 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -5,5 +5,5 @@ psycopg2-binary==2.9.6 pyodbc==4.0.35 requests==2.26.0 Authlib==1.2.1 -python-ldap==3.3.1 +python-ldap==3.4.3 flask-openid==1.3.0 From f17a4313cd58f70d9529e9146c5d27d2237f170a Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 11:03:36 +0200 Subject: [PATCH 14/24] Temporarily disable Python 3.12 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1df2eea4..906c419e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11"] #TODO: , "3.12"] env: SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app From 195344ac56351bca74f0660968d39c4eebc25a36 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 11:13:11 +0200 Subject: [PATCH 15/24] Fix oauth test --- tests/security/test_auth_oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/security/test_auth_oauth.py b/tests/security/test_auth_oauth.py index 6ee18c57b..7277198b3 100644 --- a/tests/security/test_auth_oauth.py +++ b/tests/security/test_auth_oauth.py @@ -378,7 +378,7 @@ def test__registered__multi_role__with_role_sync(self): self.assertIsInstance(user, sm.user_model) # validate - user was given the correct roles - self.assertListEqual(user.roles, [sm.find_role("Admin"), sm.find_role("User")]) + self.assertSetEqual(set(user.roles), {sm.find_role("Admin"), sm.find_role("User")}) def test__registered__jmespath_role__no_role_sync(self): """ From 4f43795c5cef61f1f4353171c8b03311ca8c5bc8 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 11:15:37 +0200 Subject: [PATCH 16/24] Bump pyodbc for Python 12 --- .github/workflows/ci.yml | 2 +- requirements-extra.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 906c419e2..c1df2eea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] #TODO: , "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] env: SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://pguser:pguserpassword@127.0.0.1:15432/app diff --git a/requirements-extra.txt b/requirements-extra.txt index b3892f511..871115f7a 100644 --- a/requirements-extra.txt +++ b/requirements-extra.txt @@ -2,7 +2,7 @@ Pillow~=9.1 cython==0.29.17 mysqlclient==2.0.1 psycopg2-binary==2.9.6 -pyodbc==4.0.35 +pyodbc==5.0.1 requests==2.26.0 Authlib==1.2.1 python-ldap==3.4.3 From e6af54927a4e206a5b4c0bce4a5fb0f0df1ed0ad Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 11:26:41 +0200 Subject: [PATCH 17/24] Try to bump mongo versions --- requirements-mongodb.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-mongodb.txt b/requirements-mongodb.txt index 39bcd05bb..f1bed051b 100644 --- a/requirements-mongodb.txt +++ b/requirements-mongodb.txt @@ -1,3 +1,3 @@ -flask-mongoengine==0.9.5 -mongoengine==0.17.0 -pymongo==3.10.0 +flask-mongoengine==1.0.0 +mongoengine==0.27.0 +pymongo==4.5.0 From 4e11cac038bc495d3147634e28899c0bf2a842fa Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 11:37:50 +0200 Subject: [PATCH 18/24] Fix mock call asserts --- flask_appbuilder/models/generic/__init__.py | 2 +- tests/security/test_base_security_manager.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flask_appbuilder/models/generic/__init__.py b/flask_appbuilder/models/generic/__init__.py index a1e1432e9..e5bf22b97 100644 --- a/flask_appbuilder/models/generic/__init__.py +++ b/flask_appbuilder/models/generic/__init__.py @@ -396,7 +396,7 @@ class PSModel(GenericModel): class PSSession(GenericSession): regexp = ( - "(\w+) +(\w+) +(\w+) +(\w+) +(\w+:\w+|\w+) (\?|tty\w+) +(\w+:\w+:\w+) +(.+)\n" + r"(\w+) +(\w+) +(\w+) +(\w+) +(\w+:\w+|\w+) (\?|tty\w+) +(\w+:\w+:\w+) +(.+)\n" ) def add_object(self, line): diff --git a/tests/security/test_base_security_manager.py b/tests/security/test_base_security_manager.py index dfb04ea1a..c58d38adf 100644 --- a/tests/security/test_base_security_manager.py +++ b/tests/security/test_base_security_manager.py @@ -23,7 +23,7 @@ def test_first_successful_auth(self, mock1, mock2): self.assertEqual(user_mock.login_count, 1) self.assertEqual(user_mock.fail_login_count, 0) self.assertEqual(type(user_mock.last_login), datetime.datetime) - self.assertTrue(bsm.update_user.called_once) + bsm.update_user.assert_called_once() def test_first_unsuccessful_auth(self, mock1, mock2): bsm = BaseSecurityManager() @@ -38,7 +38,7 @@ def test_first_unsuccessful_auth(self, mock1, mock2): self.assertEqual(user_mock.login_count, 0) self.assertEqual(user_mock.fail_login_count, 1) self.assertEqual(user_mock.last_login, None) - self.assertTrue(bsm.update_user.called_once) + bsm.update_user.assert_called_once() def test_subsequent_successful_auth(self, mock1, mock2): bsm = BaseSecurityManager() @@ -53,7 +53,7 @@ def test_subsequent_successful_auth(self, mock1, mock2): self.assertEqual(user_mock.login_count, 6) self.assertEqual(user_mock.fail_login_count, 0) self.assertEqual(type(user_mock.last_login), datetime.datetime) - self.assertTrue(bsm.update_user.called_once) + bsm.update_user.assert_called_once() def test_subsequent_unsuccessful_auth(self, mock1, mock2): bsm = BaseSecurityManager() @@ -68,4 +68,4 @@ def test_subsequent_unsuccessful_auth(self, mock1, mock2): self.assertEqual(user_mock.login_count, 5) self.assertEqual(user_mock.fail_login_count, 10) self.assertEqual(user_mock.last_login, None) - self.assertTrue(bsm.update_user.called_once) + bsm.update_user.assert_called_once() From 0e0608fdd88f63af831e72788468232d25f1b430 Mon Sep 17 00:00:00 2001 From: Dosenpfand Date: Sun, 22 Oct 2023 12:16:20 +0200 Subject: [PATCH 19/24] Autoformat with black --- flask_appbuilder/_compat.py | 1 - flask_appbuilder/actions.py | 28 ++--- flask_appbuilder/api/manager.py | 3 +- flask_appbuilder/babel/manager.py | 1 - flask_appbuilder/basemanager.py | 2 +- flask_appbuilder/baseviews.py | 2 - flask_appbuilder/charts/jsontools.py | 20 +-- flask_appbuilder/charts/views.py | 116 +++++++++--------- flask_appbuilder/console.py | 44 +++---- flask_appbuilder/filemanager.py | 44 ++++--- flask_appbuilder/filters.py | 24 ++-- flask_appbuilder/forms.py | 52 ++++---- flask_appbuilder/hooks.py | 82 ++++++------- flask_appbuilder/menu.py | 6 +- flask_appbuilder/models/decorators.py | 26 ++-- flask_appbuilder/models/filters.py | 58 ++++----- flask_appbuilder/models/generic/__init__.py | 56 ++++----- flask_appbuilder/models/generic/filters.py | 4 +- flask_appbuilder/models/generic/interface.py | 4 +- flask_appbuilder/models/group.py | 74 ++++++----- flask_appbuilder/models/mixins.py | 22 ++-- flask_appbuilder/models/mongoengine/fields.py | 4 +- .../models/mongoengine/filters.py | 4 +- .../models/mongoengine/interface.py | 16 ++- flask_appbuilder/models/sqla/__init__.py | 36 +++--- flask_appbuilder/security/api.py | 1 - .../security/mongoengine/manager.py | 84 ++++++------- flask_appbuilder/security/registerviews.py | 60 ++++----- .../security/sqla/apis/user/validator.py | 3 +- flask_appbuilder/security/sqla/manager.py | 96 +++++++-------- flask_appbuilder/upload.py | 21 ++-- flask_appbuilder/urltools.py | 24 ++-- flask_appbuilder/validators.py | 2 +- flask_appbuilder/views.py | 1 - flask_appbuilder/widgets.py | 96 +++++++-------- setup.py | 2 +- 36 files changed, 547 insertions(+), 572 deletions(-) diff --git a/flask_appbuilder/_compat.py b/flask_appbuilder/_compat.py index 5ba06128c..818158e06 100644 --- a/flask_appbuilder/_compat.py +++ b/flask_appbuilder/_compat.py @@ -26,7 +26,6 @@ def as_unicode(s): return s.decode("utf-8") return str(s) - else: text_type = unicode # noqa string_types = (str, unicode) # noqa diff --git a/flask_appbuilder/actions.py b/flask_appbuilder/actions.py index 3b645cf60..6ad0dec39 100644 --- a/flask_appbuilder/actions.py +++ b/flask_appbuilder/actions.py @@ -19,21 +19,21 @@ def __repr__(self): def action(name, text, confirmation=None, icon=None, multiple=True, single=True): """ - Use this decorator to expose actions + Use this decorator to expose actions - :param name: - Action name - :param text: - Action text. - :param confirmation: - Confirmation text. If not provided, action will be executed - unconditionally. - :param icon: - Font Awesome icon name - :param multiple: - If true will display action on list view - :param single: - If true will display action on show view + :param name: + Action name + :param text: + Action text. + :param confirmation: + Confirmation text. If not provided, action will be executed + unconditionally. + :param icon: + Font Awesome icon name + :param multiple: + If true will display action on list view + :param single: + If true will display action on show view """ def wrap(f): diff --git a/flask_appbuilder/api/manager.py b/flask_appbuilder/api/manager.py index 751331bb3..6999a9e40 100644 --- a/flask_appbuilder/api/manager.py +++ b/flask_appbuilder/api/manager.py @@ -30,7 +30,7 @@ class OpenApi(BaseApi): @protect() @safe def get(self, version): - """ Endpoint that renders an OpenApi spec for all views that belong + """Endpoint that renders an OpenApi spec for all views that belong to a certain version --- get: @@ -80,7 +80,6 @@ def _create_api_spec(version): class SwaggerView(BaseView): - route_base = "/swagger" default_view = "ui" openapi_uri = "/api/{}/_openapi" diff --git a/flask_appbuilder/babel/manager.py b/flask_appbuilder/babel/manager.py index b011e0102..f3df0c7a7 100644 --- a/flask_appbuilder/babel/manager.py +++ b/flask_appbuilder/babel/manager.py @@ -7,7 +7,6 @@ class BabelManager(BaseManager): - babel = None locale_view = None diff --git a/flask_appbuilder/basemanager.py b/flask_appbuilder/basemanager.py index 695a0a6c7..c9521beff 100644 --- a/flask_appbuilder/basemanager.py +++ b/flask_appbuilder/basemanager.py @@ -1,6 +1,6 @@ class BaseManager(object): """ - The parent class for all Managers + The parent class for all Managers """ def __init__(self, appbuilder): diff --git a/flask_appbuilder/baseviews.py b/flask_appbuilder/baseviews.py index 78788f84a..ad1dc7379 100644 --- a/flask_appbuilder/baseviews.py +++ b/flask_appbuilder/baseviews.py @@ -71,7 +71,6 @@ def wrap(f): class AbstractViewApi: - appbuilder: "AppBuilder" base_permissions: Optional[List[str]] class_permission_name: str @@ -1069,7 +1068,6 @@ def _get_list_widget( widgets=None, **args, ): - """get joined base filter and current active filter for query""" widgets = widgets or {} actions = actions or self.actions diff --git a/flask_appbuilder/charts/jsontools.py b/flask_appbuilder/charts/jsontools.py index cebb875bc..b41b7c307 100644 --- a/flask_appbuilder/charts/jsontools.py +++ b/flask_appbuilder/charts/jsontools.py @@ -5,17 +5,17 @@ def dict_to_json(xcol, ycols, labels, value_columns): # pragma: no cover """ - Converts a list of dicts from datamodel query results - to google chart json data. + Converts a list of dicts from datamodel query results + to google chart json data. - :param xcol: - The name of a string column to be used has X axis on chart - :param ycols: - A list with the names of series cols, that can be used as numeric - :param labels: - A dict with the columns labels. - :param value_columns: - A list of dicts with the values to convert + :param xcol: + The name of a string column to be used has X axis on chart + :param ycols: + A list with the names of series cols, that can be used as numeric + :param labels: + A dict with the columns labels. + :param value_columns: + A list of dicts with the values to convert """ json_data = dict() diff --git a/flask_appbuilder/charts/views.py b/flask_appbuilder/charts/views.py index 6a35b1014..c00137690 100644 --- a/flask_appbuilder/charts/views.py +++ b/flask_appbuilder/charts/views.py @@ -15,10 +15,10 @@ class BaseChartView(BaseModelView): """ - This is the base class for all chart views. - Use DirectByChartView or GroupByChartView, override their properties - and their base classes - (BaseView, BaseModelView, BaseChartView) to customise your charts + This is the base class for all chart views. + Use DirectByChartView or GroupByChartView, override their properties + and their base classes + (BaseView, BaseModelView, BaseChartView) to customise your charts """ chart_template = "appbuilder/general/charts/chart.html" @@ -60,8 +60,8 @@ def _get_chart_widget(self, filters=None, widgets=None, **args): def _get_view_widget(self, **kwargs): """ - :return: - Returns a widget + :return: + Returns a widget """ return self._get_chart_widget(**kwargs).get("chart") @@ -139,7 +139,7 @@ def __init__(self, **kwargs): def get_group_by_class(self, definition): """ - intantiates the processing class (Direct or Grouped) and returns it. + intantiates the processing class (Direct or Grouped) and returns it. """ group_by = definition["group"] series = definition["series"] @@ -160,7 +160,6 @@ def _get_chart_widget( definition="", **args ): - height = height or self.height widgets = widgets or dict() joined_filters = filters.get_joined_filters(self._base_filters) @@ -221,45 +220,45 @@ def chart(self, group_by=0): class DirectByChartView(GroupByChartView): """ - Use this class to display charts with multiple series, - based on columns or methods defined on models. - You can display multiple charts on the same view. + Use this class to display charts with multiple series, + based on columns or methods defined on models. + You can display multiple charts on the same view. - Default routing point is '/chart' + Default routing point is '/chart' - Setup definitions property to configure the chart + Setup definitions property to configure the chart - :label: (optional) String label to display on chart selection. - :group: String with the column name or method from model. - :formatter: (optional) function that formats the output of 'group' key - :series: A list of tuples with the aggregation function and the column name - to apply the aggregation + :label: (optional) String label to display on chart selection. + :group: String with the column name or method from model. + :formatter: (optional) function that formats the output of 'group' key + :series: A list of tuples with the aggregation function and the column name + to apply the aggregation - The **definitions** property respects the following grammar:: + The **definitions** property respects the following grammar:: - definitions = [ - { - 'label': 'label for chart definition', - 'group': ''|'', - 'formatter': , - 'series': [''|'',...] - }, ... - ] + definitions = [ + { + 'label': 'label for chart definition', + 'group': ''|'', + 'formatter': , + 'series': [''|'',...] + }, ... + ] - example:: + example:: - class CountryDirectChartView(DirectByChartView): - datamodel = SQLAInterface(CountryStats) - chart_title = 'Direct Data Example' + class CountryDirectChartView(DirectByChartView): + datamodel = SQLAInterface(CountryStats) + chart_title = 'Direct Data Example' - definitions = [ - { - 'label': 'Unemployment', - 'group': 'stat_date', - 'series': ['unemployed_perc', - 'college_perc'] - } - ] + definitions = [ + { + 'label': 'Unemployment', + 'group': 'stat_date', + 'series': ['unemployed_perc', + 'college_perc'] + } + ] """ @@ -293,7 +292,6 @@ def _get_chart_widget( height=None, **args ): - height = height or self.height widgets = widgets or dict() group_by = group_by or self.group_by_columns[0] @@ -333,8 +331,8 @@ def __init__(self, **kwargs): def get_group_by_columns(self): """ - returns the keys from direct_columns - Used in template, so that user can choose from options + returns the keys from direct_columns + Used in template, so that user can choose from options """ return list(self.direct_columns.keys()) @@ -348,7 +346,6 @@ def _get_chart_widget( height=None, **args ): - height = height or self.height widgets = widgets or dict() joined_filters = filters.get_joined_filters(self._base_filters) @@ -377,11 +374,11 @@ def _get_chart_widget( class ChartView(BaseSimpleGroupByChartView): # pragma: no cover """ - **DEPRECATED** + **DEPRECATED** - Provides a simple (and hopefully nice) way to draw charts on your application. + Provides a simple (and hopefully nice) way to draw charts on your application. - This will show Google Charts based on group by of your tables. + This will show Google Charts based on group by of your tables. """ @expose("/chart/") @@ -410,12 +407,12 @@ def chart(self, group_by=""): class TimeChartView(BaseSimpleGroupByChartView): # pragma: no cover """ - **DEPRECATED** + **DEPRECATED** - Provides a simple way to draw some time charts on your application. + Provides a simple way to draw some time charts on your application. - This will show Google Charts based on count and group - by month and year for your tables. + This will show Google Charts based on count and group + by month and year for your tables. """ chart_template = "appbuilder/general/charts/chart_time.html" @@ -432,7 +429,6 @@ def _get_chart_widget( height=None, **args ): - height = height or self.height widgets = widgets or dict() group_by = group_by or self.group_by_columns[0] @@ -487,17 +483,17 @@ def chart(self, group_by="", period=""): class DirectChartView(BaseSimpleDirectChartView): # pragma: no cover """ - **DEPRECATED** + **DEPRECATED** - This class is responsible for displaying a Google chart with - direct model values. Chart widget uses json. - No group by is processed, example:: + This class is responsible for displaying a Google chart with + direct model values. Chart widget uses json. + No group by is processed, example:: - class StatsChartView(DirectChartView): - datamodel = SQLAInterface(Stats) - chart_title = lazy_gettext('Statistics') - direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'), - 'Other Stats': ('X_col2', 'stat_col_3')} + class StatsChartView(DirectChartView): + datamodel = SQLAInterface(Stats) + chart_title = lazy_gettext('Statistics') + direct_columns = {'Some Stats': ('X_col_1', 'stat_col_1', 'stat_col_2'), + 'Other Stats': ('X_col2', 'stat_col_3')} """ diff --git a/flask_appbuilder/console.py b/flask_appbuilder/console.py index a8a26ca6f..cd1067977 100644 --- a/flask_appbuilder/console.py +++ b/flask_appbuilder/console.py @@ -73,17 +73,17 @@ def echo_header(title): @click.group() def cli_app(): """ - This is a set of commands to ease the creation and maintenance - of your flask-appbuilder applications. + This is a set of commands to ease the creation and maintenance + of your flask-appbuilder applications. - All commands that import your app will assume by default that - you're running on your projects directory just before the app directory. - They will also assume that __init__.py initializes AppBuilder - like this (using a var named appbuilder) just like the skeleton app:: + All commands that import your app will assume by default that + you're running on your projects directory just before the app directory. + They will also assume that __init__.py initializes AppBuilder + like this (using a var named appbuilder) just like the skeleton app:: - appbuilder = AppBuilder(......) + appbuilder = AppBuilder(......) - If you're using different namings use app and appbuilder parameters. + If you're using different namings use app and appbuilder parameters. """ pass @@ -100,7 +100,7 @@ def cli_app(): @click.password_option() def reset_password(app, appbuilder, username, password): """ - Resets a user's password + Resets a user's password """ _appbuilder = import_application(app, appbuilder) user = _appbuilder.sm.find_user(username=username) @@ -121,7 +121,7 @@ def reset_password(app, appbuilder, username, password): @click.password_option() def create_admin(app, appbuilder, username, firstname, lastname, email, password): """ - Creates an admin user + Creates an admin user """ auth_type = { c.AUTH_DB: "Database Authentications", @@ -160,7 +160,7 @@ def create_admin(app, appbuilder, username, firstname, lastname, email, password @click.password_option() def create_user(app, appbuilder, role, username, firstname, lastname, email, password): """ - Create a user + Create a user """ _appbuilder = import_application(app, appbuilder) role_object = _appbuilder.sm.find_role(role) @@ -181,7 +181,7 @@ def create_user(app, appbuilder, role, username, firstname, lastname, email, pas @click.option("--debug", default=True) def run(app, appbuilder, host, port, debug): """ - Runs Flask dev web server. + Runs Flask dev web server. """ _appbuilder = import_application(app, appbuilder) _appbuilder.get_app.run(host=host, port=port, debug=debug) @@ -192,7 +192,7 @@ def run(app, appbuilder, host, port, debug): @click.option("--appbuilder", default="appbuilder", help="your AppBuilder object") def create_db(app, appbuilder): """ - Create all your database objects (SQLAlchemy specific). + Create all your database objects (SQLAlchemy specific). """ from flask_appbuilder.models.sqla import Base @@ -207,7 +207,7 @@ def create_db(app, appbuilder): @click.option("--appbuilder", default="appbuilder", help="your AppBuilder object") def version(app, appbuilder): """ - Flask-AppBuilder package version + Flask-AppBuilder package version """ _appbuilder = import_application(app, appbuilder) click.echo( @@ -222,7 +222,7 @@ def version(app, appbuilder): @click.option("--appbuilder", default="appbuilder", help="your AppBuilder object") def security_cleanup(app, appbuilder): """ - Cleanup unused permissions from views and roles. + Cleanup unused permissions from views and roles. """ _appbuilder = import_application(app, appbuilder) _appbuilder.security_cleanup() @@ -234,7 +234,7 @@ def security_cleanup(app, appbuilder): @click.option("--appbuilder", default="appbuilder", help="your AppBuilder object") def list_views(app, appbuilder): """ - List all registered views + List all registered views """ _appbuilder = import_application(app, appbuilder) echo_header("List of registered views") @@ -251,7 +251,7 @@ def list_views(app, appbuilder): @click.option("--appbuilder", default="appbuilder", help="your AppBuilder object") def list_users(app, appbuilder): """ - List all users on the database + List all users on the database """ _appbuilder = import_application(app, appbuilder) echo_header("List of users") @@ -273,7 +273,7 @@ def list_users(app, appbuilder): ) def babel_extract(config, input, output, target, keywords): """ - Babel, Extracts and updates all messages marked for translation + Babel, Extracts and updates all messages marked for translation """ click.echo( click.style( @@ -302,7 +302,7 @@ def babel_extract(config, input, output, target, keywords): ) def babel_compile(target): """ - Babel, Compiles all translations + Babel, Compiles all translations """ click.echo(click.style("Starting Compile target:{0}".format(target), fg="green")) os.popen("pybabel compile -f -d {0}".format(target)) @@ -323,7 +323,7 @@ def babel_compile(target): ) def create_app(name, engine): """ - Create a Skeleton application (needs internet connection to github) + Create a Skeleton application (needs internet connection to github) """ try: if engine.lower() == "sqlalchemy": @@ -362,7 +362,7 @@ def create_app(name, engine): ) def create_addon(name): """ - Create a Skeleton AddOn (needs internet connection to github) + Create a Skeleton AddOn (needs internet connection to github) """ try: full_name = "fab_addon_" + name @@ -392,7 +392,7 @@ def create_addon(name): ) def collect_static(static_folder): """ - Copies flask-appbuilder static files to your projects static folder + Copies flask-appbuilder static files to your projects static folder """ appbuilder_static_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "static/appbuilder" diff --git a/flask_appbuilder/filemanager.py b/flask_appbuilder/filemanager.py index 3f1404056..463072686 100644 --- a/flask_appbuilder/filemanager.py +++ b/flask_appbuilder/filemanager.py @@ -35,7 +35,6 @@ def __init__( permission=0o755, **kwargs ): - ctx = app_stack.top if "UPLOAD_FOLDER" in ctx.app.config and not base_path: @@ -85,8 +84,8 @@ def save_file(self, data, filename): class ImageManager(FileManager): """ - Image Manager will manage your image files referenced on SQLAlchemy Model - will save files on IMG_UPLOAD_FOLDER as _sep_ + Image Manager will manage your image files referenced on SQLAlchemy Model + will save files on IMG_UPLOAD_FOLDER as _sep_ """ keep_image_formats = ("PNG",) @@ -103,7 +102,6 @@ def __init__( permission=0o755, **kwargs ): - # Check if PIL is installed if Image is None: raise Exception("PIL library was not found") @@ -161,10 +159,10 @@ def delete_thumbnail(self, filename): # Saving def save_file(self, data, filename, size=None, thumbnail_size=None): """ - Saves an image File + Saves an image File - :param data: FileStorage from Flask form upload field - :param filename: Filename with full path + :param data: FileStorage from Flask form upload field + :param filename: Filename with full path """ max_size = size or self.max_size @@ -204,10 +202,10 @@ def save_thumbnail(self, data, filename, format, thumbnail_size=None): def resize(self, image, size): """ - Resizes the image + Resizes the image - :param image: The image object - :param size: size is PIL tuple (width, heigth, force) ex: (200,100,True) + :param image: The image object + :param size: size is PIL tuple (width, heigth, force) ex: (200,100,True) """ (width, height, force) = size @@ -241,23 +239,23 @@ def uuid_namegen(file_data): def get_file_original_name(name): """ - Use this function to get the user's original filename. - Filename is concatenated with _sep_, to avoid collisions. - Use this function on your models on an additional function + Use this function to get the user's original filename. + Filename is concatenated with _sep_, to avoid collisions. + Use this function on your models on an additional function - :: + :: - class ProjectFiles(Base): - id = Column(Integer, primary_key=True) - file = Column(FileColumn, nullable=False) + class ProjectFiles(Base): + id = Column(Integer, primary_key=True) + file = Column(FileColumn, nullable=False) - def file_name(self): - return get_file_original_name(str(self.file)) + def file_name(self): + return get_file_original_name(str(self.file)) - :param name: - The file name from model - :return: - Returns the user's original filename removes _sep_ + :param name: + The file name from model + :return: + Returns the user's original filename removes _sep_ """ re_match = re.findall(".*_sep_(.*)", name) if re_match: diff --git a/flask_appbuilder/filters.py b/flask_appbuilder/filters.py index 22cbc4839..ee6e1d318 100755 --- a/flask_appbuilder/filters.py +++ b/flask_appbuilder/filters.py @@ -13,11 +13,9 @@ def wrap(f): class TemplateFilters(object): - security_manager = None def __init__(self, app, security_manager): - self.security_manager = security_manager for attr_name in dir(self): if hasattr(getattr(self, attr_name), "_filter"): @@ -52,8 +50,8 @@ def safe_url_for(self, endpoint, **values): @app_template_filter("link_order") def link_order_filter(self, column, modelview_name): """ - Arguments are passed like: - _oc_=&_od_='asc'|'desc' + Arguments are passed like: + _oc_=&_od_='asc'|'desc' """ new_args = request.view_args.copy() args = request.args.copy() @@ -74,7 +72,7 @@ def link_order_filter(self, column, modelview_name): @app_template_filter("link_page") def link_page_filter(self, page, modelview_name): """ - Arguments are passed like: page_= + Arguments are passed like: page_= """ new_args = request.view_args.copy() args = request.args.copy() @@ -144,14 +142,14 @@ def find_views_by_name(view_name): @app_template_filter("is_item_visible") def is_item_visible(self, permission: str, item: str) -> bool: """ - Check if an item is visible on the template - this changed with permission mapping feature. - This is a best effort to deliver the feature - and not break compatibility - - permission is: - - 'can_' + : On normal routes - - : when it's an action + Check if an item is visible on the template + this changed with permission mapping feature. + This is a best effort to deliver the feature + and not break compatibility + + permission is: + - 'can_' + : On normal routes + - : when it's an action """ _view = self.find_views_by_name(item) diff --git a/flask_appbuilder/forms.py b/flask_appbuilder/forms.py index 509bdc074..578b4a723 100644 --- a/flask_appbuilder/forms.py +++ b/flask_appbuilder/forms.py @@ -41,10 +41,10 @@ class FieldConverter(object): """ - Helper class that converts model fields into WTForm fields + Helper class that converts model fields into WTForm fields - it has a conversion table with type method checks from model - interfaces, these methods are invoked with a column name + it has a conversion table with type method checks from model + interfaces, these methods are invoked with a column name """ conversion_table = ( @@ -109,8 +109,8 @@ def convert(self): class GeneralModelConverter(object): """ - Returns a form from a model only one public exposed - method 'create_form' + Returns a form from a model only one public exposed + method 'create_form' """ def __init__(self, datamodel): @@ -153,9 +153,9 @@ def _convert_many_to_one( form_props, ): """ - Creates a WTForm field for many to one related fields, - will use a Select box based on a query. Will only - work with SQLAlchemy interface. + Creates a WTForm field for many to one related fields, + will use a Select box based on a query. Will only + work with SQLAlchemy interface. """ query_func = self._get_related_query_func(col_name, filter_rel_fields) get_pk_func = self._get_related_pk_func(col_name) @@ -273,28 +273,28 @@ def create_form( filter_rel_fields=None, ): """ - Converts a model to a form given + Converts a model to a form given - :param label_columns: - A dictionary with the column's labels. - :param inc_columns: - A list with the columns to include - :param description_columns: - A dictionary with a description for cols. - :param validators_columns: - A dictionary with WTForms validators ex:: + :param label_columns: + A dictionary with the column's labels. + :param inc_columns: + A list with the columns to include + :param description_columns: + A dictionary with a description for cols. + :param validators_columns: + A dictionary with WTForms validators ex:: - validators={'personal_email':EmailValidator} + validators={'personal_email':EmailValidator} - :param extra_fields: - A dictionary containing column names and a WTForm - Form fields to be added to the form, these fields do not - exist on the model itself ex:: + :param extra_fields: + A dictionary containing column names and a WTForm + Form fields to be added to the form, these fields do not + exist on the model itself ex:: - extra_fields={'some_col':BooleanField('Some Col', default=False)} + extra_fields={'some_col':BooleanField('Some Col', default=False)} - :param filter_rel_fields: - A filter to be applied on relationships + :param filter_rel_fields: + A filter to be applied on relationships """ label_columns = label_columns or {} inc_columns = inc_columns or [] @@ -319,7 +319,7 @@ def create_form( class DynamicForm(FlaskForm): """ - Refresh method will force select field to refresh + Refresh method will force select field to refresh """ @classmethod diff --git a/flask_appbuilder/hooks.py b/flask_appbuilder/hooks.py index 390040e75..a9d2827dd 100644 --- a/flask_appbuilder/hooks.py +++ b/flask_appbuilder/hooks.py @@ -5,47 +5,47 @@ def before_request( hook: Callable[[], Any] = None, only: List[str] = None ) -> Callable[..., Any]: """ - This decorator provides a way to hook into the request - lifecycle by enqueueing methods to be invoked before - each handler in the view. If the method returns a value - other than :code:`None`, then that value will be returned - to the client. If invoked with the :code:`only` kwarg, - the hook will only be invoked for the given list of - handler methods. - - Examples:: - - class MyFeature(ModelView) - - @before_request - def ensure_feature_is_enabled(self): - if self.feature_is_disabled: - return self.response_404() - return None - - # etc... - - - class MyView(ModelRestAPI): - - @before_request(only=["create", "update", "delete"]) - def ensure_write_mode_enabled(self): - if self.read_only: - return self.response_400() - return None - - # etc... - - :param hook: - A callable to be invoked before handlers in the class. If the - hook returns :code:`None`, then the request proceeds and the - handler is invoked. If it returns something other than :code:`None`, - then execution halts and that value is returned to the client. - :param only: - An optional list of the names of handler methods. If present, - :code:`hook` will only be invoked before the handlers specified - in the list. If absent, :code:`hook` will be invoked for before - all handlers in the class. + This decorator provides a way to hook into the request + lifecycle by enqueueing methods to be invoked before + each handler in the view. If the method returns a value + other than :code:`None`, then that value will be returned + to the client. If invoked with the :code:`only` kwarg, + the hook will only be invoked for the given list of + handler methods. + + Examples:: + + class MyFeature(ModelView) + + @before_request + def ensure_feature_is_enabled(self): + if self.feature_is_disabled: + return self.response_404() + return None + + # etc... + + + class MyView(ModelRestAPI): + + @before_request(only=["create", "update", "delete"]) + def ensure_write_mode_enabled(self): + if self.read_only: + return self.response_400() + return None + + # etc... + + :param hook: + A callable to be invoked before handlers in the class. If the + hook returns :code:`None`, then the request proceeds and the + handler is invoked. If it returns something other than :code:`None`, + then execution halts and that value is returned to the client. + :param only: + An optional list of the names of handler methods. If present, + :code:`hook` will only be invoked before the handlers specified + in the list. If absent, :code:`hook` will be invoked for before + all handlers in the class. """ def wrap(hook: Callable[[], Any]) -> Callable[[], Any]: diff --git a/flask_appbuilder/menu.py b/flask_appbuilder/menu.py index 4f51acedb..9c86be9b9 100644 --- a/flask_appbuilder/menu.py +++ b/flask_appbuilder/menu.py @@ -100,10 +100,10 @@ def get_data(self, menu=None): def find(self, name, menu=None): """ - Finds a menu item by name and returns it. + Finds a menu item by name and returns it. - :param name: - The menu item name. + :param name: + The menu item name. """ menu = menu or self.menu for i in menu: diff --git a/flask_appbuilder/models/decorators.py b/flask_appbuilder/models/decorators.py index f505c6358..d01db405e 100644 --- a/flask_appbuilder/models/decorators.py +++ b/flask_appbuilder/models/decorators.py @@ -1,21 +1,21 @@ def renders(col_name): """ - Use this decorator to map your custom Model properties to actual - Model db properties. As an example:: + Use this decorator to map your custom Model properties to actual + Model db properties. As an example:: - class MyModel(Model): - id = Column(Integer, primary_key=True) - name = Column(String(50), unique = True, nullable=False) - custom = Column(Integer(20)) + class MyModel(Model): + id = Column(Integer, primary_key=True) + name = Column(String(50), unique = True, nullable=False) + custom = Column(Integer(20)) - @renders('custom') - def my_custom(self): - # will render this columns as bold on ListWidget - return Markup('' + self.custom + '') + @renders('custom') + def my_custom(self): + # will render this columns as bold on ListWidget + return Markup('' + self.custom + '') - class MyModelView(ModelView): - datamodel = SQLAInterface(MyTable) - list_columns = ['name', 'my_custom'] + class MyModelView(ModelView): + datamodel = SQLAInterface(MyTable) + list_columns = ['name', 'my_custom'] """ diff --git a/flask_appbuilder/models/filters.py b/flask_appbuilder/models/filters.py index 493302920..5aaaf6b75 100644 --- a/flask_appbuilder/models/filters.py +++ b/flask_appbuilder/models/filters.py @@ -16,8 +16,8 @@ class BaseFilter(object): """ - Base class for all data filters. - Sub class to implement your own custom filters + Base class for all data filters. + Sub class to implement your own custom filters """ column_name = "" @@ -39,14 +39,14 @@ class BaseFilter(object): def __init__(self, column_name, datamodel, is_related_view=False): """ - Constructor. - - :param column_name: - Model field name - :param datamodel: - The datamodel access class - :param is_related_view: - Optional internal parameter to filter related views + Constructor. + + :param column_name: + Model field name + :param datamodel: + The datamodel access class + :param is_related_view: + Optional internal parameter to filter related views """ self.column_name = column_name self.datamodel = datamodel @@ -57,7 +57,7 @@ def __init__(self, column_name, datamodel, is_related_view=False): def apply(self, query, value): """ - Override this to implement your own new filters + Override this to implement your own new filters """ raise NotImplementedError @@ -67,21 +67,21 @@ def __repr__(self): class FilterRelation(BaseFilter): """ - Base class for all filters for relations + Base class for all filters for relations """ def apply(self, query, value): """ - Override this to implement your own new filters + Override this to implement your own new filters """ raise NotImplementedError class BaseFilterConverter: """ - Base Filter Converter, all classes responsible - for the association of columns and possible filters - will inherit from this and override the conversion_table property. + Base Filter Converter, all classes responsible + for the association of columns and possible filters + will inherit from this and override the conversion_table property. """ @@ -135,11 +135,11 @@ def __init__( ): """ - :param filter_converter: Accepts BaseFilterConverter class - :param search_columns: restricts possible columns, - accepts a list of column names - :param search_filters: Add custom defined filters to specific columns - :param datamodel: Accepts BaseInterface class + :param filter_converter: Accepts BaseFilterConverter class + :param search_columns: restricts possible columns, + accepts a list of column names + :param search_filters: Add custom defined filters to specific columns + :param datamodel: Accepts BaseInterface class """ self.search_columns = search_columns or [] self.filter_converter = filter_converter @@ -242,7 +242,7 @@ def add_filter_list(self, active_filter_list=None): def get_joined_filters(self, filters) -> "Filters": """ - Creates a new filters class with active filters joined + Creates a new filters class with active filters joined """ ret_filters = Filters(self.filter_converter, self.datamodel) ret_filters.filters = self.filters + filters.filters @@ -251,9 +251,9 @@ def get_joined_filters(self, filters) -> "Filters": def copy(self): """ - Returns a copy of this object + Returns a copy of this object - :return: A copy of self + :return: A copy of self """ retfilters = Filters(self.filter_converter, self.datamodel) retfilters.filters = copy.copy(self.filters) @@ -262,7 +262,7 @@ def copy(self): def get_relation_cols(self): """ - Returns the filter active FilterRelation cols + Returns the filter active FilterRelation cols """ retlst = [] for flt, value in zip(self.filters, self.values): @@ -272,16 +272,16 @@ def get_relation_cols(self): def get_filters_values(self) -> List[Tuple[BaseFilter, Any]]: """ - Returns a list of tuples [(FILTER, value),(...,...),....] + Returns a list of tuples [(FILTER, value),(...,...),....] """ return [(flt, value) for flt, value in zip(self.filters, self.values)] def get_filter_value(self, column_name: str) -> Any: """ - Returns the filtered value for a certain column + Returns the filtered value for a certain column - :param column_name: The name of the column that we want the value from - :return: the filter value of the column + :param column_name: The name of the column that we want the value from + :return: the filter value of the column """ for flt, value in zip(self.filters, self.values): if flt.column_name == column_name: diff --git a/flask_appbuilder/models/generic/__init__.py b/flask_appbuilder/models/generic/__init__.py index e5bf22b97..536b5bd1b 100644 --- a/flask_appbuilder/models/generic/__init__.py +++ b/flask_appbuilder/models/generic/__init__.py @@ -35,13 +35,13 @@ def check_type(self, value): class MetaGenericModel(type): """ - Meta class for GenericModel - will change default properties: - - instantiates internal '_col_defs' dict with - all the defined columns. - - Define pk property with the name of the primary key column - - Define properties with a list of all column's properties - - Define columns with a list of all column's name + Meta class for GenericModel + will change default properties: + - instantiates internal '_col_defs' dict with + all the defined columns. + - Define pk property with the name of the primary key column + - Define properties with a list of all column's properties + - Define columns with a list of all column's name """ pk = None @@ -68,18 +68,18 @@ def __new__(meta, name, bases, dct): class GenericModel(with_metaclass(MetaGenericModel, object)): """ - Generic Model class to define generic purpose models to use - with the framework. + Generic Model class to define generic purpose models to use + with the framework. - Use GenericSession much like SQLAlchemy's Session Class. - Extend GenericSession to implement specific engine features. + Use GenericSession much like SQLAlchemy's Session Class. + Extend GenericSession to implement specific engine features. - Define your models like:: + Define your models like:: - class MyGenericModel(GenericModel): - id = GenericColumn(int, primary_key=True) - age = GenericColumn(int) - name = GenericColumn(str) + class MyGenericModel(GenericModel): + id = GenericColumn(int, primary_key=True) + age = GenericColumn(int) + name = GenericColumn(str) """ @@ -111,13 +111,13 @@ def __str__(self): class GenericSession(object): """ - This class is a base, you should subclass it - to implement your own generic data source. + This class is a base, you should subclass it + to implement your own generic data source. - Override at least the **all** method. + Override at least the **all** method. - **GenericSession** will implement filter and orders - based on your data generation on the **all** method. + **GenericSession** will implement filter and orders + based on your data generation on the **all** method. """ def __init__(self): @@ -131,20 +131,20 @@ def __init__(self): def clear(self): """ - Deletes the entire store + Deletes the entire store """ self.store = dict() def delete_all(self, model_cls): """ - Deletes all objects of type model_cls + Deletes all objects of type model_cls """ self.store[model_cls._name] = [] def get(self, pk): """ - Returns the object for the key - Override it for efficiency. + Returns the object for the key + Override it for efficiency. """ for item in self.store.get(self.query_class): # coverts pk value to correct type @@ -154,7 +154,7 @@ def get(self, pk): def query(self, model_cls): """ - SQLAlchemy query like method + SQLAlchemy query like method """ self._filters_cmd = list() self.query_filters = list() @@ -350,8 +350,8 @@ def limit(self, limit=0): def all(self): """ - SQLA like 'all' method, will populate all rows and apply all - filters and orders to it. + SQLA like 'all' method, will populate all rows and apply all + filters and orders to it. """ items = list() if not self._filters_cmd: diff --git a/flask_appbuilder/models/generic/filters.py b/flask_appbuilder/models/generic/filters.py index f645375e4..e4d479086 100644 --- a/flask_appbuilder/models/generic/filters.py +++ b/flask_appbuilder/models/generic/filters.py @@ -78,8 +78,8 @@ def apply(self, query, value): class GenericFilterConverter(BaseFilterConverter): """ - Class for converting columns into a supported list of filters - specific for SQLAlchemy. + Class for converting columns into a supported list of filters + specific for SQLAlchemy. """ diff --git a/flask_appbuilder/models/generic/interface.py b/flask_appbuilder/models/generic/interface.py index beea165be..718827e49 100644 --- a/flask_appbuilder/models/generic/interface.py +++ b/flask_appbuilder/models/generic/interface.py @@ -9,7 +9,6 @@ def _include_filters(obj): class GenericInterface(BaseInterface): - filter_converter_class = filters.GenericFilterConverter def __init__(self, obj, session=None): @@ -25,7 +24,6 @@ def query( page=None, page_size=None, ): - query = self.session.query(self.obj) if filters: query = filters.apply_all(query) @@ -68,7 +66,7 @@ def get_order_columns_list(self, list_columns=None): def get_keys(self, lst): """ - return a list of pk values from object list + return a list of pk values from object list """ pk_name = self.get_pk_name() return [getattr(item, pk_name) for item in lst] diff --git a/flask_appbuilder/models/group.py b/flask_appbuilder/models/group.py index 0182b9f04..94758a913 100644 --- a/flask_appbuilder/models/group.py +++ b/flask_appbuilder/models/group.py @@ -16,10 +16,10 @@ def aggregate(label=""): """ - Use this decorator to set a label for your aggregation functions on charts. + Use this decorator to set a label for your aggregation functions on charts. - :param label: - The label to complement with the column + :param label: + The label to complement with the column """ def wrap(f): @@ -32,8 +32,8 @@ def wrap(f): @aggregate(_("Count of")) def aggregate_count(items, col): """ - Function to use on Group by Charts. - accepts a list and returns the count of the list's items + Function to use on Group by Charts. + accepts a list and returns the count of the list's items """ return len(list(items)) @@ -41,8 +41,8 @@ def aggregate_count(items, col): @aggregate(_("Sum of")) def aggregate_sum(items, col): """ - Function to use on Group by Charts. - accepts a list and returns the sum of the list's items + Function to use on Group by Charts. + accepts a list and returns the sum of the list's items """ return sum(getattr(item, col) for item in items) @@ -50,8 +50,8 @@ def aggregate_sum(items, col): @aggregate(_("Avg. of")) def aggregate_avg(items, col): """ - Function to use on Group by Charts. - accepts a list and returns the average of the list's items + Function to use on Group by Charts. + accepts a list and returns the average of the list's items """ try: return aggregate_sum(items, col) / aggregate_count(items, col) @@ -70,12 +70,12 @@ def __init__( self, column_name, name, aggregate_func=aggregate_count, aggregate_col="" ): """ - Constructor. + Constructor. - :param column_name: - Model field name - :param name: - The group by name + :param column_name: + Model field name + :param name: + The group by name """ self.column_name = column_name @@ -85,7 +85,7 @@ def __init__( def apply(self, data): """ - Override this to implement you own new filters + Override this to implement you own new filters """ pass @@ -118,7 +118,7 @@ def _apply(self, data): }, ] json_data["rows"] = [] - for (grouped, items) in groupby(data, self.get_group_col): + for grouped, items in groupby(data, self.get_group_col): aggregate_value = self.aggregate_func(items, self.aggregate_col) json_data["rows"].append( { @@ -181,13 +181,13 @@ def get_format_group_col(self, item): class BaseProcessData(object): """ - Base class to process data. - It will group data by one or many columns or functions. - The aggregation is made by an already defined function, or by a custom function + Base class to process data. + It will group data by one or many columns or functions. + The aggregation is made by an already defined function, or by a custom function - :group_bys_cols: A list of columns or functions to group data. - :aggr_by_cols: A list of tuples [(,''),...]. - :formatter_by_cols: A dict. + :group_bys_cols: A list of columns or functions to group data. + :aggr_by_cols: A list of tuples [(,''),...]. + :formatter_by_cols: A dict. """ group_bys_cols = None @@ -266,18 +266,18 @@ def to_dict(self, data): def to_json(self, data, labels=None): """ - Will return a dict with Google JSON structure for charts + Will return a dict with Google JSON structure for charts - The Google structure:: + The Google structure:: - { - cols: [{id:, label: