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

1from __future__ import annotations 

2 

3from inspect import FullArgSpec, Parameter, getfullargspec, signature 

4from typing import Any, Callable, Protocol, override 

5 

6from diy.errors import ( 

7 MissingConstructorKeywordTypeAnnotationError, 

8 UninstanciableTypeError, 

9 UnresolvableDependencyError, 

10 UnsupportedParameterTypeError, 

11) 

12from diy.specification import Specification 

13 

14 

15class 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 

24class 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 

99def 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 

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) 

111 

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

113 raise UninstanciableTypeError(abstract)