textual icon indicating copy to clipboard operation
textual copied to clipboard

Decorator madness

Open willmcgugan opened this issue 1 year ago • 5 comments

Decorators for watch, compute, and validate. Works like this:

class MyWidget(Widget):

    count = reactive(0)
    double_count = reactive(0)

    @count.watch
    def count_changed(self, new_count:int):
        ...

    @count.validate
    def _max_ten(self, value:int) -> int:
        return min(10, value)

    @double_count.compute
    def _double(self) -> int:
        return self.count * 2

willmcgugan avatar Feb 24 '24 15:02 willmcgugan

With this change, how would we override what a watcher does in a widget subclass?

If I have a count reactive declared in a widget which has a watcher defined, then I subclass that and I want my own watcher, how would I achieve this? In the child class, @count.watch doesn't work because count is not defined.

For example, this fails to run because NameError: name 'count' is not defined

    class Parent(Widget):
        count: Reactive[int] = reactive(0, init=False)

        @count.watch
        def _count_parent(self, new_value: int) -> None:
            print("parent watcher")

    class Child(Parent):

        @count.watch
        def _count_parent(self, new_value: int) -> None:
            print("child watcher")

@Parent.count.watch doesn't work because of the "only a single method may be decorated with watch".

Even if that's solved, there's still a bit of a developer experience issue that @davep mentioned, whereby we no longer know what the parent watch method is called without going looking for it.

darrenburns avatar Feb 28 '24 13:02 darrenburns

Going to park this for now. @darrenburns 's observation re inheritance may kill this idea entirely.

willmcgugan avatar Feb 28 '24 13:02 willmcgugan

@willmcgugan Wondering if we should close this?

darrenburns avatar Jul 11 '24 15:07 darrenburns

Think I'll keep it around for a bit. Would like to give it another pass at some point.

willmcgugan avatar Jul 11 '24 16:07 willmcgugan

You can work around the NameError issue with the metaclass' __prepare__ method, e.g.

class M(type):
         @classmethod
         def __prepare__(cls, name, bases, **kwargs):
             for base in bases:
                 if base.__name__ == 'A':
                     return dict(a=base.a)
             return super().__prepare__(name, bases, **kwargs)

class A(metaclass=M):
    a=1

class B(A):
    print(a)

will print out 1. When properly generalising this, I would create the MRO from the given list of bases, then go through them checking for attributes of type textual.reactive.reactive, and add the last one.

yggdr avatar Sep 18 '24 17:09 yggdr