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]