fastlifeweb 0.16.3__py3-none-any.whl → 0.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. fastlife/adapters/jinjax/renderer.py +49 -25
  2. fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
  3. fastlife/adapters/jinjax/widget_factory/base.py +38 -0
  4. fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
  5. fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
  6. fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
  7. fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
  8. fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
  9. fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
  10. fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
  11. fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
  12. fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
  13. fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
  14. fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
  15. fastlife/adapters/jinjax/widgets/base.py +6 -4
  16. fastlife/adapters/jinjax/widgets/checklist.py +1 -1
  17. fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
  18. fastlife/adapters/jinjax/widgets/hidden.py +2 -0
  19. fastlife/adapters/jinjax/widgets/model.py +4 -1
  20. fastlife/adapters/jinjax/widgets/sequence.py +3 -2
  21. fastlife/adapters/jinjax/widgets/text.py +9 -10
  22. fastlife/adapters/jinjax/widgets/union.py +9 -7
  23. fastlife/components/Form.jinja +12 -0
  24. fastlife/config/configurator.py +23 -24
  25. fastlife/config/exceptions.py +4 -1
  26. fastlife/config/openapiextra.py +1 -0
  27. fastlife/config/resources.py +26 -27
  28. fastlife/config/settings.py +2 -0
  29. fastlife/config/views.py +3 -1
  30. fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
  31. fastlife/middlewares/session/middleware.py +2 -2
  32. fastlife/middlewares/session/serializer.py +6 -5
  33. fastlife/request/form.py +7 -6
  34. fastlife/request/form_data.py +2 -6
  35. fastlife/routing/route.py +3 -1
  36. fastlife/routing/router.py +1 -0
  37. fastlife/security/csrf.py +2 -1
  38. fastlife/security/policy.py +2 -1
  39. fastlife/services/locale_negociator.py +2 -1
  40. fastlife/services/policy.py +3 -2
  41. fastlife/services/templates.py +2 -1
  42. fastlife/services/translations.py +15 -8
  43. fastlife/shared_utils/infer.py +4 -3
  44. fastlife/shared_utils/resolver.py +64 -4
  45. fastlife/templates/binding.py +2 -1
  46. fastlife/testing/__init__.py +1 -0
  47. fastlife/testing/dom.py +140 -0
  48. fastlife/testing/form.py +204 -0
  49. fastlife/testing/session.py +67 -0
  50. fastlife/testing/testclient.py +7 -390
  51. fastlife/views/pydantic_form.py +4 -4
  52. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
  53. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
  54. fastlife/adapters/jinjax/widgets/factory.py +0 -525
  55. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
  56. {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,58 @@
1
+ """Handle Sequence type."""
2
+
3
+ from collections.abc import Mapping, MutableSequence, Sequence
4
+ from typing import Any
5
+
6
+ from pydantic.fields import FieldInfo
7
+
8
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
9
+ from fastlife.adapters.jinjax.widgets.base import Widget
10
+ from fastlife.adapters.jinjax.widgets.sequence import SequenceWidget
11
+
12
+
13
+ class SequenceBuilder(BaseWidgetBuilder[Sequence[Any]]):
14
+ """Builder for Sequence values."""
15
+
16
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
17
+ """True for Sequence, MutableSequence or list"""
18
+ return origin is Sequence or origin is MutableSequence or origin is list
19
+
20
+ def build(
21
+ self,
22
+ *,
23
+ field_name: str,
24
+ field_type: type[Any],
25
+ field: FieldInfo | None,
26
+ value: Sequence[Any] | None,
27
+ form_errors: Mapping[str, Any],
28
+ removable: bool,
29
+ ) -> Widget[Sequence[Any]]:
30
+ """Build the widget."""
31
+ typ = field_type.__args__[0] # type: ignore
32
+ value = value or []
33
+ items: Sequence[Any] = [
34
+ self.factory.build(
35
+ typ, # type: ignore
36
+ name=f"{field_name}.{idx}",
37
+ value=v,
38
+ field=field,
39
+ form_errors=form_errors,
40
+ removable=True,
41
+ )
42
+ for idx, v in enumerate(value)
43
+ ]
44
+ return SequenceWidget(
45
+ field_name,
46
+ title=field.title if field else "",
47
+ hint=field.description if field else None,
48
+ aria_label=(
49
+ field.json_schema_extra.get("aria_label") # type:ignore
50
+ if field and field.json_schema_extra
51
+ else None
52
+ ),
53
+ value=items,
54
+ item_type=typ, # type: ignore
55
+ token=self.factory.token,
56
+ removable=removable,
57
+ error=form_errors.get(field_name),
58
+ )
@@ -0,0 +1,80 @@
1
+ """Handle Set type."""
2
+
3
+ from collections.abc import Mapping
4
+ from enum import Enum
5
+ from typing import Any, Literal, get_origin
6
+
7
+ from pydantic.fields import FieldInfo
8
+
9
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
10
+ from fastlife.adapters.jinjax.widgets.base import Widget
11
+ from fastlife.adapters.jinjax.widgets.checklist import Checkable, ChecklistWidget
12
+
13
+
14
+ class SetBuilder(BaseWidgetBuilder[set[Any]]):
15
+ """Builder for Set."""
16
+
17
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
18
+ """True for Set"""
19
+ return origin is set
20
+
21
+ def build(
22
+ self,
23
+ *,
24
+ field_name: str,
25
+ field_type: type[Any],
26
+ field: FieldInfo | None,
27
+ value: set[Any] | None,
28
+ form_errors: Mapping[str, Any],
29
+ removable: bool,
30
+ ) -> Widget[Any]:
31
+ """Build the widget."""
32
+ choice_wrapper = field_type.__args__[0]
33
+ choices = []
34
+ choice_wrapper_origin = get_origin(choice_wrapper)
35
+ if choice_wrapper_origin:
36
+ if choice_wrapper_origin is Literal:
37
+ litchoice: list[str] = choice_wrapper.__args__ # type: ignore
38
+ choices = [
39
+ Checkable(
40
+ label=c,
41
+ value=c,
42
+ checked=c in value if value else False, # type: ignore
43
+ name=field_name,
44
+ token=self.factory.token,
45
+ error=form_errors.get(f"{field_name}-{c}"),
46
+ )
47
+ for c in litchoice
48
+ ]
49
+
50
+ else:
51
+ raise NotImplementedError # coverage: ignore
52
+ elif issubclass(choice_wrapper, Enum):
53
+ choices = [
54
+ Checkable(
55
+ label=e.value,
56
+ value=e.name,
57
+ checked=e.name in value if value else False, # type: ignore
58
+ name=field_name,
59
+ token=self.factory.token,
60
+ error=form_errors.get(f"{field_name}-{e.name}"),
61
+ )
62
+ for e in choice_wrapper
63
+ ]
64
+ else:
65
+ raise NotImplementedError # coverage: ignore
66
+
67
+ return ChecklistWidget(
68
+ field_name,
69
+ title=field.title if field else "",
70
+ hint=field.description if field else None,
71
+ aria_label=(
72
+ field.json_schema_extra.get("aria_label") # type:ignore
73
+ if field and field.json_schema_extra
74
+ else None
75
+ ),
76
+ token=self.factory.token,
77
+ value=choices,
78
+ removable=removable,
79
+ error=form_errors.get(field_name),
80
+ )
@@ -0,0 +1,47 @@
1
+ """Handle simple types (str, int, float, ...)."""
2
+
3
+ from collections.abc import Mapping
4
+ from decimal import Decimal
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from pydantic.fields import FieldInfo
9
+
10
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
11
+ from fastlife.adapters.jinjax.widgets.base import Widget
12
+ from fastlife.adapters.jinjax.widgets.text import TextWidget
13
+
14
+
15
+ class SimpleTypeBuilder(BaseWidgetBuilder[str | int | str | float | Decimal | UUID]):
16
+ """Builder for simple types."""
17
+
18
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
19
+ """True for simple types: int, str, float, Decimal, UUID"""
20
+ return issubclass(typ, int | str | float | Decimal | UUID)
21
+
22
+ def build(
23
+ self,
24
+ *,
25
+ field_name: str,
26
+ field_type: type[Any],
27
+ field: FieldInfo | None,
28
+ value: int | str | float | Decimal | UUID | None,
29
+ form_errors: Mapping[str, Any],
30
+ removable: bool,
31
+ ) -> Widget[int | str | float | Decimal | UUID]:
32
+ """Build the widget."""
33
+ return TextWidget(
34
+ field_name,
35
+ placeholder=str(field.examples[0]) if field and field.examples else None,
36
+ title=field.title if field else "",
37
+ hint=field.description if field else None,
38
+ aria_label=(
39
+ field.json_schema_extra.get("aria_label") # type:ignore
40
+ if field and field.json_schema_extra
41
+ else None
42
+ ),
43
+ removable=removable,
44
+ token=self.factory.token,
45
+ value=str(value) if value else "",
46
+ error=form_errors.get(field_name),
47
+ )
@@ -0,0 +1,90 @@
1
+ """Handle Union type."""
2
+
3
+ from collections.abc import Mapping
4
+ from types import NoneType
5
+ from typing import Any
6
+
7
+ from pydantic import ValidationError
8
+ from pydantic.fields import FieldInfo
9
+
10
+ from fastlife.adapters.jinjax.widget_factory.base import BaseWidgetBuilder
11
+ from fastlife.adapters.jinjax.widgets.base import Widget
12
+ from fastlife.adapters.jinjax.widgets.union import UnionWidget
13
+ from fastlife.shared_utils.infer import is_complex_type, is_union
14
+
15
+
16
+ class UnionBuilder(BaseWidgetBuilder[Any]):
17
+ """Builder for Union."""
18
+
19
+ def accept(self, typ: type[Any], origin: type[Any] | None) -> bool:
20
+ """True for unions Union[A,B], A | B or event Optional[A], A | None"""
21
+ return is_union(typ)
22
+
23
+ def build(
24
+ self,
25
+ *,
26
+ field_name: str,
27
+ field_type: type[Any],
28
+ field: FieldInfo | None,
29
+ value: Any | None,
30
+ form_errors: Mapping[str, Any],
31
+ removable: bool,
32
+ ) -> Widget[Any]:
33
+ """Build the widget."""
34
+ types: list[type[Any]] = []
35
+ # required = True
36
+ for typ in field_type.__args__: # type: ignore
37
+ if typ is NoneType:
38
+ # required = False
39
+ continue
40
+ types.append(typ) # type: ignore
41
+
42
+ if (
43
+ not removable
44
+ and len(types) == 1
45
+ # if the optional type is a complex type,
46
+ and not is_complex_type(types[0])
47
+ ):
48
+ return self.factory.build( # coverage: ignore
49
+ types[0],
50
+ name=field_name,
51
+ field=field,
52
+ value=value,
53
+ form_errors=form_errors,
54
+ removable=False,
55
+ )
56
+ child = None
57
+ if value:
58
+ for typ in types:
59
+ try:
60
+ typ(**value)
61
+ except ValidationError:
62
+ pass
63
+ else:
64
+ child = self.factory.build(
65
+ typ,
66
+ name=field_name,
67
+ field=field,
68
+ value=value,
69
+ form_errors=form_errors,
70
+ removable=False,
71
+ )
72
+
73
+ widget = UnionWidget(
74
+ field_name,
75
+ # we assume those types are BaseModel
76
+ value=child,
77
+ children_types=types, # type: ignore
78
+ title=field.title if field else "",
79
+ hint=field.description if field else None,
80
+ aria_label=(
81
+ field.json_schema_extra.get("aria_label") # type:ignore
82
+ if field and field.json_schema_extra
83
+ else None
84
+ ),
85
+ token=self.factory.token,
86
+ removable=removable,
87
+ error=form_errors.get(field_name),
88
+ )
89
+
90
+ return widget
@@ -1,7 +1,9 @@
1
1
  """Widget base class."""
2
+
2
3
  import abc
3
4
  import secrets
4
- from typing import Any, Generic, Mapping, Type, TypeVar
5
+ from collections.abc import Mapping
6
+ from typing import Any, Generic, TypeVar
5
7
 
6
8
  from markupsafe import Markup
7
9
 
@@ -11,7 +13,7 @@ from fastlife.shared_utils.infer import is_union
11
13
  T = TypeVar("T")
12
14
 
13
15
 
14
- def get_title(typ: Type[Any]) -> str:
16
+ def get_title(typ: type[Any]) -> str:
15
17
  return getattr(
16
18
  getattr(typ, "__meta__", None),
17
19
  "title",
@@ -81,7 +83,7 @@ class Widget(abc.ABC, Generic[T]):
81
83
  return Markup(renderer.render_template(self.get_template(), widget=self))
82
84
 
83
85
 
84
- def _get_fullname(typ: Type[Any]) -> str:
86
+ def _get_fullname(typ: type[Any]) -> str:
85
87
  if is_union(typ):
86
88
  typs = [_get_fullname(t) for t in typ.__args__] # type: ignore
87
89
  return "|".join(typs) # type: ignore
@@ -102,7 +104,7 @@ class TypeWrapper:
102
104
 
103
105
  def __init__(
104
106
  self,
105
- typ: Type[Any],
107
+ typ: type[Any],
106
108
  route_prefix: str,
107
109
  name: str,
108
110
  token: str,
@@ -2,7 +2,7 @@
2
2
  Widget for field of type Set.
3
3
  """
4
4
 
5
- from typing import Sequence
5
+ from collections.abc import Sequence
6
6
 
7
7
  from pydantic import BaseModel, Field
8
8
 
@@ -2,7 +2,7 @@
2
2
  Widget for field of type Enum or Literal.
3
3
  """
4
4
 
5
- from typing import Optional, Sequence, Tuple
5
+ from collections.abc import Sequence
6
6
 
7
7
  from .base import Widget
8
8
 
@@ -26,14 +26,14 @@ class DropDownWidget(Widget[str]):
26
26
  self,
27
27
  name: str,
28
28
  *,
29
- title: Optional[str],
30
- hint: Optional[str] = None,
31
- aria_label: Optional[str] = None,
32
- value: Optional[str] = None,
29
+ title: str | None,
30
+ hint: str | None = None,
31
+ aria_label: str | None = None,
32
+ value: str | None = None,
33
33
  error: str | None = None,
34
- options: Sequence[Tuple[str, str]] | Sequence[str],
34
+ options: Sequence[tuple[str, str]] | Sequence[str],
35
35
  removable: bool = False,
36
- token: Optional[str] = None,
36
+ token: str | None = None,
37
37
  ) -> None:
38
38
  super().__init__(
39
39
  name,
@@ -1,3 +1,5 @@
1
+ """Hidden fields"""
2
+
1
3
  from typing import Any
2
4
 
3
5
  from .base import Widget
@@ -1,4 +1,7 @@
1
- from typing import Any, Sequence
1
+ """Pydantic models"""
2
+
3
+ from collections.abc import Sequence
4
+ from typing import Any
2
5
 
3
6
  from markupsafe import Markup
4
7
 
@@ -1,4 +1,5 @@
1
- from typing import Any, Sequence, Type
1
+ from collections.abc import Sequence
2
+ from typing import Any
2
3
 
3
4
  from markupsafe import Markup
4
5
 
@@ -17,7 +18,7 @@ class SequenceWidget(Widget[Sequence[Widget[Any]]]):
17
18
  aria_label: str | None = None,
18
19
  value: Sequence[Widget[Any]] | None,
19
20
  error: str | None = None,
20
- item_type: Type[Any],
21
+ item_type: type[Any],
21
22
  token: str,
22
23
  removable: bool,
23
24
  ):
@@ -1,5 +1,4 @@
1
1
  from collections.abc import Sequence
2
- from typing import Optional
3
2
 
4
3
  from .base import Widget
5
4
 
@@ -23,10 +22,10 @@ class TextWidget(Widget[str]):
23
22
  self,
24
23
  name: str,
25
24
  *,
26
- title: Optional[str],
27
- hint: Optional[str] = None,
28
- aria_label: Optional[str] = None,
29
- placeholder: Optional[str] = None,
25
+ title: str | None,
26
+ hint: str | None = None,
27
+ aria_label: str | None = None,
28
+ placeholder: str | None = None,
30
29
  error: str | None = None,
31
30
  value: str = "",
32
31
  input_type: str = "text",
@@ -87,12 +86,12 @@ class TextareaWidget(Widget[Sequence[str]]):
87
86
  self,
88
87
  name: str,
89
88
  *,
90
- title: Optional[str],
91
- hint: Optional[str] = None,
92
- aria_label: Optional[str] = None,
93
- placeholder: Optional[str] = None,
89
+ title: str | None,
90
+ hint: str | None = None,
91
+ aria_label: str | None = None,
92
+ placeholder: str | None = None,
94
93
  error: str | None = None,
95
- value: Optional[Sequence[str]] = None,
94
+ value: Sequence[str] | None = None,
96
95
  removable: bool = False,
97
96
  token: str,
98
97
  ) -> None:
@@ -1,7 +1,9 @@
1
1
  """
2
2
  Widget for field of type Union.
3
3
  """
4
- from typing import Any, Optional, Sequence, Type, Union
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any, Union
5
7
 
6
8
  from markupsafe import Markup
7
9
  from pydantic import BaseModel
@@ -31,12 +33,12 @@ class UnionWidget(Widget[Widget[Any]]):
31
33
  self,
32
34
  name: str,
33
35
  *,
34
- title: Optional[str],
35
- hint: Optional[str] = None,
36
- aria_label: Optional[str] = None,
37
- value: Optional[Widget[Any]],
36
+ title: str | None,
37
+ hint: str | None = None,
38
+ aria_label: str | None = None,
39
+ value: Widget[Any] | None,
38
40
  error: str | None = None,
39
- children_types: Sequence[Type[BaseModel]],
41
+ children_types: Sequence[type[BaseModel]],
40
42
  removable: bool = False,
41
43
  token: str,
42
44
  ):
@@ -72,7 +74,7 @@ class UnionWidget(Widget[Widget[Any]]):
72
74
  widget=self,
73
75
  types=self.build_types(renderer.route_prefix),
74
76
  parent_type=TypeWrapper(
75
- Union[tuple(self.children_types)], # type: ignore
77
+ Union[tuple(self.children_types)], # type: ignore # noqa: UP007
76
78
  renderer.route_prefix,
77
79
  self.parent_name,
78
80
  self.token,
@@ -22,6 +22,15 @@
22
22
  ] = None,
23
23
  method: Annotated[Literal["get", "post"] | None, "Http method used"] = None,
24
24
  action: Annotated[str | None, "url where the form will be submitted"] = None,
25
+ hx_target: Annotated[
26
+ str | None,
27
+ "target the element for swapping than the one issuing the AJAX request."
28
+ ] = None,
29
+ hx_select: Annotated[str | None, "select the content swapped from response of the AJAX request."] = None,
30
+ hx_swap: Annotated[
31
+ str | None,
32
+ "specify how the response will be swapped in relative to the target of an AJAX request."
33
+ ] = None,
25
34
  hx_post: Annotated[
26
35
  str | Literal[True] | None,
27
36
  "url where the form will be submitted using htmx. if ``True``, the current url is used."\
@@ -40,6 +49,9 @@
40
49
  {%- if hx_post is not none %}
41
50
  hx-post="{% if hx_post is not true %}{{hx_post}}{% endif %}"
42
51
  {%- endif %}
52
+ {%- if hx_select %} hx-select="{{ hx_select }}" {%- endif %}
53
+ {%- if hx_swap %} hx-swap="{{ hx_swap }}" {%- endif %}
54
+ {%- if hx_target %} hx-target="{{ hx_target }}" {%- endif %}
43
55
  {%- endif %}
44
56
  {%- if action is not none %} action="{{action}}" {%- endif %}
45
57
  {%- if method %} method="{{method}}" {%- endif -%}
@@ -11,32 +11,29 @@ The configurator is designed to handle the setup during the configuration
11
11
  phase.
12
12
  """
13
13
 
14
- import importlib
15
- import inspect
16
14
  import logging
17
15
  from collections import defaultdict
18
- from collections.abc import Mapping, Sequence
16
+ from collections.abc import Callable, Mapping, Sequence
19
17
  from enum import Enum
20
18
  from pathlib import Path
21
19
  from types import ModuleType
22
- from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, Self, Tuple, Type
20
+ from typing import TYPE_CHECKING, Annotated, Any, Generic, Self, TypeVar
23
21
 
24
22
  import venusian
25
- from fastapi import Depends, FastAPI
23
+ from fastapi import Depends, FastAPI, Response
26
24
  from fastapi import Request as BaseRequest
27
- from fastapi import Response
28
25
  from fastapi.params import Depends as DependsType
29
26
  from fastapi.staticfiles import StaticFiles
30
27
  from fastapi.types import IncEx
31
28
 
32
29
  from fastlife.config.openapiextra import OpenApiTag
33
30
  from fastlife.middlewares.base import AbstractMiddleware
34
- from fastlife.request.request import Request
31
+ from fastlife.request.request import GenericRequest, Request
35
32
  from fastlife.routing.route import Route
36
33
  from fastlife.routing.router import Router
37
34
  from fastlife.security.csrf import check_csrf
38
35
  from fastlife.services.policy import check_permission
39
- from fastlife.shared_utils.resolver import resolve
36
+ from fastlife.shared_utils.resolver import resolve, resolve_maybe_relative
40
37
 
41
38
  from .registry import DefaultRegistry, TRegistry
42
39
  from .settings import Settings
@@ -127,9 +124,9 @@ class GenericConfigurator(Generic[TRegistry]):
127
124
  self.registry = registry_cls(settings)
128
125
  Route._registry = self.registry # type: ignore
129
126
 
130
- self.middlewares: list[Tuple[Type[AbstractMiddleware], Any]] = []
131
- self.exception_handlers: list[Tuple[int | Type[Exception], Any]] = []
132
- self.mounts: list[Tuple[str, Path, str]] = []
127
+ self.middlewares: list[tuple[type[AbstractMiddleware], Any]] = []
128
+ self.exception_handlers: list[tuple[int | type[Exception], Any]] = []
129
+ self.mounts: list[tuple[str, Path, str]] = []
133
130
  self.tags: dict[str, OpenApiTag] = {}
134
131
 
135
132
  self.api_title = "OpenAPI"
@@ -142,7 +139,7 @@ class GenericConfigurator(Generic[TRegistry]):
142
139
  self._route_prefix: str = ""
143
140
  self._routers: dict[str, Router] = defaultdict(Router)
144
141
  self._security_policies: dict[
145
- str, "type[AbstractSecurityPolicy[Any, TRegistry]]"
142
+ str, type[AbstractSecurityPolicy[Any, TRegistry]]
146
143
  ] = {}
147
144
 
148
145
  self._registered_permissions: set[str] = set()
@@ -239,12 +236,11 @@ class GenericConfigurator(Generic[TRegistry]):
239
236
  :param ignore: ignore submodules
240
237
  """
241
238
  if isinstance(module, str):
242
- package = None
243
- if module.startswith("."):
244
- caller_module = inspect.getmodule(inspect.stack()[1][0])
245
- package = caller_module.__name__ if caller_module else "__main__"
239
+ try:
240
+ module = resolve_maybe_relative(module, stack_depth=2)
241
+ except ModuleNotFoundError as exc:
242
+ raise ConfigurationError(f"Can't resolve {module}") from exc
246
243
 
247
- module = importlib.import_module(module, package)
248
244
  old, self._route_prefix = self._route_prefix, route_prefix
249
245
  try:
250
246
  self.scanner.scan( # type: ignore
@@ -302,7 +298,7 @@ class GenericConfigurator(Generic[TRegistry]):
302
298
  self.api_redoc_url = redoc_url
303
299
  return self
304
300
 
305
- def add_open_tag(self, tag: OpenApiTag) -> Self:
301
+ def add_openapi_tag(self, tag: OpenApiTag) -> Self:
306
302
  """Register a tag description in the documentation."""
307
303
  if tag.name in self.tags:
308
304
  raise ConfigurationError(f"Tag {tag.name} can't be registered twice.")
@@ -310,7 +306,7 @@ class GenericConfigurator(Generic[TRegistry]):
310
306
  return self
311
307
 
312
308
  def add_middleware(
313
- self, middleware_class: Type[AbstractMiddleware], **options: Any
309
+ self, middleware_class: type[AbstractMiddleware], **options: Any
314
310
  ) -> Self:
315
311
  """
316
312
  Add a starlette middleware to the FastAPI app.
@@ -522,7 +518,7 @@ class GenericConfigurator(Generic[TRegistry]):
522
518
 
523
519
  def add_exception_handler(
524
520
  self,
525
- status_code_or_exc: int | Type[Exception],
521
+ status_code_or_exc: int | type[Exception],
526
522
  handler: Any,
527
523
  *,
528
524
  template: str | None = None,
@@ -539,8 +535,8 @@ class GenericConfigurator(Generic[TRegistry]):
539
535
  # class is wrong.
540
536
  # Until we store a security policy per rooter, we rebuild an
541
537
  # incomplete request here.
542
- request = Request(self.registry, request)
543
- resp = handler(request, exc)
538
+ req = GenericRequest[DefaultRegistry](self.registry, request)
539
+ resp = handler(req, exc)
544
540
  if isinstance(resp, Response):
545
541
  return resp
546
542
 
@@ -552,7 +548,7 @@ class GenericConfigurator(Generic[TRegistry]):
552
548
  "did not return a Response"
553
549
  )
554
550
 
555
- return request.registry.get_renderer(template)(request).render(
551
+ return req.registry.get_renderer(template)(req).render(
556
552
  template,
557
553
  params=resp,
558
554
  status_code=status_code,
@@ -594,8 +590,11 @@ class Configurator(GenericConfigurator[DefaultRegistry]):
594
590
  """
595
591
 
596
592
 
593
+ TConfigurator = TypeVar("TConfigurator", bound=GenericConfigurator[Any])
594
+
595
+
597
596
  def configure(
598
- wrapped: Callable[[Configurator], None],
597
+ wrapped: Callable[[TConfigurator], None],
599
598
  ) -> Callable[[Any], None]:
600
599
  """
601
600
  Decorator used to attach route in a submodule while using the configurator.include.
@@ -1,4 +1,7 @@
1
- from typing import Any, Callable
1
+ """Customize error pages."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
2
5
 
3
6
  import venusian
4
7
 
@@ -1,4 +1,5 @@
1
1
  """Types for OpenAPI documentation."""
2
+
2
3
  from pydantic import BaseModel, Field
3
4
 
4
5