Skip to content

Commit 3f1e56b

Browse files
Merge pull request #2608 from ElLorans/master
Type hints
2 parents 87f7c0b + 7a51179 commit 3f1e56b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3037
-1530
lines changed

flask_admin/_backwards.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
"""
77

88
import sys
9+
import typing as t
910
import warnings
11+
from types import ModuleType
1012

1113

12-
def get_property(obj, name, old_name, default=None):
14+
def get_property(obj: t.Any, name: str, old_name: str, default: t.Any = None) -> t.Any:
1315
"""
1416
Check if old property name exists and if it does - show warning message
1517
and return value.
@@ -34,13 +36,13 @@ def get_property(obj, name, old_name, default=None):
3436

3537

3638
class ObsoleteAttr:
37-
def __init__(self, new_name, old_name, default):
39+
def __init__(self, new_name: str, old_name: str, default: t.Any):
3840
self.new_name = new_name
3941
self.old_name = old_name
4042
self.cache = "_cache_" + new_name
4143
self.default = default
4244

43-
def __get__(self, obj, objtype=None):
45+
def __get__(self, obj: t.Any, objtype: t.Optional[type] = None) -> "ObsoleteAttr":
4446
if obj is None:
4547
return self
4648

@@ -62,20 +64,23 @@ def __get__(self, obj, objtype=None):
6264
# Return default otherwise
6365
return self.default
6466

65-
def __set__(self, obj, value):
67+
def __set__(self, obj: t.Any, value: t.Any) -> None:
6668
setattr(obj, self.cache, value)
6769

6870

6971
class ImportRedirect:
70-
def __init__(self, prefix, target):
72+
def __init__(self, prefix: str, target: str):
7173
self.prefix = prefix
7274
self.target = target
7375

74-
def find_module(self, fullname, path=None):
76+
def find_module(
77+
self, fullname: str, path: t.Optional[str] = None
78+
) -> t.Optional["ImportRedirect"]:
7579
if fullname.startswith(self.prefix):
7680
return self
81+
return None
7782

78-
def load_module(self, fullname):
83+
def load_module(self, fullname: str) -> ModuleType:
7984
if fullname in sys.modules:
8085
return sys.modules[fullname]
8186

@@ -86,5 +91,5 @@ def load_module(self, fullname):
8691
return module
8792

8893

89-
def import_redirect(old, new):
90-
sys.meta_path.append(ImportRedirect(old, new))
94+
def import_redirect(old: str, new: str) -> None:
95+
sys.meta_path.append(ImportRedirect(old, new)) # type: ignore[arg-type]

flask_admin/_compat.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,43 @@
1111
:license: BSD, see LICENSE for more details.
1212
"""
1313

14-
from typing import Callable
14+
import typing as t
15+
from types import MappingProxyType
16+
from flask_admin._types import T_TRANSLATABLE, T_ITER_CHOICES
1517

1618
text_type = str
1719
string_types = (str,)
1820

1921

20-
def itervalues(d: dict):
22+
def itervalues(d: dict) -> t.Iterator[t.Any]:
2123
return iter(d.values())
2224

2325

24-
def iteritems(d: dict):
26+
def iteritems(
27+
d: t.Union[dict, MappingProxyType[str, t.Any], t.Mapping[str, t.Any]],
28+
) -> t.Iterator[tuple[t.Any, t.Any]]:
2529
return iter(d.items())
2630

2731

28-
def filter_list(f: Callable, l: list):
32+
def filter_list(f: t.Callable, l: list) -> list[t.Any]:
2933
return list(filter(f, l))
3034

3135

32-
def as_unicode(s):
36+
def as_unicode(s: t.Union[str, bytes]) -> str:
3337
if isinstance(s, bytes):
3438
return s.decode("utf-8")
3539

3640
return str(s)
3741

3842

39-
def csv_encode(s):
43+
def csv_encode(s: t.Union[str, bytes]) -> str:
4044
"""Returns unicode string expected by Python 3's csv module"""
4145
return as_unicode(s)
4246

4347

44-
def _iter_choices_wtforms_compat(val, label, selected):
48+
def _iter_choices_wtforms_compat(
49+
val: str, label: T_TRANSLATABLE, selected: bool
50+
) -> T_ITER_CHOICES:
4551
"""Compatibility for 3-tuples and 4-tuples in iter_choices
4652
4753
https://wtforms.readthedocs.io/en/3.2.x/changes/#version-3-2-0

flask_admin/_types.py

Lines changed: 227 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,230 @@
1-
from collections.abc import Sequence
2-
from typing import Any
3-
from typing import Callable
4-
from typing import TYPE_CHECKING
5-
from typing import Union
1+
import typing as t
2+
from enum import Enum
3+
from os import PathLike
4+
from types import TracebackType
65

7-
if TYPE_CHECKING:
6+
import wtforms.widgets
7+
from flask import Response
8+
from jinja2.runtime import Context
9+
from markupsafe import Markup
10+
from typing_extensions import NotRequired
11+
from werkzeug.wrappers import Response as Wkzg_Response
12+
from wtforms import Field
13+
from wtforms.form import BaseForm
14+
from wtforms.utils import UnsetValue
15+
from wtforms.widgets import Input
16+
17+
if t.TYPE_CHECKING:
18+
from flask_admin.base import BaseView as T_VIEW # noqa
19+
from flask_admin.contrib.sqla.validators import InputRequired as T_INPUT_REQUIRED
20+
from flask_admin.contrib.sqla.validators import (
21+
TimeZoneValidator as T_TIMEZONE_VALIDATOR,
22+
)
23+
from flask_admin.contrib.sqla.validators import Unique as T_UNIQUE
24+
from flask_admin.form import FormOpts as T_FORM_OPTS # noqa
25+
from flask_admin.model import BaseModelView as T_MODEL_VIEW
26+
from flask_admin.model.ajax import AjaxModelLoader as T_AJAX_MODEL_LOADER # noqa
27+
from flask_admin.model.fields import AjaxSelectField as T_AJAX_SELECT_FIELD # noqa
28+
from flask_admin.model.form import ( # noqa
29+
InlineBaseFormAdmin as T_INLINE_BASE_FORM_ADMIN,
30+
)
31+
from flask_admin.model.form import InlineFormAdmin as T_INLINE_FORM_ADMIN
32+
from flask_admin.model.widgets import (
33+
InlineFieldListWidget as T_INLINE_FIELD_LIST_WIDGET,
34+
)
35+
from flask_admin.model.widgets import InlineFormWidget as T_INLINE_FORM_WIDGET
36+
from flask_admin.model.widgets import (
37+
AjaxSelect2Widget as T_INLINE_AJAX_SELECT2_WIDGET,
38+
)
39+
from flask_admin.model.widgets import XEditableWidget as T_INLINE_X_EDITABLE_WIDGET
40+
41+
# optional dependencies
42+
from arrow import Arrow as T_ARROW # noqa
43+
from flask_babel import LazyString as T_LAZY_STRING # noqa
44+
45+
from flask_sqlalchemy import Model as T_SQLALCHEMY_MODEL
46+
from flask_admin.contrib.peewee.form import BaseModel as T_PEEWEE_MODEL
47+
from peewee import Field as T_PEEWEE_FIELD # noqa
48+
from pymongo import MongoClient as T_MONGO_CLIENT
849
import sqlalchemy # noqa
50+
from sqlalchemy import Column as T_SQLALCHEMY_COLUMN
51+
from sqlalchemy import Table as T_TABLE # noqa
52+
from sqlalchemy.orm import scoped_session as T_SQLALCHEMY_SESSION # noqa
53+
from sqlalchemy.orm.query import Query
54+
from sqlalchemy.sql.selectable import Select
55+
56+
T_SQLALCHEMY_QUERY = t.Union[Query, Select]
57+
from redis import Redis as T_REDIS # noqa
58+
from flask_admin.contrib.peewee.ajax import (
59+
QueryAjaxModelLoader as T_PEEWEE_QUERY_AJAX_MODEL_LOADER,
60+
) # noqa
61+
from flask_admin.contrib.sqla.ajax import (
62+
QueryAjaxModelLoader as T_SQLA_QUERY_AJAX_MODEL_LOADER,
63+
) # noqa
64+
else:
65+
T_VIEW = "flask_admin.base.BaseView"
66+
T_INPUT_REQUIRED = "InputRequired"
67+
T_TIMEZONE_VALIDATOR = "TimeZoneValidator"
68+
T_UNIQUE = "Unique"
69+
T_FORM_OPTS = "flask_admin.form.FormOpts"
70+
T_MODEL_VIEW = "flask_admin.model.BaseModelView"
71+
T_AJAX_MODEL_LOADER = "flask_admin.model.ajax.AjaxModelLoader"
72+
T_AJAX_SELECT_FIELD = "flask_admin.model.fields.AjaxSelectField"
73+
T_INLINE_BASE_FORM_ADMIN = "flask_admin.model.form.InlineBaseFormAdmin"
74+
T_INLINE_FORM_ADMIN = "flask_admin.model.form.InlineFormAdmin"
75+
T_INLINE_FIELD_LIST_WIDGET = "flask_admin.model.widgets.InlineFieldListWidget"
76+
T_INLINE_FORM_WIDGET = "flask_admin.model.widgets.InlineFormWidget"
77+
T_INLINE_AJAX_SELECT2_WIDGET = "flask_admin.model.widgets.AjaxSelect2Widget"
78+
T_INLINE_X_EDITABLE_WIDGET = "flask_admin.model.widgets.XEditableWidget"
79+
80+
# optional dependencies
81+
T_ARROW = "arrow.Arrow"
82+
T_LAZY_STRING = "flask_babel.LazyString"
83+
T_SQLALCHEMY_COLUMN = "sqlalchemy.Column"
84+
T_SQLALCHEMY_MODEL = "flask_sqlalchemy.Model"
85+
T_PEEWEE_FIELD = "peewee.Field"
86+
T_PEEWEE_MODEL = "peewee.BaseModel"
87+
T_MONGO_CLIENT = "pymongo.MongoClient"
88+
T_TABLE = "sqlalchemy.Table"
89+
T_SQLALCHEMY_QUERY = t.Union[
90+
"sqlalchemy.sql.selectable.Select", "sqlalchemy.orm.query.Query"
91+
]
92+
T_SQLALCHEMY_SESSION = "sqlalchemy.orm.scoped_session"
93+
T_REDIS = "redis.Redis"
94+
T_PEEWEE_QUERY_AJAX_MODEL_LOADER = (
95+
"flask_admin.contrib.peewee.ajax.QueryAjaxModelLoader"
96+
)
97+
T_SQLA_QUERY_AJAX_MODEL_LOADER = (
98+
"flask_admin.contrib.sqla.ajax.QueryAjaxModelLoader"
99+
)
100+
101+
T_COLUMN = t.Union[str, T_SQLALCHEMY_COLUMN]
102+
T_FILTER = tuple[int, T_COLUMN, str]
103+
T_COLUMN_LIST = t.Sequence[T_COLUMN]
104+
# Compatibility for 3-tuples and 4-tuples in iter_choices
105+
# https://wtforms.readthedocs.io/en/3.2.x/changes/#version-3-2-0
106+
T_ITER_CHOICES = t.Union[
107+
tuple[t.Any, str, bool, dict[str, t.Any]], tuple[str, str, bool]
108+
]
109+
T_FORMATTER = t.Callable[[T_MODEL_VIEW, t.Optional[Context], t.Any, str], str]
110+
T_COLUMN_FORMATTERS = dict[str, T_FORMATTER]
111+
T_TYPE_FORMATTER = t.Callable[[T_MODEL_VIEW, t.Any, str], t.Union[str, Markup]]
112+
T_COLUMN_TYPE_FORMATTERS = dict[type, T_TYPE_FORMATTER]
113+
T_TRANSLATABLE = t.Union[str, T_LAZY_STRING]
114+
T_OPTION = tuple[str, T_TRANSLATABLE]
115+
T_OPTION_LIST = t.Sequence[T_OPTION]
116+
T_OPTIONS = t.Union[None, T_OPTION_LIST, t.Callable[[], T_OPTION_LIST]]
117+
T_ORM_MODEL = t.Union[T_SQLALCHEMY_MODEL, T_PEEWEE_MODEL, T_MONGO_CLIENT]
118+
T_QUERY_AJAX_MODEL_LOADER = t.Union[
119+
T_PEEWEE_QUERY_AJAX_MODEL_LOADER, T_SQLA_QUERY_AJAX_MODEL_LOADER
120+
]
121+
T_RESPONSE = t.Union[Response, Wkzg_Response]
122+
T_SQLALCHEMY_INLINE_MODELS = t.Union[
123+
t.Sequence[t.Union[T_INLINE_FORM_ADMIN, T_SQLALCHEMY_MODEL]],
124+
tuple[T_SQLALCHEMY_MODEL, dict[str, t.Any]],
125+
]
126+
T_VALIDATOR = t.Union[
127+
t.Callable[[t.Any, t.Any], t.Any],
128+
T_UNIQUE,
129+
T_INPUT_REQUIRED,
130+
wtforms.validators.Optional,
131+
wtforms.validators.Length,
132+
wtforms.validators.AnyOf,
133+
wtforms.validators.Email,
134+
wtforms.validators.URL,
135+
wtforms.validators.IPAddress,
136+
T_TIMEZONE_VALIDATOR,
137+
wtforms.validators.NumberRange,
138+
wtforms.validators.MacAddress,
139+
]
140+
T_PATH_LIKE = t.Union[str, bytes, PathLike[str], PathLike[bytes]]
141+
142+
143+
class WidgetProtocol(t.Protocol):
144+
def __call__(self, field: Field, **kwargs: t.Any) -> t.Union[str, Markup]: ...
145+
146+
147+
T_WIDGET = t.Union[
148+
Input,
149+
T_INLINE_FIELD_LIST_WIDGET,
150+
T_INLINE_FORM_WIDGET,
151+
T_INLINE_AJAX_SELECT2_WIDGET,
152+
T_INLINE_X_EDITABLE_WIDGET,
153+
WidgetProtocol,
154+
]
155+
156+
T_WIDGET_TYPE = t.Optional[
157+
t.Union[
158+
t.Literal[
159+
"daterangepicker",
160+
"datetimepicker",
161+
"datetimerangepicker",
162+
"datepicker",
163+
"select2-tags",
164+
"timepicker",
165+
"timerangepicker",
166+
"uuid",
167+
],
168+
str,
169+
]
170+
]
171+
172+
173+
class T_FIELD_ARGS_DESCRIPTION(t.TypedDict, total=False):
174+
description: NotRequired[str]
175+
176+
177+
class T_FIELD_ARGS_FILTERS(t.TypedDict):
178+
filters: NotRequired[list[t.Callable[[t.Any], t.Any]]]
179+
allow_blank: NotRequired[bool]
180+
choices: NotRequired[t.Union[list[tuple[int, str]], list[Enum]]]
181+
validators: NotRequired[list[T_VALIDATOR]]
182+
coerce: NotRequired[t.Callable[[t.Any], t.Any]]
183+
184+
185+
class T_FIELD_ARGS_LABEL(t.TypedDict):
186+
label: NotRequired[str]
187+
188+
189+
class T_FIELD_ARGS_PLACES(t.TypedDict):
190+
places: t.Optional[UnsetValue]
191+
192+
193+
class T_FIELD_ARGS_VALIDATORS(t.TypedDict, total=False):
194+
label: NotRequired[str]
195+
description: NotRequired[str]
196+
filters: NotRequired[list[t.Callable[[t.Any], t.Any]]]
197+
default: NotRequired[t.Any]
198+
widget: NotRequired[Input]
199+
validators: NotRequired[list[T_VALIDATOR]]
200+
render_kw: NotRequired[dict[str, t.Any]]
201+
name: NotRequired[str]
202+
_form: NotRequired[BaseForm]
203+
_prefix: NotRequired[str]
204+
205+
206+
class T_FIELD_ARGS_VALIDATORS_ALLOW_BLANK(T_FIELD_ARGS_VALIDATORS):
207+
allow_blank: NotRequired[bool]
208+
209+
210+
# Flask types
211+
_ExcInfo = tuple[
212+
t.Optional[type[BaseException]],
213+
t.Optional[BaseException],
214+
t.Optional[TracebackType],
215+
]
216+
_StartResponse = t.Callable[
217+
[str, list[tuple[str, str]], t.Optional[_ExcInfo]], t.Callable[[bytes], t.Any]
218+
]
219+
_WSGICallable = t.Callable[[dict[str, t.Any], _StartResponse], t.Iterable[bytes]]
220+
_Status = t.Union[str, int]
221+
_Headers = t.Union[dict[t.Any, t.Any], list[tuple[t.Any, t.Any]]]
222+
_Body = t.Union[str, t.ByteString, dict[str, t.Any], Response, _WSGICallable]
223+
_ViewFuncReturnType = t.Union[
224+
_Body,
225+
tuple[_Body, _Status, _Headers],
226+
tuple[_Body, _Status],
227+
tuple[_Body, _Headers],
228+
]
9229

10-
T_COLUMN_LIST = Sequence[Union[str, "sqlalchemy.Column"]]
11-
T_FORMATTER = Callable[[Any, Any, Any], Any]
12-
T_FORMATTERS = dict[type, T_FORMATTER]
230+
_ViewFunc = t.Union[t.Callable[..., t.NoReturn], t.Callable[..., _ViewFuncReturnType]]

0 commit comments

Comments
 (0)