Skip to content
Open
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
3 changes: 2 additions & 1 deletion examples/csp_nonce/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Callable
from typing import Any

from flask import Flask
from flask_admin import Admin
Expand All @@ -19,7 +20,7 @@
content_security_policy_nonce_in=["script-src", "style-src"],
)
# Get the CSP nonce generator from jinja environment globals which is added by Talisman
csp_nonce_generator: Callable = app.jinja_env.globals["csp_nonce"] # type: ignore[assignment]
csp_nonce_generator: Callable[[], Any] = app.jinja_env.globals["csp_nonce"] # type: ignore[assignment]


@app.route("/")
Expand Down
4 changes: 3 additions & 1 deletion examples/pymongo_simple/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from bson.objectid import ObjectId
from flask import Flask
from flask import url_for
Expand Down Expand Up @@ -107,7 +109,7 @@ def index():

if __name__ == "__main__":
with MongoDbContainer("mongo:7.0.7") as mongo:
conn: MongoClient = MongoClient(mongo.get_connection_url())
conn: MongoClient[Any] = MongoClient(mongo.get_connection_url())
db = conn.test

admin.add_view(UserView(db.user, "User"))
Expand Down
10 changes: 5 additions & 5 deletions flask_admin/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,27 @@

import typing as t
from types import MappingProxyType
from flask_admin._types import T_TRANSLATABLE, T_ITER_CHOICES
from flask_admin._types import T_TRANSLATABLE, T_ITER_CHOICES, T_ORM_MODEL

text_type = str
string_types = (str,)


def itervalues(d: dict) -> t.Iterator[t.Any]:
def itervalues(d: dict[t.Any, t.Any]) -> t.Iterator[t.Any]:
return iter(d.values())


def iteritems(
d: dict | MappingProxyType[str, t.Any] | t.Mapping[str, t.Any],
d: dict[t.Any, t.Any] | MappingProxyType[str, t.Any] | t.Mapping[t.Any, t.Any],
) -> t.Iterator[tuple[t.Any, t.Any]]:
return iter(d.items())


def filter_list(f: t.Callable, l: list) -> list[t.Any]:
def filter_list(f: t.Callable[[t.Any], t.Any], l: list[t.Any]) -> list[t.Any]:
return list(filter(f, l))


def as_unicode(s: str | bytes | int) -> str:
def as_unicode(s: t.Any) -> str:
if isinstance(s, bytes):
return s.decode("utf-8")

Expand Down
44 changes: 20 additions & 24 deletions flask_admin/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import wtforms.widgets
from flask import Response
from jinja2.runtime import Context
from markupsafe import Markup
from werkzeug.wrappers import Response as Wkzg_Response
from wtforms import Field
Expand All @@ -14,9 +13,9 @@
from wtforms.widgets import Input

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

if t.TYPE_CHECKING:
from flask_admin.base import BaseView as T_VIEW # noqa
Expand Down Expand Up @@ -56,18 +55,24 @@
from flask_sqlalchemy import Model as T_SQLALCHEMY_MODEL
from peewee import Model as T_PEEWEE_MODEL
from peewee import Field as T_PEEWEE_FIELD # noqa
from pymongo import MongoClient as T_MONGO_CLIENT
from pymongo import MongoClient
from mongoengine import Document as T_MONGO_ENGINE_CLIENT
import sqlalchemy # noqa
from sqlalchemy import Column as T_SQLALCHEMY_COLUMN
from sqlalchemy import Column
from sqlalchemy import Table as T_TABLE # noqa
from sqlalchemy.orm import InstrumentedAttribute as T_INSTRUMENTED_ATTRIBUTE # noqa
from sqlalchemy.orm import scoped_session as T_SQLALCHEMY_SESSION # noqa
from sqlalchemy.orm.query import Query
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy_utils import Choice as T_CHOICE # noqa
from sqlalchemy_utils import ChoiceType as T_CHOICE_TYPE # noqa

T_SQLALCHEMY_QUERY = Query
try:
T_INSTRUMENTED_ATTRIBUTE = InstrumentedAttribute[t.Any]
except TypeError: # Fall back to non-generic types for older SQLAlchemy
T_INSTRUMENTED_ATTRIBUTE = InstrumentedAttribute # type: ignore[misc]

try:
T_SQLALCHEMY_COLUMN = Column[t.Any]
except TypeError: # Fall back to non-generic types for older SQLAlchemy
T_SQLALCHEMY_COLUMN = Column # type: ignore[misc]
T_MONGO_CLIENT = MongoClient[t.Any]
from redis import Redis as T_REDIS # noqa
from flask_admin.contrib.peewee.ajax import (
QueryAjaxModelLoader as T_PEEWEE_QUERY_AJAX_MODEL_LOADER,
Expand Down Expand Up @@ -101,19 +106,17 @@
# optional dependencies
T_ARROW = "arrow.Arrow"
T_LAZY_STRING = "flask_babel.LazyString"
T_SQLALCHEMY_COLUMN = "sqlalchemy.Column"
T_SQLALCHEMY_MODEL = "flask_sqlalchemy.Model"
T_SQLALCHEMY_COLUMN = "sqlalchemy.Column[t.Any]"
T_SQLALCHEMY_MODEL = t.TypeVar("T_SQLALCHEMY_MODEL", bound=t.Any)
T_PEEWEE_FIELD = "peewee.Field"
T_PEEWEE_MODEL = "peewee.Model"
T_MONGO_CLIENT = "pymongo.MongoClient"
T_PEEWEE_MODEL = t.TypeVar("T_PEEWEE_MODEL", bound=t.Any)
T_MONGO_CLIENT = "pymongo.MongoClient[t.Any]"
T_MONGO_ENGINE_CLIENT = "mongoengine.Document"
T_TABLE = "sqlalchemy.Table"
T_CHOICE_TYPE = "sqlalchemy_utils.ChoiceType"
T_CHOICE = "sqlalchemy_utils.Choice"

T_SQLALCHEMY_QUERY = "sqlalchemy.orm.query.Query"
T_INSTRUMENTED_ATTRIBUTE = "sqlalchemy.orm.InstrumentedAttribute"
T_SQLALCHEMY_SESSION = "sqlalchemy.orm.scoped_session"
T_INSTRUMENTED_ATTRIBUTE = t.TypeVar("T_INSTRUMENTED_ATTRIBUTE", bound=t.Any)
T_REDIS = "redis.Redis"
T_PEEWEE_QUERY_AJAX_MODEL_LOADER = (
"flask_admin.contrib.peewee.ajax.QueryAjaxModelLoader"
Expand All @@ -129,13 +132,6 @@
T_COLUMN_LIST = t.Sequence[
T_ORM_COLUMN | t.Iterable[T_ORM_COLUMN] | tuple[str, tuple[T_ORM_COLUMN, ...]]
]
T_CONTRAVARIANT_MODEL_VIEW = t.TypeVar(
"T_CONTRAVARIANT_MODEL_VIEW", bound=T_MODEL_VIEW, contravariant=True
)
T_FORMATTER = t.Callable[
[T_CONTRAVARIANT_MODEL_VIEW, Context | None, t.Any, str], str | Markup
]
T_COLUMN_FORMATTERS = dict[str, T_FORMATTER]
T_TYPE_FORMATTER = t.Callable[[T_MODEL_VIEW, t.Any, str], str | Markup]
T_COLUMN_TYPE_FORMATTERS = dict[type, T_TYPE_FORMATTER]
T_TRANSLATABLE = t.Union[str, T_LAZY_STRING]
Expand Down
6 changes: 4 additions & 2 deletions flask_admin/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from flask_admin.helpers import get_redirect_target


def action(name: str, text: str, confirmation: str | None = None) -> t.Callable:
def action(
name: str, text: str, confirmation: str | None = None
) -> t.Callable[..., t.Any]:
"""
Use this decorator to expose actions that span more than one
entity (model, file, etc)
Expand All @@ -25,7 +27,7 @@ def action(name: str, text: str, confirmation: str | None = None) -> t.Callable:
unconditionally.
"""

def wrap(f: t.Callable) -> t.Callable:
def wrap(f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
f._action = (name, text, confirmation) # type: ignore[attr-defined]
return f

Expand Down
2 changes: 1 addition & 1 deletion flask_admin/babel.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def ngettext(self, singular: str, plural: str, n: int) -> str:
else:
from flask_admin import translations

class CustomDomain(Domain):
class CustomDomain(Domain): # type: ignore[misc]
def __init__(self) -> None:
super().__init__(translations.__path__[0], domain="admin")

Expand Down
25 changes: 14 additions & 11 deletions flask_admin/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from flask_admin import babel
from flask_admin import helpers as h
from flask_admin._compat import as_unicode
from flask_admin._types import T_VIEW

# For compatibility reasons import MenuLink
from flask_admin.blueprints import _BlueprintWithHostSupport as Blueprint
Expand All @@ -29,7 +30,9 @@
from flask_admin.theme import Theme


def expose(url: str = "/", methods: t.Iterable[str] | None = ("GET",)) -> t.Callable:
def expose(
url: str = "/", methods: t.Iterable[str] | None = ("GET",)
) -> t.Callable[[t.Any], t.Any]:
"""
Use this decorator to expose views in your view classes.

Expand All @@ -48,7 +51,7 @@ def wrap(f: AdminViewMeta) -> AdminViewMeta:
return wrap


def expose_plugview(url: str = "/") -> t.Callable:
def expose_plugview(url: str = "/") -> t.Callable[[t.Any], t.Any]:
"""
Decorator to expose Flask's pluggable view classes
(``flask.views.View`` or ``flask.views.MethodView``).
Expand All @@ -71,7 +74,7 @@ def wrap(v: View | MethodView) -> t.Any:


# Base views
def _wrap_view(f: t.Callable) -> t.Callable:
def _wrap_view(f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
# Avoid wrapping view method twice
if hasattr(f, "_wrapped"):
return f
Expand Down Expand Up @@ -161,7 +164,7 @@ def index(self):
"""Extra JavaScript files to include in the page"""

@property
def _template_args(self) -> dict:
def _template_args(self) -> dict[str, str]:
"""
Extra template arguments.

Expand Down Expand Up @@ -418,7 +421,7 @@ def _handle_view(self, name: str, **kwargs: dict[str, t.Any]) -> t.Any:
return self.inaccessible_callback(name, **kwargs)

def _run_view(
self, fn: t.Callable, *args: tuple[t.Any], **kwargs: dict[str, t.Any]
self, fn: t.Callable[..., t.Any], *args: t.Any, **kwargs: t.Any
) -> t.Any:
"""
This method will run actual view function.
Expand Down Expand Up @@ -547,7 +550,7 @@ def __init__(
theme: Theme | None = None,
category_icon_classes: dict[str, str] | None = None,
host: str | None = None,
csp_nonce_generator: t.Callable | None = None,
csp_nonce_generator: t.Callable[[], t.Any] | None = None,
) -> None:
"""
Constructor.
Expand Down Expand Up @@ -587,10 +590,10 @@ def __init__(

self.translations_path = translations_path

self._views = [] # type: ignore[var-annotated]
self._menu = [] # type: ignore[var-annotated]
self._views: list[T_VIEW] = []
self._menu: list[MenuView | MenuCategory | BaseMenu] = []
self._menu_categories: dict[str, MenuCategory] = dict()
self._menu_links = [] # type: ignore[var-annotated]
self._menu_links: list[MenuLink] = []

if name is None:
name = "Admin"
Expand Down Expand Up @@ -891,13 +894,13 @@ def _init_extension(self) -> None:
admins.append(self)
self.app.extensions["admin"] = admins # type: ignore[union-attr]

def menu(self) -> list:
def menu(self) -> list[MenuView | MenuCategory | BaseMenu]:
"""
Return the menu hierarchy.
"""
return self._menu

def menu_links(self) -> list:
def menu_links(self) -> list[MenuLink]:
"""
Return menu links.
"""
Expand Down
10 changes: 5 additions & 5 deletions flask_admin/contrib/fileadmin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def get_upload_form(self) -> type[form.BaseForm]:
Override to implement customized behavior.
"""

class UploadForm(self.form_base_class): # type: ignore[name-defined]
class UploadForm(self.form_base_class): # type: ignore[name-defined, misc]
"""
File upload form. Works with FileAdmin instance to check if it
is allowed to upload file with given extension.
Expand Down Expand Up @@ -435,7 +435,7 @@ def get_edit_form(self) -> type[form.BaseForm]:
Override to implement customized behavior.
"""

class EditForm(self.form_base_class): # type: ignore[name-defined]
class EditForm(self.form_base_class): # type: ignore[name-defined, misc]
content = fields.TextAreaField(
lazy_gettext("Content"), (validators.InputRequired(),)
)
Expand All @@ -456,7 +456,7 @@ def validate_name(self: type[form.BaseForm], field: Field) -> None:
if not regexp.match(field.data):
raise validators.ValidationError(gettext("Invalid name"))

class NameForm(self.form_base_class): # type: ignore[name-defined]
class NameForm(self.form_base_class): # type: ignore[name-defined, misc]
"""
Form with a filename input field.

Expand All @@ -478,7 +478,7 @@ def get_delete_form(self) -> type[form.BaseForm]:
Override to implement customized behavior.
"""

class DeleteForm(self.form_base_class): # type: ignore[name-defined]
class DeleteForm(self.form_base_class): # type: ignore[name-defined, misc]
path = fields.HiddenField(validators=[validators.InputRequired()])

return DeleteForm
Expand All @@ -490,7 +490,7 @@ def get_action_form(self) -> type[form.BaseForm]:
Override to implement customized behavior.
"""

class ActionForm(self.form_base_class): # type: ignore[name-defined]
class ActionForm(self.form_base_class): # type: ignore[name-defined, misc]
action = fields.HiddenField()
url = fields.HiddenField()
# rowid is retrieved using getlist, for backward compatibility
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 @@ -22,11 +22,11 @@ def _strip_leading_slash_from(
"""

def decorator(
func: t.Callable,
func: t.Callable[..., t.Any],
) -> t.Callable[[tuple[t.Any, ...], dict[str, t.Any]], t.Any]:
@functools.wraps(func)
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
args: list = list(args) # type: ignore[no-redef]
args: list[t.Any] = list(args) # type: ignore[no-redef]
arg_names = func.__code__.co_varnames[: func.__code__.co_argcount]

if arg_name in arg_names:
Expand Down Expand Up @@ -82,7 +82,7 @@ def __init__(self, s3_client: BaseClient, bucket_name: str) -> None:
self.separator = "/"

@_strip_leading_slash_from("path")
def get_files(self, path: str, directory: str) -> list:
def get_files(self, path: str, directory: str) -> list[t.Any]:
def _strip_path(name: str, path: str) -> str:
if name.startswith(path):
return name.replace(path, "", 1)
Expand Down
3 changes: 1 addition & 2 deletions flask_admin/contrib/mongoengine/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from flask_admin.model import BaseModelView
from flask_admin.model.form import create_editable_list_form

from ...model.filters import BaseFilter
from .ajax import create_ajax_loader
from .ajax import process_ajax_references
from .filters import BaseMongoEngineFilter
Expand Down Expand Up @@ -60,7 +59,7 @@ class ModelView(BaseModelView):
MongoEngine model scaffolding.
"""

column_filters: t.Collection[str | BaseFilter] | None = None
column_filters: t.Collection[str | BaseMongoEngineFilter] | None = None
"""
Collection of the column filters.
Expand Down
8 changes: 4 additions & 4 deletions flask_admin/contrib/peewee/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, name: str, model: t.Any, **options: t.Any) -> None:
super().__init__(name, options)

self.model = model
self.fields = t.cast(t.Iterable, options.get("fields"))
self.fields = t.cast(t.Iterable[t.Any], options.get("fields"))

if not self.fields:
raise ValueError(
Expand Down Expand Up @@ -48,7 +48,7 @@ def _process_fields(self) -> list[t.Any]:

return remote_fields

def format(self, model: None | str | bytes) -> tuple[t.Any, str] | None:
def format(self, model: T_PEEWEE_MODEL | None) -> tuple[t.Any, str] | None: # type: ignore[override]
if not model:
return None

Expand Down Expand Up @@ -84,7 +84,7 @@ def create_ajax_loader(
model: type[T_PEEWEE_MODEL],
name: str,
field_name: str,
options: dict[str, t.Any] | list | tuple,
options: dict[str, t.Any],
) -> QueryAjaxModelLoader:
prop = getattr(model, field_name, None)

Expand All @@ -93,4 +93,4 @@ def create_ajax_loader(

# TODO: Check for field
remote_model = prop.rel_model
return QueryAjaxModelLoader(name, remote_model, **options) # type: ignore[arg-type]
return QueryAjaxModelLoader(name, remote_model, **options)
Loading