From be113fc75112b54d65dfaa147655cc814c84d913 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 13 Jul 2024 12:45:28 +0100 Subject: [PATCH 1/4] Re-enable typing check github action --- .github/workflows/tests.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3e15e30a9..9d8e446e1 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -75,13 +75,12 @@ jobs: strategy: fail-fast: false matrix: - # tox: ['docs', 'typing'] - tox: ['docs'] # disable typing tests until we have typed the project correctly + tox: ['docs', 'typing'] steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 with: - python-version: '3.x' + python-version: 3.11 cache: pip cache-dependency-path: requirements*/*.txt - name: cache mypy From ee85290b7d566d875aea8b358a25c44a9fab0838 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 9 Jul 2024 19:03:03 +0100 Subject: [PATCH 2/4] Disable strict checks temporarily Disables all of the flags that `--strict` adds, so that we can gradually pick away at them more easily over time. --- pyproject.toml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9d9f0a8a7..cb4d45e60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,35 @@ show_error_codes = true pretty = true strict = true +# Start off with these +warn_unused_configs = false +warn_redundant_casts = false +warn_unused_ignores = false + +# Getting these passing should be easy +strict_equality = false +strict_concatenate = false + +# Strongly recommend enabling this one as soon as you can +check_untyped_defs = false + +# These shouldn't be too much additional work, but may be tricky to +# get passing if you use a lot of untyped libraries +disallow_subclassing_any = false +disallow_untyped_decorators = false +disallow_any_generics = false + +# These next few are various gradations of forcing use of type annotations +disallow_untyped_calls = false +disallow_incomplete_defs = false +disallow_untyped_defs = false + +# This one isn't too hard to get passing, but return on investment is lower +no_implicit_reexport = false + +# This one can be tricky to get passing if you use a lot of untyped libraries +warn_return_any = false + [[tool.mypy.overrides]] module = [ "sqlparse.*" From e577a2e8009b53c56c1bfd15f75c7d28099ac344 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Tue, 9 Jul 2024 19:03:15 +0100 Subject: [PATCH 3/4] Update requirements files for dev/test * Install missing types for mypy * Use shapely>=1.8, which has the correct resolution for libgeos on darwin. * Use '-r tests.in' in `typing.in`, so that all packages are installed to aid in type resolution. --- README.md | 1 - requirements/dev.txt | 227 ++++++++++++++++++++++++++++++---------- requirements/tests.in | 4 +- requirements/tests.txt | 206 ++++++++++++++++++++++++++++++++++++ requirements/typing.in | 11 +- requirements/typing.txt | 208 +++++++++++++++++++++++++++++++++++- 6 files changed, 596 insertions(+), 61 deletions(-) create mode 100644 requirements/tests.txt diff --git a/README.md b/README.md index 4f1b839e6..b4529a87e 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ If you\'re using Homebrew on MacOS, you might need this: # install postgis and geos > brew install postgis > brew install geos - > export DYLD_LIBRARY_PATH=/opt/homebrew/opt/geos/lib/ # set up postgresql user > createuser -s postgresql diff --git a/requirements/dev.txt b/requirements/dev.txt index c86237c95..ad7c9747d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -9,17 +9,30 @@ alabaster==0.7.13 # -r docs.txt # sphinx arrow==0.13.2 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt astroid==2.15.8 - # via pylint -azure-core==1.30.1 - # via azure-storage-blob -azure-storage-blob==12.19.1 - # via -r tests.in + # via + # -r typing.txt + # pylint +azure-common==1.1.28 + # via + # -r typing.txt + # azure-storage-blob + # azure-storage-common +azure-storage-blob==2.1.0 + # via + # -r tests.in + # -r typing.txt +azure-storage-common==2.1.0 + # via + # -r typing.txt + # azure-storage-blob babel==2.9.1 # via # -r docs.txt - # -r tests.in + # -r typing.txt # flask-babelex # sphinx cachetools==5.3.3 @@ -27,9 +40,12 @@ cachetools==5.3.3 certifi==2024.7.4 # via # -r docs.txt + # -r typing.txt # requests cffi==1.15.1 - # via cryptography + # via + # -r typing.txt + # cryptography cfgv==3.3.1 # via pre-commit chardet==5.2.0 @@ -37,36 +53,54 @@ chardet==5.2.0 charset-normalizer==3.3.2 # via # -r docs.txt + # -r typing.txt # requests click==8.1.7 - # via flask + # via + # -r typing.txt + # flask colorama==0.4.6 # via tox colour==0.1.5 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt coverage[toml]==6.5.0 # via + # -r typing.txt # coverage # coveralls # pytest-cov coveralls==3.3.1 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt cryptography==42.0.8 - # via azure-storage-blob + # via + # -r typing.txt + # azure-storage-common dill==0.3.7 - # via pylint + # via + # -r typing.txt + # pylint distlib==0.3.8 # via virtualenv dnspython==2.3.0 - # via email-validator + # via + # -r typing.txt + # email-validator docopt==0.6.2 - # via coveralls + # via + # -r typing.txt + # coveralls docutils==0.19 # via # -r docs.txt # sphinx email-validator==2.0.0.post2 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt exceptiongroup==1.2.1 # via # -r typing.txt @@ -76,28 +110,42 @@ filelock==3.12.2 # tox # virtualenv flake8==3.9.2 - # via -r tests.in -flask==1.1.2 # via + # -r tests.in + # -r typing.txt +flask==2.1.3 + # via + # -r typing.txt # flask-babelex # flask-mongoengine # flask-sqlalchemy # flask-wtf flask-babelex==0.9.4 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt flask-mongoengine==0.8.2 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt flask-sqlalchemy==2.5.1 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt flask-wtf==1.1.1 - # via flask-mongoengine -geoalchemy2==0.15.1 - # via -r tests.in + # via + # -r typing.txt + # flask-mongoengine +geoalchemy2==0.15.2 + # via + # -r tests.in + # -r typing.txt identify==2.5.24 # via pre-commit idna==3.7 # via # -r docs.txt + # -r typing.txt # email-validator # requests imagesize==1.4.1 @@ -110,6 +158,7 @@ importlib-metadata==6.7.0 # -r typing.txt # click # flake8 + # flask # pallets-sphinx-themes # pluggy # pre-commit @@ -123,37 +172,42 @@ iniconfig==2.0.0 # via # -r typing.txt # pytest -isodate==0.6.1 - # via azure-storage-blob isort==5.11.5 - # via pylint + # via + # -r typing.txt + # pylint itsdangerous==2.0.1 # via # -r tests.in + # -r typing.txt # flask # flask-wtf jinja2==3.0.0 # via # -r docs.txt - # -r tests.in + # -r typing.txt # flask # flask-babelex # sphinx lazy-object-proxy==1.9.0 - # via astroid + # via + # -r typing.txt + # astroid markupsafe==2.0.1 # via # -r docs.txt - # -r tests.in + # -r typing.txt # jinja2 # wtforms mccabe==0.6.1 # via + # -r typing.txt # flake8 # pylint mongoengine==0.21.0 # via # -r tests.in + # -r typing.txt # flask-mongoengine mypy==1.4.1 # via -r typing.txt @@ -178,14 +232,18 @@ packaging==24.0 # tox pallets-sphinx-themes==2.0.3 # via -r docs.txt -peewee==3.17.5 +peewee==3.17.6 # via # -r tests.in + # -r typing.txt # wtf-peewee pillow==9.5.0 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt platformdirs==4.0.0 # via + # -r typing.txt # pylint # tox # virtualenv @@ -197,22 +255,33 @@ pluggy==1.2.0 pre-commit==2.21.0 # via -r dev.in psycopg2==2.9.9 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt pycodestyle==2.7.0 - # via flake8 + # via + # -r typing.txt + # flake8 pycparser==2.21 - # via cffi + # via + # -r typing.txt + # cffi pyflakes==2.3.1 - # via flake8 + # via + # -r typing.txt + # flake8 pygments==2.17.2 # via # -r docs.txt # sphinx pylint==2.17.7 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt pymongo==3.13.0 # via # -r tests.in + # -r typing.txt # mongoengine pyproject-api==1.5.3 # via tox @@ -224,35 +293,45 @@ pytest==7.4.4 # -r typing.txt # pytest-cov pytest-cov==4.1.0 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt python-dateutil==2.9.0.post0 - # via arrow + # via + # -r typing.txt + # arrow + # azure-storage-common pytz==2024.1 # via # -r docs.txt + # -r typing.txt # babel pyyaml==6.0.1 # via pre-commit requests==2.31.0 # via # -r docs.txt - # azure-core + # -r typing.txt + # azure-storage-common # coveralls # sphinx -shapely==1.5.9 - # via -r tests.in +shapely==1.8.5.post1 + # via + # -r tests.in + # -r typing.txt six==1.16.0 # via - # azure-core + # -r typing.txt # flask-mongoengine - # isodate # python-dateutil snowballstemmer==2.2.0 # via # -r docs.txt # sphinx speaklater==1.3 - # via flask-babelex + # via + # -r typing.txt + # flask-babelex sphinx==5.3.0 # via # -r docs.txt @@ -287,14 +366,19 @@ sphinxcontrib-serializinghtml==1.1.5 sqlalchemy==1.4.52 # via # -r tests.in + # -r typing.txt # flask-sqlalchemy # geoalchemy2 # sqlalchemy-citext # sqlalchemy-utils sqlalchemy-citext==1.8.0 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt sqlalchemy-utils==0.41.2 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt tomli==2.0.1 # via # -r typing.txt @@ -305,7 +389,9 @@ tomli==2.0.1 # pytest # tox tomlkit==0.12.5 - # via pylint + # via + # -r typing.txt + # pylint tox==4.8.0 # via -r dev.in typed-ast==1.5.5 @@ -313,13 +399,41 @@ typed-ast==1.5.5 # -r typing.txt # astroid # mypy +types-boto==2.49.18.9 + # via -r typing.txt +types-click==7.1.8 + # via + # -r typing.txt + # types-flask +types-flask==1.1.6 + # via -r typing.txt +types-flask-sqlalchemy==2.5.9.4 + # via -r typing.txt +types-jinja2==2.11.9 + # via + # -r typing.txt + # types-flask +types-markupsafe==1.1.10 + # via + # -r typing.txt + # types-jinja2 +types-peewee==3.17.0.0 + # via -r typing.txt +types-pillow==10.1.0.2 + # via -r typing.txt +types-sqlalchemy==1.4.53.38 + # via + # -r typing.txt + # types-flask-sqlalchemy +types-werkzeug==1.0.9 + # via + # -r typing.txt + # types-flask typing-extensions==4.7.1 # via # -r docs.txt # -r typing.txt # astroid - # azure-core - # azure-storage-blob # importlib-metadata # mypy # platformdirs @@ -329,22 +443,29 @@ typing-extensions==4.7.1 urllib3==2.0.7 # via # -r docs.txt + # -r typing.txt # requests virtualenv==20.26.3 # via # pre-commit # tox -werkzeug==1.0.1 +werkzeug==2.1.2 # via # -r tests.in + # -r typing.txt # flask wrapt==1.16.0 - # via astroid + # via + # -r typing.txt + # astroid wtf-peewee==3.0.5 - # via -r tests.in + # via + # -r tests.in + # -r typing.txt wtforms==3.0.1 # via # -r tests.in + # -r typing.txt # flask-wtf # wtf-peewee zipp==3.15.0 diff --git a/requirements/tests.in b/requirements/tests.in index b908912a5..a2a4256f2 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -13,7 +13,7 @@ flask-mongoengine==0.8.2 pillow>=3.3.2 Babel<=2.9.1 flask-babelex -shapely==1.5.9 +shapely>=1.8,<2 geoalchemy2 psycopg2 pytest @@ -22,7 +22,7 @@ coveralls pylint sqlalchemy-citext sqlalchemy-utils>=0.36.6 -azure-storage-blob +azure-storage-blob<=3 arrow<0.14.0 colour email-validator diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 000000000..af35a37f5 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,206 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --resolver=backtracking tests.in +# +arrow==0.13.2 + # via -r tests.in +astroid==2.15.8 + # via pylint +azure-common==1.1.28 + # via + # azure-storage-blob + # azure-storage-common +azure-storage-blob==2.1.0 + # via -r tests.in +azure-storage-common==2.1.0 + # via azure-storage-blob +babel==2.9.1 + # via + # -r tests.in + # flask-babelex +certifi==2024.7.4 + # via requests +cffi==1.15.1 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via flask +colour==0.1.5 + # via -r tests.in +coverage[toml]==6.5.0 + # via + # coverage + # coveralls + # pytest-cov +coveralls==3.3.1 + # via -r tests.in +cryptography==42.0.8 + # via azure-storage-common +dill==0.3.7 + # via pylint +dnspython==2.3.0 + # via email-validator +docopt==0.6.2 + # via coveralls +email-validator==2.0.0.post2 + # via -r tests.in +exceptiongroup==1.2.2 + # via pytest +flake8==5.0.4 + # via -r tests.in +flask==2.1.3 + # via + # flask-babelex + # flask-mongoengine + # flask-sqlalchemy + # flask-wtf +flask-babelex==0.9.4 + # via -r tests.in +flask-mongoengine==0.8.2 + # via -r tests.in +flask-sqlalchemy==2.5.1 + # via -r tests.in +flask-wtf==1.1.1 + # via flask-mongoengine +geoalchemy2==0.15.2 + # via -r tests.in +idna==3.7 + # via + # email-validator + # requests +importlib-metadata==4.2.0 + # via + # click + # flake8 + # flask + # pluggy + # pytest + # sqlalchemy + # sqlalchemy-utils +iniconfig==2.0.0 + # via pytest +isort==5.11.5 + # via pylint +itsdangerous==2.0.1 + # via + # -r tests.in + # flask + # flask-wtf +jinja2==3.0.0 + # via + # -r tests.in + # flask + # flask-babelex +lazy-object-proxy==1.9.0 + # via astroid +markupsafe==2.0.1 + # via + # -r tests.in + # jinja2 + # wtforms +mccabe==0.7.0 + # via + # flake8 + # pylint +mongoengine==0.21.0 + # via + # -r tests.in + # flask-mongoengine +packaging==24.0 + # via + # geoalchemy2 + # pytest +peewee==3.17.6 + # via + # -r tests.in + # wtf-peewee +pillow==9.5.0 + # via -r tests.in +platformdirs==4.0.0 + # via pylint +pluggy==1.2.0 + # via pytest +psycopg2==2.9.9 + # via -r tests.in +pycodestyle==2.9.1 + # via flake8 +pycparser==2.21 + # via cffi +pyflakes==2.5.0 + # via flake8 +pylint==2.17.7 + # via -r tests.in +pymongo==3.13.0 + # via + # -r tests.in + # mongoengine +pytest==7.4.4 + # via + # -r tests.in + # pytest-cov +pytest-cov==4.1.0 + # via -r tests.in +python-dateutil==2.9.0.post0 + # via + # arrow + # azure-storage-common +pytz==2024.1 + # via babel +requests==2.31.0 + # via + # azure-storage-common + # coveralls +shapely==1.8.5.post1 + # via -r tests.in +six==1.16.0 + # via + # flask-mongoengine + # python-dateutil +speaklater==1.3 + # via flask-babelex +sqlalchemy==1.4.52 + # via + # -r tests.in + # flask-sqlalchemy + # geoalchemy2 + # sqlalchemy-citext + # sqlalchemy-utils +sqlalchemy-citext==1.8.0 + # via -r tests.in +sqlalchemy-utils==0.41.2 + # via -r tests.in +tomli==2.0.1 + # via + # coverage + # pylint + # pytest +tomlkit==0.12.5 + # via pylint +typed-ast==1.5.5 + # via astroid +typing-extensions==4.7.1 + # via + # astroid + # importlib-metadata + # platformdirs + # pylint +urllib3==2.0.7 + # via requests +werkzeug==2.1.2 + # via + # -r tests.in + # flask +wrapt==1.16.0 + # via astroid +wtf-peewee==3.0.5 + # via -r tests.in +wtforms==3.0.1 + # via + # -r tests.in + # flask-wtf + # wtf-peewee +zipp==3.15.0 + # via importlib-metadata diff --git a/requirements/typing.in b/requirements/typing.in index 30febfdb8..f37d61b92 100644 --- a/requirements/typing.in +++ b/requirements/typing.in @@ -1,5 +1,14 @@ --c tests.in +-r tests.in mypy pyright pytest +types-Flask-SQLAlchemy +types-Pillow +types-boto +types-peewee +types-Flask +types-WTForms + +# Requires Python 3.8+ +# types-shapely diff --git a/requirements/typing.txt b/requirements/typing.txt index 02f370e38..ac9400fc1 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -2,16 +2,114 @@ # This file is autogenerated by pip-compile with Python 3.7 # by the following command: # -# pip-compile typing.in +# pip-compile --resolver=backtracking typing.in # +arrow==0.13.2 + # via -r tests.in +astroid==2.15.8 + # via pylint +azure-common==1.1.28 + # via + # azure-storage-blob + # azure-storage-common +azure-storage-blob==2.1.0 + # via -r tests.in +azure-storage-common==2.1.0 + # via azure-storage-blob +babel==2.9.1 + # via + # -r tests.in + # flask-babelex +certifi==2024.7.4 + # via requests +cffi==1.15.1 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via flask +colour==0.1.5 + # via -r tests.in +coverage[toml]==6.5.0 + # via + # coverage + # coveralls + # pytest-cov +coveralls==3.3.1 + # via -r tests.in +cryptography==42.0.8 + # via azure-storage-common +dill==0.3.7 + # via pylint +dnspython==2.3.0 + # via email-validator +docopt==0.6.2 + # via coveralls +email-validator==2.0.0.post2 + # via -r tests.in exceptiongroup==1.2.1 # via pytest +flake8==3.9.2 + # via -r tests.in +flask==2.1.3 + # via + # flask-babelex + # flask-mongoengine + # flask-sqlalchemy + # flask-wtf +flask-babelex==0.9.4 + # via -r tests.in +flask-mongoengine==0.8.2 + # via -r tests.in +flask-sqlalchemy==2.5.1 + # via -r tests.in +flask-wtf==1.1.1 + # via flask-mongoengine +geoalchemy2==0.15.2 + # via -r tests.in +idna==3.7 + # via + # email-validator + # requests importlib-metadata==6.7.0 # via + # click + # flake8 + # flask # pluggy # pytest + # sqlalchemy + # sqlalchemy-utils iniconfig==2.0.0 # via pytest +isort==5.11.5 + # via pylint +itsdangerous==2.0.1 + # via + # -r tests.in + # flask + # flask-wtf +jinja2==3.0.0 + # via + # -r tests.in + # flask + # flask-babelex +lazy-object-proxy==1.9.0 + # via astroid +markupsafe==2.0.1 + # via + # -r tests.in + # jinja2 + # types-wtforms + # wtforms +mccabe==0.6.1 + # via + # flake8 + # pylint +mongoengine==0.21.0 + # via + # -r tests.in + # flask-mongoengine mypy==1.4.1 # via -r typing.in mypy-extensions==1.0.0 @@ -19,25 +117,127 @@ mypy-extensions==1.0.0 nodeenv==1.9.1 # via pyright packaging==24.0 - # via pytest + # via + # geoalchemy2 + # pytest +peewee==3.17.6 + # via + # -r tests.in + # wtf-peewee +pillow==9.5.0 + # via -r tests.in +platformdirs==4.0.0 + # via pylint pluggy==1.2.0 # via pytest +psycopg2==2.9.9 + # via -r tests.in +pycodestyle==2.7.0 + # via flake8 +pycparser==2.21 + # via cffi +pyflakes==2.3.1 + # via flake8 +pylint==2.17.7 + # via -r tests.in +pymongo==3.13.0 + # via + # -r tests.in + # mongoengine pyright==1.1.370 # via -r typing.in pytest==7.4.4 # via - # -c tests.in + # -r tests.in # -r typing.in + # pytest-cov +pytest-cov==4.1.0 + # via -r tests.in +python-dateutil==2.9.0.post0 + # via + # arrow + # azure-storage-common +pytz==2024.1 + # via babel +requests==2.31.0 + # via + # azure-storage-common + # coveralls +shapely==1.8.5.post1 + # via -r tests.in +six==1.16.0 + # via + # flask-mongoengine + # python-dateutil +speaklater==1.3 + # via flask-babelex +sqlalchemy==1.4.52 + # via + # -r tests.in + # flask-sqlalchemy + # geoalchemy2 + # sqlalchemy-citext + # sqlalchemy-utils +sqlalchemy-citext==1.8.0 + # via -r tests.in +sqlalchemy-utils==0.41.2 + # via -r tests.in tomli==2.0.1 # via + # coverage # mypy + # pylint # pytest +tomlkit==0.12.5 + # via pylint typed-ast==1.5.5 - # via mypy + # via + # astroid + # mypy +types-boto==2.49.18.9 + # via -r typing.in +types-click==7.1.8 + # via types-flask +types-flask==1.1.6 + # via -r typing.in +types-flask-sqlalchemy==2.5.9.4 + # via -r typing.in +types-jinja2==2.11.9 + # via types-flask +types-markupsafe==1.1.10 + # via types-jinja2 +types-peewee==3.17.0.0 + # via -r typing.in +types-pillow==10.1.0.2 + # via -r typing.in +types-sqlalchemy==1.4.53.38 + # via types-flask-sqlalchemy +types-werkzeug==1.0.9 + # via types-flask +types-wtforms==3.1.0.2 + # via -r typing.in typing-extensions==4.7.1 # via + # astroid # importlib-metadata # mypy + # platformdirs + # pylint # pyright +urllib3==2.0.7 + # via requests +werkzeug==2.1.2 + # via + # -r tests.in + # flask +wrapt==1.16.0 + # via astroid +wtf-peewee==3.0.5 + # via -r tests.in +wtforms==3.0.1 + # via + # -r tests.in + # flask-wtf + # wtf-peewee zipp==3.15.0 # via importlib-metadata From 29090c9a852f44a77dd14d93d20e382c2e7bacdb Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sat, 13 Jul 2024 13:30:25 +0100 Subject: [PATCH 4/4] Add type hints to pass basic checking --- flask_admin/_backwards.py | 2 +- flask_admin/_compat.py | 39 ++++++++--------------- flask_admin/_types.py | 6 ++++ flask_admin/babel.py | 2 +- flask_admin/base.py | 4 +-- flask_admin/contrib/fileadmin/__init__.py | 2 +- flask_admin/contrib/fileadmin/s3.py | 4 +++ flask_admin/contrib/geoa/typefmt.py | 2 +- flask_admin/contrib/mongoengine/fields.py | 2 +- flask_admin/contrib/peewee/form.py | 2 +- flask_admin/contrib/sqla/fields.py | 2 +- flask_admin/contrib/sqla/form.py | 2 +- flask_admin/contrib/sqla/tools.py | 6 ++-- flask_admin/contrib/sqla/typefmt.py | 6 ++-- flask_admin/contrib/sqla/view.py | 20 +++++++----- flask_admin/contrib/sqla/widgets.py | 2 +- flask_admin/form/upload.py | 9 ++++-- flask_admin/model/base.py | 37 ++++++++++++++------- pyproject.toml | 24 +++++++++++++- 19 files changed, 109 insertions(+), 64 deletions(-) create mode 100644 flask_admin/_types.py diff --git a/flask_admin/_backwards.py b/flask_admin/_backwards.py index 6a6c9e8d9..03929e02e 100644 --- a/flask_admin/_backwards.py +++ b/flask_admin/_backwards.py @@ -9,7 +9,7 @@ import warnings try: - from wtforms.widgets import HTMLString as Markup + from wtforms.widgets import HTMLString as Markup # type: ignore[attr-defined] except ImportError: # WTForms 2.3.0 from markupsafe import Markup # noqa: F401 diff --git a/flask_admin/_compat.py b/flask_admin/_compat.py index 5b8fa27bf..934ace4f9 100644 --- a/flask_admin/_compat.py +++ b/flask_admin/_compat.py @@ -10,13 +10,23 @@ :copyright: (c) 2013 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from typing import Callable text_type = str string_types = (str,) -itervalues = lambda d: iter(d.values()) -iteritems = lambda d: iter(d.items()) -filter_list = lambda f, l: list(filter(f, l)) + +def itervalues(d: dict): + return iter(d.values()) + + +def iteritems(d: dict): + return iter(d.items()) + + +def filter_list(f: Callable, l: list): + return list(filter(f, l)) + def as_unicode(s): if isinstance(s, bytes): @@ -29,29 +39,8 @@ def csv_encode(s): return as_unicode(s) -def with_metaclass(meta, *bases): - # This requires a bit of explanation: the basic idea is to make a - # dummy metaclass for one level of class instantiation that replaces - # itself with the actual metaclass. Because of internal type checks - # we also need to make sure that we downgrade the custom metaclass - # for one level to something closer to type (that's why __call__ and - # __init__ comes back from type etc.). - # - # This has the advantage over six.with_metaclass in that it does not - # introduce dummy classes into the final MRO. - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - try: # jinja2 3.0.0 - from jinja2 import pass_context + from jinja2 import pass_context # type: ignore[attr-defined] except ImportError: from jinja2 import contextfunction as pass_context diff --git a/flask_admin/_types.py b/flask_admin/_types.py new file mode 100644 index 000000000..ac30fab6a --- /dev/null +++ b/flask_admin/_types.py @@ -0,0 +1,6 @@ +from typing import Union, Sequence, Dict, Callable + +import sqlalchemy + +T_COLUMN_LIST = Sequence[Union[str, sqlalchemy.Column]] +T_FORMATTERS = Dict[type, Callable] # todo: Make this tighter diff --git a/flask_admin/babel.py b/flask_admin/babel.py index 41dcae548..cb5ff1641 100644 --- a/flask_admin/babel.py +++ b/flask_admin/babel.py @@ -49,7 +49,7 @@ def get_translations_path(self, ctx): wtforms_domain = Domain(messages_path(), domain='wtforms') - class Translations(object): + class Translations(object): # type: ignore[no-redef] ''' Fixes WTForms translation support and uses wtforms translations ''' def gettext(self, string): t = wtforms_domain.get_translations() diff --git a/flask_admin/base.py b/flask_admin/base.py index 39c8654b3..1c913da8d 100644 --- a/flask_admin/base.py +++ b/flask_admin/base.py @@ -5,7 +5,7 @@ from flask import Blueprint, current_app, render_template, abort, g, url_for from flask_admin import babel -from flask_admin._compat import with_metaclass, as_unicode +from flask_admin._compat import as_unicode from flask_admin import helpers as h # For compatibility reasons import MenuLink @@ -106,7 +106,7 @@ class BaseViewClass(object): pass -class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)): +class BaseView(BaseViewClass, metaclass=AdminViewMeta): """ Base administrative view. diff --git a/flask_admin/contrib/fileadmin/__init__.py b/flask_admin/contrib/fileadmin/__init__.py index 95c42d95c..887ad5f7a 100644 --- a/flask_admin/contrib/fileadmin/__init__.py +++ b/flask_admin/contrib/fileadmin/__init__.py @@ -176,7 +176,7 @@ class MyAdmin(FileAdmin): allowed_extensions = ('swf', 'jpg', 'gif', 'png') """ - editable_extensions = tuple() + editable_extensions: tuple = tuple() """ List of editable extensions, in lower case. diff --git a/flask_admin/contrib/fileadmin/s3.py b/flask_admin/contrib/fileadmin/s3.py index 1c9bdc719..55340167a 100644 --- a/flask_admin/contrib/fileadmin/s3.py +++ b/flask_admin/contrib/fileadmin/s3.py @@ -1,4 +1,8 @@ import time +from types import ModuleType +from typing import Optional + +s3: Optional[ModuleType] try: from boto import s3 diff --git a/flask_admin/contrib/geoa/typefmt.py b/flask_admin/contrib/geoa/typefmt.py index 339997b56..276d4743c 100644 --- a/flask_admin/contrib/geoa/typefmt.py +++ b/flask_admin/contrib/geoa/typefmt.py @@ -33,4 +33,4 @@ def geom_formatter(view, value): DEFAULT_FORMATTERS = BASE_FORMATTERS.copy() -DEFAULT_FORMATTERS[WKBElement] = geom_formatter +DEFAULT_FORMATTERS[WKBElement] = geom_formatter # type: ignore[assignment] diff --git a/flask_admin/contrib/mongoengine/fields.py b/flask_admin/contrib/mongoengine/fields.py index 3dcbed00b..23a0287fe 100644 --- a/flask_admin/contrib/mongoengine/fields.py +++ b/flask_admin/contrib/mongoengine/fields.py @@ -85,4 +85,4 @@ class MongoImageField(MongoFileField): GridFS image field. """ - widget = widgets.MongoImageInput() + widget = widgets.MongoImageInput() # type: ignore[assignment] diff --git a/flask_admin/contrib/peewee/form.py b/flask_admin/contrib/peewee/form.py index e48390ffd..be27e9a4b 100644 --- a/flask_admin/contrib/peewee/form.py +++ b/flask_admin/contrib/peewee/form.py @@ -4,7 +4,7 @@ PrimaryKeyField, ForeignKeyField) try: - from peewee import BaseModel + from peewee import BaseModel # type: ignore[attr-defined] except ImportError: from peewee import ModelBase as BaseModel diff --git a/flask_admin/contrib/sqla/fields.py b/flask_admin/contrib/sqla/fields.py index 02bfcca8e..441af0312 100644 --- a/flask_admin/contrib/sqla/fields.py +++ b/flask_admin/contrib/sqla/fields.py @@ -193,7 +193,7 @@ class MyView(ModelView): 'languages': CheckboxListField, } """ - widget = CheckboxListInput() + widget = CheckboxListInput() # type: ignore[assignment] class HstoreForm(BaseForm): diff --git a/flask_admin/contrib/sqla/form.py b/flask_admin/contrib/sqla/form.py index 5f15287c2..6bc42f7e7 100644 --- a/flask_admin/contrib/sqla/form.py +++ b/flask_admin/contrib/sqla/form.py @@ -780,7 +780,7 @@ def contribute(self, model, form_class, inline_model): class InlineOneToOneModelConverter(InlineModelConverter): - inline_field_list_type = InlineModelOneToOneField + inline_field_list_type = InlineModelOneToOneField # type: ignore[assignment] def _calculate_mapping_key_pair(self, model, info): diff --git a/flask_admin/contrib/sqla/tools.py b/flask_admin/contrib/sqla/tools.py index aa4e4b5ec..4149d5214 100644 --- a/flask_admin/contrib/sqla/tools.py +++ b/flask_admin/contrib/sqla/tools.py @@ -6,15 +6,15 @@ from sqlalchemy.orm.clsregistry import _class_resolver except ImportError: # If 1.4/2.0 module import fails, fall back to <1.3.x architecture. - from sqlalchemy.ext.declarative.clsregistry import _class_resolver + from sqlalchemy.ext.declarative.clsregistry import _class_resolver # type: ignore[no-redef] from sqlalchemy.ext.hybrid import hybrid_property try: # Attempt ASSOCATION_PROXY import from pre-2.0 release from sqlalchemy.ext.associationproxy import ASSOCIATION_PROXY except ImportError: - from sqlalchemy.ext.associationproxy import AssociationProxyExtensionType + from sqlalchemy.ext.associationproxy import AssociationProxyExtensionType # type: ignore[attr-defined] ASSOCIATION_PROXY = AssociationProxyExtensionType.ASSOCIATION_PROXY -from sqlalchemy.sql.operators import eq +from sqlalchemy.sql.operators import eq # type: ignore[attr-defined] from sqlalchemy.exc import DBAPIError from sqlalchemy.orm.attributes import InstrumentedAttribute diff --git a/flask_admin/contrib/sqla/typefmt.py b/flask_admin/contrib/sqla/typefmt.py index 596b19921..1a4614595 100644 --- a/flask_admin/contrib/sqla/typefmt.py +++ b/flask_admin/contrib/sqla/typefmt.py @@ -47,13 +47,13 @@ def arrow_export_formatter(view, arrow_time): }) try: from sqlalchemy_utils import Choice - DEFAULT_FORMATTERS[Choice] = choice_formatter + DEFAULT_FORMATTERS[Choice] = choice_formatter # type: ignore[assignment] except ImportError: pass try: from arrow import Arrow - DEFAULT_FORMATTERS[Arrow] = arrow_formatter - EXPORT_FORMATTERS[Arrow] = arrow_export_formatter + DEFAULT_FORMATTERS[Arrow] = arrow_formatter # type: ignore[assignment] + EXPORT_FORMATTERS[Arrow] = arrow_export_formatter # type: ignore[assignment] except ImportError: pass diff --git a/flask_admin/contrib/sqla/view.py b/flask_admin/contrib/sqla/view.py index 6886e0702..9b4622d5b 100755 --- a/flask_admin/contrib/sqla/view.py +++ b/flask_admin/contrib/sqla/view.py @@ -1,6 +1,7 @@ import logging import warnings import inspect +from typing import Optional, Dict, List, Tuple, cast as t_cast from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.base import manager_of_class, instance_state @@ -72,16 +73,19 @@ class PostAdmin(ModelView): Please refer to the `subqueryload` on list of possible values. """ - column_display_all_relations = ObsoleteAttr('column_display_all_relations', - 'list_display_all_relations', - False) + column_display_all_relations = ObsoleteAttr( + 'column_display_all_relations', + 'list_display_all_relations', + False + ) """ Controls if list view should display all relations, not only many-to-one. """ - column_searchable_list = ObsoleteAttr('column_searchable_list', - 'searchable_columns', - None) + column_searchable_list = t_cast( + None, + ObsoleteAttr('column_searchable_list', 'searchable_columns', None), + ) """ Collection of the searchable columns. @@ -264,9 +268,9 @@ class MyModelView(ModelView): inline_models = (MyInlineModelForm(MyInlineModel),) """ - column_type_formatters = DEFAULT_FORMATTERS + column_type_formatters = DEFAULT_FORMATTERS # type: ignore[assignment] - form_choices = None + form_choices: Optional[Dict[str, List[Tuple[str, str]]]] = None """ Map choices to form fields diff --git a/flask_admin/contrib/sqla/widgets.py b/flask_admin/contrib/sqla/widgets.py index ecf9d65bd..8bf760a99 100644 --- a/flask_admin/contrib/sqla/widgets.py +++ b/flask_admin/contrib/sqla/widgets.py @@ -1,4 +1,4 @@ -from wtforms.widgets.core import escape +from wtforms.widgets.core import escape # type: ignore[attr-defined] from flask_admin._backwards import Markup diff --git a/flask_admin/form/upload.py b/flask_admin/form/upload.py index 1afc42f4e..7c702c64c 100644 --- a/flask_admin/form/upload.py +++ b/flask_admin/form/upload.py @@ -1,11 +1,13 @@ import os import os.path as op +from types import ModuleType +from typing import Optional from urllib.parse import urljoin from werkzeug.utils import secure_filename from werkzeug.datastructures import FileStorage -from wtforms import ValidationError, fields, __version__ as wtforms_version +from wtforms import ValidationError, fields, __version__ as wtforms_version # type: ignore[attr-defined] from wtforms.utils import unset_value from wtforms.widgets import html_params @@ -15,6 +17,9 @@ from flask_admin._backwards import Markup from flask_admin._compat import string_types +Image: Optional[ModuleType] +ImageOps: Optional[ModuleType] + try: from PIL import Image, ImageOps @@ -299,7 +304,7 @@ class ImageUploadField(FileUploadField): Requires PIL (or Pillow) to be installed. """ - widget = ImageUploadInput() + widget = ImageUploadInput() # type: ignore[assignment] keep_image_formats = ('PNG',) """ diff --git a/flask_admin/model/base.py b/flask_admin/model/base.py index a9fe94694..f4452453c 100755 --- a/flask_admin/model/base.py +++ b/flask_admin/model/base.py @@ -3,6 +3,8 @@ import csv import mimetypes import time +from typing import Optional, cast, Type + from math import ceil import inspect from collections import OrderedDict @@ -11,10 +13,14 @@ from flask import (current_app, request, redirect, flash, abort, json, Response, get_flashed_messages, stream_with_context) + +from .._types import T_COLUMN_LIST, T_FORMATTERS + try: import tablib except ImportError: tablib = None +from wtforms.form import Form from wtforms.fields import HiddenField from wtforms.fields.core import UnboundField from wtforms.validators import ValidationError, InputRequired @@ -169,7 +175,10 @@ class BaseModelView(BaseView, ActionsMixin): """Setting this to true will display the details_view as a modal dialog.""" # Customizations - column_list = ObsoleteAttr('column_list', 'list_columns', None) + column_list: Optional[T_COLUMN_LIST] = cast( + None, + ObsoleteAttr('column_list', 'list_columns', None) + ) """ Collection of the model field names for the list view. If set to `None`, will get them from the model. @@ -189,8 +198,9 @@ class MyModelView(BaseModelView): column_list = ('.',) """ - column_exclude_list = ObsoleteAttr('column_exclude_list', - 'excluded_list_columns', None) + column_exclude_list: Optional[T_COLUMN_LIST] = cast( + None, + ObsoleteAttr('column_exclude_list', 'excluded_list_columns', None)) """ Collection of excluded list column names. @@ -274,7 +284,10 @@ def formatter(view, context, model, name): that macros are not supported. """ - column_type_formatters = ObsoleteAttr('column_type_formatters', 'list_type_formatters', None) + column_type_formatters: Optional[T_FORMATTERS] = cast( + None, + ObsoleteAttr('column_type_formatters', 'list_type_formatters', None) + ) """ Dictionary of value type formatters to be used in the list view. @@ -365,9 +378,10 @@ class MyModelView(BaseModelView): ) """ - column_sortable_list = ObsoleteAttr('column_sortable_list', - 'sortable_columns', - None) + column_sortable_list: Optional[T_COLUMN_LIST] = cast( + None, + ObsoleteAttr('column_sortable_list', 'sortable_columns', None), + ) """ Collection of the sortable columns for the list view. If set to `None`, will get them from the model. @@ -418,9 +432,10 @@ class MyModelView(BaseModelView): column_default_sort = [('name', True), ('last_name', True)] """ - column_searchable_list = ObsoleteAttr('column_searchable_list', - 'searchable_columns', - None) + column_searchable_list: Optional[T_COLUMN_LIST] = cast( + None, + ObsoleteAttr('column_searchable_list', 'searchable_columns', None), + ) """ A collection of the searchable columns. It is assumed that only text-only fields are searchable, but it is up to the model @@ -522,7 +537,7 @@ class MyModelView(BaseModelView): If enabled, model interface would not run count query and will only show prev/next pager buttons. """ - form = None + form: Optional[Type[Form]] = None """ Form class. Override if you want to use custom form for your model. Will completely disable form scaffolding functionality. diff --git a/pyproject.toml b/pyproject.toml index cb4d45e60..035fd27ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,29 @@ warn_return_any = false [[tool.mypy.overrides]] module = [ - "sqlparse.*" + "arrow", + "azure.*", + "bson.*", + "citext", + "colour", + "flask_babel", + "flask_babelex", + "flask_mongoengine.*", + "flask_wtf", + "google.appengine.ext", + "gridfs", + "marker", + "mongoengine.*", + "playhouse.*", + "pymongo", + "shapely.*", # types-shapely requires python 3.8+ + "sqlalchemy.*", + "sqlalchemy_enum34", + "sqlalchemy_utils", + "tablib", + "wtforms.*", + "wtforms_appengine.*", + "wtfpeewee.*", ] ignore_missing_imports = true