Coverage for src/diy/container.py: 84%
58 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 22:09 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 22:09 +0200
1from __future__ import annotations
3from inspect import FullArgSpec, Parameter, getfullargspec, signature
4from typing import Any, Callable, Protocol, override
6from diy.errors import (
7 MissingConstructorKeywordTypeAnnotationError,
8 UninstanciableTypeError,
9 UnresolvableDependencyError,
10 UnsupportedParameterTypeError,
11)
12from diy.specification import Specification
15class Container(Protocol):
16 # TODO: Write documentation
17 def resolve[T](self, abstract: type[T]) -> T:
18 pass
20 def call[R](self, function: Callable[..., R]) -> R:
21 pass
24class RuntimeContainer(Container):
25 """
26 A :class:`Container` that reflects dependencies at runtime.
27 """
29 spec: Specification
31 def __init__(self, spec: Specification | None = None) -> None:
32 super().__init__()
33 self.spec = spec or Specification()
35 @override
36 def resolve[T](self, abstract: type[T]) -> T:
37 # Maybe we already know how to build this
38 builder = self.spec.builders.get(abstract)
39 if builder is not None:
40 return builder()
42 # if not first check if it even can be built
43 assert_is_instantiable(abstract)
45 # if yes, try to resolve it based on the knowledge we have
46 [args, kwargs] = self.resolve_args(abstract.__init__)
47 return abstract(*args, **kwargs)
49 def resolve_args(
50 self, subject: Callable[..., Any]
51 ) -> tuple[list[Any], dict[str, Any]]:
52 spec = signature(subject)
53 args: list[Any] = []
54 kwargs: dict[str, Any] = {}
56 for name, parameter in spec.parameters.items():
57 # TODO: This can definitely be done better
58 if name == "self" or name[0:1] == "*" or name[0:2] == "**":
59 continue
61 if parameter.kind in [Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD]:
62 # TODO: Maybe introduce a way of supplying these?
63 # We skip them for now, since the empty constructor
64 # has these at the end.
65 continue
67 if parameter.kind not in [
68 Parameter.KEYWORD_ONLY,
69 Parameter.POSITIONAL_OR_KEYWORD,
70 ]:
71 # HINT: Take a look at Signature.apply_defaults for supporting
72 # positional argument defaults
73 # TODO: Support other cases
74 raise UnsupportedParameterTypeError()
76 if parameter.default is not Parameter.empty:
77 continue # We will just use the default from python
79 abstract = parameter.annotation
80 if abstract is Parameter.empty:
81 raise MissingConstructorKeywordTypeAnnotationError(abstract, name)
83 builder = self.spec.builders.get(abstract)
84 if builder is None:
85 raise UnresolvableDependencyError(
86 abstract, list(self.spec.builders._by_type.keys())
87 )
89 kwargs[name] = builder()
91 return (args, kwargs)
93 @override
94 def call[R](self, function: Callable[..., R]) -> R:
95 [args, kwargs] = self.resolve_args(function)
96 return function(*args, **kwargs)
99def requires_arguments(spec: FullArgSpec) -> bool:
100 if len(spec.args) == 0:
101 return False
103 return not (len(spec.args) == 1 and spec.args == ["self"])
106def assert_is_instantiable(abstract: type[Any]) -> None:
107 # TODO: Maybe we can also use `signature` here
108 spec = getfullargspec(abstract.__init__)
109 if len(spec.args) <= 0:
110 raise UninstanciableTypeError(abstract)
112 if spec.args[0] != "self":
113 raise UninstanciableTypeError(abstract)