Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Changelog
Bugfixes:
* Fix encoding for editing file in FileAdmin. Now it uses UTF-8 and accepts non-ASCII characters.

Type hints:
* Type hints added to all functions and methods (some using `typing.Any` where full typing not yet available)

2.2.0
------------------
Expand Down
4 changes: 2 additions & 2 deletions flask_admin/_backwards.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def get_property(obj: t.Any, name: str, old_name: str, default: t.Any = None) ->


class ObsoleteAttr:
def __init__(self, new_name: str, old_name: str, default: t.Any):
def __init__(self, new_name: str, old_name: str, default: t.Any) -> None:
self.new_name = new_name
self.old_name = old_name
self.cache = "_cache_" + new_name
Expand Down Expand Up @@ -69,7 +69,7 @@ def __set__(self, obj: t.Any, value: t.Any) -> None:


class ImportRedirect:
def __init__(self, prefix: str, target: str):
def __init__(self, prefix: str, target: str) -> None:
self.prefix = prefix
self.target = target

Expand Down
62 changes: 32 additions & 30 deletions flask_admin/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,43 @@
from wtforms.widgets import Input

if sys.version_info >= (3, 11):
from typing import NotRequired # noqa
from typing import NotRequired
else:
from typing_extensions import NotRequired # noqa
from typing_extensions import NotRequired

if t.TYPE_CHECKING:
from flask_admin.base import BaseView as T_VIEW # noqa
# optional dependencies
from arrow import Arrow as T_ARROW # noqa
from flask_babel import LazyString as T_LAZY_STRING
from flask_sqlalchemy import Model as T_SQLALCHEMY_LEGACY_MODEL
from sqlalchemy.orm import DeclarativeBase as T_DECLARATIVE_BASE

from flask_admin.base import BaseView as T_VIEW
from flask_admin.contrib.sqla.validators import InputRequired as T_INPUT_REQUIRED
from flask_admin.contrib.sqla.validators import (
TimeZoneValidator as T_TIMEZONE_VALIDATOR,
)
from flask_admin.contrib.sqla.validators import Unique as T_UNIQUE
from flask_admin.form import FormOpts as T_FORM_OPTS # noqa
from flask_admin.form.rules import (
FieldSet as T_FIELD_SET,
BaseRule as T_BASE_RULE,
Header as T_HEADER,
Field as T_FLASK_ADMIN_FIELD,
Macro as T_MACRO,
)
from flask_admin.form import FormOpts as T_FORM_OPTS
from flask_admin.form.rules import BaseRule as T_BASE_RULE
from flask_admin.form.rules import Field as T_FLASK_ADMIN_FIELD
from flask_admin.form.rules import FieldSet as T_FIELD_SET
from flask_admin.form.rules import Header as T_HEADER
from flask_admin.form.rules import Macro as T_MACRO
from flask_admin.model import BaseModelView as T_MODEL_VIEW
from flask_admin.model.ajax import AjaxModelLoader as T_AJAX_MODEL_LOADER # noqa
from flask_admin.model.fields import AjaxSelectField as T_AJAX_SELECT_FIELD # noqa
from flask_admin.model.form import ( # noqa
InlineBaseFormAdmin as T_INLINE_BASE_FORM_ADMIN,
)
from flask_admin.model.ajax import AjaxModelLoader as T_AJAX_MODEL_LOADER
from flask_admin.model.fields import AjaxSelectField as T_AJAX_SELECT_FIELD
from flask_admin.model.form import InlineBaseFormAdmin as T_INLINE_BASE_FORM_ADMIN
from flask_admin.model.form import InlineFormAdmin as T_INLINE_FORM_ADMIN
from flask_admin.model.widgets import (
InlineFieldListWidget as T_INLINE_FIELD_LIST_WIDGET,
AjaxSelect2Widget as T_INLINE_AJAX_SELECT2_WIDGET,
)
from flask_admin.model.widgets import InlineFormWidget as T_INLINE_FORM_WIDGET
from flask_admin.model.widgets import (
AjaxSelect2Widget as T_INLINE_AJAX_SELECT2_WIDGET,
InlineFieldListWidget as T_INLINE_FIELD_LIST_WIDGET,
)
from flask_admin.model.widgets import InlineFormWidget as T_INLINE_FORM_WIDGET
from flask_admin.model.widgets import XEditableWidget as T_INLINE_X_EDITABLE_WIDGET

# optional dependencies
from arrow import Arrow as T_ARROW # noqa
from flask_babel import LazyString as T_LAZY_STRING # noqa

from flask_sqlalchemy import Model as T_SQLALCHEMY_LEGACY_MODEL
from sqlalchemy.orm import DeclarativeBase as T_DECLARATIVE_BASE

T_SQLALCHEMY_MODEL: t.TypeAlias = t.Union[
T_SQLALCHEMY_LEGACY_MODEL, T_DECLARATIVE_BASE
]
Expand Down Expand Up @@ -82,14 +77,15 @@
T_SQLALCHEMY_COLUMN = Column # type: ignore[misc]

T_MONGO_CLIENT = MongoClient[t.Any]
from redis import Redis as T_REDIS # noqa
from PIL.Image import Image as T_PIL_IMAGE
from redis import Redis as T_REDIS

from flask_admin.contrib.peewee.ajax import (
QueryAjaxModelLoader as T_PEEWEE_QUERY_AJAX_MODEL_LOADER,
) # noqa
)
from flask_admin.contrib.sqla.ajax import (
QueryAjaxModelLoader as T_SQLA_QUERY_AJAX_MODEL_LOADER,
) # noqa
from PIL.Image import Image as T_PIL_IMAGE # noqa
)

T_ORM_MODEL: t.TypeAlias = t.Union[
T_SQLALCHEMY_LEGACY_MODEL,
Expand Down Expand Up @@ -278,3 +274,9 @@ def __contains__(self, key: t.Any, /) -> bool: ...

class _MultiDictLikeWithGetlist(_MultiDictLikeBase, t.Protocol):
def getlist(self, key: str, /) -> list[t.Any]: ...


class _T_MONGOENGINE_FIELD_PROTOCOL(t.Protocol):
id: t.Any
data: t.Any
name: str
4 changes: 2 additions & 2 deletions flask_admin/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def get_actions_list(self) -> tuple[list[t.Any], dict[t.Any, t.Any]]:
"""
Return a list and a dictionary of allowed actions.
"""
actions = []
actions_confirmation = {}
actions: list[tuple[str, str]] = []
actions_confirmation: dict[str, str] = {}

for act in self._actions:
name, text = act
Expand Down
18 changes: 11 additions & 7 deletions flask_admin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from flask import g
from flask import render_template
from flask import url_for
from flask.typing import ResponseReturnValue
from flask.views import MethodView
from flask.views import View
from markupsafe import Markup
Expand All @@ -21,11 +22,11 @@
# For compatibility reasons import MenuLink
from flask_admin.blueprints import _BlueprintWithHostSupport as Blueprint
from flask_admin.consts import ADMIN_ROUTES_HOST_VARIABLE
from flask_admin.menu import BaseMenu # noqa: F401
from flask_admin.menu import MenuCategory # noqa: F401
from flask_admin.menu import MenuLink # noqa: F401
from flask_admin.menu import MenuView # noqa: F401
from flask_admin.menu import SubMenuCategory # noqa: F401
from flask_admin.menu import BaseMenu
from flask_admin.menu import MenuCategory
from flask_admin.menu import MenuLink
from flask_admin.menu import MenuView
from flask_admin.menu import SubMenuCategory
from flask_admin.theme import Bootstrap4Theme
from flask_admin.theme import Theme

Expand Down Expand Up @@ -405,7 +406,7 @@ def is_accessible(self) -> bool:
"""
return True

def _handle_view(self, name: str, **kwargs: dict[str, t.Any]) -> t.Any:
def _handle_view(self, name: str, **kwargs: t.Any) -> ResponseReturnValue | None:
"""
This method will be executed before calling any view method.

Expand All @@ -420,6 +421,7 @@ def _handle_view(self, name: str, **kwargs: dict[str, t.Any]) -> t.Any:
# abort(403)
if not self.is_accessible():
return self.inaccessible_callback(name, **kwargs) or abort(403)
return None

def _run_view(
self, fn: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any
Expand All @@ -440,7 +442,9 @@ def _run_view(
except TypeError:
return fn(cls=self, **kwargs)

def inaccessible_callback(self, name: t.Any, **kwargs: t.Any) -> t.Any:
def inaccessible_callback(
self, name: t.Any, **kwargs: t.Any
) -> ResponseReturnValue:
"""
Handle the response to inaccessible views.

Expand Down
12 changes: 6 additions & 6 deletions flask_admin/contrib/fileadmin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def __init__(self, on_windows: bool) -> None:
"""
self.on_windows = on_windows

def normpath(self, path):
def normpath(self, path: str) -> str:
"""
returns the correct normalized path based on the platform of the storage.
Expand Down Expand Up @@ -122,7 +122,7 @@ def get_files(
the relative path, a flag signifying if it is a directory, the file
size in bytes and the time last modified in seconds since the epoch
"""
items = []
items: list[tuple[str, str, bool, int, float]] = []
for f in os.listdir(directory):
fp = op.join(directory, f)
rel_path = op.join(path, f)
Expand Down Expand Up @@ -645,7 +645,7 @@ def is_in_folder(self, base_path: str, directory: T_PATH_LIKE) -> bool:
:param directory:
Directory path to check
"""
return self.normpath(directory).startswith(base_path)
return self.normpath(directory).startswith(base_path) # type: ignore[arg-type]

def save_file(self, path: str, file_data: FileStorage) -> None:
"""
Expand Down Expand Up @@ -874,7 +874,7 @@ def _save_form_files(self, directory: str, path: str, form: t.Any) -> None:
self.save_file(filename, form.upload.data)
self.on_file_upload(directory, path, filename)

def normpath(self, path):
def normpath(self, path: str) -> str:
return self.storage.normpath(path) # type: ignore[union-attr]

@property
Expand All @@ -886,8 +886,8 @@ def _get_breadcrumbs(self, path: str) -> list[tuple[str, str]]:
Returns a list of tuples with each tuple containing the folder and
the tree up to that folder when traversing down the `path`
"""
accumulator = []
breadcrumbs = []
accumulator: list[str] = []
breadcrumbs: list[tuple[str, str]] = []

for n in path.split(self._separator):
accumulator.append(n)
Expand Down
10 changes: 5 additions & 5 deletions flask_admin/contrib/fileadmin/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(
blob_service_client: BlobServiceClient,
container_name: str,
on_windows: bool = False,
):
) -> None:
"""
Constructor

Expand Down Expand Up @@ -106,7 +106,7 @@ def get_files(
num_path_parts = len(path_parts)

folders = set()
files = []
files: list[tuple[str, str, bool, int, float]] = []

container_client = self._client.get_container_client(self._container_name)

Expand Down Expand Up @@ -168,8 +168,8 @@ def get_base_path(self) -> str:
def get_breadcrumbs(self, path: str | None) -> list[tuple[str, str]]:
path = self._ensure_blob_path(path)

accumulator = []
breadcrumbs = []
accumulator: list[str] = []
breadcrumbs: list[tuple[str, str]] = []
if path is not None:
for folder in path.split(self.separator):
accumulator.append(folder)
Expand All @@ -181,7 +181,7 @@ def send_file(self, file_path: str) -> flask.Response:
if path is None:
raise ValueError("No path provided")
blob = self._container_client.get_blob_client(path).download_blob()
if not blob.properties or not blob.properties.has_key("content_settings"):
if not blob.properties or not blob.properties.has_key("content_settings"): # type: ignore[no-untyped-call]
raise ValueError("Blob has no properties")
mime_type = blob.properties["content_settings"]["content_type"]
blob_file = io.BytesIO()
Expand Down
6 changes: 3 additions & 3 deletions flask_admin/contrib/fileadmin/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _remove_trailing_slash(name: str) -> str:
return name[:-1]

files = []
directories = []
directories: list[tuple[str, str, bool, int, int]] = []
if path and not path.endswith(self.separator):
path += self.separator

Expand Down Expand Up @@ -173,8 +173,8 @@ def get_base_path(self) -> str:

@_strip_leading_slash_from("path")
def get_breadcrumbs(self, path: str) -> list[tuple[str, str]]:
accumulator = []
breadcrumbs = []
accumulator: list[str] = []
breadcrumbs: list[tuple[str, str]] = []
for n in path.split(self.separator):
accumulator.append(n)
breadcrumbs.append((n, self.separator.join(accumulator)))
Expand Down
2 changes: 1 addition & 1 deletion flask_admin/contrib/geoa/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _value(self):
else:
return ""

def process_formdata(self, valuelist):
def process_formdata(self, valuelist: t.Sequence[str] | None) -> None:
super().process_formdata(valuelist)
if str(self.data) == "":
self.data = None
Expand Down
11 changes: 8 additions & 3 deletions flask_admin/contrib/geoa/form.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import typing as t

from flask_admin.contrib.sqla.form import AdminModelConverter as SQLAAdminConverter
from flask_admin.model.form import converts

from ..._types import T_COL_NO_STR
from .fields import GeoJSONField


class AdminModelConverter(SQLAAdminConverter):
@converts("Geography", "Geometry")
def convert_geom(self, column, field_args, **extra):
field_args["geometry_type"] = column.type.geometry_type
field_args["srid"] = column.type.srid
def convert_geom(
self, column: T_COL_NO_STR, field_args: dict[str, t.Any], **extra: t.Any
) -> GeoJSONField:
field_args["geometry_type"] = column.type.geometry_type # type: ignore[union-attr]
field_args["srid"] = column.type.srid # type: ignore[union-attr]
field_args["session"] = self.session
field_args["tile_layer_url"] = self.view.tile_layer_url # type: ignore[attr-defined]
field_args["tile_layer_attribution"] = self.view.tile_layer_attribution # type: ignore[attr-defined]
Expand Down
Loading