Coverage for src/diy/specification.py: 61%

71 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-10 22:09 +0200

1from __future__ import annotations 

2 

3from collections import defaultdict 

4from inspect import Parameter, Signature, signature 

5from typing import Any, Callable, Self 

6 

7from diy.errors import ( 

8 InvalidConstructorKeywordArgumentError, 

9 MissingConstructorKeywordArgumentError, 

10 MissingReturnTypeAnnotationError, 

11) 

12 

13 

14class Builders: 

15 """ 

16 Add and retrieve builder functions for types.""" 

17 

18 _by_type: dict[str, Callable[..., Any]] 

19 

20 def __init__(self) -> None: 

21 super().__init__() 

22 self._by_type = {} 

23 

24 def add[T](self, abstract: type[T], builder: Callable[[], T]) -> Self: 

25 """ 

26 Imperatively register a builder function for the abstract type. 

27 

28 >>> from diy import Specification 

29 ... 

30 >>> class Greeter: 

31 ... def __init__(self, name: str): 

32 ... self.name = name 

33 ... 

34 ... def greet(self): 

35 ... print(f"Hello {self.name}!") 

36 ... 

37 >>> spec = Specification() 

38 >>> spec.builders.add(Greeter, lambda: Greeter("Ella")) 

39 ... 

40 >>> builder = spec.builders.get(Greeter) 

41 >>> instance = builder() 

42 >>> instance.greet() 

43 Hello Ella! 

44 """ 

45 if not isinstance(abstract, str): 

46 abstract = abstract.__qualname__ 

47 self._by_type[abstract] = builder 

48 return self 

49 

50 def decorate[T](self, builder: Callable[..., T]): 

51 """ 

52 Mark an existing function as a builder for an abstract type by 

53 decorating it. 

54 

55 >>> from diy import Specification 

56 ... 

57 >>> class Greeter: 

58 ... def __init__(self, name: str): 

59 ... self.name = name 

60 ... 

61 ... def greet(self): 

62 ... print(f"Hello {self.name}!") 

63 ... 

64 >>> spec = Specification() 

65 ... 

66 >>> @spec.builders.decorate 

67 ... def build_greeter() -> Greeter: 

68 ... return Greeter("Ella") 

69 ... 

70 >>> builder = spec.builders.get(Greeter) 

71 >>> instance = builder() 

72 >>> instance.greet() 

73 Hello Ella! 

74 """ 

75 abstract = assert_annotates_return_type(builder) 

76 if not isinstance(abstract, str): 

77 abstract = abstract.__qualname__ 

78 self._by_type[abstract] = builder 

79 return builder 

80 

81 def get[T](self, abstract: type[T]) -> Callable[[], T] | None: 

82 """ 

83 Retrieve a builder function for the given abstract type. 

84 

85 >>> from diy import Specification 

86 ... 

87 >>> class Greeter: 

88 ... def __init__(self, name: str): 

89 ... self.name = name 

90 ... 

91 ... def greet(self): 

92 ... print(f"Hello {self.name}!") 

93 ... 

94 >>> spec = Specification() 

95 >>> spec.builders.add(Greeter, lambda: Greeter("Ella")) 

96 ... 

97 >>> builder = spec.builders.get(Greeter) 

98 >>> instance = builder() 

99 >>> instance.greet() 

100 Hello Ella! 

101 """ 

102 if not isinstance(abstract, str): 

103 abstract = abstract.__qualname__ 

104 return self._by_type.get(abstract) 

105 

106 

107class Partials: 

108 """ 

109 Add and retrieve builder functions for constructor paramters of types. 

110 """ 

111 

112 _by_type: defaultdict[type[Any], dict[str, Callable[..., Any]]] 

113 

114 def __init__(self) -> None: 

115 super().__init__() 

116 self._by_type = defaultdict(default_factory=dict) 

117 

118 def add[P]( 

119 self, 

120 abstract: type[Any], 

121 name: str, 

122 parameter_type: type[P], 

123 builder: Callable[..., P], 

124 ) -> Self: 

125 """ 

126 Add the given partial function for the given parameter of the given 

127 abstract type. 

128 

129 >>> from diy import Specification 

130 .. 

131 >>> class Simple: 

132 >>> pass 

133 ... 

134 >>> class Greeter: 

135 ... def __init__(self, name: str, simple: Simple): 

136 ... self.name = name 

137 ... self.simple = simple 

138 ... 

139 ... def greet(self): 

140 ... print(f"Hello {self.name}!") 

141 ... 

142 >>> spec = Specification() 

143 >>> spec.partials.add(Greeter, "name", str, lambda: "Ella") 

144 ... 

145 >>> builder = spec.partials.get(Greeter, "name") 

146 >>> instance = builder() 

147 >>> print(builder()) 

148 Ella 

149 """ 

150 parameter = assert_constructor_has_parameter(abstract, name) 

151 assert_parameter_annotation_matches(abstract, parameter, parameter_type) 

152 self._by_type[abstract][name] = builder 

153 return self 

154 

155 def decorate[P](self, abstract: type[Any], name: str): 

156 """ 

157 Add the given partial function for the given parameter of the given 

158 abstract type by decorating it. 

159 

160 >>> from diy import Specification 

161 .. 

162 >>> class Simple: 

163 >>> pass 

164 ... 

165 >>> class Greeter: 

166 ... def __init__(self, name: str, simple: Simple): 

167 ... self.name = name 

168 ... self.simple = simple 

169 ... 

170 ... def greet(self): 

171 ... print(f"Hello {self.name}!") 

172 ... 

173 >>> spec = Specification() 

174 ... 

175 >>> spec.partials.decorate(Greeter, "name") 

176 ... def build_greeter_name() -> str: 

177 ... return "Ella" 

178 ... 

179 >>> builder = spec.partials.get(Greeter, "name") 

180 >>> instance = builder() 

181 >>> print(builder()) 

182 Ella 

183 """ 

184 

185 def decorator(builder: Callable[..., P]): 

186 builder_returns = assert_annotates_return_type(builder) 

187 parameter = assert_constructor_has_parameter(abstract, name) 

188 assert_parameter_annotation_matches(abstract, parameter, builder_returns) 

189 self._by_type[abstract][name] = builder 

190 

191 def inner(): 

192 return builder 

193 

194 return inner 

195 

196 return decorator 

197 

198 

199class Specification: 

200 """ 

201 Registers functions that construct certain types. 

202 Intended to be used by a :class:`diy.Container`. 

203 """ 

204 

205 builders: Builders 

206 """Add and retrieve builder functions for types.""" 

207 

208 partials: Partials 

209 """Add and retrieve builder functions for constructor paramters of types.""" 

210 

211 def __init__(self) -> None: 

212 super().__init__() 

213 self.builders = Builders() 

214 self.partials = Partials() 

215 

216 

217def assert_constructor_has_parameter(abstract: type[Any], name: str) -> Parameter: 

218 sig = signature(abstract.__init__) 

219 parameter = sig.parameters.get(name) 

220 if parameter is None: 

221 raise MissingConstructorKeywordArgumentError(abstract, name) 

222 return parameter 

223 

224 

225def assert_parameter_annotation_matches( 

226 abstract: type[Any], parameter: Parameter, builder_returns: type[Any] 

227) -> None: 

228 accepts = parameter.annotation 

229 if accepts is Parameter.empty: 

230 # TODO: We could add a strict mode here and throw, if the 

231 return 

232 

233 # TODO: We need to check assignability here! Maybe defer to a third-party library? 

234 if accepts is not builder_returns: 

235 raise InvalidConstructorKeywordArgumentError( 

236 abstract, parameter.name, builder_returns, accepts 

237 ) 

238 

239 

240def assert_annotates_return_type[R](builder: Callable[..., R]) -> type[R] | str: 

241 abstract = signature(builder).return_annotation 

242 if abstract is Signature.empty: 

243 raise MissingReturnTypeAnnotationError() 

244 return abstract