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
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-10 22:09 +0200
1from __future__ import annotations
3from collections import defaultdict
4from inspect import Parameter, Signature, signature
5from typing import Any, Callable, Self
7from diy.errors import (
8 InvalidConstructorKeywordArgumentError,
9 MissingConstructorKeywordArgumentError,
10 MissingReturnTypeAnnotationError,
11)
14class Builders:
15 """
16 Add and retrieve builder functions for types."""
18 _by_type: dict[str, Callable[..., Any]]
20 def __init__(self) -> None:
21 super().__init__()
22 self._by_type = {}
24 def add[T](self, abstract: type[T], builder: Callable[[], T]) -> Self:
25 """
26 Imperatively register a builder function for the abstract type.
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
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.
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
81 def get[T](self, abstract: type[T]) -> Callable[[], T] | None:
82 """
83 Retrieve a builder function for the given abstract type.
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)
107class Partials:
108 """
109 Add and retrieve builder functions for constructor paramters of types.
110 """
112 _by_type: defaultdict[type[Any], dict[str, Callable[..., Any]]]
114 def __init__(self) -> None:
115 super().__init__()
116 self._by_type = defaultdict(default_factory=dict)
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.
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
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.
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 """
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
191 def inner():
192 return builder
194 return inner
196 return decorator
199class Specification:
200 """
201 Registers functions that construct certain types.
202 Intended to be used by a :class:`diy.Container`.
203 """
205 builders: Builders
206 """Add and retrieve builder functions for types."""
208 partials: Partials
209 """Add and retrieve builder functions for constructor paramters of types."""
211 def __init__(self) -> None:
212 super().__init__()
213 self.builders = Builders()
214 self.partials = Partials()
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
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
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 )
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