engin 0.0.10__py3-none-any.whl → 0.0.12__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.
engin/_assembler.py CHANGED
@@ -141,7 +141,44 @@ class Assembler:
141
141
  return value # type: ignore[return-value]
142
142
 
143
143
  def has(self, type_: type[T]) -> bool:
144
- return type_id_of(type_) in self._providers
144
+ """
145
+ Returns True if this Assembler has a provider for the given type.
146
+
147
+ Args:
148
+ type_: the type to check.
149
+
150
+ Returns:
151
+ True if the Assembler has a provider for type else False.
152
+ """
153
+ type_id = type_id_of(type_)
154
+ if type_id.multi:
155
+ return type_id in self._multiproviders
156
+ else:
157
+ return type_id in self._providers
158
+
159
+ def add(self, provider: Provide) -> None:
160
+ """
161
+ Add a provider to the Assembler post-initialisation.
162
+
163
+ Args:
164
+ provider: the Provide instance to add.
165
+
166
+ Returns:
167
+ None
168
+
169
+ Raises:
170
+ ValueError: if a provider for this type already exists.
171
+ """
172
+ type_id = provider.return_type_id
173
+ if provider.is_multiprovider:
174
+ if type_id in self._multiproviders:
175
+ self._multiproviders[type_id].append(provider)
176
+ else:
177
+ self._multiproviders[type_id] = [provider]
178
+ else:
179
+ if type_id in self._providers:
180
+ raise ValueError(f"A provider for '{type_id}' already exists")
181
+ self._providers[type_id] = provider
145
182
 
146
183
  def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
147
184
  if type_id.multi:
engin/_dependency.py CHANGED
@@ -3,6 +3,7 @@ import typing
3
3
  from abc import ABC
4
4
  from collections.abc import Awaitable, Callable
5
5
  from inspect import Parameter, Signature, isclass, iscoroutinefunction
6
+ from types import FrameType
6
7
  from typing import (
7
8
  Any,
8
9
  Generic,
@@ -23,22 +24,43 @@ Func: TypeAlias = Callable[P, T]
23
24
  def _noop(*args: Any, **kwargs: Any) -> None: ...
24
25
 
25
26
 
27
+ def _walk_stack() -> FrameType:
28
+ stack = inspect.stack()[1]
29
+ frame = stack.frame
30
+ while True:
31
+ if frame.f_globals["__package__"] != "engin" or frame.f_back is None:
32
+ return frame
33
+ else:
34
+ frame = frame.f_back
35
+
36
+
26
37
  class Dependency(ABC, Generic[P, T]):
27
38
  def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
28
39
  self._func = func
29
40
  self._is_async = iscoroutinefunction(func)
30
41
  self._signature = inspect.signature(self._func)
31
42
  self._block_name = block_name
43
+ self._source_frame = _walk_stack()
32
44
 
33
45
  @property
34
- def origin(self) -> str:
46
+ def source_module(self) -> str:
35
47
  """
36
48
  The module that this Dependency originated from.
37
49
 
38
50
  Returns:
39
51
  A string, e.g. "examples.fastapi.app"
40
52
  """
41
- return self._func.__module__
53
+ return self._source_frame.f_globals["__name__"] # type: ignore[no-any-return]
54
+
55
+ @property
56
+ def source_package(self) -> str:
57
+ """
58
+ The package that this Dependency originated from.
59
+
60
+ Returns:
61
+ A string, e.g. "engin"
62
+ """
63
+ return self._source_frame.f_globals["__package__"] # type: ignore[no-any-return]
42
64
 
43
65
  @property
44
66
  def block_name(self) -> str | None:
@@ -115,10 +137,6 @@ class Entrypoint(Invoke):
115
137
  self._type = type_
116
138
  super().__init__(invocation=_noop, block_name=block_name)
117
139
 
118
- @property
119
- def origin(self) -> str:
120
- return self._type.__module__
121
-
122
140
  @property
123
141
  def parameter_types(self) -> list[TypeId]:
124
142
  return [type_id_of(self._type)]
@@ -140,6 +158,15 @@ class Provide(Dependency[Any, T]):
140
158
  super().__init__(func=builder, block_name=block_name)
141
159
  self._is_multi = typing.get_origin(self.return_type) is list
142
160
 
161
+ # Validate that the provider does to depend on its own output value, as this will
162
+ # cause a recursion error and is undefined behaviour wise.
163
+ if any(
164
+ self.return_type == param.annotation
165
+ for param in self.signature.parameters.values()
166
+ ):
167
+ raise ValueError("A provider cannot depend on its own return type")
168
+
169
+ # Validate that multiproviders only return a list of one type.
143
170
  if self._is_multi:
144
171
  args = typing.get_args(self.return_type)
145
172
  if len(args) != 1:
@@ -186,10 +213,6 @@ class Supply(Provide, Generic[T]):
186
213
  self._get_val.__annotations__["return"] = type_hint
187
214
  super().__init__(builder=self._get_val, block_name=block_name)
188
215
 
189
- @property
190
- def origin(self) -> str:
191
- return self._value.__module__
192
-
193
216
  @property
194
217
  def return_type(self) -> type[T]:
195
218
  if self._type_hint is not None:
engin/ext/asgi.py CHANGED
@@ -1,9 +1,10 @@
1
+ import contextlib
1
2
  import traceback
2
3
  from collections.abc import AsyncIterator, Awaitable, Callable, MutableMapping
3
4
  from contextlib import AbstractAsyncContextManager, asynccontextmanager
4
5
  from typing import Any, ClassVar, Protocol, TypeAlias
5
6
 
6
- from engin import Engin, Entrypoint, Option
7
+ from engin import Engin, Entrypoint, Option, Supply
7
8
 
8
9
  __all__ = ["ASGIEngin", "ASGIType", "engin_to_lifespan"]
9
10
 
@@ -79,7 +80,13 @@ def engin_to_lifespan(engin: Engin) -> Callable[[ASGIType], AbstractAsyncContext
79
80
  """
80
81
 
81
82
  @asynccontextmanager
82
- async def engin_lifespan(_: ASGIType) -> AsyncIterator[None]:
83
+ async def engin_lifespan(app: ASGIType) -> AsyncIterator[None]:
84
+ # ensure the Engin
85
+ with contextlib.suppress(ValueError):
86
+ engin.assembler.add(Supply(app))
87
+
88
+ app.state.assembler = engin.assembler # type: ignore[attr-defined]
89
+
83
90
  await engin.start()
84
91
  yield
85
92
  await engin.stop()
engin/ext/fastapi.py CHANGED
@@ -6,8 +6,8 @@ from typing import ClassVar, TypeVar
6
6
 
7
7
  from fastapi.routing import APIRoute
8
8
 
9
- from engin import Engin, Entrypoint, Invoke, Option
10
- from engin._dependency import Dependency, Supply
9
+ from engin import Assembler, Engin, Entrypoint, Invoke, Option
10
+ from engin._dependency import Dependency, Supply, _noop
11
11
  from engin._graph import DependencyGrapher, Node
12
12
  from engin._type_utils import TypeId, type_id_of
13
13
  from engin.ext.asgi import ASGIEngin
@@ -24,15 +24,16 @@ except ImportError as err:
24
24
  __all__ = ["APIRouteDependency", "FastAPIEngin", "Inject"]
25
25
 
26
26
 
27
- def _attach_engin(
28
- app: FastAPI,
29
- engin: Engin,
30
- ) -> None:
31
- app.state.engin = engin
27
+ def _attach_assembler(app: FastAPI, engin: Engin) -> None:
28
+ """
29
+ An invocation that attaches the Engin's Assembler to the FastAPI application, enabling
30
+ the Inject marker.
31
+ """
32
+ app.state.assembler = engin.assembler
32
33
 
33
34
 
34
35
  class FastAPIEngin(ASGIEngin):
35
- _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
36
+ _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_assembler)]
36
37
  _asgi_type = FastAPI
37
38
 
38
39
  def graph(self) -> list[Node]:
@@ -40,7 +41,7 @@ class FastAPIEngin(ASGIEngin):
40
41
  return grapher.resolve(
41
42
  [
42
43
  Entrypoint(self._asgi_type),
43
- *[i for i in self._invocations if i.func_name != "_attach_engin"],
44
+ *[i for i in self._invocations if i.func_name != "_attach_assembler"],
44
45
  ]
45
46
  )
46
47
 
@@ -50,8 +51,11 @@ T = TypeVar("T")
50
51
 
51
52
  def Inject(interface: type[T]) -> Depends:
52
53
  async def inner(conn: HTTPConnection) -> T:
53
- engin: Engin = conn.app.state.engin
54
- return await engin.assembler.get(interface)
54
+ try:
55
+ assembler: Assembler = conn.app.state.assembler
56
+ except AttributeError:
57
+ raise RuntimeError("Assembler is not attached to Application state") from None
58
+ return await assembler.get(interface)
55
59
 
56
60
  dep = Depends(inner)
57
61
  dep.__engin__ = True # type: ignore[attr-defined]
@@ -113,7 +117,7 @@ def _extract_routes_from_supply(supply: Supply) -> list[Dependency]:
113
117
  inner = supply._value[0]
114
118
  if isinstance(inner, APIRouter):
115
119
  return [
116
- APIRouteDependency(route, block_name=supply.block_name)
120
+ APIRouteDependency(supply, route)
117
121
  for route in inner.routes
118
122
  if isinstance(route, APIRoute)
119
123
  ]
@@ -128,13 +132,26 @@ class APIRouteDependency(Dependency):
128
132
  This class should never be constructed in application code.
129
133
  """
130
134
 
131
- def __init__(self, route: APIRoute, block_name: str | None = None) -> None:
135
+ def __init__(
136
+ self,
137
+ wraps: Dependency,
138
+ route: APIRoute,
139
+ ) -> None:
132
140
  """
133
141
  Warning: this should never be constructed in application code.
134
142
  """
143
+ super().__init__(_noop, wraps.block_name)
144
+ self._wrapped = wraps
135
145
  self._route = route
136
146
  self._signature = inspect.signature(route.endpoint)
137
- self._block_name = block_name
147
+
148
+ @property
149
+ def source_module(self) -> str:
150
+ return self._wrapped.source_module
151
+
152
+ @property
153
+ def source_package(self) -> str:
154
+ return self._wrapped.source_package
138
155
 
139
156
  @property
140
157
  def route(self) -> APIRoute:
engin/scripts/graph.py CHANGED
@@ -28,6 +28,8 @@ args.add_argument(
28
28
  ),
29
29
  )
30
30
 
31
+ _APP_ORIGIN = ""
32
+
31
33
 
32
34
  def serve_graph() -> None:
33
35
  # add cwd to path to enable local package imports
@@ -44,6 +46,9 @@ def serve_graph() -> None:
44
46
  "Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
45
47
  ) from None
46
48
 
49
+ global _APP_ORIGIN
50
+ _APP_ORIGIN = module_name.split(".", maxsplit=1)[0]
51
+
47
52
  module = importlib.import_module(module_name)
48
53
 
49
54
  try:
@@ -112,7 +117,17 @@ def _render_node(node: Dependency) -> str:
112
117
  if n not in _BLOCK_IDX:
113
118
  _BLOCK_IDX[n] = len(_SEEN_BLOCKS) % 8
114
119
  _SEEN_BLOCKS.append(n)
115
- style = f":::b{_BLOCK_IDX[n]}"
120
+ style = f"b{_BLOCK_IDX[n]}"
121
+
122
+ node_root_package = node.source_package.split(".", maxsplit=1)[0]
123
+ if node_root_package != _APP_ORIGIN:
124
+ if style:
125
+ style += "E"
126
+ else:
127
+ style = "external"
128
+
129
+ if style:
130
+ style = f":::{style}"
116
131
 
117
132
  if isinstance(node, Supply):
118
133
  md += f"{node.return_type_id}"
@@ -144,6 +159,7 @@ _GRAPH_HTML = """
144
159
  graph LR
145
160
  %%LEGEND%%
146
161
  classDef b0 fill:#7fc97f;
162
+ classDef external stroke-dasharray: 5 5;
147
163
  </pre>
148
164
  </div>
149
165
  <pre class="mermaid">
@@ -157,6 +173,15 @@ _GRAPH_HTML = """
157
173
  classDef b5 fill:#f0027f;
158
174
  classDef b6 fill:#bf5b17;
159
175
  classDef b7 fill:#666666;
176
+ classDef b0E fill:#7fc97f,stroke-dasharray: 5 5;
177
+ classDef b1E fill:#beaed4,stroke-dasharray: 5 5;
178
+ classDef b2E fill:#fdc086,stroke-dasharray: 5 5;
179
+ classDef b3E fill:#ffff99,stroke-dasharray: 5 5;
180
+ classDef b4E fill:#386cb0,stroke-dasharray: 5 5;
181
+ classDef b5E fill:#f0027f,stroke-dasharray: 5 5;
182
+ classDef b6E fill:#bf5b17,stroke-dasharray: 5 5;
183
+ classDef b7E fill:#666666,stroke-dasharray: 5 5;
184
+ classDef external stroke-dasharray: 5 5;
160
185
  </pre>
161
186
  <script type="module">
162
187
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
@@ -169,6 +194,6 @@ _GRAPH_HTML = """
169
194
 
170
195
  DEFAULT_LEGEND = (
171
196
  "0[/Invoke/] ~~~ 1[/Entrypoint\\] ~~~ 2[Provide] ~~~ 3(Supply)"
172
- ' ~~~ 4["`Block Grouping`"]:::b0'
197
+ ' ~~~ 4["`Block Grouping`"]:::b0 ~~~ 5[External Dependency]:::external'
173
198
  )
174
- ASGI_ENGIN_LEGEND = DEFAULT_LEGEND + " ~~~ 5[[API Route]]"
199
+ ASGI_ENGIN_LEGEND = DEFAULT_LEGEND + " ~~~ 6[[API Route]]"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.10
3
+ Version: 0.0.12
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/
@@ -12,6 +12,8 @@ Keywords: Application Framework,Dependency Injection
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
 
15
+ [![codecov](https://codecov.io/gh/invokermain/engin/graph/badge.svg?token=4PJOIMV6IB)](https://codecov.io/gh/invokermain/engin)
16
+
15
17
  # Engin 🏎️
16
18
 
17
19
  Engin is a zero-dependency application framework for modern Python.
@@ -1,7 +1,7 @@
1
1
  engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
2
- engin/_assembler.py,sha256=6n8RBOwano3U3WM1P8Y6HLHo8IuPD_7jUMoTHBJoEA8,7713
2
+ engin/_assembler.py,sha256=1ODW3HenDlIQLetg0LNEPPbsI6HkFnPU_AHzkR9Zxmc,8844
3
3
  engin/_block.py,sha256=0QJtqyP5uTFjXsdVGr4ZONLI2LhfzUKmQGnNQWouB3o,2121
4
- engin/_dependency.py,sha256=Nmyk4cGcK6ZZA8YWNZWgckLpDdKsRwOeRQhqRlxZmI0,5883
4
+ engin/_dependency.py,sha256=RWOyGpMFp_5aLq5TP2wLKz0HT5f6CRoLvFyWvVif9jY,6825
5
5
  engin/_engin.py,sha256=MTE4MkLrK45h0Nv7p5H92Kv5URa1nX246B9Pp1JkM3A,9134
6
6
  engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
7
7
  engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
@@ -9,12 +9,12 @@ engin/_lifecycle.py,sha256=_jQnGFj4RYXsxMpcXPJQagFOwnoTVh7oSN8oUYoYuW0,3246
9
9
  engin/_type_utils.py,sha256=C71kX2Dr-gluGSL018K4uihX3zkTe7QNWaHhFU10ZmA,2127
10
10
  engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- engin/ext/asgi.py,sha256=PCnemGVVB0jUZVcphz7y283sZ7D5Gog1o6rJ6g2TuCQ,2944
13
- engin/ext/fastapi.py,sha256=p9NekfRHbdNzK_1ttVNr500AO-IwLpBlG9Fhk6R0mmg,5649
12
+ engin/ext/asgi.py,sha256=RUxkG03VTlvI6EG19c1nEJY8FnQw6MQwolfJSFnhUFE,3168
13
+ engin/ext/fastapi.py,sha256=GO3AIZNQ69MtzbWuACffx_6Pp34wC5a5Fi_fIAaQvTg,6186
14
14
  engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- engin/scripts/graph.py,sha256=kmC_sSypGvIH89GddHz3KoRBEvooGNsGtrwLgw_uxNQ,4968
16
- engin-0.0.10.dist-info/METADATA,sha256=qal9-bNGzl3dvBhuoc9wZUN3F8dCl23oxvZKLMXSPFc,2162
17
- engin-0.0.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- engin-0.0.10.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
- engin-0.0.10.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
- engin-0.0.10.dist-info/RECORD,,
15
+ engin/scripts/graph.py,sha256=por62FkzcWx72V2Ha9sIoki-o99fe2Ifm1w-mdoHZIQ,5922
16
+ engin-0.0.12.dist-info/METADATA,sha256=lNUoVBIDpm9KlzvkPQWdY29gZsgfMI-3HHoQT7slA6k,2291
17
+ engin-0.0.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ engin-0.0.12.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
+ engin-0.0.12.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
+ engin-0.0.12.dist-info/RECORD,,
File without changes