1from __future__ import annotations
2
3from abc import ABCMeta, abstractmethod
4from typing import Callable, Iterable, Union
5
6__all__ = ["Filter", "Never", "Always", "Condition", "FilterOrBool"]
7
8
9class Filter(metaclass=ABCMeta):
10 """
11 Base class for any filter to activate/deactivate a feature, depending on a
12 condition.
13
14 The return value of ``__call__`` will tell if the feature should be active.
15 """
16
17 def __init__(self) -> None:
18 self._and_cache: dict[Filter, Filter] = {}
19 self._or_cache: dict[Filter, Filter] = {}
20 self._invert_result: Filter | None = None
21
22 @abstractmethod
23 def __call__(self) -> bool:
24 """
25 The actual call to evaluate the filter.
26 """
27 return True
28
29 def __and__(self, other: Filter) -> Filter:
30 """
31 Chaining of filters using the & operator.
32 """
33 assert isinstance(other, Filter), f"Expecting filter, got {other!r}"
34
35 if isinstance(other, Always):
36 return self
37 if isinstance(other, Never):
38 return other
39
40 if other in self._and_cache:
41 return self._and_cache[other]
42
43 result = _AndList.create([self, other])
44 self._and_cache[other] = result
45 return result
46
47 def __or__(self, other: Filter) -> Filter:
48 """
49 Chaining of filters using the | operator.
50 """
51 assert isinstance(other, Filter), f"Expecting filter, got {other!r}"
52
53 if isinstance(other, Always):
54 return other
55 if isinstance(other, Never):
56 return self
57
58 if other in self._or_cache:
59 return self._or_cache[other]
60
61 result = _OrList.create([self, other])
62 self._or_cache[other] = result
63 return result
64
65 def __invert__(self) -> Filter:
66 """
67 Inverting of filters using the ~ operator.
68 """
69 if self._invert_result is None:
70 self._invert_result = _Invert(self)
71
72 return self._invert_result
73
74 def __bool__(self) -> None:
75 """
76 By purpose, we don't allow bool(...) operations directly on a filter,
77 because the meaning is ambiguous.
78
79 Executing a filter has to be done always by calling it. Providing
80 defaults for `None` values should be done through an `is None` check
81 instead of for instance ``filter1 or Always()``.
82 """
83 raise ValueError(
84 "The truth value of a Filter is ambiguous. Instead, call it as a function."
85 )
86
87
88def _remove_duplicates(filters: list[Filter]) -> list[Filter]:
89 result = []
90 for f in filters:
91 if f not in result:
92 result.append(f)
93 return result
94
95
96class _AndList(Filter):
97 """
98 Result of &-operation between several filters.
99 """
100
101 def __init__(self, filters: list[Filter]) -> None:
102 super().__init__()
103 self.filters = filters
104
105 @classmethod
106 def create(cls, filters: Iterable[Filter]) -> Filter:
107 """
108 Create a new filter by applying an `&` operator between them.
109
110 If there's only one unique filter in the given iterable, it will return
111 that one filter instead of an `_AndList`.
112 """
113 filters_2: list[Filter] = []
114
115 for f in filters:
116 if isinstance(f, _AndList): # Turn nested _AndLists into one.
117 filters_2.extend(f.filters)
118 else:
119 filters_2.append(f)
120
121 # Remove duplicates. This could speed up execution, and doesn't make a
122 # difference for the evaluation.
123 filters = _remove_duplicates(filters_2)
124
125 # If only one filter is left, return that without wrapping into an
126 # `_AndList`.
127 if len(filters) == 1:
128 return filters[0]
129
130 return cls(filters)
131
132 def __call__(self) -> bool:
133 return all(f() for f in self.filters)
134
135 def __repr__(self) -> str:
136 return "&".join(repr(f) for f in self.filters)
137
138
139class _OrList(Filter):
140 """
141 Result of |-operation between several filters.
142 """
143
144 def __init__(self, filters: list[Filter]) -> None:
145 super().__init__()
146 self.filters = filters
147
148 @classmethod
149 def create(cls, filters: Iterable[Filter]) -> Filter:
150 """
151 Create a new filter by applying an `|` operator between them.
152
153 If there's only one unique filter in the given iterable, it will return
154 that one filter instead of an `_OrList`.
155 """
156 filters_2: list[Filter] = []
157
158 for f in filters:
159 if isinstance(f, _OrList): # Turn nested _AndLists into one.
160 filters_2.extend(f.filters)
161 else:
162 filters_2.append(f)
163
164 # Remove duplicates. This could speed up execution, and doesn't make a
165 # difference for the evaluation.
166 filters = _remove_duplicates(filters_2)
167
168 # If only one filter is left, return that without wrapping into an
169 # `_AndList`.
170 if len(filters) == 1:
171 return filters[0]
172
173 return cls(filters)
174
175 def __call__(self) -> bool:
176 return any(f() for f in self.filters)
177
178 def __repr__(self) -> str:
179 return "|".join(repr(f) for f in self.filters)
180
181
182class _Invert(Filter):
183 """
184 Negation of another filter.
185 """
186
187 def __init__(self, filter: Filter) -> None:
188 super().__init__()
189 self.filter = filter
190
191 def __call__(self) -> bool:
192 return not self.filter()
193
194 def __repr__(self) -> str:
195 return f"~{self.filter!r}"
196
197
198class Always(Filter):
199 """
200 Always enable feature.
201 """
202
203 def __call__(self) -> bool:
204 return True
205
206 def __or__(self, other: Filter) -> Filter:
207 return self
208
209 def __and__(self, other: Filter) -> Filter:
210 return other
211
212 def __invert__(self) -> Never:
213 return Never()
214
215
216class Never(Filter):
217 """
218 Never enable feature.
219 """
220
221 def __call__(self) -> bool:
222 return False
223
224 def __and__(self, other: Filter) -> Filter:
225 return self
226
227 def __or__(self, other: Filter) -> Filter:
228 return other
229
230 def __invert__(self) -> Always:
231 return Always()
232
233
234class Condition(Filter):
235 """
236 Turn any callable into a Filter. The callable is supposed to not take any
237 arguments.
238
239 This can be used as a decorator::
240
241 @Condition
242 def feature_is_active(): # `feature_is_active` becomes a Filter.
243 return True
244
245 :param func: Callable which takes no inputs and returns a boolean.
246 """
247
248 def __init__(self, func: Callable[[], bool]) -> None:
249 super().__init__()
250 self.func = func
251
252 def __call__(self) -> bool:
253 return self.func()
254
255 def __repr__(self) -> str:
256 return f"Condition({self.func!r})"
257
258
259# Often used as type annotation.
260FilterOrBool = Union[Filter, bool]