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,10 @@
1
+ from modern_di_flask.main import FromDI, fetch_di_container, flask_request_provider, inject, setup_di
2
+
3
+
4
+ __all__ = [
5
+ "FromDI",
6
+ "fetch_di_container",
7
+ "flask_request_provider",
8
+ "inject",
9
+ "setup_di",
10
+ ]
@@ -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
+ ]