Coverage for src/diy/container.py: 84%

1 from __future__ import annotations

2

3 from inspect import FullArgSpec, Parameter, getfullargspec, signature

4 from typing import Any, Callable, Protocol, override

5

6 from diy.errors import (

7 MissingConstructorKeywordTypeAnnotationError,

8 UninstanciableTypeError,

9 UnresolvableDependencyError,

10 UnsupportedParameterTypeError,

11)

12 from diy.specification import Specification

13

14

15 class Container(Protocol):

16 # TODO: Write documentation

17 def resolve[T](self, abstract: type[T]) -> T:

18 pass

19

20 def call[R](self, function: Callable[..., R]) -> R:

21 pass

22

23

24 class RuntimeContainer(Container):

25 """

26 A :class:`Container` that reflects dependencies at runtime.

27 """

28

29 spec: Specification

30

31 def __init__(self, spec: Specification | None = None) -> None:

32 super().__init__()

33 self.spec = spec or Specification()

34

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()

41

42 # if not first check if it even can be built

43 assert_is_instantiable(abstract)

44

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)

48

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] = {}

55

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

60

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

66

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()

75

76 if parameter.default is not Parameter.empty:

77 continue # We will just use the default from python

78

79 abstract = parameter.annotation

80 if abstract is Parameter.empty:

81 raise MissingConstructorKeywordTypeAnnotationError(abstract, name)

82

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 )

88

89 kwargs[name] = builder()

90

91 return (args, kwargs)

92

93 @override

94 def call[R](self, function: Callable[..., R]) -> R:

95 [args, kwargs] = self.resolve_args(function)

96 return function(*args, **kwargs)

97

98

99 def requires_arguments(spec: FullArgSpec) -> bool:

100 if len(spec.args) == 0:

101 return False

102

103 return not (len(spec.args) == 1 and spec.args == ["self"])

104

105

106 def 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)

111

112 if spec.args[0] != "self":

113 raise UninstanciableTypeError(abstract)