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.
Files changed (97) hide show
  1. {engin-0.0.19 → engin-0.0.20}/CHANGELOG.md +12 -0
  2. {engin-0.0.19 → engin-0.0.20}/PKG-INFO +1 -1
  3. {engin-0.0.19 → engin-0.0.20}/mkdocs.yaml +1 -0
  4. {engin-0.0.19 → engin-0.0.20}/pyproject.toml +2 -1
  5. {engin-0.0.19 → engin-0.0.20}/src/engin/_dependency.py +18 -11
  6. {engin-0.0.19 → engin-0.0.20}/src/engin/_type_utils.py +2 -2
  7. {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_fastapi.py +25 -3
  8. {engin-0.0.19 → engin-0.0.20}/tests/test_dependencies.py +14 -1
  9. engin-0.0.19/tests/test_utils.py → engin-0.0.20/tests/test_type_id.py +1 -1
  10. engin-0.0.20/uv.lock +1286 -0
  11. engin-0.0.19/uv.lock +0 -1225
  12. {engin-0.0.19 → engin-0.0.20}/.github/workflows/benchmark.yaml +0 -0
  13. {engin-0.0.19 → engin-0.0.20}/.github/workflows/check.yaml +0 -0
  14. {engin-0.0.19 → engin-0.0.20}/.github/workflows/publish.yaml +0 -0
  15. {engin-0.0.19 → engin-0.0.20}/.gitignore +0 -0
  16. {engin-0.0.19 → engin-0.0.20}/.readthedocs.yaml +0 -0
  17. {engin-0.0.19 → engin-0.0.20}/LICENSE +0 -0
  18. {engin-0.0.19 → engin-0.0.20}/README.md +0 -0
  19. /engin-0.0.19/docs/concepts/block.md → /engin-0.0.20/docs/concepts/blocks.md +0 -0
  20. {engin-0.0.19 → engin-0.0.20}/docs/concepts/engin.md +0 -0
  21. {engin-0.0.19 → engin-0.0.20}/docs/concepts/invocations.md +0 -0
  22. {engin-0.0.19 → engin-0.0.20}/docs/concepts/lifecycle.md +0 -0
  23. {engin-0.0.19 → engin-0.0.20}/docs/concepts/providers.md +0 -0
  24. {engin-0.0.19 → engin-0.0.20}/docs/getting-started.md +0 -0
  25. {engin-0.0.19 → engin-0.0.20}/docs/guides/fastapi-graph.png +0 -0
  26. {engin-0.0.19 → engin-0.0.20}/docs/guides/fastapi.md +0 -0
  27. {engin-0.0.19 → engin-0.0.20}/docs/index.md +0 -0
  28. {engin-0.0.19 → engin-0.0.20}/docs/js/readthedocs.js +0 -0
  29. {engin-0.0.19 → engin-0.0.20}/docs/overrides/main.html +0 -0
  30. {engin-0.0.19 → engin-0.0.20}/docs/reference.md +0 -0
  31. {engin-0.0.19 → engin-0.0.20}/examples/__init__.py +0 -0
  32. {engin-0.0.19 → engin-0.0.20}/examples/asgi/__init__.py +0 -0
  33. {engin-0.0.19 → engin-0.0.20}/examples/asgi/app.py +0 -0
  34. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/__init__.py +0 -0
  35. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/__init__.py +0 -0
  36. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  37. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/adapaters/memory.py +0 -0
  38. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/block.py +0 -0
  39. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/db/ports.py +0 -0
  40. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/starlette/__init__.py +0 -0
  41. {engin-0.0.19 → engin-0.0.20}/examples/asgi/common/starlette/endpoint.py +0 -0
  42. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/__init__.py +0 -0
  43. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/__init__.py +0 -0
  44. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/__init__.py +0 -0
  45. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/get.py +0 -0
  46. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/api/post.py +0 -0
  47. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/block.py +0 -0
  48. {engin-0.0.19 → engin-0.0.20}/examples/asgi/features/cats/domain.py +0 -0
  49. {engin-0.0.19 → engin-0.0.20}/examples/asgi/main.py +0 -0
  50. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/__init__.py +0 -0
  51. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/app.py +0 -0
  52. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/main.py +0 -0
  53. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/__init__.py +0 -0
  54. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/__init__.py +0 -0
  55. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  56. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  57. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/api.py +0 -0
  58. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/block.py +0 -0
  59. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/domain.py +0 -0
  60. {engin-0.0.19 → engin-0.0.20}/examples/fastapi/routes/cats/ports.py +0 -0
  61. {engin-0.0.19 → engin-0.0.20}/examples/simple/__init__.py +0 -0
  62. {engin-0.0.19 → engin-0.0.20}/examples/simple/main.py +0 -0
  63. {engin-0.0.19 → engin-0.0.20}/src/engin/__init__.py +0 -0
  64. {engin-0.0.19 → engin-0.0.20}/src/engin/_assembler.py +0 -0
  65. {engin-0.0.19 → engin-0.0.20}/src/engin/_block.py +0 -0
  66. {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/__init__.py +0 -0
  67. {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_common.py +0 -0
  68. {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_graph.html +0 -0
  69. {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_graph.py +0 -0
  70. {engin-0.0.19 → engin-0.0.20}/src/engin/_cli/_inspect.py +0 -0
  71. {engin-0.0.19 → engin-0.0.20}/src/engin/_engin.py +0 -0
  72. {engin-0.0.19 → engin-0.0.20}/src/engin/_graph.py +0 -0
  73. {engin-0.0.19 → engin-0.0.20}/src/engin/_introspect.py +0 -0
  74. {engin-0.0.19 → engin-0.0.20}/src/engin/_lifecycle.py +0 -0
  75. {engin-0.0.19 → engin-0.0.20}/src/engin/_option.py +0 -0
  76. {engin-0.0.19 → engin-0.0.20}/src/engin/exceptions.py +0 -0
  77. {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/__init__.py +0 -0
  78. {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/asgi.py +0 -0
  79. {engin-0.0.19 → engin-0.0.20}/src/engin/extensions/fastapi.py +0 -0
  80. {engin-0.0.19 → engin-0.0.20}/src/engin/py.typed +0 -0
  81. {engin-0.0.19 → engin-0.0.20}/tests/__init__.py +0 -0
  82. {engin-0.0.19 → engin-0.0.20}/tests/acceptance/__init__.py +0 -0
  83. {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_error_in_shutdown.py +0 -0
  84. {engin-0.0.19 → engin-0.0.20}/tests/acceptance/test_error_in_start_up.py +0 -0
  85. {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/__init__.py +0 -0
  86. {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/conftest.py +0 -0
  87. {engin-0.0.19 → engin-0.0.20}/tests/benchmarks/test_bench_assembler.py +0 -0
  88. {engin-0.0.19 → engin-0.0.20}/tests/cli/__init__.py +0 -0
  89. {engin-0.0.19 → engin-0.0.20}/tests/cli/test_graph.py +0 -0
  90. {engin-0.0.19 → engin-0.0.20}/tests/cli/test_inspect.py +0 -0
  91. {engin-0.0.19 → engin-0.0.20}/tests/conftest.py +0 -0
  92. {engin-0.0.19 → engin-0.0.20}/tests/deps.py +0 -0
  93. {engin-0.0.19 → engin-0.0.20}/tests/test_assembler.py +0 -0
  94. {engin-0.0.19 → engin-0.0.20}/tests/test_block.py +0 -0
  95. {engin-0.0.19 → engin-0.0.20}/tests/test_engin.py +0 -0
  96. {engin-0.0.19 → engin-0.0.20}/tests/test_graph.py +0 -0
  97. {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
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: An async-first modular application framework
5
5
  Project-URL: Homepage, https://github.com/invokermain/engin
6
6
  Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
@@ -33,6 +33,7 @@ nav:
33
33
  - Providers: "concepts/providers.md"
34
34
  - Invocations: "concepts/invocations.md"
35
35
  - Lifecycle: "concepts/lifecycle.md"
36
+ - Blocks: "concepts/blocks.md"
36
37
  - Guides:
37
38
  - FastAPI: "guides/fastapi.md"
38
39
  - Reference: "reference.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.0.19"
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
- builder: Func[P, T],
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 builder or factory function.
164
+ Provide a type via a factory function.
165
165
 
166
166
  Args:
167
- builder: the builder function that returns the type.
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=builder)
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"Provider '{self.name}' is implicitly overriding "
235
- f"'{existing_provider.name}', if this is intended specify "
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.return_type_id})"
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__(builder=self._get_val, as_type=as_type, override=override)
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.__module__
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
- async def test_fastapi():
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
- async def test_inject():
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("http://127.0.0.1:8000/inject")
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 test_provides_implicit_overrides():
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)
@@ -1,7 +1,7 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Annotated, TypeAlias
3
3
 
4
- from engin._type_utils import TypeId
4
+ from engin import TypeId
5
5
 
6
6
 
7
7
  def test_type_id_of_int():