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.
- fastlife/adapters/jinjax/renderer.py +49 -25
- fastlife/adapters/jinjax/widget_factory/__init__.py +1 -0
- fastlife/adapters/jinjax/widget_factory/base.py +38 -0
- fastlife/adapters/jinjax/widget_factory/bool_builder.py +43 -0
- fastlife/adapters/jinjax/widget_factory/emailstr_builder.py +46 -0
- fastlife/adapters/jinjax/widget_factory/enum_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/factory.py +165 -0
- fastlife/adapters/jinjax/widget_factory/literal_builder.py +52 -0
- fastlife/adapters/jinjax/widget_factory/model_builder.py +64 -0
- fastlife/adapters/jinjax/widget_factory/secretstr_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/sequence_builder.py +58 -0
- fastlife/adapters/jinjax/widget_factory/set_builder.py +80 -0
- fastlife/adapters/jinjax/widget_factory/simpletype_builder.py +47 -0
- fastlife/adapters/jinjax/widget_factory/union_builder.py +90 -0
- fastlife/adapters/jinjax/widgets/base.py +6 -4
- fastlife/adapters/jinjax/widgets/checklist.py +1 -1
- fastlife/adapters/jinjax/widgets/dropdown.py +7 -7
- fastlife/adapters/jinjax/widgets/hidden.py +2 -0
- fastlife/adapters/jinjax/widgets/model.py +4 -1
- fastlife/adapters/jinjax/widgets/sequence.py +3 -2
- fastlife/adapters/jinjax/widgets/text.py +9 -10
- fastlife/adapters/jinjax/widgets/union.py +9 -7
- fastlife/components/Form.jinja +12 -0
- fastlife/config/configurator.py +23 -24
- fastlife/config/exceptions.py +4 -1
- fastlife/config/openapiextra.py +1 -0
- fastlife/config/resources.py +26 -27
- fastlife/config/settings.py +2 -0
- fastlife/config/views.py +3 -1
- fastlife/middlewares/reverse_proxy/x_forwarded.py +22 -15
- fastlife/middlewares/session/middleware.py +2 -2
- fastlife/middlewares/session/serializer.py +6 -5
- fastlife/request/form.py +7 -6
- fastlife/request/form_data.py +2 -6
- fastlife/routing/route.py +3 -1
- fastlife/routing/router.py +1 -0
- fastlife/security/csrf.py +2 -1
- fastlife/security/policy.py +2 -1
- fastlife/services/locale_negociator.py +2 -1
- fastlife/services/policy.py +3 -2
- fastlife/services/templates.py +2 -1
- fastlife/services/translations.py +15 -8
- fastlife/shared_utils/infer.py +4 -3
- fastlife/shared_utils/resolver.py +64 -4
- fastlife/templates/binding.py +2 -1
- fastlife/testing/__init__.py +1 -0
- fastlife/testing/dom.py +140 -0
- fastlife/testing/form.py +204 -0
- fastlife/testing/session.py +67 -0
- fastlife/testing/testclient.py +7 -390
- fastlife/views/pydantic_form.py +4 -4
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/METADATA +6 -6
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/RECORD +55 -40
- fastlife/adapters/jinjax/widgets/factory.py +0 -525
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/LICENSE +0 -0
- {fastlifeweb-0.16.3.dist-info → fastlifeweb-0.17.0.dist-info}/WHEEL +0 -0
fastlife/config/resources.py
CHANGED
@@ -8,7 +8,8 @@ API Resources declaration using a decorator.
|
|
8
8
|
|
9
9
|
"""
|
10
10
|
|
11
|
-
from
|
11
|
+
from collections.abc import Callable
|
12
|
+
from typing import Any
|
12
13
|
|
13
14
|
import venusian
|
14
15
|
from fastapi.types import IncEx
|
@@ -54,7 +55,7 @@ def resource(
|
|
54
55
|
|
55
56
|
Note that there is no abstract class that declare this method, this is done by
|
56
57
|
introspection while returning the configuration method
|
57
|
-
{meth}`fastlife.config.configurator.
|
58
|
+
{meth}`fastlife.config.configurator.GenericConfigurator.include`
|
58
59
|
"""
|
59
60
|
tag = name
|
60
61
|
|
@@ -67,7 +68,7 @@ def resource(
|
|
67
68
|
|
68
69
|
config: Configurator = getattr(scanner, VENUSIAN_CATEGORY)
|
69
70
|
if description:
|
70
|
-
config.
|
71
|
+
config.add_openapi_tag(
|
71
72
|
OpenApiTag(
|
72
73
|
name=tag, description=description, externalDocs=external_docs
|
73
74
|
)
|
@@ -91,29 +92,21 @@ def resource(
|
|
91
92
|
endpoint=endpoint,
|
92
93
|
tags=[tag],
|
93
94
|
methods=[method.split("_").pop()],
|
94
|
-
permission=
|
95
|
-
status_code=
|
96
|
-
summary=
|
97
|
-
description=
|
98
|
-
response_description=
|
99
|
-
deprecated=
|
100
|
-
operation_id=
|
101
|
-
response_model_include=
|
102
|
-
response_model_exclude=
|
103
|
-
response_model_by_alias=
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
response_model_exclude_defaults=getattr(
|
110
|
-
endpoint, "response_model_exclude_defaults"
|
111
|
-
),
|
112
|
-
response_model_exclude_none=getattr(
|
113
|
-
endpoint, "response_model_exclude_none"
|
114
|
-
),
|
115
|
-
include_in_schema=getattr(endpoint, "include_in_schema"),
|
116
|
-
openapi_extra=getattr(endpoint, "openapi_extra"),
|
95
|
+
permission=endpoint.permission,
|
96
|
+
status_code=endpoint.status_code,
|
97
|
+
summary=endpoint.summary,
|
98
|
+
description=endpoint.description,
|
99
|
+
response_description=endpoint.response_description,
|
100
|
+
deprecated=endpoint.deprecated,
|
101
|
+
operation_id=endpoint.operation_id,
|
102
|
+
response_model_include=endpoint.response_model_include,
|
103
|
+
response_model_exclude=endpoint.response_model_exclude,
|
104
|
+
response_model_by_alias=endpoint.response_model_by_alias,
|
105
|
+
response_model_exclude_unset=endpoint.response_model_exclude_unset,
|
106
|
+
response_model_exclude_defaults=endpoint.response_model_exclude_defaults,
|
107
|
+
response_model_exclude_none=endpoint.response_model_exclude_none,
|
108
|
+
include_in_schema=endpoint.include_in_schema,
|
109
|
+
openapi_extra=endpoint.openapi_extra,
|
117
110
|
)
|
118
111
|
|
119
112
|
for method in dir(ob):
|
@@ -131,7 +124,13 @@ def resource(
|
|
131
124
|
config, method, collection_path, getattr(api, method)
|
132
125
|
)
|
133
126
|
case (
|
134
|
-
"get"
|
127
|
+
"get"
|
128
|
+
| "post"
|
129
|
+
| "put"
|
130
|
+
| "patch"
|
131
|
+
| "delete"
|
132
|
+
| "head"
|
133
|
+
| "options"
|
135
134
|
):
|
136
135
|
bind_config(config, method, path, getattr(api, method))
|
137
136
|
case _:
|
fastlife/config/settings.py
CHANGED
@@ -74,6 +74,8 @@ class Settings(BaseSettings):
|
|
74
74
|
)
|
75
75
|
"""
|
76
76
|
Set global constants accessible in every templates.
|
77
|
+
Defaults to `fastlife.templates.constants:Constants`
|
78
|
+
See {class}`fastlife.templates.constants.Constants`
|
77
79
|
"""
|
78
80
|
|
79
81
|
session_secret_key: str = Field(default="")
|
fastlife/config/views.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import logging
|
2
|
-
from
|
2
|
+
from collections.abc import Sequence
|
3
3
|
|
4
4
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
5
5
|
|
@@ -8,21 +8,20 @@ from fastlife.middlewares.base import AbstractMiddleware
|
|
8
8
|
log = logging.getLogger(__name__)
|
9
9
|
|
10
10
|
|
11
|
-
def get_header(headers: Sequence[
|
12
|
-
for
|
13
|
-
if
|
14
|
-
return
|
11
|
+
def get_header(headers: Sequence[tuple[bytes, bytes]], key: bytes) -> str | None:
|
12
|
+
for hkey, val in headers:
|
13
|
+
if hkey.lower() == key:
|
14
|
+
return val.decode("latin1")
|
15
15
|
return None
|
16
16
|
|
17
17
|
|
18
|
-
def get_header_int(headers: Sequence[
|
19
|
-
for
|
20
|
-
if
|
21
|
-
ret = hdr[1].decode("latin1")
|
18
|
+
def get_header_int(headers: Sequence[tuple[bytes, bytes]], key: bytes) -> int | None:
|
19
|
+
for hkey, val in headers:
|
20
|
+
if hkey.lower() == key:
|
22
21
|
try:
|
23
|
-
return int(
|
22
|
+
return int(val.decode("latin1"))
|
24
23
|
except ValueError:
|
25
|
-
|
24
|
+
break
|
26
25
|
return None
|
27
26
|
|
28
27
|
|
@@ -36,11 +35,19 @@ class XForwardedStar(AbstractMiddleware):
|
|
36
35
|
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
37
36
|
if scope["type"] in ("http", "websocket"):
|
38
37
|
headers = scope["headers"]
|
38
|
+
x_real_ip = get_header(headers, b"x-real-ip")
|
39
|
+
client = (
|
40
|
+
(
|
41
|
+
x_real_ip,
|
42
|
+
get_header_int(headers, b"x-real-port")
|
43
|
+
or get_header_int(headers, b"x-forwarded-port")
|
44
|
+
or 0,
|
45
|
+
)
|
46
|
+
if x_real_ip
|
47
|
+
else None
|
48
|
+
)
|
39
49
|
new_vals = {
|
40
|
-
"client":
|
41
|
-
get_header(headers, b"x-real-ip"),
|
42
|
-
get_header_int(headers, b"x-forwarded-port"),
|
43
|
-
),
|
50
|
+
"client": client,
|
44
51
|
"host": get_header(headers, b"x-forwarded-host"),
|
45
52
|
"scheme": get_header(headers, b"x-forwarded-proto"),
|
46
53
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Deal with http session."""
|
2
2
|
|
3
3
|
from datetime import timedelta
|
4
|
-
from typing import Literal
|
4
|
+
from typing import Literal
|
5
5
|
|
6
6
|
from starlette.datastructures import MutableHeaders
|
7
7
|
from starlette.requests import HTTPConnection
|
@@ -25,7 +25,7 @@ class SessionMiddleware(AbstractMiddleware):
|
|
25
25
|
cookie_same_site: Literal["lax", "strict", "none"] = "lax",
|
26
26
|
cookie_secure: bool = False,
|
27
27
|
cookie_domain: str = "",
|
28
|
-
serializer:
|
28
|
+
serializer: type[AbsractSessionSerializer] = SignedSessionSerializer,
|
29
29
|
) -> None:
|
30
30
|
self.app = app
|
31
31
|
self.max_age = int(duration.total_seconds())
|
@@ -1,8 +1,10 @@
|
|
1
1
|
"""Serialize session."""
|
2
|
+
|
2
3
|
import abc
|
3
4
|
import json
|
4
5
|
from base64 import b64decode, b64encode
|
5
|
-
from
|
6
|
+
from collections.abc import Mapping
|
7
|
+
from typing import Any
|
6
8
|
|
7
9
|
import itsdangerous
|
8
10
|
|
@@ -11,8 +13,7 @@ class AbsractSessionSerializer(abc.ABC):
|
|
11
13
|
"""Session serializer base class"""
|
12
14
|
|
13
15
|
@abc.abstractmethod
|
14
|
-
def __init__(self, secret_key: str, max_age: int) -> None:
|
15
|
-
...
|
16
|
+
def __init__(self, secret_key: str, max_age: int) -> None: ...
|
16
17
|
|
17
18
|
@abc.abstractmethod
|
18
19
|
def serialize(self, data: Mapping[str, Any]) -> bytes:
|
@@ -20,7 +21,7 @@ class AbsractSessionSerializer(abc.ABC):
|
|
20
21
|
...
|
21
22
|
|
22
23
|
@abc.abstractmethod
|
23
|
-
def deserialize(self, data: bytes) ->
|
24
|
+
def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
|
24
25
|
"""Derialize the session raw bytes content and return it as a mapping."""
|
25
26
|
...
|
26
27
|
|
@@ -47,7 +48,7 @@ class SignedSessionSerializer(AbsractSessionSerializer):
|
|
47
48
|
signed = self.signer.sign(encoded)
|
48
49
|
return signed
|
49
50
|
|
50
|
-
def deserialize(self, data: bytes) ->
|
51
|
+
def deserialize(self, data: bytes) -> tuple[Mapping[str, Any], bool]:
|
51
52
|
"""Deserialize the session.
|
52
53
|
|
53
54
|
If the signature is incorect, the session restart from the begining.
|
fastlife/request/form.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""HTTP Form serialization."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from collections.abc import Callable, Mapping
|
4
|
+
from typing import Any, Generic, TypeVar, get_origin
|
4
5
|
|
5
6
|
from fastapi import Depends
|
6
7
|
from pydantic import BaseModel, ValidationError
|
@@ -28,7 +29,7 @@ class FormModel(Generic[T]):
|
|
28
29
|
self.is_valid = is_valid
|
29
30
|
|
30
31
|
@classmethod
|
31
|
-
def default(cls, prefix: str, pydantic_type:
|
32
|
+
def default(cls, prefix: str, pydantic_type: type[T]) -> "FormModel[T]":
|
32
33
|
return cls(prefix, pydantic_type.model_construct(), {})
|
33
34
|
|
34
35
|
def edit(self, pydantic_type: T) -> None:
|
@@ -47,7 +48,7 @@ class FormModel(Generic[T]):
|
|
47
48
|
|
48
49
|
@classmethod
|
49
50
|
def from_payload(
|
50
|
-
cls, prefix: str, pydantic_type:
|
51
|
+
cls, prefix: str, pydantic_type: type[T], data: Mapping[str, Any]
|
51
52
|
) -> "FormModel[T]":
|
52
53
|
try:
|
53
54
|
return cls(prefix, pydantic_type(**data.get(prefix, {})), {}, True)
|
@@ -68,12 +69,12 @@ class FormModel(Generic[T]):
|
|
68
69
|
continue
|
69
70
|
|
70
71
|
else:
|
71
|
-
raise NotImplementedError
|
72
|
+
raise NotImplementedError from exc # coverage: ignore
|
72
73
|
elif issubclass(typ, BaseModel):
|
73
74
|
typ = typ.model_fields[part].annotation
|
74
75
|
loc = f"{loc}.{part}"
|
75
76
|
else:
|
76
|
-
raise NotImplementedError
|
77
|
+
raise NotImplementedError from exc # coverage: ignore
|
77
78
|
|
78
79
|
else:
|
79
80
|
# it is an integer and it part of the list
|
@@ -88,7 +89,7 @@ class FormModel(Generic[T]):
|
|
88
89
|
|
89
90
|
|
90
91
|
def form_model(
|
91
|
-
cls:
|
92
|
+
cls: type[T], name: str | None = None
|
92
93
|
) -> Callable[[Mapping[str, Any]], FormModel[T]]:
|
93
94
|
"""
|
94
95
|
Build a model, a class of type T based on Pydandic Base Model from a form payload.
|
fastlife/request/form_data.py
CHANGED
@@ -2,14 +2,10 @@
|
|
2
2
|
Set of functions to unserialize www-form-urlencoded format to python simple types.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
|
5
6
|
from typing import (
|
6
7
|
Annotated,
|
7
8
|
Any,
|
8
|
-
Mapping,
|
9
|
-
MutableMapping,
|
10
|
-
MutableSequence,
|
11
|
-
Optional,
|
12
|
-
Sequence,
|
13
9
|
)
|
14
10
|
|
15
11
|
from fastapi import Depends
|
@@ -22,7 +18,7 @@ def unflatten_struct(
|
|
22
18
|
unflattened_output: MutableMapping[str, Any] | MutableSequence[Any],
|
23
19
|
level: int = 0,
|
24
20
|
*,
|
25
|
-
csrf_token_name:
|
21
|
+
csrf_token_name: str | None = None,
|
26
22
|
) -> Mapping[str, Any] | Sequence[Any]:
|
27
23
|
"""
|
28
24
|
Take a flatten_input map, with key segmented by `.` and build a nested dict.
|
fastlife/routing/route.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
"""HTTP Route."""
|
2
|
-
|
2
|
+
|
3
|
+
from collections.abc import Callable, Coroutine
|
4
|
+
from typing import TYPE_CHECKING, Any
|
3
5
|
|
4
6
|
from fastapi.routing import APIRoute
|
5
7
|
from starlette.requests import Request as StarletteRequest
|
fastlife/routing/router.py
CHANGED
fastlife/security/csrf.py
CHANGED
fastlife/security/policy.py
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
import abc
|
4
4
|
import logging
|
5
|
-
from
|
5
|
+
from collections.abc import Callable, Coroutine
|
6
|
+
from typing import Annotated, Any, Generic, Literal, TypeVar
|
6
7
|
from uuid import UUID
|
7
8
|
|
8
9
|
from fastapi import Depends, HTTPException
|
fastlife/services/policy.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Security policy."""
|
2
2
|
|
3
|
-
from
|
3
|
+
from collections.abc import Callable, Coroutine
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
CheckPermissionHook = Callable[..., Coroutine[Any, Any, None]] | Callable[..., None]
|
6
7
|
CheckPermission = Callable[[str], CheckPermissionHook]
|
@@ -12,7 +13,7 @@ def check_permission(permission_name: str) -> CheckPermissionHook:
|
|
12
13
|
|
13
14
|
Adding a permission on the route requires that a security policy has been
|
14
15
|
added using the method
|
15
|
-
{meth}`fastlife.config.configurator.
|
16
|
+
{meth}`fastlife.config.configurator.GenericConfigurator.set_security_policy`
|
16
17
|
|
17
18
|
:param permission_name: a permission name set in a view to check access.
|
18
19
|
:return: a function that raise http exceptions or any configured exception here.
|
fastlife/services/templates.py
CHANGED
@@ -10,7 +10,8 @@ In that case, those base classes have to be implemented.
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
import abc
|
13
|
-
from
|
13
|
+
from collections.abc import Callable, Mapping
|
14
|
+
from typing import Any
|
14
15
|
|
15
16
|
from fastlife import Request, Response
|
16
17
|
from fastlife.security.csrf import create_csrf_token
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import pathlib
|
2
|
-
from
|
2
|
+
from collections.abc import Iterator
|
3
|
+
from typing import TYPE_CHECKING
|
3
4
|
|
4
5
|
from babel.support import NullTranslations, Translations
|
5
6
|
|
@@ -11,16 +12,22 @@ if TYPE_CHECKING:
|
|
11
12
|
locale_name = str
|
12
13
|
|
13
14
|
|
14
|
-
def find_mo_files(root_path: str) -> Iterator[
|
15
|
+
def find_mo_files(root_path: str) -> Iterator[tuple[str, str, pathlib.Path]]:
|
16
|
+
"""
|
17
|
+
Find .mo files in a locales directory.
|
18
|
+
|
19
|
+
:param root_path: locales directory.
|
20
|
+
:return: a tupple containing locale_name, domain, file.
|
21
|
+
"""
|
15
22
|
root = pathlib.Path(root_path)
|
16
23
|
|
17
|
-
# Walk through the directory structure and match the pattern
|
18
24
|
for locale_dir in root.iterdir():
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
lc_messages_dir = locale_dir / "LC_MESSAGES"
|
26
|
+
if not (locale_dir.is_dir() and lc_messages_dir.is_dir()):
|
27
|
+
continue
|
28
|
+
|
29
|
+
for mo_file in lc_messages_dir.glob("*.mo"):
|
30
|
+
yield locale_dir.name, mo_file.stem, mo_file
|
24
31
|
|
25
32
|
|
26
33
|
class Localizer:
|
fastlife/shared_utils/infer.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
"""Type inference."""
|
2
|
+
|
2
3
|
from types import UnionType
|
3
|
-
from typing import Any,
|
4
|
+
from typing import Any, Union, get_origin
|
4
5
|
|
5
6
|
from pydantic import BaseModel
|
6
7
|
|
7
8
|
|
8
|
-
def is_complex_type(typ:
|
9
|
+
def is_complex_type(typ: type[Any]) -> bool:
|
9
10
|
"""
|
10
11
|
Used to detect complex type such as Mapping, Sequence and pydantic BaseModel.
|
11
12
|
|
@@ -14,7 +15,7 @@ def is_complex_type(typ: Type[Any]) -> bool:
|
|
14
15
|
return bool(get_origin(typ) or issubclass(typ, BaseModel))
|
15
16
|
|
16
17
|
|
17
|
-
def is_union(typ:
|
18
|
+
def is_union(typ: type[Any]) -> bool:
|
18
19
|
"""Used to detect unions like Optional[T], Union[T, U] or T | U."""
|
19
20
|
type_origin = get_origin(typ)
|
20
21
|
if type_origin:
|
@@ -1,7 +1,9 @@
|
|
1
1
|
"""Resolution of python objects for dependency injection and more."""
|
2
|
+
|
2
3
|
import importlib.util
|
4
|
+
import inspect
|
3
5
|
from pathlib import Path
|
4
|
-
from types import UnionType
|
6
|
+
from types import ModuleType, UnionType
|
5
7
|
from typing import Any, Union
|
6
8
|
|
7
9
|
|
@@ -20,8 +22,10 @@ def resolve(value: str) -> Any:
|
|
20
22
|
|
21
23
|
try:
|
22
24
|
attr = getattr(module, attr_name)
|
23
|
-
except AttributeError:
|
24
|
-
raise ValueError(
|
25
|
+
except AttributeError as exc:
|
26
|
+
raise ValueError(
|
27
|
+
f"Attribute {attr_name} not found in module {module_name}"
|
28
|
+
) from exc
|
25
29
|
|
26
30
|
return attr
|
27
31
|
|
@@ -32,7 +36,7 @@ def resolve_extended(value: str) -> UnionType:
|
|
32
36
|
if len(values) == 1:
|
33
37
|
return resolve(value)
|
34
38
|
types = [resolve(t) for t in values if t != "builtins:NoneType"]
|
35
|
-
return Union[tuple(types)] # type: ignore
|
39
|
+
return Union[tuple(types)] # type: ignore # noqa: UP007
|
36
40
|
|
37
41
|
|
38
42
|
def resolve_path(value: str) -> str:
|
@@ -48,3 +52,59 @@ def resolve_path(value: str) -> str:
|
|
48
52
|
package_path = spec.origin
|
49
53
|
full_path = Path(package_path).parent / resource_name
|
50
54
|
return str(full_path)
|
55
|
+
|
56
|
+
|
57
|
+
def resolve_package(mod: ModuleType) -> ModuleType:
|
58
|
+
"""
|
59
|
+
Return the
|
60
|
+
[regular package](https://docs.python.org/3/glossary.html#term-regular-package)
|
61
|
+
of a module or itself if it is the ini file of a package.
|
62
|
+
|
63
|
+
"""
|
64
|
+
|
65
|
+
# Compiled package has no __file__ attribute, ModuleType declare it as NoneType
|
66
|
+
if not hasattr(mod, "__file__") or mod.__file__ is None:
|
67
|
+
return mod
|
68
|
+
|
69
|
+
module_path = Path(mod.__file__)
|
70
|
+
if module_path.name == "__init__.py":
|
71
|
+
return mod
|
72
|
+
|
73
|
+
parent_module_name = mod.__name__.rsplit(".", 1)[0]
|
74
|
+
parent_module = importlib.import_module(parent_module_name)
|
75
|
+
return parent_module
|
76
|
+
|
77
|
+
|
78
|
+
def _strip_left_dots(s: str) -> tuple[str, int]:
|
79
|
+
stripped_string = s.lstrip(".")
|
80
|
+
num_stripped_dots = len(s) - len(stripped_string)
|
81
|
+
return stripped_string, num_stripped_dots - 1
|
82
|
+
|
83
|
+
|
84
|
+
def _get_parent(pkg: str, num_parents: int) -> str:
|
85
|
+
if num_parents == 0:
|
86
|
+
return pkg
|
87
|
+
segments = pkg.split(".")
|
88
|
+
return ".".join(segments[:-num_parents])
|
89
|
+
|
90
|
+
|
91
|
+
def resolve_maybe_relative(mod: str, stack_depth: int = 1) -> ModuleType:
|
92
|
+
"""
|
93
|
+
Resolve a module, maybe relative to the stack frame and import it.
|
94
|
+
|
95
|
+
:param mod: the module to import. starts with a dot if it is relative.
|
96
|
+
:param stack_depth: relative to which module in the stack.
|
97
|
+
used to do an api that call it instead of resolve the module directly.
|
98
|
+
:return: the imported module
|
99
|
+
"""
|
100
|
+
if mod.startswith("."):
|
101
|
+
caller_module = inspect.getmodule(inspect.stack()[stack_depth][0])
|
102
|
+
# we could do an assert here but caller_module could really be none ?
|
103
|
+
parent_module = resolve_package(caller_module) # type: ignore
|
104
|
+
package = parent_module.__name__
|
105
|
+
mod, count = _strip_left_dots(mod)
|
106
|
+
package = _get_parent(package, count)
|
107
|
+
mod = f"{package}.{mod}".rstrip(".")
|
108
|
+
|
109
|
+
module = importlib.import_module(mod)
|
110
|
+
return module
|
fastlife/templates/binding.py
CHANGED