0

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: ...
6
  • regarding "a mechanism to create a set of filters": 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. Commented Feb 26 at 16:49
  • You might consider explaining why a simple list won't do what you want. You can simply append to it using standard methods, and the type hinting system will flag attempts to append the wrong thing. Commented Feb 26 at 16:52
  • @PaulCornelius nope - the -> bool hint is for the undecorated method, but there's @Filters.Filter stacked on top, and its __call__ returns a homemade self type. This part seems perfectly fine. Commented Feb 26 at 17:30
  • @STerliakov I was referring to the first example where the decorator was add_filter, which is not defined. You are absolutely right about the Filters.Filter decorator, which I did not notice. Commented Feb 26 at 17:56
  • 1
    The inverse of a function isn't necessarily the right type for your collection. You can't, for example, add the inverse of a Callable[[int], bool] to a FnColl[int]. Commented Feb 26 at 20:13

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.