I want to create a custom Collection (FnColl) that is specific for an ObjectType T and holds a list of Callables, each taking an object of type T and returning any value.
class FnColl[T]:
def __init__(self, fns: list[Callable[[T], Any]] = []) -> None:
self.fns: list[Callable[[T], Any]] = fns
def add(self, fn: Callable[[T], Any], invert: bool) -> Self:
if invert:
fn = self._invert(fn=fn)
self.fns.append(fn)
return self
@staticmethod
def _invert(fn: Callable[[T], Any]) -> Callable[[T], Any]: ...
This class is supposed to be further subclassed in more specific function collections (e.g., a set of filters to be used on an object collection).
class Filters[T](FnColl[T]):
def __init__(self, fns: list[Callable[[T], bool]] = []):
super().__init__(fns=fns)
def add(self, fn: Callable[[T], bool], invert: bool) -> Self:
return super().add(fn=fn, invert=invert)
I then have object specific implementations like the following. Every filter function is also supposed to receive an additional argument if the result shall be inverted or not, that I do not want to explicitly define for each filter function.
class A
x: int
y: str
class AFilter(Filters[A]):
@add_filter
def has_value_for_x(self, obj: A) -> bool: ...
@add_filter
def has_specific_value_for_y(self, obj: A, specific_value: str) -> bool: ...
Hint: @add_filter here is only a dummy not yet defined to showcase that I want to achieve a decorator mechanism to create a set of filters like the following without repeating code.
f = AFilter().has_value_for_x(exclude=True).has_specific_value_for_y(specific_value="abc")
I want to create a collection of filters in a chained manner with every filter function accepting the exclude argument without explicitly defining and optionally additional arguments depending on the function.
I tried to create a decorator class within my Filters but it does not seem to be the correct implementation / type hinting.
class Filters[T](FnColl[T]):
def __init__(self, fns: list[Callable[[T], bool]] = []):
super().__init__(fns=fns)
def add(self, fn: Callable[[T], bool], invert: bool) -> Self:
return super().add(fn=fn, invert=invert)
class Filter[**P, T1: Filters]:
type FilterFn = Callable[Concatenate[T1, T, P], bool]
type FilterApply = Callable[Concatenate[bool, P], T1]
def __init__(self, f: FilterFn) -> None:
self.fn = f
def __call__(
self, _self: T1, exclude: bool = False, *args: P.args, **kwds: P.kwargs
) -> T1:
def f(obj: T) -> bool:
return self.fn(_self, obj, *args, **kwds)
return _self.add(fn=f, invert=exclude)
def __get__(self, instance: T1, owner: type[T1]) -> FilterApply:
return partial(self.__call__, _self=instance)
When using it in AFilter type checking reports T@Filters is not compatible with A for line @Filters.Filter
class AFilter(Filters[A]):
@Filters.Filter
def has_value_for_x(self, obj: A) -> bool: ...
@Filters.Filter
def has_specific_value_for_y(self, obj: A, specific_value: str) -> bool: ...
How can I define valid type hinting / a decorator to avoid repeated code for adding the filter function to the instances list of callables?
Edit:
Without a decorator the Filter collections could be written something like:
class AFilter(Filters[A]):
def has_value_for_x(self, exclude: bool = False) -> Self:
def f(a: A) -> bool:
return a.x is not None
return self.add(fn=f, invert=exclude)
def has_specific_value_for_y(self, specific_value: str, exclude: bool = False) -> Self:
def f(a: A) -> bool:
return a.y == specific_value
return self.add(fn=f, invert=exclude)
For multiple Filter collections for different objects with many different filter functions that is a lot of boilerplate. Under the hood it still is a simple list of functions but I want to have a standardized and defined interface for Filter collections and adding filters.
In general I especially try to understand, how dependant generic types are working to achieve the described behaviour.
Edit2:
I found a first working solution by separating the collection and the decorator class. However, I don't have an interdependency between between generic type T of Collection and Filter. Also I am missing to have introspection into the arguments of the decorated method (i.e., popup in VSCode to display method arguments)
class Filters[T](FnColl[T]):
def __init__(self, fns: list[Callable[[T], bool]] = []):
super().__init__(fns=fns)
def add(self, fn: Callable[[T], bool], invert: bool) -> Self:
return super().add(fn=fn, invert=invert)
class Filter[T, T1: Filters]:
type FilterFn[**P] = Callable[Concatenate[T1, T, P], bool]
def __init__(self, f: FilterFn) -> None:
self.fn = f
def __call__(
self, _self: T1, exclude: bool = False, *args, **kwds
) -> T1:
@wraps(self.fn)
def f(obj: T) -> bool:
return self.fn(_self, obj, *args, **kwds)
return _self.add(fn=f, invert=exclude)
def __get__(self, instance: T1, owner: type[T1]) -> FilterApply:
return partial(self.__call__, _self=instance)
To use I need to define:
class AFilter(Filter[A, "AFilters"]): ...
class AFilters(Filters[A]):
@AFilter
def has_value_for_x(self, obj: A) -> bool: ...
@AFilter
def has_specific_value_for_y(self, obj: A, specific_value: str) -> bool: ...
AFilter().has_value_for_x(exclude=True)evaluates to a boolean, according to your type hint.bool.has_value_for_x(exclude=True)will then raise because booleans don't have such an attribute. So try again to tell us what you want to do.-> boolhint is for the undecorated method, but there's@Filters.Filterstacked on top, and its__call__returns a homemade self type. This part seems perfectly fine.add_filter, which is not defined. You are absolutely right about theFilters.Filterdecorator, which I did not notice.Callable[[int], bool]to aFnColl[int].