holoviz / param

Param: Make your Python code clearer and more reliable by declaring Parameters
https://param.holoviz.org
BSD 3-Clause "New" or "Revised" License
423 stars 73 forks source link

param.watch cannot handle parameters from Parameterized, unlike @param.depends #721

Open phi6ias opened 1 year ago

phi6ias commented 1 year ago

ALL software version info

param 1.12.2

run in powershell prompt using panel serve running Bokeh server 2.4.3 on Tornado 6.2

run in VS Code jupyter notebook Version: 1.76.2 (user setup) Commit: ee2b180d582a7f601fa6ecfdad8d9fd269ab1884 Date: 2023-03-14T17:55:54.936Z Electron: 19.1.11 Chromium: 102.0.5005.196 Node.js: 16.14.2 V8: 10.2.154.26-electron.0 OS: Windows_NT x64 10.0.19045 Sandboxed: No

Description of expected behavior and the observed behavior

When using the decorator @param.depends() it is possible to use a parameter from another class that is stored as a Parameterized instance in the current class, similar as described in this thread --> Holoviz Discourse: Shared Parameter? However, trying the same with the lower-level self.param.watch() yields an error message.

In the example below, the decorated function works, while the param.watched throws an error on executing the cell comprising anB = Bclass(otherclass=anA). If the "param.watch'ed" function is changed to a decorated, then again everything works fine.

Am I missing something? Should the behaviour not be the same?

Complete, minimal, self-contained example code that reproduces the issue

import param

class Aclass(param.Parameterized):

    Aa = param.Integer(12)
    Ab = param.String("qwe")

    def __init__(self, **params):
        super().__init__(**params)

    def incrAa(self):
        self.Aa += 1

    def moreAb(self):
        self.Ab += self.Ab

class Bclass(param.Parameterized):

    theA = param.Parameterized()
    Ba = param.Integer(3)
    Bb = param.String("asd")

    def __init__(self, otherclass: param.Parameterized, **params):
        self.theA = otherclass
        super().__init__(**params)
        self.param.watch(self.moreAtoB, ['theA.Ab'], queued=True, precedence=1)

    @param.depends('theA.Aa', watch=True)
    def addAtoB(self):
        print("\nBclass - self.theA.Aa: ", self.theA.Aa)

    def moreAtoB(self, *events):
        print("\nBclass - self.theA.Ab: ", self.theA.Ab)

anA = Aclass()
anB = Bclass(otherclass=anA)

anA.incrAa()
anA.moreAb()

Stack traceback and/or browser JavaScript console output

Traceback (most recent call last): File "c:...\envs...\lib\site-packages\IPython\core\interactiveshell.py", line 3433, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "C:\Users...\AppData\Local\Temp\ipykernel_4172\3905510252.py", line 1, in anB = Bclass(otherclass=anA) File "C:\Users...\AppData\Local\Temp\ipykernel4172\3622346801.py", line 10, in init self.param.watch(self.moreAtoB, ['theA.Ab'], queued=True, precedence=1) File "c:...\envs...\lib\site-packages\param\parameterized.py", line 2450, in watch return self._watch(fn, parameter_names, what, onlychanged, queued, precedence) File "c:...\envs...\lib\site-packages\param\parameterized.py", line 2457, in watch self._register_watcher('append', watcher, what) File "c:...\envs...\lib\site-packages\param\parameterized.py", line 2392, in _register_watcher raise ValueError("%s parameter was not found in list of " ValueError: theA.Ab parameter was not found in list of parameters of class Bclass

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File "c:...\envs...\lib\site-packages\IPython\core\interactiveshell.py", line 2052, in showtraceback stb = self.InteractiveTB.structured_traceback( File "c:...\envs...\lib\site-packages\IPython\core\ultratb.py", line 1112, in structured_traceback return FormattedTB.structured_traceback( File "c:...\envs...\lib\site-packages\IPython\core\ultratb.py", line 1006, in structured_traceback return VerboseTB.structured_traceback( File "c:...\envs...\lib\site-packages\IPython\core\ultratb.py", line 859, in structured_traceback formatted_exception = self.format_exception_as_a_whole(etype, evalue, etb, number_of_lines_of_context, File "c:...\envs...\lib\site-packages\IPython\core\ultratb.py", line 812, in format_exception_as_a_whole frames.append(self.format_record(r)) File "c:...\envs...\lib\site-packages\IPython\core\ultratb.py", line 730, in format_record result += ''.join(_format_traceback_lines(frame_info.lines, Colors, self.has_colors, lvals)) File "c:...\envs...\lib\site-packages\stack_data\utils.py", line 145, in cached_property_wrapper value = obj.dict[self.func.name] = self.func(obj) File "c:...\envs...\lib\site-packages\stack_data\core.py", line 734, in lines pieces = self.included_pieces File "c:...\envs...\lib\site-packages\stack_data\utils.py", line 145, in cached_property_wrapper value = obj.dict[self.func.name] = self.func(obj) File "c:...\envs...\lib\site-packages\stack_data\core.py", line 677, in included_pieces scope_pieces = self.scope_pieces File "c:...\envs...\lib\site-packages\stack_data\utils.py", line 145, in cached_property_wrapper value = obj.dict[self.func.name] = self.func(obj) File "c:...\envs...\lib\site-packages\stack_data\core.py", line 617, in scope_pieces for piece in self.source.pieces File "c:...\envs...\lib\site-packages\stack_data\utils.py", line 145, in cached_property_wrapper value = obj.dict[self.func.name] = self.func(obj) File "c:...\envs...\lib\site-packages\stack_data\core.py", line 106, in pieces return list(self._clean_pieces()) File "c:...\envs...\lib\site-packages\stack_data\core.py", line 130, in _clean_pieces raise AssertionError("Pieces mismatches: %s" % mismatches) AssertionError: Pieces mismatches: [{680, 681}, {696, 695}, {708, 709}, {714, 715}]

Screenshots or screencasts of the bug in action

maximlt commented 1 year ago

Thanks for the issue report, I can indeed reproduce it with this simplified version:

import param

class Other(param.Parameterized):

    x = param.Integer(1)

other = Other()

class P(param.Parameterized):

    other = param.Parameter(other)

    def __init__(self, **params):
        super().__init__(**params)
        self.param.watch(self.debug, ['other.x'])

    def debug(self, *events):
        print('DEBUG')

p = P()

Traceback:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[118], line 20
     17     def debug(self, *events):
     18         print('DEBUG')
---> 20 p = P()

Cell In[118], line 15, in P.__init__(self, **params)
     13 def __init__(self, **params):
     14     super().__init__(**params)
---> 15     self.param.watch(self.debug, ['other.x'])

File ~/dev/param/param/parameterized.py:2454, in Parameters.watch(self_, fn, parameter_names, what, onlychanged, queued, precedence)
   2450 if precedence < 0:
   2451     raise ValueError("User-defined watch callbacks must declare "
   2452                      "a positive precedence. Negative precedences "
   2453                      "are reserved for internal Watchers.")
-> 2454 return self_._watch(fn, parameter_names, what, onlychanged, queued, precedence)

File ~/dev/param/param/parameterized.py:2461, in Parameters._watch(self_, fn, parameter_names, what, onlychanged, queued, precedence)
   2457 parameter_names = tuple(parameter_names) if isinstance(parameter_names, list) else (parameter_names,)
   2458 watcher = Watcher(inst=self_.self, cls=self_.cls, fn=fn, mode='args',
   2459                   onlychanged=onlychanged, parameter_names=parameter_names,
   2460                   what=what, queued=queued, precedence=precedence)
-> 2461 self_._register_watcher('append', watcher, what)
   2462 return watcher

File ~/dev/param/param/parameterized.py:2396, in Parameters._register_watcher(self_, action, watcher, what)
   2394 for parameter_name in parameter_names:
   2395     if parameter_name not in self_.cls.param:
-> 2396         raise ValueError("%s parameter was not found in list of "
   2397                          "parameters of class %s" %
   2398                          (parameter_name, self_.cls.__name__))
   2400     if self_.self is not None and what == "value":
   2401         watchers = self_.self._param_watchers

ValueError: other.x parameter was not found in list of parameters of class P
phi6ias commented 1 year ago

Hi @maximlt

what I did find out in the meantime is that defining the watch on the objects param instead of the class param does work. I don't know if that is the intended behaviour, which is more for you to decide.

import param

class Other(param.Parameterized):

    x = param.Integer(1)

other = Other()

class P(param.Parameterized):

    other_ = param.Parameter(other)

    def __init__(self, **params):
        super().__init__(**params)
        self.other_.param.watch(self.debug, ['x'])

    def debug(self, *events):
        print('DEBUG')

p = P()
other.x=2