engin 0.0.18__py3-none-any.whl → 0.0.19__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/__init__.py CHANGED
@@ -1,9 +1,7 @@
1
- from engin import ext
2
1
  from engin._assembler import Assembler
3
2
  from engin._block import Block, invoke, provide
4
3
  from engin._dependency import Entrypoint, Invoke, Provide, Supply
5
4
  from engin._engin import Engin
6
- from engin._exceptions import ProviderError
7
5
  from engin._lifecycle import Lifecycle
8
6
  from engin._option import Option
9
7
  from engin._type_utils import TypeId
@@ -17,10 +15,8 @@ __all__ = [
17
15
  "Lifecycle",
18
16
  "Option",
19
17
  "Provide",
20
- "ProviderError",
21
18
  "Supply",
22
19
  "TypeId",
23
- "ext",
24
20
  "invoke",
25
21
  "provide",
26
22
  ]
engin/_assembler.py CHANGED
@@ -9,8 +9,8 @@ from types import TracebackType
9
9
  from typing import Any, Generic, TypeVar, cast
10
10
 
11
11
  from engin._dependency import Dependency, Provide, Supply
12
- from engin._exceptions import NotInScopeError, ProviderError
13
12
  from engin._type_utils import TypeId
13
+ from engin.exceptions import NotInScopeError, ProviderError
14
14
 
15
15
  LOG = logging.getLogger("engin")
16
16
 
@@ -65,6 +65,7 @@ class Assembler:
65
65
  self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
66
66
  self._assembled_outputs: dict[TypeId, Any] = {}
67
67
  self._lock = asyncio.Lock()
68
+ self._graph_cache: dict[TypeId, set[Provide]] = defaultdict(set)
68
69
 
69
70
  for provider in providers:
70
71
  type_id = provider.return_type_id
@@ -179,8 +180,8 @@ class Assembler:
179
180
  """
180
181
  Add a provider to the Assembler post-initialisation.
181
182
 
182
- If this replaces an existing provider, this will clear any previously assembled
183
- output for the existing Provider.
183
+ If this replaces an existing provider, this will clear all previously assembled
184
+ output. Note: multiproviders cannot be replaced, they are always appended.
184
185
 
185
186
  Args:
186
187
  provider: the Provide instance to add.
@@ -190,14 +191,13 @@ class Assembler:
190
191
  """
191
192
  type_id = provider.return_type_id
192
193
  if provider.is_multiprovider:
193
- if type_id in self._assembled_outputs:
194
- del self._assembled_outputs[type_id]
195
194
  self._multiproviders[type_id].append(provider)
196
195
  else:
197
- if type_id in self._assembled_outputs:
198
- del self._assembled_outputs[type_id]
199
196
  self._providers[type_id] = provider
200
197
 
198
+ self._assembled_outputs.clear()
199
+ self._graph_cache.clear()
200
+
201
201
  def scope(self, scope: str) -> "_ScopeContextManager":
202
202
  return _ScopeContextManager(scope=scope, assembler=self)
203
203
 
@@ -206,13 +206,14 @@ class Assembler:
206
206
  if provider.scope == scope:
207
207
  self._assembled_outputs.pop(type_id, None)
208
208
 
209
- def _resolve_providers(self, type_id: TypeId) -> Iterable[Provide]:
209
+ def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) -> set[Provide]:
210
210
  """
211
211
  Resolves the chain of providers required to satisfy the provider of a given type.
212
212
  Ordering of the return value is very important!
213
-
214
- # TODO: performance optimisation, do not recurse for already satisfied providers?
215
213
  """
214
+ if type_id in self._graph_cache:
215
+ return self._graph_cache[type_id]
216
+
216
217
  if type_id.multi:
217
218
  root_providers = self._multiproviders.get(type_id)
218
219
  else:
@@ -230,22 +231,28 @@ class Assembler:
230
231
  raise LookupError(msg)
231
232
 
232
233
  # providers that must be satisfied to satisfy the root level providers
233
- yield from (
234
+ resolved_providers = {
234
235
  child_provider
235
236
  for root_provider in root_providers
236
237
  for root_provider_param in root_provider.parameter_type_ids
237
- for child_provider in self._resolve_providers(root_provider_param)
238
- )
239
- yield from root_providers
238
+ for child_provider in self._resolve_providers(root_provider_param, resolved)
239
+ if root_provider_param not in resolved
240
+ } | set(root_providers)
241
+
242
+ resolved.add(type_id)
243
+ self._graph_cache[type_id] = resolved_providers
244
+
245
+ return resolved_providers
240
246
 
241
247
  async def _satisfy(self, target: TypeId) -> None:
242
- for provider in self._resolve_providers(target):
248
+ for provider in self._resolve_providers(target, set()):
243
249
  if (
244
250
  not provider.is_multiprovider
245
251
  and provider.return_type_id in self._assembled_outputs
246
252
  ):
247
253
  continue
248
254
  type_id = provider.return_type_id
255
+
249
256
  bound_args = await self._bind_arguments(provider.signature)
250
257
  try:
251
258
  value = await provider(*bound_args.args, **bound_args.kwargs)
@@ -253,6 +260,7 @@ class Assembler:
253
260
  raise ProviderError(
254
261
  provider=provider, error_type=type(err), error_message=str(err)
255
262
  ) from err
263
+
256
264
  if provider.is_multiprovider:
257
265
  if type_id in self._assembled_outputs:
258
266
  self._assembled_outputs[type_id].extend(value)
@@ -269,8 +277,7 @@ class Assembler:
269
277
  args.append(object())
270
278
  continue
271
279
  param_key = TypeId.from_type(param.annotation)
272
- has_dependency = param_key in self._assembled_outputs
273
- if not has_dependency:
280
+ if param_key not in self._assembled_outputs:
274
281
  await self._satisfy(param_key)
275
282
  val = self._assembled_outputs[param_key]
276
283
  if param.kind == param.POSITIONAL_ONLY:
engin/_block.py CHANGED
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
5
5
 
6
6
  from engin._dependency import Dependency, Func, Invoke, Provide
7
7
  from engin._option import Option
8
+ from engin.exceptions import InvalidBlockError
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from engin._engin import Engin
@@ -42,7 +43,7 @@ def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
42
43
  return _inner(func_)
43
44
 
44
45
 
45
- class Block(Option):
46
+ class Block:
46
47
  """
47
48
  A Block is a collection of providers and invocations.
48
49
 
@@ -74,7 +75,7 @@ class Block(Option):
74
75
 
75
76
  @classmethod
76
77
  def apply(cls, engin: "Engin") -> None:
77
- block_name = cls.name or f"{cls.__name__}"
78
+ block_name = cls.name or cls.__name__
78
79
  for option in chain(cls.options, cls._method_options()):
79
80
  if isinstance(option, Dependency):
80
81
  option._block_name = block_name
@@ -82,8 +83,19 @@ class Block(Option):
82
83
 
83
84
  @classmethod
84
85
  def _method_options(cls) -> Iterable[Provide | Invoke]:
85
- for _, method in inspect.getmembers(cls):
86
+ for name, method in inspect.getmembers(cls, inspect.isfunction):
86
87
  if option := getattr(method, "_opt", None):
87
88
  if not isinstance(option, Provide | Invoke):
88
- raise RuntimeError("Block option is not an instance of Provide or Invoke")
89
+ raise InvalidBlockError(
90
+ block=cls,
91
+ reason="Block option is not an instance of Provide or Invoke",
92
+ )
89
93
  yield option
94
+ else:
95
+ raise InvalidBlockError(
96
+ block=cls,
97
+ reason=(
98
+ f"Method '{name}' is not a Provider or Invocation, did you "
99
+ "forget to decorate it?"
100
+ ),
101
+ )
engin/_cli/_graph.py CHANGED
@@ -12,10 +12,10 @@ from rich import print
12
12
  from engin import Entrypoint, Invoke, TypeId
13
13
  from engin._cli._common import COMMON_HELP, get_engin_instance
14
14
  from engin._dependency import Dependency, Provide, Supply
15
- from engin.ext.asgi import ASGIEngin
15
+ from engin.extensions.asgi import ASGIEngin
16
16
 
17
17
  try:
18
- from engin.ext.fastapi import APIRouteDependency
18
+ from engin.extensions.fastapi import APIRouteDependency
19
19
  except ImportError:
20
20
  APIRouteDependency = None # type: ignore[assignment,misc]
21
21
 
engin/_dependency.py CHANGED
@@ -175,47 +175,37 @@ class Provide(Dependency[Any, T]):
175
175
  self._scope = scope
176
176
  self._override = override
177
177
  self._explicit_type = as_type
178
+ self._return_type = self._resolve_return_type()
179
+ self._return_type_id = TypeId.from_type(self._return_type)
178
180
 
179
181
  if self._explicit_type is not None:
180
182
  self._signature = self._signature.replace(return_annotation=self._explicit_type)
181
183
 
182
- self._is_multi = typing.get_origin(self.return_type) is list
184
+ self._is_multi = typing.get_origin(self._return_type) is list
183
185
 
184
186
  # Validate that the provider does to depend on its own output value, as this will
185
187
  # cause a recursion error and is undefined behaviour wise.
186
188
  if any(
187
- self.return_type == param.annotation
189
+ self._return_type == param.annotation
188
190
  for param in self._signature.parameters.values()
189
191
  ):
190
192
  raise ValueError("A provider cannot depend on its own return type")
191
193
 
192
194
  # Validate that multiproviders only return a list of one type.
193
195
  if self._is_multi:
194
- args = typing.get_args(self.return_type)
196
+ args = typing.get_args(self._return_type)
195
197
  if len(args) != 1:
196
198
  raise ValueError(
197
- f"A multiprovider must be of the form list[X], not '{self.return_type}'"
199
+ f"A multiprovider must be of the form list[X], not '{self._return_type}'"
198
200
  )
199
201
 
200
202
  @property
201
203
  def return_type(self) -> type[T]:
202
- if self._explicit_type is not None:
203
- return self._explicit_type
204
- if isclass(self._func):
205
- return_type = self._func # __init__ returns self
206
- else:
207
- try:
208
- return_type = get_type_hints(self._func, include_extras=True)["return"]
209
- except KeyError as err:
210
- raise RuntimeError(
211
- f"Dependency '{self.name}' requires a return typehint"
212
- ) from err
213
-
214
- return return_type
204
+ return self._return_type
215
205
 
216
206
  @property
217
207
  def return_type_id(self) -> TypeId:
218
- return TypeId.from_type(self.return_type)
208
+ return self._return_type_id
219
209
 
220
210
  @property
221
211
  def is_multiprovider(self) -> bool:
@@ -255,6 +245,21 @@ class Provide(Dependency[Any, T]):
255
245
  def __str__(self) -> str:
256
246
  return f"Provide({self.return_type_id})"
257
247
 
248
+ def _resolve_return_type(self) -> type[T]:
249
+ if self._explicit_type is not None:
250
+ return self._explicit_type
251
+ if isclass(self._func):
252
+ return_type = self._func # __init__ returns self
253
+ else:
254
+ try:
255
+ return_type = get_type_hints(self._func, include_extras=True)["return"]
256
+ except KeyError as err:
257
+ raise RuntimeError(
258
+ f"Dependency '{self.name}' requires a return typehint"
259
+ ) from err
260
+
261
+ return return_type
262
+
258
263
 
259
264
  class Supply(Provide, Generic[T]):
260
265
  def __init__(
@@ -276,8 +281,7 @@ class Supply(Provide, Generic[T]):
276
281
  self._value = value
277
282
  super().__init__(builder=self._get_val, as_type=as_type, override=override)
278
283
 
279
- @property
280
- def return_type(self) -> type[T]:
284
+ def _resolve_return_type(self) -> type[T]:
281
285
  if self._explicit_type is not None:
282
286
  return self._explicit_type
283
287
  if isinstance(self._value, list):
@@ -1,7 +1,10 @@
1
- from typing import Any
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
3
  from engin._dependency import Provide
4
4
 
5
+ if TYPE_CHECKING:
6
+ from engin._block import Block
7
+
5
8
 
6
9
  class EnginError(Exception):
7
10
  """
@@ -15,6 +18,20 @@ class AssemblerError(EnginError):
15
18
  """
16
19
 
17
20
 
21
+ class InvalidBlockError(EnginError):
22
+ """
23
+ Raised when an invalid block is instantiated.
24
+ """
25
+
26
+ def __init__(self, block: "type[Block]", reason: str) -> None:
27
+ self.block = block
28
+ self.block_name = block.name or block.__name__
29
+ self.message = f"block '{self.block_name}' is invalid, reason: '{reason}'"
30
+
31
+ def __str__(self) -> str:
32
+ return self.message
33
+
34
+
18
35
  class ProviderError(AssemblerError):
19
36
  """
20
37
  Raised when a Provider errors during Assembly.
@@ -52,3 +69,12 @@ class NotInScopeError(AssemblerError):
52
69
 
53
70
  def __str__(self) -> str:
54
71
  return self.message
72
+
73
+
74
+ __all__ = [
75
+ "AssemblerError",
76
+ "EnginError",
77
+ "InvalidBlockError",
78
+ "NotInScopeError",
79
+ "ProviderError",
80
+ ]
@@ -10,7 +10,7 @@ from engin import Assembler, Engin, Entrypoint, Invoke, Option
10
10
  from engin._dependency import Dependency, Supply, _noop
11
11
  from engin._graph import DependencyGrapher, Node
12
12
  from engin._type_utils import TypeId
13
- from engin.ext.asgi import ASGIEngin
13
+ from engin.extensions.asgi import ASGIEngin
14
14
 
15
15
  try:
16
16
  from fastapi import APIRouter, FastAPI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.18
3
+ Version: 0.0.19
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/
@@ -0,0 +1,25 @@
1
+ engin/__init__.py,sha256=A8TE_ci7idoR683535YoBrWZbYTgXXS-q7Y2y51nZ5M,486
2
+ engin/_assembler.py,sha256=-ENSrXPMWacionIYrTSQO7th9DDBOPyAT8ybPbBRtQw,11318
3
+ engin/_block.py,sha256=IacP4PoJKRhSQCbQSdoyCtmu362a4vj6qoUQKyaJwzI,3062
4
+ engin/_dependency.py,sha256=Nfq6L92LN4X53QpiMCIF3MjmWfuntYVOnZmmoPYYJEw,9165
5
+ engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
6
+ engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
7
+ engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
8
+ engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
9
+ engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
10
+ engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
11
+ engin/exceptions.py,sha256=-VPwPReZb9YEIkrWMR9TW2K5HEwmHHgEO7QWH6wfV8c,1946
12
+ engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ engin/_cli/__init__.py,sha256=koD5WTkZXb8QQIiVU5bJiSR1wwPGb5rv2iwd-v-BA7A,564
14
+ engin/_cli/_common.py,sha256=zMYb1Bs1yUuR3qf3r6WuVozYzDwHJvTVthVbTQfTF9w,1261
15
+ engin/_cli/_graph.html,sha256=rR5dnDKoz7KtSff0ERCi2UKuoH_Z03MRYiXI_W03G5k,2430
16
+ engin/_cli/_graph.py,sha256=HMC91nWvTOr6_czPBNx1RU55Ib3qesJRCmbnL2DsdDk,4659
17
+ engin/_cli/_inspect.py,sha256=0jm25d4wcbXVNJkyaeECSKY-irsxd-EIYBH1GDW_Yjc,3163
18
+ engin/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ engin/extensions/asgi.py,sha256=d5Z6gtMVWDZdAlvrTaMt987sKyiq__A0X4gJQ7IETmA,3247
20
+ engin/extensions/fastapi.py,sha256=e8F4L_nZ9dU9j8mb9lXKwJG6CTu5aIk4N5faRj4EyUA,6369
21
+ engin-0.0.19.dist-info/METADATA,sha256=Rb1VPxLjnzVxhnq8Llie5XRxpI0GANR-dK31BEcPAqg,2354
22
+ engin-0.0.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ engin-0.0.19.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
24
+ engin-0.0.19.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
25
+ engin-0.0.19.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
2
- engin/_assembler.py,sha256=saxYTjT67WR2HLJAFXyDsDeQmLGp1uyDboTDiKTaZ_s,11177
3
- engin/_block.py,sha256=8ysWrmHkWpTm6bmSc6jZVoO0Ax5Svu1HwxpZwAtIF_o,2617
4
- engin/_dependency.py,sha256=5x4_0QvHtqv6R_brKHRc-INKE4oMh1JU8-9RCmulp4Q,8976
5
- engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
6
- engin/_exceptions.py,sha256=UzMppJWDk_Hx3qWAypcPVLw9OYCibqiZjLYeTl22zaE,1355
7
- engin/_graph.py,sha256=y1g7Lm_Zy5GPEgRsggCKV5DDaDzcwUl8v3IZCK8jyGI,1631
8
- engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
9
- engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
10
- engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
11
- engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
12
- engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- engin/_cli/__init__.py,sha256=koD5WTkZXb8QQIiVU5bJiSR1wwPGb5rv2iwd-v-BA7A,564
14
- engin/_cli/_common.py,sha256=zMYb1Bs1yUuR3qf3r6WuVozYzDwHJvTVthVbTQfTF9w,1261
15
- engin/_cli/_graph.html,sha256=rR5dnDKoz7KtSff0ERCi2UKuoH_Z03MRYiXI_W03G5k,2430
16
- engin/_cli/_graph.py,sha256=S0HKWb3PlC1ygYTdsFzEm-eYmrbHhOOMZ7nApOe7ac8,4645
17
- engin/_cli/_inspect.py,sha256=0jm25d4wcbXVNJkyaeECSKY-irsxd-EIYBH1GDW_Yjc,3163
18
- engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- engin/ext/asgi.py,sha256=d5Z6gtMVWDZdAlvrTaMt987sKyiq__A0X4gJQ7IETmA,3247
20
- engin/ext/fastapi.py,sha256=TGNf0LFLaTLMLlAycH7GgP_GcBld262v9xboGOwhvgE,6362
21
- engin-0.0.18.dist-info/METADATA,sha256=4d8IsPLHnEekTIP5Qdy2LfNYHHZ-G0DLWcjB2RRQdSs,2354
22
- engin-0.0.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- engin-0.0.18.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
24
- engin-0.0.18.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
25
- engin-0.0.18.dist-info/RECORD,,
File without changes
File without changes
File without changes