modern-di-flask 2.0.0__tar.gz
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.
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modern-di-flask
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: modern-di integration for Flask
|
|
5
|
+
Keywords: dependency-injection,di,ioc-container,modern-di,flask,python
|
|
6
|
+
Author: Artur Shiriev
|
|
7
|
+
Author-email: Artur Shiriev <me@shiriev.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Dist: flask>=3,<4
|
|
19
|
+
Requires-Dist: modern-di>=2.25,<3
|
|
20
|
+
Requires-Python: >=3.10, <4
|
|
21
|
+
Project-URL: Homepage, https://modern-di.modern-python.org
|
|
22
|
+
Project-URL: Documentation, https://modern-di.modern-python.org
|
|
23
|
+
Project-URL: Repository, https://github.com/modern-python/modern-di-flask
|
|
24
|
+
Project-URL: Issues, https://github.com/modern-python/modern-di-flask/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/modern-python/modern-di-flask/releases
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# modern-di-flask
|
|
29
|
+
|
|
30
|
+
[Modern-DI](https://github.com/modern-python/modern-di) integration for [Flask](https://flask.palletsprojects.com).
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import typing
|
|
36
|
+
|
|
37
|
+
from flask import Flask
|
|
38
|
+
from modern_di import Container, Group, Scope, providers
|
|
39
|
+
from modern_di_flask import FromDI, inject, setup_di
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Settings:
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self.greeting = "hello"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Dependencies(Group):
|
|
48
|
+
settings = providers.Factory(scope=Scope.APP, creator=Settings)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
app = Flask(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.route("/hello/<name>")
|
|
55
|
+
@inject
|
|
56
|
+
def hello(name: str, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> str:
|
|
57
|
+
return f"{settings.greeting}, {name}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# call setup_di AFTER registering routes (required for auto_inject)
|
|
61
|
+
setup_di(app, Container(groups=[Dependencies], validate=True))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Pass `auto_inject=True` to `setup_di` to inject every view without a per-view
|
|
65
|
+
`@inject`. See the [documentation](https://modern-di.modern-python.org).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# modern-di-flask
|
|
2
|
+
|
|
3
|
+
[Modern-DI](https://github.com/modern-python/modern-di) integration for [Flask](https://flask.palletsprojects.com).
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
from flask import Flask
|
|
11
|
+
from modern_di import Container, Group, Scope, providers
|
|
12
|
+
from modern_di_flask import FromDI, inject, setup_di
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Settings:
|
|
16
|
+
def __init__(self) -> None:
|
|
17
|
+
self.greeting = "hello"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Dependencies(Group):
|
|
21
|
+
settings = providers.Factory(scope=Scope.APP, creator=Settings)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
app = Flask(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.route("/hello/<name>")
|
|
28
|
+
@inject
|
|
29
|
+
def hello(name: str, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> str:
|
|
30
|
+
return f"{settings.greeting}, {name}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# call setup_di AFTER registering routes (required for auto_inject)
|
|
34
|
+
setup_di(app, Container(groups=[Dependencies], validate=True))
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Pass `auto_inject=True` to `setup_di` to inject every view without a per-view
|
|
38
|
+
`@inject`. See the [documentation](https://modern-di.modern-python.org).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import functools
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import flask
|
|
6
|
+
from flask import Flask, Request, g
|
|
7
|
+
from modern_di import Container, Scope, providers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
flask_request_provider = providers.ContextProvider(Request, scope=Scope.REQUEST)
|
|
11
|
+
_CONNECTION_PROVIDERS = (flask_request_provider,)
|
|
12
|
+
|
|
13
|
+
# Root container on ``app.extensions``; per-request child on ``flask.g``. Named
|
|
14
|
+
# constants keep each writer and reader in provable agreement.
|
|
15
|
+
_ROOT_CONTAINER_KEY = "modern_di_container"
|
|
16
|
+
_CHILD_CONTAINER_ATTR = "modern_di_request_container"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fetch_di_container(app: Flask) -> Container:
|
|
20
|
+
return typing.cast(Container, app.extensions[_ROOT_CONTAINER_KEY])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _close_request(_exception: BaseException | None) -> None:
|
|
24
|
+
child: Container | None = getattr(g, _CHILD_CONTAINER_ATTR, None)
|
|
25
|
+
if child is not None:
|
|
26
|
+
child.close_sync()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def setup_di(app: Flask, container: Container, *, auto_inject: bool = False) -> Container:
|
|
30
|
+
app.extensions[_ROOT_CONTAINER_KEY] = container
|
|
31
|
+
container.add_providers(*_CONNECTION_PROVIDERS)
|
|
32
|
+
|
|
33
|
+
def _enter_request() -> None:
|
|
34
|
+
child = container.build_child_container(
|
|
35
|
+
scope=Scope.REQUEST,
|
|
36
|
+
context={Request: flask.request._get_current_object()}, # noqa: SLF001 # ty: ignore[unresolved-attribute]
|
|
37
|
+
)
|
|
38
|
+
setattr(g, _CHILD_CONTAINER_ATTR, child)
|
|
39
|
+
|
|
40
|
+
app.before_request(_enter_request)
|
|
41
|
+
app.teardown_appcontext(_close_request)
|
|
42
|
+
if auto_inject:
|
|
43
|
+
_inject_views(app)
|
|
44
|
+
return container
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _inject_views(app: Flask) -> None:
|
|
48
|
+
# Flask registers every dispatched view (app routes and blueprint routes,
|
|
49
|
+
# the latter under dotted endpoints like "bp.view") in ``app.view_functions``
|
|
50
|
+
# and always dispatches through it, so wrapping that one registry covers all.
|
|
51
|
+
for endpoint, view in list(app.view_functions.items()):
|
|
52
|
+
if not getattr(view, "__modern_di_injected__", False):
|
|
53
|
+
app.view_functions[endpoint] = inject(view) # ty: ignore[invalid-assignment]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
T = typing.TypeVar("T")
|
|
57
|
+
T_co = typing.TypeVar("T_co", covariant=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
61
|
+
class _FromDI(typing.Generic[T_co]):
|
|
62
|
+
dependency: providers.AbstractProvider[T_co] | type[T_co]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def FromDI(dependency: providers.AbstractProvider[T] | type[T], /) -> T: # noqa: N802
|
|
66
|
+
return typing.cast(T, _FromDI(dependency))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_inject_params(func: typing.Callable[..., typing.Any]) -> dict[str, _FromDI[typing.Any]]:
|
|
70
|
+
hints = typing.get_type_hints(func, include_extras=True)
|
|
71
|
+
di_params: dict[str, _FromDI[typing.Any]] = {}
|
|
72
|
+
for name, hint in hints.items():
|
|
73
|
+
if name == "return":
|
|
74
|
+
continue
|
|
75
|
+
if typing.get_origin(hint) is typing.Annotated:
|
|
76
|
+
for meta in typing.get_args(hint)[1:]:
|
|
77
|
+
if isinstance(meta, _FromDI):
|
|
78
|
+
di_params[name] = meta
|
|
79
|
+
break
|
|
80
|
+
return di_params
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def inject(func: typing.Callable[..., T]) -> typing.Callable[..., T]:
|
|
84
|
+
di_params = _parse_inject_params(func)
|
|
85
|
+
if not di_params:
|
|
86
|
+
func.__modern_di_injected__ = True # ty: ignore[unresolved-attribute]
|
|
87
|
+
return func
|
|
88
|
+
|
|
89
|
+
@functools.wraps(func)
|
|
90
|
+
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> T: # noqa: ANN401
|
|
91
|
+
child: Container = getattr(g, _CHILD_CONTAINER_ATTR)
|
|
92
|
+
resolved = {name: child.resolve_dependency(marker.dependency) for name, marker in di_params.items()}
|
|
93
|
+
return func(*args, **kwargs, **resolved)
|
|
94
|
+
|
|
95
|
+
wrapper.__modern_di_injected__ = True # ty: ignore[unresolved-attribute]
|
|
96
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "modern-di-flask"
|
|
3
|
+
description = "modern-di integration for Flask"
|
|
4
|
+
authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }]
|
|
5
|
+
requires-python = ">=3.10,<4"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = ["dependency-injection", "di", "ioc-container", "modern-di", "flask", "python"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["flask>=3,<4", "modern-di>=2.25,<3"]
|
|
21
|
+
version = "2.0.0"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://modern-di.modern-python.org"
|
|
25
|
+
Documentation = "https://modern-di.modern-python.org"
|
|
26
|
+
Repository = "https://github.com/modern-python/modern-di-flask"
|
|
27
|
+
Issues = "https://github.com/modern-python/modern-di-flask/issues"
|
|
28
|
+
Changelog = "https://github.com/modern-python/modern-di-flask/releases"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest",
|
|
33
|
+
"pytest-cov",
|
|
34
|
+
]
|
|
35
|
+
lint = [
|
|
36
|
+
"ty",
|
|
37
|
+
"ruff",
|
|
38
|
+
"eof-fixer",
|
|
39
|
+
"typing-extensions",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["uv_build>=0.11,<1.0"]
|
|
44
|
+
build-backend = "uv_build"
|
|
45
|
+
|
|
46
|
+
[tool.uv.build-backend]
|
|
47
|
+
module-name = "modern_di_flask"
|
|
48
|
+
module-root = ""
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
fix = false
|
|
52
|
+
unsafe-fixes = true
|
|
53
|
+
line-length = 120
|
|
54
|
+
target-version = "py310"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.format]
|
|
57
|
+
docstring-code-format = true
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["ALL"]
|
|
61
|
+
ignore = [
|
|
62
|
+
"D1", # allow missing docstrings
|
|
63
|
+
"S101", # allow asserts
|
|
64
|
+
"TCH", # ignore flake8-type-checking
|
|
65
|
+
"FBT", # allow boolean args
|
|
66
|
+
"D203", # "one-blank-line-before-class" conflicting with D211
|
|
67
|
+
"D213", # "multi-line-summary-second-line" conflicting with D212
|
|
68
|
+
"COM812", # flake8-commas "Trailing comma missing"
|
|
69
|
+
"ISC001", # flake8-implicit-str-concat
|
|
70
|
+
"G004", # allow f-strings in logging
|
|
71
|
+
]
|
|
72
|
+
isort.lines-after-imports = 2
|
|
73
|
+
isort.no-lines-before = ["standard-library", "local-folder"]
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
addopts = ""
|
|
77
|
+
|
|
78
|
+
[tool.coverage.report]
|
|
79
|
+
exclude_also = [
|
|
80
|
+
"if typing.TYPE_CHECKING:",
|
|
81
|
+
]
|