engin 0.0.19__tar.gz → 0.0.20__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.
- {engin-0.0.19 → engin-0.0.20}/CHANGELOG.md +12 -0
- {engin-0.0.19 → engin-0.0.20}/PKG-INFO +1 -1
- {engin-0.0.19 → engin-0.0.20}/mkdocs.yaml +1 -0
- {engin-0.0.19 → engin-0.0.20}/pyproject.toml +2 -1
- {engin-0.0.19 → engin-0.0.20}/src/engin/_dependency.py +18 -11
- {engin-0.0.19 → engin-0.0.20}/src/engin/_type_utils.py +2 -2
- {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_fastapi.py +25 -3
- {engin-0.0.19 → engin-0.0.20}/tests/test_dependencies.py +14 -1
- engin-0.0.19/tests/test_utils.py → engin-0.0.20/tests/test_type_id.py +1 -1
- engin-0.0.20/uv.lock +1286 -0
- engin-0.0.19/uv.lock +0 -1225
- {engin-0.0.19 → engin-0.0.20}/.github/workflows/benchmark.yaml +0 -0
- {engin-0.0.19 → engin-0.0.20}/.github/workflows/check.yaml +0 -0
- {engin-0.0.19 → engin-0.0.20}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.19 → engin-0.0.20}/.gitignore +0 -0
- {engin-0.0.19 → engin-0.0.20}/.readthedocs.yaml +0 -0
- {engin-0.0.19 → engin-0.0.20}/LICENSE +0 -0
- {engin-0.0.19 → engin-0.0.20}/README.md +0 -0
- /engin-0.0.19/docs/concepts/block.md → /engin-0.0.20/docs/concepts/blocks.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/concepts/engin.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/concepts/invocations.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/concepts/lifecycle.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/concepts/providers.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/getting-started.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/guides/fastapi.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/index.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/js/readthedocs.js +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/overrides/main.html +0 -0
- {engin-0.0.19 → engin-0.0.20}/docs/reference.md +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/app.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/asgi/main.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/app.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/main.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/simple/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/examples/simple/main.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_assembler.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_block.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_common.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_graph.html +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_graph.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_inspect.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_engin.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_graph.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_introspect.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/_option.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/exceptions.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/asgi.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/fastapi.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/src/engin/py.typed +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/acceptance/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_error_in_shutdown.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_error_in_start_up.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/conftest.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/test_bench_assembler.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/cli/__init__.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/cli/test_graph.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/cli/test_inspect.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/conftest.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/deps.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/test_assembler.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/test_block.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/test_engin.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/test_graph.py +0 -0
- {engin-0.0.19 → engin-0.0.20}/tests/test_lifecycle.py +0 -0
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
8
|
|
9
|
+
## [0.0.20] - 2025-06-18
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Improved string representation of Provide & Supply to make error messages more helpful.
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- Engin now correctly supports postponed evaluation of annotations, e.g. `x: "MyType"` in
|
18
|
+
a factory function.
|
19
|
+
|
20
|
+
|
9
21
|
## [0.0.19] - 2025-04-27
|
10
22
|
|
11
23
|
### Added
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.20"
|
4
4
|
description = "An async-first modular application framework"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -38,6 +38,7 @@ dev = [
|
|
38
38
|
"typer>=0.15.2",
|
39
39
|
"pytest-mock>=3.14.0",
|
40
40
|
"pytest-benchmark>=5.1.0",
|
41
|
+
"websockets>=15.0.1",
|
41
42
|
]
|
42
43
|
docs = [
|
43
44
|
"mkdocs-material>=9.5.50",
|
@@ -33,7 +33,7 @@ class Dependency(ABC, Option, Generic[P, T]):
|
|
33
33
|
def __init__(self, func: Func[P, T]) -> None:
|
34
34
|
self._func = func
|
35
35
|
self._is_async = iscoroutinefunction(func)
|
36
|
-
self._signature = inspect.signature(self._func)
|
36
|
+
self._signature = inspect.signature(self._func, eval_str=True)
|
37
37
|
self._block_name: str | None = None
|
38
38
|
|
39
39
|
source_frame = get_first_external_frame()
|
@@ -154,24 +154,24 @@ class Entrypoint(Invoke):
|
|
154
154
|
class Provide(Dependency[Any, T]):
|
155
155
|
def __init__(
|
156
156
|
self,
|
157
|
-
|
157
|
+
factory: Func[P, T],
|
158
158
|
*,
|
159
159
|
scope: str | None = None,
|
160
160
|
as_type: type | None = None,
|
161
161
|
override: bool = False,
|
162
162
|
) -> None:
|
163
163
|
"""
|
164
|
-
Provide a type via a
|
164
|
+
Provide a type via a factory function.
|
165
165
|
|
166
166
|
Args:
|
167
|
-
|
167
|
+
factory: the factory function that returns the type.
|
168
168
|
scope: (optional) associate this provider with a specific scope.
|
169
169
|
as_type: (optional) allows you to explicitly specify the provided type, e.g.
|
170
170
|
to type erase a concrete type, or to provide a mock implementation.
|
171
171
|
override: (optional) allow this provider to override other providers for the
|
172
172
|
same type from the same package.
|
173
173
|
"""
|
174
|
-
super().__init__(func=
|
174
|
+
super().__init__(func=factory)
|
175
175
|
self._scope = scope
|
176
176
|
self._override = override
|
177
177
|
self._explicit_type = as_type
|
@@ -231,9 +231,9 @@ class Provide(Dependency[Any, T]):
|
|
231
231
|
# overwriting a dependency from the same package must be explicit
|
232
232
|
if is_same_package and not self._override:
|
233
233
|
msg = (
|
234
|
-
f"
|
235
|
-
f"'{existing_provider.
|
236
|
-
"`override=True` for the overriding Provider"
|
234
|
+
f"{self} from '{self._source_frame}' is implicitly overriding "
|
235
|
+
f"{existing_provider} from '{existing_provider.source_module}', if this "
|
236
|
+
"is intentional specify `override=True` for the overriding Provider"
|
237
237
|
)
|
238
238
|
raise RuntimeError(msg)
|
239
239
|
|
@@ -243,7 +243,7 @@ class Provide(Dependency[Any, T]):
|
|
243
243
|
return hash(self.return_type_id)
|
244
244
|
|
245
245
|
def __str__(self) -> str:
|
246
|
-
return f"Provide({self.
|
246
|
+
return f"Provide(factory={self.func_name}, type={self._return_type_id})"
|
247
247
|
|
248
248
|
def _resolve_return_type(self) -> type[T]:
|
249
249
|
if self._explicit_type is not None:
|
@@ -279,7 +279,14 @@ class Supply(Provide, Generic[T]):
|
|
279
279
|
same type from the same package.
|
280
280
|
"""
|
281
281
|
self._value = value
|
282
|
-
super().__init__(
|
282
|
+
super().__init__(factory=self._get_val, as_type=as_type, override=override)
|
283
|
+
|
284
|
+
@property
|
285
|
+
def name(self) -> str:
|
286
|
+
if self._block_name:
|
287
|
+
return f"{self._block_name}.supply"
|
288
|
+
else:
|
289
|
+
return f"{self._source_frame}.supply"
|
283
290
|
|
284
291
|
def _resolve_return_type(self) -> type[T]:
|
285
292
|
if self._explicit_type is not None:
|
@@ -292,4 +299,4 @@ class Supply(Provide, Generic[T]):
|
|
292
299
|
return self._value
|
293
300
|
|
294
301
|
def __str__(self) -> str:
|
295
|
-
return f"Supply({self.return_type_id})"
|
302
|
+
return f"Supply(value={self._value}, type={self.return_type_id})"
|
@@ -33,8 +33,8 @@ class TypeId:
|
|
33
33
|
return TypeId(type=type_, multi=False)
|
34
34
|
|
35
35
|
def __str__(self) -> str:
|
36
|
-
module = self.type
|
37
|
-
out = f"{module}." if module not in _implict_modules else ""
|
36
|
+
module = getattr(self.type, "__module__", None)
|
37
|
+
out = f"{module}." if module and module not in _implict_modules else ""
|
38
38
|
out += _args_to_str(self.type)
|
39
39
|
if self.multi:
|
40
40
|
out += "[]"
|
@@ -4,6 +4,7 @@ from typing import Annotated
|
|
4
4
|
import pytest
|
5
5
|
import starlette.testclient
|
6
6
|
from fastapi import APIRouter, FastAPI
|
7
|
+
from starlette.websockets import WebSocket
|
7
8
|
|
8
9
|
from engin import Engin, Provide, Supply
|
9
10
|
from engin.extensions.asgi import engin_to_lifespan
|
@@ -22,6 +23,16 @@ async def route_with_dep(some_int: Annotated[int, Inject(int)]) -> int:
|
|
22
23
|
return some_int
|
23
24
|
|
24
25
|
|
26
|
+
@ROUTER.websocket("/websocket")
|
27
|
+
async def websocket_with_dep(
|
28
|
+
websocket: WebSocket, some_int: Annotated[int, Inject(int)]
|
29
|
+
) -> None:
|
30
|
+
await websocket.accept()
|
31
|
+
for i in range(5):
|
32
|
+
await websocket.send_text(str(i + some_int))
|
33
|
+
await websocket.close()
|
34
|
+
|
35
|
+
|
25
36
|
@ROUTER.get("/inject2")
|
26
37
|
async def route_with_dep_2(
|
27
38
|
some_int: Annotated[int, Inject(int)], some_str: Annotated[str, Inject(str)]
|
@@ -37,7 +48,7 @@ def app_factory(routers: list[APIRouter]) -> FastAPI:
|
|
37
48
|
return app
|
38
49
|
|
39
50
|
|
40
|
-
|
51
|
+
def test_fastapi():
|
41
52
|
engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]))
|
42
53
|
|
43
54
|
with starlette.testclient.TestClient(engin) as client:
|
@@ -46,15 +57,26 @@ async def test_fastapi():
|
|
46
57
|
assert result.json() == "hello world"
|
47
58
|
|
48
59
|
|
49
|
-
|
60
|
+
def test_inject():
|
50
61
|
engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]), Supply(10))
|
51
62
|
|
52
63
|
with starlette.testclient.TestClient(engin) as client:
|
53
|
-
result = client.get("
|
64
|
+
result = client.get("/inject")
|
54
65
|
|
55
66
|
assert result.json() == 10
|
56
67
|
|
57
68
|
|
69
|
+
def test_inject_websocket():
|
70
|
+
engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]), Supply(10))
|
71
|
+
|
72
|
+
with (
|
73
|
+
starlette.testclient.TestClient(engin) as client,
|
74
|
+
client.websocket_connect("/websocket") as ws,
|
75
|
+
):
|
76
|
+
data = ws.receive_text()
|
77
|
+
assert data == "10"
|
78
|
+
|
79
|
+
|
58
80
|
async def test_graph():
|
59
81
|
engin = FastAPIEngin(Provide(app_factory), Supply([ROUTER]), Supply(10), Supply("a"))
|
60
82
|
|
@@ -90,7 +90,7 @@ def test_provider_cannot_depend_on_self():
|
|
90
90
|
Provide(invalid_provider_2)
|
91
91
|
|
92
92
|
|
93
|
-
def
|
93
|
+
def test_provides_implicit_overrides_providers():
|
94
94
|
provide_a = int_provider()
|
95
95
|
provide_b = int_provider()
|
96
96
|
|
@@ -103,6 +103,19 @@ def test_provides_implicit_overrides():
|
|
103
103
|
provide_b.apply(engin)
|
104
104
|
|
105
105
|
|
106
|
+
def test_provides_implicit_overrides_supply():
|
107
|
+
provide_a = Supply(3)
|
108
|
+
provide_b = Supply(4)
|
109
|
+
|
110
|
+
engin = Mock()
|
111
|
+
engin._providers = {}
|
112
|
+
|
113
|
+
provide_a.apply(engin)
|
114
|
+
|
115
|
+
with pytest.raises(RuntimeError, match="implicit"):
|
116
|
+
provide_b.apply(engin)
|
117
|
+
|
118
|
+
|
106
119
|
def test_provides_explicit_overrides_allowed():
|
107
120
|
provide_a = int_provider()
|
108
121
|
provide_b = int_provider(override=True)
|