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

1 from __future__ import annotations

2

3 from collections import defaultdict

4 from inspect import Parameter, Signature, signature

5 from typing import Any, Callable, Self

6

7 from diy.errors import (

8 InvalidConstructorKeywordArgumentError,

9 MissingConstructorKeywordArgumentError,

10 MissingReturnTypeAnnotationError,

11)

12

13

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

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

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

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

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

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